0%

gdb实现原理

参考链接

gdb命令

  • thread apply [threadno] [all] args - 将命令传递给一个或多个线程,参见链接
    比如,thread apply all continue表示将continue命令传递给所有线程,也就是让所有线程都继续运行。

  • rbreak - Set a breakpoint for all functions matching REGEXP. 参考链接

    e.g. rbreak file.C:.* - 给file.C的所有函数加上断点。

  • info

    • info inferior - 可以查看当前调试的进程的PID。另外一种方法是在gdb命令行中直接调用C函数:print (int)getpid()。参考:链接
    • info source - 当前调试的源文件路径。
    • info proc - 当前进程信息
      • info proc files - 当前进程打开的文件(和文件描述符)。
  • attach - 连接到正在运行的进程。与gdb -p效果相同。

  • detach - 取消连接的进程。

  • handle <signal> print pass nostop - 捕获信号(比如SIGSEGV)并且忽略它。handle <signal nostop

  • set - 修改变量的值,比如set x=10(或set var x=10)将变量x的值改为10。参考博客

  • show directories

  • print - gdb默认设置打印字符串的长度为200;更改打印最大长度:set print elements <number-of-elements>0表示unlimited.

  • ptype <variable name> - 打印变量类型。

  • finish - 从函数中返回,并打印函数返回值(即使函数的return语句很复杂,也可以获取返回值)。

环境变量

链接

断点

添加断点:

1
break file:line_no

查看断点:

1
info break

删除第2个断点:

1
delete 2

条件断点

参考:博客文档

break ... if cond

观察断点

捕捉断点

1
try...catch

打印长度的限制

  • Value sizes - 参考:文档
1
2
set max-value-size bytes
set max-value-size unlimited
  • 打印字符长度限制

    gdb默认设置打印字符串的长度为200;更改打印最大长度:set print elements

coredump

gdb命令:gcore

Reference

WSL无法使用gdb

WSL指Windows虚拟机。

解决方法

安装PPA的daily build版本

1
2
3
sudo add-apt-repository ppa:ubuntu-support-team/gdb
sudo apt update
sudo apt install gdb

gdb attach 权限报错

This is due to kernel hardening in Linux; you can disable this behavior by echo 0 > /proc/sys/kernel/yama/ptrace_scope or by modifying it in /etc/sysctl.d/10-ptrace.conf.

How to solve “ptrace operation not permitted” when trying to attach GDB to a process?

gdb debug forks

Reference

By default, when a program forks, gdb will continue to debug the parent process and the child process will run unimpeded.

If you want to follow the child process instead of the parent process, use the command set follow-fork-mode.

set follow-fork-mode mode
Set the debugger response to a program call of fork or vfork. A call to fork or vfork creates a new process. The mode argument can be:
parent
The original process is debugged after a fork. The child process runs unimpeded. This is the default.
child
The new process is debugged after a fork. The parent process runs unimpeded.
ask
gdb 会提示让你选择 parent 还是 child

show follow-fork-mode
Display the current debugger response to a fork or vfork call.
On Linux, if you want to debug both the parent and child processes, use the command set detach-on-fork.

set detach-on-fork mode
Tells gdb whether to detach one of the processes after a fork, or retain debugger control over them both.
on
The child process (or parent process, depending on the value of follow-fork-mode) will be detached and allowed to run independently. This is the default.
off
Both processes will be held under the control of gdb. One process (child or parent, depending on the value of follow-fork-mode) is debugged as usual, while the other is held suspended.

show detach-on-fork
Show whether detach-on-fork mode is on/off.

If you issue a run command to gdb after an exec call executes, the new target restarts. To restart the parent process, use the file command with the parent executable name as its argument. By default, after an exec call executes, gdb discards the symbols of the previous executable image. You can change this behaviour with the set follow-exec-mode command.

set follow-exec-mode mode
Set debugger response to a program call of exec. An exec call replaces the program image of a process.
follow-exec-mode can be:

new
gdb creates a new inferior and rebinds the process to this new inferior. The program the process was running before the exec call can be restarted afterwards by restarting the original inferior.
For example:

1
2
3
4
5
6
7
8
9
10
11
(gdb) info inferiors
(gdb) info inferior
Id Description Executable
* 1 <null> prog1
(gdb) run
process 12020 is executing new program: prog2
Program exited normally.
(gdb) info inferiors
Id Description Executable
* 2 <null> prog2
1 <null> prog1

same
gdb keeps the process bound to the same inferior. The new executable image replaces the previous executable loaded in the inferior. Restarting the inferior after the exec call, with e.g., the run command, restarts the executable the process was running after the exec call. This is the default mode.
For example:

1
2
3
4
5
6
7
8
9
(gdb) info inferiors
Id Description Executable
* 1 <null> prog1
(gdb) run
process 12020 is executing new program: prog2
Program exited normally.
(gdb) info inferiors
Id Description Executable
* 1 <null> prog2

Setting Catchpoints

Reference

gdb redirect to a log file

You need to enable logging:

1
2
3
4
5
6
7
(gdb) set logging on
Now GDB will log to ./gdb.txt. You can tell it which file to use:

(gdb) set logging file my_god_object.log
And you can examine the current logging configuration:

(gdb) show logging

记录输入的命令:

1
(gdb) set trace-commands on

Refercence

递归处理顺序

gcc 的输入文件和库是从左往右处理的。也就是说,以下命令是错误的:

1
gcc -L. -la main.cc

