GDB 及用 Valgrind 检测内存泄漏

1 GDB 的启动与退出

在使用 gdb 调试之前,需要在编译时向可执行程序中添加调试信息(通过添加 -g 参数),包括变量名,函数名等。

gcc -g hello.c -o hello

然后就可以启动 gdb 调试了。

最通常的方法: gdb 可执行程序

对于正在运行的程序(假设它的进程号为 PID),如果想用 GDB 调试它,可以先切换到 root,然后: gdb 可执行程序 PID 也可以是在切到 root 后: gdb 可执行程序 然后在 GDB 环境里 attach PID

attach 的意思是将可执行程序挂接到 PID 上。如果要取消挂接,则执行: detach

如果一个程序执行时发生了 segmentation fault 错误,那么系统会将系统当前的内存状况写入到一个 core 文件中。默认情况下,系统关闭了这项功能,因为: ulimit -c 返回 0。表示系统允许 core 文件的大小为 0,也即禁止产生 core 文件。如果想打开这项功能,那么可以设置 core 文件的限制: ulimit -c unlimited

该设置只对 当前shell 及其 子shell 有效。

一旦有了 core 文件(一般来说与可执行程序在同一目录下),就可以将其用于 GDB 调试: gdb 可执行程序 core

若要退出 GDB 环境,可以: q 然后回车。或者 ctr + d

2 GDB 的常用操作

2.1 指定与查看主函数运行参数

指定主函数运行的参数: set args 参数1 参数2 ...

查看主函数运行的参数: shwo args

2.2 切换与显示当前工作目录

切换: cd 目录 显示当前工作目录: pwd

2.3 输出重定向

r >out.txt 与在 shell 下运行程序时重定向一样,将运行结果重定向到文件之中。

2.4 运行与单步调试

运行程序: r

继续执行: c

执行一句(不会进入调用的函数内部): n

执行后 num 句(不会进入调用的函数内部): n num

执行一句(会进入调用的函数内部): s

执行后 num 句(会进入调用的函数内部): s num

运行程序,直到当前函数返回: finish

运行程序,直至目前的循环结束: until

2.5 显示源代码

打印出一次能够最大显示的行数: show listsize

设置一次能够最大显示的行数为 SIZE: set listsize SIZE

显示当前所查看的行后面最大 SIZE 行的内容: l

显示当前所查看的行所处行前面最大 SIZE 行的内容: l -

显示某行周围最大 SIZE 行的内容: l 行号

显示某个函数的函数体: l 函数名

显示指定行后面最大 SIZE 行的内容: l 行号,

显示指定行前面最大 SIZE 行的内容: l ,行号

2.6 设置断点

在指定函数入口处暂停: b 函数名

在指定行号暂停: b 行号

在指定文件中的指定函数的入口处暂停: b 文件名:函数名

在指定文件的指定行暂停: b 文件名:行号

在该程序运行时的指定内存地址暂停: b *内存地址

例如:

1
2
Breakpoint 1, main (argc=1, argv=0x7fffffffeaf8) at test.cc:13
13 int a = 1;

假设我在第 13 行设了断点,然后在运行的时候,出现了上面的信息,它表示在 test.c 的第 13 行暂停了。

2.7 设置观测点

为表达式(包括变量)设置观测点,一旦其值产生了变化,立即暂停: watch 表达式

为表达式(包括变量)设置观测点,一旦其值被读取时,立即暂停: rwatch 表达式

为表达式(包括变量)设置观测点,一旦其值被读取或改变时,立即暂停: awatch 表达式

查询所有观测点: info watchpoints

例如:

1
2
3
4
Old value = 1
New value = 4
main (argc=1, argv=0x7fffffffeaf8) at test.cc:15
15 a += 4;

假设我设置了 a 为观测点,在运行的时候出现了上面的信息。它表示在 test.cc 这个文件的第 15 行暂停了。a 原来的值为 1,改变后为 4。

注意:只能设置当前正在调试的代码所处的作用域中的表达式为观测点。如果当前作用域中没有观测点对应的表达式,会显示:No symbol "xxx" in current context

另外,一旦离开了这个作用域到了外层作用域,在内层设置的观测点会自动删除。

2.8 设置捕捉点

捕捉事件,当这个事件产生时暂停: catch 事件名

捕捉事件一次,当因这个事件暂停后自动删除: tcatch 事件名

常用的事件有:

  • throw。C++ 中抛出异常时。
  • catch。C++ 捕捉到异常时。
  • exec。调用 exec 系统调用时。
  • fork。调用 fork 系统调用时。
  • vfork。调用 vfork 系统调用时。
  • load。载入动态链接库时。
  • load 库名。载入指定的动态链接库时。
  • unload。卸载动态链接库时。
  • unload 库名。卸载指定的动态链接库时。

2.9 查看暂停点