链接器处理到某个目标文件(如 main.cc 编译后的目标代码)时,如果遇到未解析的符号(比如 f() ),
它会从接下来的库中查找这些符号。因此顺序非常重要。

这里,-L. -la 选项在 main.cc 之前,链接器会首先尝试从 liba.so 中查找引用的符号,
但是,因为此时 main.cc 还未被处理,所以链接器还不知道有对 liba.so 中的函数 f() 的引用。
到了 main.cc ,链接器解析出引用,但它不会回头再去 liba.so 中查找,导致报错:”undefined reference to f()”。

正确的命令如下:

1
gcc main.cc -L. -la

注意:liba.so 的指定必须去掉 lib 和 .so ,也就是说不允许直接指定“库文件名”,而是只能指定“库名”。
如果想直接指定库文件名,那么应该把 liba.so 当成输入文件:

1
gcc main.cc ./liba.so

属性语法(Attribute Syntax)

参考:官方文档

函数属性(Function Attributes)

参考:

属性列举:

  • format (archetype, string-index, first-to-check)

    format 属性,指定函数采用 printf、scanf、strftime 或 strfmon 风格的参数,
    这些参数应根据格式字符串(format string)进行类型检查(type-checked)。
    类型检查发生在编译期。

    举例:

    1
    2
    3
    extern int
    my_printf (void *my_object, const char *my_format, ...)
    __attribute__ ((format (printf, 2, 3)));
    • archetype决定format string应该如何解释。
      可选为printfscanfstrftimestrfmon(也可以使用__printf____scanf____strftime____strfmon__)。
    • string-index指定哪个参数是format string(从1开始)。
    • first-to-check指定format string对应的第一个参数的序号。
      对于那些无法检查参数的函数(比如vprintf),该参数指定为0。在这种情况下,编译器仅检查format string的一致性。对于strftime格式,该参数必须为0

选项

  • -save-temps: 可以保留所有中间文件,例如预编译文件、汇编文件、目标文件等。

ulimit

1
2
3
4
#查看配置
ulimit -a
#设置core file size
ulimit -c unlimited

ulimit只对当前终端有效。

以下两种方法对所有用户和终端有效:

  1. /etc/security/limits.conf中设置(redhat衍生系linux)。
  2. 或注释掉/etc/profile中的这一行:
    1
    2
    # No core files by default
    ulimit -S -c 0 > /dev/null 2>&1

core_pattern

core_pattern解释

链接

Read /usr/src/linux/Documentation/sysctl/kernel.txt.

core_pattern is used to specify a core dumpfile pattern name.

在系统启动时,Apport(crash reporting service)会生成配置文件/proc/sys/kernel/core_pattern。参考这里

Apport uses /proc/sys/kernel/core_pattern to directly pipe the core dump into apport:

1
2
3
$ cat /proc/sys/kernel/core_pattern
|/usr/share/apport/apport %p %s %c
$

Note that even if ulimit is set to disabled core files (by specyfing a core file size of zero using ulimit -c 0), apport will still capture the crash.

For intercepting Python crashes it installs a /etc/python*/sitecustomize.py to call apport on unhandled exceptions.

其中,/usr/share/apport/apport是一个python脚本。

以下是core_pattern文件的参数说明(参考Linux Manual Page:man core):

1
2
3
4
5
6
7
8
9
10
%c - Core file size soft resource limit of crashing process (since Linux 2.6.24).
%p - insert pid into filename 添加pid
%u - insert current uid into filename 添加当前uid
%g - insert current gid into filename 添加当前gid
%s - insert signal that caused the coredump into the filename 添加导致产生core的信号
%t - insert UNIX time that the coredump occurred into filename 添加core文件生成时的unix时间
%h - insert hostname where the coredump happened into filename 添加主机名
%e - insert coredumping executable name into filename 添加命令名

If the first character of the pattern is a '|', the kernel will treat the rest of the pattern as a command to run. The core dump will be written to the standard input of that program instead of to a file.

Apport的拦截组件默认是关闭的:

Apport itself is running at all times because it collects crash data for whoopsie (see ErrorTracker). However, the crash interception component is still disabled. To enable it permanently, do:

1
sudo nano /etc/apport/crashdb.conf

… and add a hash symbol # in the beginning of the following line:

'problem_types': ['Bug', 'Package'],

To disable crash reporting just remove the hash symbol.

设置core_pattern

链接

  1. /proc/sys/kernel/core_uses_pid可以控制产生的core文件的文件名中是否添加pid作为扩展,如果添加则文件内容为1,否则为0;

  2. /proc/sys/kernel/core_pattern可以设置格式化的core文件保存位置或文件名:

    1
    2
    3
    $ cat /proc/sys/kernel/core_pattern
    |/usr/share/apport/apport %p %s %c
    $ echo "/corefile/core-%e-%p-%t" > /proc/sys/kernel/core_pattern

    你可以用下列方式来完成:

    1
    2
    3
    4
    #查看所有sysctl所有变量的值。
    sysctl -a
    #设置变量kernel.core_pattern为如下值。
    sudo sysctl -w kernel.core_pattern=/tmp/core-%e.%p.%h.%t

    这些操作一旦计算机重启,则会丢失,如果你想持久化这些操作,可以在 /etc/sysctl.conf文件中增加:

    1
    kernel.core_pattern=/tmp/core%p

    加好后,如果你想不重启看看效果的话,则用下面的命令:

    1
    sysctl -p /etc/sysctl.conf

相关命令行工具

参考资料:

Linux Manual Page: man core

SEE ALSO
    bash(1),  coredumpctl(1),  gdb(1),  getrlimit(2), mmap(2), prctl(2), sigaction(2), elf(5), proc(5), pthreads(7), signal(7), systemd-coredump(8)

Segment Fault排查

参考链接

  1. dmesg + nm + addr2line

    addr2line只能找出executable的行号;如果是shared libraries,请使用gdb。参考这里

    dmesg输出的含义:

    ip: 表示instruction pointer.

  2. fprintf

  3. gdb

  4. signal(SIGSEGV,handler)

  5. valgrind
    参考:
    博客
    文档
    可以使用PostScript查看图形化结果

  6. heaptrack

    github仓库

  7. Jemalloc

    官网

编译主要步骤

参考博客

  1. 预处理
1
gcc -E test.c -o test.i
  1. 编译

    编译是将高级语言(例如C、C++、Jave等)代码转换成机器码的过程。
    编译可以分成多个阶段,包括词法分析、语法分析、语义分析、优化和代码生成。
    编译器首先将源代码转换一种中间表示(通常是汇编代码或字节码),然后再将其转换为目标机器的机器代码。
    经过编译的代码通常是二进制的,可以直接在目标机器上执行。

先生成汇编代码:

1
gcc -S test.i -o test.s
  1. 汇编

    将汇编代码转成机器码。

1
gcc -c test.s -o test.o
  1. 链接

    该目标文件与其他目标文件、库文件、启动文件等链接起来生成可执行文件。

1
gcc test.o -o test

Note: 可以使用 -save-temps 选项以保留编译过程中的所有中间文件:

1
gcc -save-temps test.c

链接

C++名称修饰

Name mangling (C++ only): 名称修饰,也称为名称重整、名称改编。参见链接12

环境变量

参考:man ld.so

PATH
LD_LIBRARY_PATH
LD_PRELOAD

函数

命令

参考:man ld.so, man vdso, man elf, scanelf

ld(1), ldd(1), pldd(1), sprof(1), dlopen(3),getauxval(3), elf(5), capabilities(7),
rtld-audit(7), ldconfig(8), sln(8), vdso(7), as(1), elfedit(1), gdb(1), nm(1),
objcopy(1), objdump(1), patchelf(1), readelf(1), size(1), strings(1), strip(1),
execve(2), dl_iterate_phdr(3), core(5), ld.so(8)

ldconfig

配置动态连接器(dynamic linker)的运行时绑定(dynamic bindings)。
如果你刚刚安装好共享库,可能需要运行ldconfig:
    sudo ldconfig
通常需要超级用户来运行ldconfig,因为可能对需要某些root用户所有的目录和文件有写入权限。
lddconfig 为从以下目录找到的共享库创建必要的 links 和 cache :
    command line指定的目录;
    /etc/ld.so.conf文件中指定的目录;
    受信任的目录: /lib, /lib64, /usr/lib, /usr/lib64 。
该 cache 被运行时连接器(run-time linker) ld.so 或 ld-linux.so 使用。

ldconfig 尝试基于该库连接的 C 库来推断 ELF 库(比如 libc5 或 libc6/glibc)的类型。

一些现有的库没有包含足够的信息来推断其类型。
因此, /etc/ld.so.conf 文件格式允许指定期望的类型。
这只在这些 ELF 库不能被解决的情况下使用。

ldconfig 期望的符号链接有某种特定的形式,比如:

    libfoo.so -> libfoo.so.1 -> libfoo.so.1.12

其中,中间的文件 libfoo.so.1 是库的 SONAME 。

如果不遵循这种格式可能会导致升级后的兼容性问题。

ldd

描述:
    ldd调用标准动态连接器(见 ld.so(8)),并且将环境变量 LD_TRACE_LODADED_OBJECTS 为 1 。
    这会让动态连接器检查程序的动态依赖,并且寻找(根据 ld.so(8) 描述的规则)
    和加载满足这些依赖的目标。对于每一条依赖,
    ldd 显示匹配的目标的位置和其载入处的16进制地址。
    (linux-vdso和ld-linux共享依赖是特殊的;见vdso(7)和ld.so(8))

安全性:
    注意,在某些情况下,一些版本的ldd可能会尝试通过直接运行程序(可能导致程序中的ELF解释器
    或程序本身的运行)来获取依赖信息。

    因此,永远不要在不受信任的可执行文件上使用ldd,因为会导致随意代码的运行。更安全替代方法为:
        $ objdump -p /path/to/program | grep NEEDED
    注意,这种替代方法只会显示该可执行文件的直接依赖,而ldd显示该可执行文件的整个依赖树。