暂停点指的就是断点,观测点和捕捉点。

查看所有暂停点: info b

2.10 删除暂停点

删除某个函数相关的暂停点: clear 函数名

删除某个文件里指定函数的相关的暂停点: clear 文件名:函数名

删除某行相关的暂停点: clear 行号

删除某个文件里某行相关的暂停点: clear 文件名:行号

删除某个暂停点号对应的暂停点: delete 暂停点号

删除某个范围内的暂停点号对应的暂停点: delete 3-7

停用某个暂停点号对应的暂停点: disable 暂停点号

停用某个范围内的暂停点号对应的暂停点: disable 3-7

启用某个暂停点号对应的暂停点: enable 暂停点号

启用某个范围内的暂停点号对应的暂停点: enable 3-7

2.11 处理信号

handle 信号名 操作1 操作2 ...

操作有:

  • stop。当收到信号时,暂停程序的执行。
  • print。当收到信号时,打印信号信息。
  • pass。当被调试的程序收到信号时,GDB 不处理信号,它会把这个信号交给被调试程序处理。

可以通过: info handle 来查看 GDB 对所有信号的处理操作。

2.12 查看函数调用信息

查看当前程序暂停时栈的所有信息: bt 或者 info stack

例如:

1
2
#0  func () at haha.cc:7
#1 0x000000000040094b in main (argc=1, argv=0x7fffffffeaf8) at test.cc:17

其中,#0 表示栈顶,haha.cc:7 表示在 haha.cc 这个文件的第 7 行暂停,test.cc:17 表示在 test.cc 这个文件的第 17 行暂停(第 17 行调用了 haha.cc 里定义的函数 func)。

查看栈上面 n 层的信息: bt n

查看栈下面 n 层的信息: bt -n

查看栈第 n 层的信息: f n

查看当前程序暂停时所处的函数在栈中的信息: f

2.13 查看系统类的全局信息

查看进程号及其他信息: info proc

查看当前程序暂停时所处行的源代码在内存中的信息: info line

查看当前程序暂停时所处的函数在栈中的详细信息: info f

查看当前程序暂停时所处的函数的参数: info args

查看当前程序暂停时所处的函数的所有局部变量: info locals

查看虚函数表: info vtbl 对象 或者 info vtbl *指针 或者 info vtbl 引用

前提是用如下命令打开虚函数表设置: set print vtbl on 该值默认为 off。

查看设置为自动显示的表达式: info display

查看某一行的代码在内存中的信息: info line 行号

查看某个函数的代码块在内存中的信息: info line 函数名

查看某个文件里某行的代码在内存中的信息: info line 文件名:行号

查看某个文件里某个函数的代码块在内存中的信息: info line 文件名:函数名

显示当前可调试的所有线程: info threads 每个线程会有一个 GDB 为其分配的 ID,后面操作线程的时候会用到这个 ID。前面有 * 的是当前调试的线程。

2.14 查看运行时数据

查看 (print) 程序暂停时表达式的值(该表达式在程序暂停时所处的作用域中可见,或在当前作用域中不存在,但在外层作用域中可见): p 表达式

按照某种格式进行打印: p/f 表达式

其中,f 为格式控制符,同 C 中格式控制符。

  • x 表示按照十六进制数打印;
  • a 表示将值当作地址打印,且按照十六进制数打印,同时还会打印出该地址所指示内存中内容的描述性信息。
  • d 表示按照十进制数打印。
  • i 表示将值当作指令打印。
  • s 表示将值当作字符串打印。

查看某个变量的地址: p &表达式

查看数组中第 i 个元素的值(i 从 0 开始取值): p *数组名[i]

查看数组中的各个元素的值: p *数组名@长度 其中,数组名会被解析为首元素的地址。该命令的意思是,从该数组的首元素开始连续打印指定长度的元素的值。

如对于一个数组 int arr[10],要查看其中所有元素的值: p *arr@10

查看全局变量: p 文件名::表达式

如: p 'f2.c'::x

查看全局的数组: p 文件名::*数组名@长度

查看某个多态类型的指针或引用所指向或绑定的对象的动态类型在内存中的信息: p *指针 或者 p 引用

前提是用如下命令打开相应设置: set print object on 该值默认为 off。如果不打开该设置,默认显示的是该指针或引用所指向或绑定的对象的静态类型。

查看某个变量/对象或类型/类所占空间: p sizeof(变量/对象或类型/类)

设置自动显示 (display),使得程序暂停时自动显示表达式的值: display 表达式

停用设置为自动显示的表达式: diable display 自动显示号

启动设置为自动显示的表达式: enable display 自动显示号

删除设置为自动显示的表达式: delete display 自动显示号

2.15 根据地址查看数据在内存中的信息

使用 examine 命令,简写为 x。

x/nfu 地址