解释ldd的输出:
$ ldd -v libibsupport_real.so 
./libibsupport_real.so: /usr/lib64/libibverbs.so.1: version `IBVERBS_1.8' not found (required by ./libibsupport_real.so)
    linux-vdso.so.1 =>  (0x00002ad3e49e3000)
    libibverbs.so.1 => /usr/lib64/libibverbs.so.1 (0x00002ad3e589b000)
    ...

    Version information:
    ./libibsupport_real.so:
            libgcc_s.so.1 (GCC_3.0) => /usr/lib64/libgcc_s.so.1
            libibverbs.so.1 (IBVERBS_1.8) => not found
            libibverbs.so.1 (IBVERBS_1.1) => /usr/lib64/libibverbs.so.1
            ...
    /usr/lib64/libnl-3.so.200:
                libm.so.6 (GLIBC_2.2.5) => /usr/lib64/libm.so.6
                ...
在"Version information"中,"libgcc_s.so.1 (GCC_3.0) => /usr/lib64/libgcc_s.so.1"表示:
    "libgcc_s.so.1"指定一个shared library的名字(libgcc_s.so.1是GCC runtime library的一部分),该shared library为"./libibsupport_real.so"所依赖;
    "(GCC_3.0)"表明"libgcc_s.so.1"需要3.0版本及以上的GNU Compiler Collection (GCC);
    "=>"指出满足依赖的shared library;
    "/usr/lib64/libgcc_s.so.1"是满足要求的shared library的路径。

ldd output说明

sprof

参考:stackoverflows

objdump

[-p|--private-headers]
[-x|--all-headers]

readelf

[-d|--dynamic]

文件

/lib/ld.so
    Run-time linker/loader.
/etc/ld.so.conf
    File containing a list of directories, one per line, in which to search for libraries.
/etc/ld.so.cache
    File containing an ordered list of libraries found in the directories specified in /etc/ld.so.conf, as well as those found in the trusted directories.

The trusted directories:
    /lib
    /lib64
    /usr/lib
    /usr/lib64

ld.so

名字
    ld.so, ld-linux.so - 动态连接/加载器。

简介
    动态连接器可以被间接运行或直接运行。
    间接运行:
        运行某些动态连接程序或共享库。在这种情况下,不能向动态连接器传递命令行选项;并且在ELF情况下,存储在程序的.interp section中的动态连接器被执行。
    直接运行:
        /lib/ld-linux.so.* [OPTIONS] [PRAGRAM [ARGUMENTS]]

描述
    ld.so 和 ld-linux.so* 寻找和加载程序所需的共享对象(共享库),准备程序的运行,然后运行它。

    如果在编译期没有向 ld(1) 指定 -static 选项,则Linux二进制文件需要动态连接(在运行时连接)。

SONAME

参考:man ldconfig
参考:SONAME Wiki

GNU linker使用 -hname 或 -soname=name 来指定该库的library name field。
在内部,linker会创建一个 DT_SONAME field并且用 name 来填充它。

  1. 指定SONAME:
1
2
3
4
#Use ld:
$ ld -shared -soname libexample.so.1 -o libexample.so.1.2.3 file1.o file2.o
#Use gcc or g++:
$ gcc -shared -Wl,-soname,libexample.so.1 -o libexample.so.1.2.3 file1.o file2.o
  1. 安装时创建软链接:
1
2
ln -s libexample.so.1.2.3 libexample.so.1
ln -s libexample.so.1 libexample.so
  1. 查看SONAME:
1
2
3
4
5
6
#Use objdump
$ objdump -p libexample.so.1.3 | grep SONAME
SONAME libexample.so.1
#Use readelf
$ readelf -d libexample.so | grep SONAME
0x000000000000000e (SONAME) Library soname: [libexample.so.1]

符号版本控制(symbol versioning)

定制函数版本:

  1. example.c中定义函数my_printf
1
2
3
4
5
#include <stdio.h>

void my_printf(const char* format) {
printf("%s", format);
}
  1. version_script.txt中定义函数的版本为VERSION_1
1
2
3
4
5
6
VERSION_1 {
global:
my_printf;
local:
*;
};
  1. 编译时指定version_script.txt为version-script:
1
gcc -shared -Wl,--version-script=version_script.txt -o libexample.so example.o

其中,-Wl,引出连接器选项。

  1. 查看:
    1. 使用readelf -sW libexample.so查看函数my_printf的版本号(”VERSION_1”):
1
2
3
4
5
6
7
8
9
10
Symbol table '.dynsym' contains 8 entries:
Num: Value Size Type Bind Vis Ndx Name
0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND
1: 0000000000000000 0 NOTYPE WEAK DEFAULT UND _ITM_deregisterTMCloneTable
2: 0000000000000000 0 FUNC GLOBAL DEFAULT UND printf@GLIBC_2.2.5 (3)
3: 0000000000000000 0 NOTYPE WEAK DEFAULT UND __gmon_start__
4: 0000000000000000 0 NOTYPE WEAK DEFAULT UND _ITM_registerTMCloneTable
5: 0000000000000000 0 FUNC WEAK DEFAULT UND __cxa_finalize@GLIBC_2.2.5 (3)
6: 0000000000001105 39 FUNC GLOBAL DEFAULT 13 my_printf@@VERSION_1
7: 0000000000000000 0 OBJECT GLOBAL DEFAULT ABS VERSION_1
  1. 使用objdump -T libexample.so
  2. 使用nm -D libexample.so

实现同一个函数有多个版本

  1. 编写源代码
1
2
3
4
5
6
7
8
9
#include <stdio.h>

void foo() {
printf("foo version 1.0\n");
}

void foo_v2() {
printf("foo_v2 version 2.0\n");
}
  1. 编写版本脚本

例如,version_script.map文件如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
VERS_1.0 {
global: # global表示符号全局可见;默认为局部(也可能通过local显式声明为局部),不会被导出
foo;
} VERS_1.1;

VERS_1.1 {
global:
foo_v2;
foo; # VER_1.1继承自VER_1.0
# 1. foo在VER_1.0中已经声明为了global,不需要再次声明global
# 2. 当然可以再次显式声明为global,但是这不是推荐的做法
# 3. 假设foo在VER_1.0中不是global,此处声明为global,
# 会将原本不是global的foo变得全局可见
# 4. 我们其实无需再次导出foo,因为除非我们显式隐藏(使用local关键字),foo就在
# VER_1.1是和VER_1.0中的可见性保持一致
# 5. 如果希望在VER_1.1中的foo与VER_1.0有不同的行为,那么需要再次将其导出
} VERS_2.0;

VERS_2.0 {
global:
foo_v2
};

VER_1.1继承了VER_1.0的全部符号,同时VER_1.1可以增加或修改符号。

VER_1.0:第一个版本,导出foo符号;
VER_1.1:第二个版本,引入foo_v2,并重新导出foo;

程序依赖的库版本

有如下简单代码:

1
2
3
4
// file: main.cc
int main() {
return 0;
}

正常编译:

1
g++ main.cc -o a.out

查看符号:

1
strings a.out

你会发现其中有一些版本信息:

1
2
3
4
...
GLIBC_2.2.5
GLIBC_2.34
...

这是因为编译器和链接器生成库或可执行文件时,会根据系统上安装的glibc版本(因为库和可执行文件依赖glibc),
为库或可执行文件的函数符号附加上特定的版本信息。
为了保证向下兼容,glibc通过符号控制保留了多个版本的符号,支持老版本的软件能在较新的系统上运行。
例如,如果某个函数在glibc2.2.5中引入,它可以继续保留在之后的版本中,但符号标记为GLIBC_2.2.5。

ELF

ELF - Executable and Linking Format.
ELF描述了normal executable files、relocatable object files、core files和shared objects的格式。

参考:man elf, 博客:elf介绍

链接名

参考:程序员的自我修养, man ld

GCC的提供了不同的方法指定链接的共享库:

  • l<link_name>参数

    指定需要链接的共享库lib.so

  • l:<filename>参数

    通过文件名指定共享库,参考LD手册

  • 全路径指定

  • Wl,-static参数

    指定查找静态库,通过-Wl,-Bdynamic恢复成动态库查找

链接器选项

参考:man ld

ld - The GNU linker

选项:

-rpath=dir
    添加一个目录到运行时库搜寻路径中。

gcc通过 -Wl 前缀指定链接器选项,例如:

 gcc -Wl,--start-group foo.o bar.o -Wl,--end-group
 gcc -Wl,-rpath,'$ORIGIN/../lib'

Tips

  1. How to find out the dynamic libraries are loaded when running an executable?

Answer:

  • ldd /path/to/program
  • objdump -p /path/to/program | grep NEEDED
  • lddtree (from pax-utils) or readelf -d /bin/ls | grep 'NEEDED'
  • lsof -p PID | grep mem
1
2
3
4
$ pidof nginx
6920 6919

$ lsof -p 6919 | grep mem
  • strace -e trace=open myprogram

ldd and lsof show the libraries loaded either directly or at a given moment. They do not account for libraries loaded via dlopen (or discarded by dlclose). You can get a better picture of this using strace.

  • pmap <pid> -p

Notice

  1. 生成so时,链接器不会寻找其so依赖;executable会寻找so的依赖关系。换句话说,即使so生成过程不报错,但是executable生成时可能会报错。

  2. 生成so时,如果引用了其他 so ,只要 include 其他 so 的头文件,引用头文件中的符号时,就能编译通过。但是这样生成的 so 没有加上对应符号的依赖。
    此时用 readelf -s 查看对应的符号,其 type 为 NOTYPE 。

    1
    2
    3
    4
    5
    $ readelf -sW libb.so

    Symbol table '.dynsym' contains 14 entries:
    Num: Value Size Type Bind Vis Ndx Name
    6: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND _Z1fv
  3. 如果加上对应 -la 选项,就会从 liba.so 中寻找依赖的符号,如果找到,则添加进入 so 的依赖项中。当运行时则会加载 liba.so ,否则运行时不会加载。
    用 readelf -s 查看对应的符号,其 type 不再时 NOTYPE ,而是 FUNC (如果该符号是一个函数的话)。

    1
    6: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND _Z1fv

    如果没有找到符号,或者说 liba.so 中根本没有实现这个函数,那么其 type 依然是 NOTYPE ,此时也不会报错。
    注意:如果只是 include 头文件,也就说只有某个符号的声明,并没有其定义或引用它,那么 so 中不会生成其信息。

示例

运行时错误:”/path/to/a.out: /usr/lib/x86_64-linux-gnu/libstdc++.so.6: version `GLIBCXX_3.4.30’ not found (required by /path/to/libxxx.so)”

解释:libxxx.so 引用了一个 GLIBCXX_3.4.30 的符号(可能是全局变量或函数),但是在系统的 /usr/lib/x86_64-linux-gnu/libstdc++.so.6 中没有这个版本的符号。

1
2
3
4
5
6
7
8
9
10
$ strings /usr/lib/x86_64-linux-gnu/libstdc++.so.6 | grep 'GLIBCXX_3.4.'
GLIBCXX_3.4.1
...
GLIBCXX_3.4.27
GLIBCXX_3.4.28
# 没有 GLIBCXX_3.4.30

$ strings /path/to/libxxx.so | grep 'GLIBCXX_3.4.30' | c++filt
GLIBCXX_3.4.30
std::condition_variable::wait(std::unique_lock<std::mutex>&)@@GLIBCXX_3.4.30

这往往是因为原始的版本中,libxxx.so 是在一台机器中编译的,此时将该机器的 GLIBCXX_3.4.30 符号编译到其中了。但是运行时的机器是另一台,该机器上
不存在 GLIBCXX_3.4.30 版本,所以运行时报错。

或者,因为环境变量的配置错误,编译时和运行时引用的 libstdc++.so.6 不是同一个,这也会导致运行时可能找不到编译时的 GLIBCXX_3.4.30 版本。