其中,

  • n 代表打印当前地址开始后面的 n 个域中的值(也即 n 为最后打印得到的值的个数)。
  • f 代表格式控制符,同 C 中的格式控制符。
  • u 代表每个域的字节数。默认为 8 字节(64位系统下)。
    • b 表示单字节;
    • h 表示双字节;
    • w 表示四字节;
    • g 表示八字节。

例如: x/3xg 0x4009ee 表示从 0x4009ee 这个地址开始打印 3 个域的址,每个域长为 8 字节,每个域的值用十六进制数显示。

2.16 查看源代码的汇编形式

查看当前内存中的源代码对应的汇编代码: disassemble

查看某个函数的源代码对应的汇编代码: disassemble func

例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
Dump of assembler code for function main(int, char const**):
0x000000000040090c <+0>: push %rbp
0x000000000040090d <+1>: mov %rsp,%rbp
0x0000000000400910 <+4>: sub $0x20,%rsp
0x0000000000400914 <+8>: mov %edi,-0x14(%rbp)
0x0000000000400917 <+11>: mov %rsi,-0x20(%rbp)
0x000000000040091b <+15>: movl $0x1,-0x4(%rbp)
0x0000000000400922 <+22>: addl $0x3,-0x4(%rbp)
=> 0x0000000000400926 <+26>: addl $0x4,-0x4(%rbp)
0x000000000040092a <+30>: mov -0x4(%rbp),%eax
0x000000000040092d <+33>: mov %eax,%esi
0x000000000040092f <+35>: mov $0x601080,%edi
0x0000000000400934 <+40>: callq 0x400710 <_ZNSolsEi@plt>
0x0000000000400939 <+45>: mov $0x400780,%esi
0x000000000040093e <+50>: mov %rax,%rdi
0x0000000000400941 <+53>: callq 0x400770 <_ZNSolsEPFRSoS_E@plt>
0x0000000000400946 <+58>: callq 0x400896 <func()>
0x000000000040094b <+63>: mov $0x0,%eax
0x0000000000400950 <+68>: leaveq
0x0000000000400951 <+69>: retq
End of assembler dump.

其中,那个箭头所指表示当前程序暂停时所处的行对应的指令。

还可以根据地址来查看: disassemble 起始地址,结束地址

2.17 多线程调试

切换当前调试的线程为指定 ID 的线程: thread ID

让一个或者多个线程执行 GDB 命令 command: thread apply ID1 ID2 command

让所有被调试线程执行 GDB 命令 command: thread apply all command

对某个线程设置断点: b 行号或者函数 thread ID

对所有线程设置断点: b 行号或者函数

注意:当任意一个运行中的线程停住时,所有其他运行的线程都会被停住。

默认情况下,所有线程都是同时运行的,直到任意一个运行中的线程停住时,所有其他运行的线程才会停住。然后再用 s 或者 c 命令使调试中的线程继续运行时,其他所有线程都会继续运行。要使只有调试中的线程继续运行,其他线程不运行,则执行: set scheduler-locking on 默认情况下为 off。

2.18 运行 shell 命令

shell 命令

2.19 其他设置选项

2.19.1 符号解析

由于编译器会将各个名字翻译成很多符号,而用户在调试程序的时候对这些符号是不易懂的。

GNU 提供了一个命令,可以将 GCC 编译器中的符号解析成用户易懂的名字: c++filt 符号

在 GDB 调试的时候,也可以通过设置相关选项,将调试时打印出的符号解析成用户易懂的名字: set print demangle on set print asm-demangle on

2.19.2 优雅地显示结构体或类

一般情况下,在查看结构体对象或类的对象地时,所显示的内容都会拍成一行,查看起来不太能方便。可以将其设置成多行显示: set print pretty on

3 Valgrind 及其常用操作

valgrind 是一款内核调试工具,可以用来检查内存相关的问题(memcheck 工具),程序中的函数调用问题(callgrind 工具),多线程中出现的竞争问题(helgrind)等等。

安装 valgrind: sudo apt-get install valgrind

同 gdb 一样,要使用 valgrind 进行调试需要再编译链接生成应用程序的时候使用 -g 参数,同时允许生成 core 文件。

运行程序并检查内存泄漏,并将分析出来的信息写入到文件: valgrind --tool=memcheck --leak-check=full --log-file=mem_leak.log 应用程序

有时候,也可以将 valgrind 与 gdb 一起使用: valgrind --tool=memcheck --leak-check=full --log-file=mem_leak.log gdb 应用程序

4 检测内存泄漏

一般情况下,首先使用 ps 命令 或者 top 命令或者 free 命令等查看内存使用。如果随着时间的推移,内存占用保持稳定增大,则说明存在内存泄漏。

然后就可以使用 gdb 和 valgrind 定位内存泄漏的地方。


Reference