可以在源文件中使用指定的版本(使用运行时的版本,比如 GLIBCXX_3.4.11 ):

  1. 查找版本:
1
2
3
4
5
6
7
$ objdump -T /usr/lib/x86_64-linux-gnu/libstdc++.so.6 | grep condition_variable | c++filt
00000000000d52d0 g DF .text 000000000000000c GLIBCXX_3.4.30 std::condition_variable::wait(std::unique_lock<std::mutex>&)
00000000000ac730 g DF .text 000000000000001c (GLIBCXX_3.4.11) std::condition_variable::wait(std::unique_lock<std::mutex>&)

$ objdump -T /usr/lib/x86_64-linux-gnu/libstdc++.so.6 | grep condition_variable
00000000000d52d0 g DF .text 000000000000000c GLIBCXX_3.4.30 _ZNSt18condition_variable4waitERSt11unique_lockISt5mutexE
00000000000ac730 g DF .text 000000000000001c (GLIBCXX_3.4.11) _ZNSt18condition_variable4waitERSt11unique_lockISt5mutexE

可以确定符号为 _ZNSt18condition_variable4waitERSt11unique_lockISt5mutexE ,版本为 GLIBCXX_3.4.11

在源文件中指定版本

symverview raw
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
/** 指定特定版本的符号 START */

#ifdef __cplusplus
extern "C" {
#endif /** __cplusplus */

__asm__ (".symver _ZNSt18condition_variable4waitERSt11unique_lockISt5mutexE, _ZNSt18condition_variable4waitERSt11unique_lockISt5mutexE@GLIBCXX_3.4.11");

#ifdef __cplusplus
}
#endif /** __cplusplus */

/** 指定特定版本的符号 END */

// 之后就可以使用 std::condition_variable::wait(std::unique_lock<std::mutex>&) 就是 GLIBCXX_3.4.11 的
#include <condition_variable>
#include <iostream>
#include <mutex>
#include <string>
#include <thread>

std::mutex m;
std::condition_variable cv;
std::string data;
bool ready = false;
bool processed = false;

void worker_thread()
{
// wait until main() sends data
std::unique_lock lk(m);
cv.wait(lk, []{ return ready; });

// after the wait, we own the lock
std::cout << "Worker thread is processing data\n";
data += " after processing";

// send data back to main()
processed = true;
std::cout << "Worker thread signals data processing completed\n";

// manual unlocking is done before notifying, to avoid waking up
// the waiting thread only to block again (see notify_one for details)
lk.unlock();
cv.notify_one();
}

int main()
{
std::thread worker(worker_thread);

data = "Example data";
// send data to the worker thread
{
std::lock_guard lk(m);
ready = true;
std::cout << "main() signals data ready for processing\n";
}
cv.notify_one();

// wait for the worker
{
std::unique_lock lk(m);
cv.wait(lk, []{ return processed; });
}
std::cout << "Back in main(), data = " << data << '\n';

worker.join();
}

编译:

1
g++ symver.cpp

验证符号版本:

1
2
3
$ strings a.out | grep condition_variable | c++filt
std::condition_variable::wait(std::unique_lock<std::mutex>&)
std::condition_variable::wait(std::unique_lock<std::mutex>&)@GLIBCXX_3.4.11

basic_string::_M_construct null not valid

C++有一个缺陷,请看以下代码:

1
2
3
4
5
6
7
8
//cpp defeat: basic_string::_M_construct null not valid
void fun(string s) {
cout << "fun" << endl;
}

int main() {
fun(0); // Run-time error: basic_string::_M_construct null not valid
}

其中,fun(0)0会被视为const char*类型,也就是nullptr,所以在编译期可以通过。
但是运行期会触发string对象的构造错误“basic_string::_M_construct null not valid”。

隐蔽一点的代码:

1
2
3
4
5
6
7
8
9
10
11
char * get_a_string() {
return nullptr;
}

int main() {
// Attention: Alaways take care that a parameter to a string should not be NULL!
fun(get_a_string()); // Run-time error: basic_string::_M_construct null not valid
// Better code
char * str = get_a_string();
fun(str != NULL? str : "");
}

Unit testing framework

CppTest

Memory check

valgrind
参考:
博客
文档
可以使用PostScript查看图形化结果

AddressSanitizer

Performance analyzer

Oracle Developer Studio:

Performance Analyzer手册

Thread Analyzer

gprof: 使用方法

timer

1
$ /usr/bin/time -p ls

Or,

1
$ time ls

其中(参考链接),

1
2
3
$ type -a time
time is a shell keyword
time is /usr/bin/time

CPU时间

Function’s CPU time:

* Inclusive time: total cpu time, include all functions it calls.
* Exclusive time: only the time used by the function itself, exclusive all its children.

Refer to [here](https://stackoverflow.com/questions/15760447/what-is-the-meaning-of-incl-cpu-time-excl-cpu-time-incl-real-cpu-time-excl-re/74426370).
  1. Wall time: total time the process used, containing IO time.

  2. CPU usage (CPU利用率) = CPU time / Wall time.

  3. real/user/system time

    • Real is wall clock time - time from start to finish of the call. This is all elapsed time including time slices used by other processes and time the process spends blocked (for example if it is waiting for I/O to complete).
    • User is the amount of CPU time spent in user-mode code (outside the kernel) within the process. This is only actual CPU time used in executing the process. Other processes and time the process spends blocked do not count towards this figure.
    • Sys is the amount of CPU time spent in the kernel within the process. This means executing CPU time spent in system calls within the kernel, as opposed to library code, which is still running in user-space. Like ‘user’, this is only CPU time used by the process. See below for a brief description of kernel mode (also known as ‘supervisor’ mode) and the system call mechanism.

    Refer to here.

  4. CPU 时间可能大于墙上时间:

    这是因为 CPU 时间是所有 CPU 核的运行时间的累加和,墙上时间则是实际的时间。此时 CPU 利用率大于 100%. (这是自己的理解)

  5. TODO: Is CPU time in flame graph sum of all the CPU time? Or is it the wall time when CPU works?

debug tool

gdb

1
2
3
4
(gdb) breakpoint exit
(gdb) breakpoint _exit
(gdb) breakpoint atexit
(gdb) breakpoint abort

Enable coredump: how to do

1
ulimit -c unlimited

Where is the core dumped file:

1
grep 'kernel.core_pattern' /etc/sysctl.conf

strace

Example:

1
strace -f -o strace.log -tt -y -yy -e trace=desc,process,network

Refer to here

-e trace=ipc – communication between processes (IPC)
-e trace=memory – memory syscalls
-e trace=network – network syscalls
-e trace=process – process calls (like fork, exec)
-e trace=signal – process signal handling (like HUP, exit)
-e trace=file – file related syscalls
-e trace=desc – all file descriptor related system calls

DDT

DDT

Configuration

mkdocs.yml 文件是 MkDocs 文档生成器的配置文件。

示例:mkdocs.yml

theme

该文件中的 theme 部分用于指定用于生成文档的主题。

在这种情况下,使用的主题是 Material。 name 字段指定主题的名称,而 custom_dir 字段指定包含主题自定义内容的目录。 features 字段指定可以为主题启用的可选功能列表。

以下是在此配置文件中列出的可选功能:

  • announce.dismiss: 启用可关闭的公告横幅,显示在文档的每个页面顶部。
  • content.action.edit: 在每个页面上启用“编辑”按钮,以便用户可以轻松编辑页面内容。
  • content.action.view: 在每个页面上启用“查看源代码”按钮,以便用户可以查看页面的源代码。
  • content.code.annotate: 启用代码注释功能,使用户可以添加注释以解释代码。
  • content.code.copy: 启用代码复制功能,使用户可以轻松复制代码。
  • content.tooltips: 启用工具提示功能,使用户可以在鼠标悬停时查看有关页面元素的信息。
  • navigation.footer: 在每个页面上启用页脚导航栏。
  • navigation.indexes: 启用索引导航栏,使用户可以轻松浏览文档中的索引。
  • navigation.sections: 启用部分导航栏,使用户可以轻松浏览文档中的各个部分。
  • navigation.tabs: 启用选项卡导航栏,使用户可以轻松浏览文档中的各个选项卡。
  • navigation.top: 在每个页面上启用顶部导航栏。
  • navigation.tracking: 启用导航跟踪功能,使用户可以跟踪他们在文档中的位置。
  • search.highlight: 启用搜索结果高亮显示功能。
  • search.share: 启用共享搜索结果功能,使用户可以轻松共享搜索结果。
  • search.suggest: 启用搜索建议功能,使用户可以在输入搜索查询时获得建议。
  • toc.follow: 启用目录跟随功能,使目录始终保持可见。

palette 字段指定了一个颜色方案,该方案包含以下内容:

  • scheme: 指定颜色方案的名称。
  • primary: 指定主要颜色。
  • accent: 指定强调颜色。
  • toggle: 指定切换到暗模式时使用的图标和名称。

第一个方案名为 default,其中主要颜色和强调颜色均为 indigo。切换到暗模式时,使用的图标为 material/brightness-7,名称为“切换到暗模式”。

第二个方案名为 slate,其中主要颜色和强调颜色均为 indigo。切换到亮模式时,使用的图标为 material/brightness-4,名称为“切换到亮模式”。

font 字段指定了用于文本和代码的字体。在此配置文件中,文本字体为 Roboto,代码字体为 Roboto Mono。 favicon 字段指定了网站图标的路径。 icon 字段指定了网站标志的路径。

favicon 和 icon 是网站的两个不同元素。

favicon 是网站的图标,通常显示在浏览器标签页上。它可以是一个小的图像文件,通常是 .ico 格式。在 mkdocs.yml 文件中,可以使用 favicon 字段来指定网站图标的路径。

icon 是网站的标志,通常显示在网站的标题栏或页眉中。它可以是一个图像文件,例如 .png 或 .jpg 文件。在 mkdocs.yml 文件中,可以使用 icon 字段来指定网站标志的路径。

plugin

这里列出了三个插件:blog、search和minify。其中,blog插件用于支持博客功能,search插件用于支持搜索功能,而minify插件用于压缩HTML文件。在search插件中,separator参数指定了搜索时的分隔符,这里的分隔符包括空格、连字符、逗号、冒号、等号、感叹号、方括号、括号、引号、反引号和斜杠等。

extra

analytics

analytics参数用于指定网站分析服务提供商和属性ID,例如衡量网站流量。

常见问题

See the doc.

  1. Keep underscore in file name

refer to:

文件示例

  • /proc: 查看手册 man proc

  • /proc/self/exe: 是指向当前进程的程序文件的软链接。

  • /proc/<pid>/exe: 是指向进程<pid>的程序文件的软链接。

    How to Find File System Names

  • /proc/[pid]/status:可查看进程的内存使用峰值等信息,关键字为”VmHWM”、”VmPeak”、”VmRSS”。

  • /proc/[pid]/stat:get current memory

  • /dev/shm:

    What Is /dev/shm And Its Practical Usage

    1
    $ df -h

    Sample outputs:

    1
    2
    3
    4
    5
    6
    7
    Filesystem            Size  Used Avail Use% Mounted on
    /dev/mapper/wks01-root
    444G 70G 351G 17% /
    tmpfs 3.9G 0 3.9G 0% /lib/init/rw
    udev 3.9G 332K 3.9G 1% /dev
    tmpfs 3.9G 168K 3.9G 1% /dev/shm
    /dev/sda1 228M 32M 184M 15% /boot

分区(Partition)和文件系统(Filesystem)

本小节内容来自 参考链接

分区:

Linux上的分区指:存储设备中划分出来的一个片段,该片段与其他片段逻辑上分离,好比一个个独立的房间。

分区表(partition table):

分区表存储各个分区的元数据,比如起始位置、终止位置、大小等。
有两种主要的分区表类型,MBR(older)和GPT(newer):

Partition tables Maximum primary partitions Maximum size for each partition Security Operating system Support
Master Boot Record (MBR) 4 2TB No such security features Supports most modern OS
Guid Partition Table (GPT) No such limit 18 Exabytes CRC32 checksum mechanism to verify the integrity of files Supports most modern OS

从上表可以很明显地看出,为什么GPT更推荐。

文件系统

文件系统是我们在每个分区中管理数据的方式。它负责索引、存储、检索、命名文件和维护文件的元数据(文件所有者、大小、权限等)。存储在分区中。

一个文件保存在多个连续的 扇区(sector) 中,现代每个扇区大约为4096字节。
文件系统负责组织哪些扇区准备好使用了、一个文件必须存储在哪个扇区、哪个扇区存储了什么文件。
如果没有这种组织,就无法无法检索任何文件,因为系统无法得知文件的位置(block,块)。

主要的文件系统分类:

  • FAT

文件分配表(FAT,File Allocation Table)是Microsoft开发的第一个文件系统。
从1997发布之后,有多个版本,称为FAT12、FAT16、FAT32,连续地增加了最大支持文件大小(file size)和驱动器大小(drive size)。

FAT32允许的最大文件大小为4Gb。直至WindowsXP,FAT32是默认的文件系统,之后被NTFS取代。
虽然FAT非常基础,但是它支持几乎所有的设备和操作系统。

注: 驱动器(drive),是一个能存储和读取非易失信息的位置,比如磁盘(disk)或光盘(disc)。

如下图,驱动器A:是一个软盘(floppy drive),
驱动器C:是主硬盘(primary hard drivce),
驱动器D:和E:是分区,F:是CD-ROM
CD-ROM常常是最后一个盘符(drive letter)。
在多数情况下,硬盘是C:驱动器,CD-ROM或其他光盘是D:驱动器。

驱动器示例

  • NTFS

新技术文件系统(New Technology File System,NTFS)是FAT的现代替代者。
除了支持高达16EB(大于170亿GB)的驱动器大小和256TB的单文件大小外,还支持日志系统(journaling system)。

  • ext/ext2/ext3/ext4

Linux的扩展文件系统(extended file system)或ext于1992年发布。之后有了3次更新:

ext2引入了文件属性(文件权限),ext3引入了日志功能(journaling)。

ext4对ext2和ext3向后兼容,增加了存储限制和一些性能调整。
可以支持高达1EB的卷(volumn),单个文件可以达16TB。

ext4也引入了延迟内存分配的概念,即在文件被强制刷新到存储设备时才为其分配扇区。
这提高了CPU的性能并减少了坏的扇区。
今天几乎所有的现代Linux发行版都使用ext4作为默认的文件系统。

  • ZFS

  • Btrfs

总结

下图是一个分区和文件系统的层次结构示例:

分区和文件系统的层次结构示例图

我有一个500GB的SSD,分成3个分区(boot、home、root),使用GPT作为分区表。

我没有分出swap分区。所有的分区都跑在ext4文件系统上。

在一个双启动(dual-booted)存储设备(Windows和Linux)上,还有几个适用于Windwows的NTFS分区。

你可以使用以下命令在任意存储设备上查看分区:

1
lsblk

更多资源请访问:

挂载

挂载:使设备上的文件和目录可以通过文件系统访问的一个过程。见维基百科

挂载点:A mount point is a location in the partition used as a root filesystem.

驱动

相关命令一览表

lsblk

lsblk [options] [device...]

list all avaivable or specified block devices.
Reads the sysfs filesystem to gather information.

df

df [OPTION]... [FILE]...

report file system disk space usage on which each FILE resides.

df -T 打印文件系统的类型。

du

du [OPTION]... [FILE]...

estimate file space usage.

quota

quota -s -u user...

display users' disk usage and limits.
quota reports the quotas of all the filesystems listed in /etc/mtab.
For filesystems that are NFS-mounted a call to the rpc.rquotad on the server machine is performed to get the information.

-s, --human-readable

repquota

prints a summary of disc usage and quotas for the specified file system.

mount

lsof - list open files

https://unix.stackexchange.com/questions/11238/how-to-get-over-device-or-resource-busy