GDB-1——GDB初探
一、GDB简介
在 Linux 编程中,通常使用 gdb 来调试 C/C++ 应用程序。若想调试内核可以使用GTAG(不方便,需要硬件上飞线)或使用Qemu,清华大学相关网址上有介绍Qemu是怎么配置的。
二、给被调试程序加调试信息
1. gcc 编译程序时加上 -g 编译选项以便能晰地看到调试的每一行代码、调用的堆栈信息、变量名、函数名等信息。若没有加-g编译选项,是无法使用gdb进行调试的。gdb <elf_file> 可以确认被调试程序是否带有调试信息。
$ gcc -g -o pp test.c $ gdb pp ... Reading symbols from pp...done. $ gcc -o qq test.c $ gdb qq Reading symbols from qq...(no debugging symbols found)...done.
编译三方软件包时需要加上 make CFLAGS="-g -O0"。若是Android工程中编译,需要在 Makefile.mk 中加上 LOCAL_C_FLAGS += -g -O0。
2. 使用 strip 命令可以移除程序中的调试信息
$ ls -l pp 9648 pp $ strip pp $ ls -l pp 6312 pp $ gdb pp ... Reading symbols from pp...(no debugging symbols found)...done. $ ls -l qq 8552 qq $ strip qq $ ls -l qq 6312 qq
如上实验可见strip后失去了调试信息且可执行文件也变小了。即使没有加-g编译选项的qq,strip后也变小了。实际调试时为了能使显示的符号与源码对应上,还会关闭编译优化选项,编译优化分5个等级O0-O4,其中O0是不优化,O1-O4优化级别越来越高。
三、启动gdb的调试方法
1. 一般有三种情方式:
(1) gdb <elf_file_name> (2) gdb -p <pid> 或 gdb -p `pidof -s <process name>` 或 gdb attach <pid> //(gdb) detach 解绑 (3) gdb <elf_file_name> <core_name>
方式(1)比如gdb pp,调试前pp还没有启动。
方式(2)是被调试程序已经启动了。执行后若有显示"Attaching to process 585429"就表示attach成功了。若想结束调试,又不影响被调试程序,可以使用 detach 命令进行解绑(第一种方式也可以)。quit(q)命令退出gdb.
方式(3)是程序崩溃后,有些设备会生效 core.<pid> 的崩溃文件,比如pp运行崩溃了,生成的是core.1234这个文件,使用 gdb pp core.1234 来启动调试,可以看到蹦迪的地方,然后输入 bt 就可以查看崩溃的调用栈了。
可以通过ulimit -a 看"core file size"是否为0来确认是否关闭了core文件的生成,可以"ulimit -c unlimited"打开产生core文件的功能。
/proc/sys/kernel/core_uses_pid 可以控制产生的 core 文件的文件名中是否添加 PID 作为扩展,如果添加则文件内容为 1,否则为 0;
/proc/sys/kernel/core_pattern 可以设置格式化的core 文件保存位置或文件名。若 echo 进去 "/sdcard/core-%e-%p-%t" 就会显示崩溃的程序名-崩溃进程pid-core文件生成的时间戳。此外还有%u表示uid,%g表示gid, %s表示添加导致core产生的信号到文件名,%h表示主机名。
注:由于被调试程序使用了一些系统库(如libc.so)是没有调试符号的,所以 gdb 会提示找不到这些库的调试符号。core文件在有些设备上默认是不支持的。
2. gdb常用命令列表
命令 缩写 说明 --------------------------------------------- run r 运行一个程序 continue c 让暂停的程序继续运行 break b 添加断点 tbreak tb 添加临时断点,只生效一次 backtrace bt 查看当前进程的调用堆栈 frame f 切换到当前调用线程的指定堆栈 info info 查看断点/线程等信息 enable enable 启用某个断点 disable disable 禁用某个断点 delete del 删除断点 list l 显示源码 TODO: 显示的源码行数太少 print p 打印或修改变量或寄存器的值 ptype ptype 查看变量类型 thread thread 切换到指定线程 next n 运行到下一行 step s 如果有调用函数,进入调用函数内部,相当于step into until u 运行到指定行停下来 finish fi 结束当前调用函数,到上一层函数调用处 return return 结束当前调用函数并返回指定值,到上一层函数调用处 jump j 将当前程序执行流跳转到指定行或地址 disassemble dis 查看汇编代码 set args 设置程序命令行参数 show args 查看设置的命令行参数 watch watch 监视某一个变量或内存地址的值是否发生变化 display display 监视变量或内存地址,当程序中断后自动输出监控的变量或地址 dir dir 重定向源文件位置 help 加命令名,查看每一个命令的作用
3. 行号要以 gdb 调试器中的行号为准,不是源码文件中的行号,由于存在条件编译,部分代码可能不会被编译进可执行文件中,所以实际的调试符号文件中的行号与源码文件中的行号可能会不完全一致。
四、GDB命令详解
1. run
若run后重复输入r,则会提示是否重启程序,或是选重启,那么设置的所有断点将消失。
2. continue
当程序触发断点或者使用 Ctrl + C 命令中断下来后,如果想让程序继续运行,只要输入c命令即可。
3. break
添加断点的命令,添加断点的方式如下:
b <func_name> b <line_number> b <filename:line_number>
一般是先通过第3种方法,先定位到出问题的函数,然后再通过第2中方法按行号设置断点进行调试。
4. tbreak
break 命令是添加一个永久断点,tbreak 命令也是添加一个临时断点,触发一次后就删除。
5. backtrace & frame
用来查看当前所在线程的调用堆栈。如下例子一共3层调用栈,可以使用frame加堆栈编号来查看每一层调用栈。每一层函数调用传参可以看到
(gdb) bt #0 print (n=2, sum=1) at test.c:6 #1 0x00000000004005cc in sleep_print (to=100) at test.c:17 #2 0x00000000004005f0 in main () at test.c:25 (gdb) f 0 #0 print (n=2, sum=1) at test.c:6 6 printf("n=%d, sum=%d\n", n, sum); (gdb) f 1 #1 0x00000000004005cc in sleep_print (to=100) at test.c:17 17 print(n, sum); (gdb) f 2 #2 0x00000000004005f0 in main () at test.c:25 25 sleep_print(to);
6. info break/enable/disable/delete
info b 查看所有断点,enable <断点编号> 表示删除指定断点,disable <断点编号> 表示关闭指定断点,delete <断点编号> 表示删除指定断点。若 enable/disable/delete 不加参数则表示操作所有断点。
(gdb) info b Num Type Disp Enb Address What 1 breakpoint keep y 0x0000000000400574 in print at test.c:6 breakpoint already hit 3 times 2 breakpoint keep y 0x0000000000400599 in sleep_print at test.c:9 (gdb) disable 2 (gdb) info b Num Type Disp Enb Address What 1 breakpoint keep y 0x0000000000400574 in print at test.c:6 breakpoint already hit 3 times 2 breakpoint keep n 0x0000000000400599 in sleep_print at test.c:9 (gdb) enable 2 (gdb) info b Num Type Disp Enb Address What 1 breakpoint keep y 0x0000000000400574 in print at test.c:6 breakpoint already hit 3 times 2 breakpoint keep y 0x0000000000400599 in sleep_print at test.c:9 (gdb) delete 2 (gdb) info b Num Type Disp Enb Address What 1 breakpoint keep y 0x0000000000400574 in print at test.c:6 breakpoint already hit 3 times
上面"hit 3 times"表示断点1触发了3次,断点2没有显示表示触发了0次。Enb 那一栏为y表示断点是enable状态的,为n表示是disable状态的。
更多关于 info 的组合命令可以在 gdb 中输入 help info 来进行查看:
info address -- 描述符号 SYM 的存储位置 info all-registers -- 所有寄存器及其内容的列表 info args -- 当前堆栈帧的参数变量 info auto-load -- 打印自动加载文件的当前状态 info auto-load-scripts -- 打印自动加载的 Python 脚本列表 info auxv -- 显示下级的辅助向量 info bookmarks -- 用户可设置书签的状态 info breakpoints -- 指定断点的状态(如果没有参数,则为所有用户可设置的断点) info checkpoints -- 当前已知检查点的 ID info classes -- 所有 Objective-C 类 info common -- 打印出 Fortran COMMON 块中包含的值 info copying -- 重新分发 GDB 副本的条件 info dcache -- 打印关于 dcache 性能的信息 info display -- 程序停止时显示的表达式 info exceptions -- 列出所有 Ada 异常名称 info extensions -- 与源语言相关的所有文件扩展名 info files -- 正在调试的目标和文件的名称 info float -- 打印浮点单元的状态 info frame -- 所有关于选定的堆栈帧 info frame-filter -- 列出所有已注册的 Python 帧过滤器 info functions -- 所有函数名 info guile -- Guile 信息显示的前缀命令 info handle -- 当程序收到各种信号时调试器会做什么 info inferiors -- 指定低级的 ID(如果没有参数,则为所有低级) info line -- 源代码行的核心地址 info locals -- 当前栈帧的局部变量 info macro -- 显示 MACRO 的定义 info macros -- 在 LINESPEC 显示所有宏的定义 info mem -- 内存区域属性 info os -- 显示操作系统数据 ARG info pretty-printer -- GDB 命令列出所有注册的漂亮打印机 info probes -- 显示可用的静态探针 info proc -- 显示有关任何正在运行的进程的 /proc 进程信息 info program -- 程序的执行状态 info record -- 信息记录选项 info registers -- 整数寄存器列表及其内容 info scope -- 列出作用域的局部变量 info selectors -- 所有 Objective-C 选择器 info set -- 显示所有 GDB 设置 info sharedlibrary -- 加载的共享库的状态 info signals -- 当程序得到各种信号时调试器会做什么 info skip -- 显示跳过的状态 info source -- 有关当前源文件的信息 info sources -- 程序中的源文件 info stack -- 堆栈的回溯 info static-tracepoint-markers -- 列出目标静态跟踪点标记 info symbol -- 描述 ADDR 位置的符号 info target -- 正在调试的目标和文件的名称 info tasks -- 提供所有已知 Ada 任务的信息 info terminal -- 打印下级保存的终端状态 info threads -- 显示当前已知的线程信息,run后才有效 info tracepoints -- 指定跟踪点的状态(如果没有参数,则为所有跟踪点) info tvariables -- 跟踪状态变量的状态及其值 info type-printers -- GDB 命令列出所有已注册的类型打印机 info types -- 所有类型名称 info unwinder -- 列出展开器的 GDB 命令 info variables -- 所有全局和静态变量名 info vector -- 打印向量单元的状态 info vtbl -- 显示 C++ 对象的虚函数表 info warranty -- 您没有的各种保修 info watchpoints -- 指定观察点的状态(如果没有参数,则为所有观察点) info win -- 所有显示窗口的列表 info xmethod -- GDB 命令列出已注册的 xmethod 匹配器
7. list
简写为l,查看当前断点前后各5行的代码,默认显示多少行可以通过 set listsize <num> 修改相关的gdb配置。若重复输入list会递增行号继续显示剩下的代码行,一直到文件结束为止。list指令还可以往前和王后查看代码行,分别对应"list +"和"list -"。list常与frame搭配使用。使用"help list"可以查看更多用法:
list <line> 显示指定行附近的代码。 list <start line>,<end line> 显示开始和结束行之间的代码。 list <file:line> 显示file文件line行附近的代码 list <func> 显示fun开始位置附近的代码 list <file:func> 显示指定文件指定函数开始位置附近的代码,可以区分同名函数 list <addr> 显示包含addr地址附近的行
如下例子直接输入l看不到main()附近的情况,使用bt看栈回溯,然后f 2切到main()的栈帧,然后l就可以看到main()的实现了。
(gdb) l 1 #include <stdio.h> 2 #include <unistd.h> 3 4 void print(int n, int sum) 5 { 6 printf("n=%d, sum=%d\n", n, sum); 7 } 8 9 void sleep_print(int to) 10 { (gdb) bt #0 print (n=4, sum=6) at test.c:6 #1 0x00000000004005cc in sleep_print (to=100) at test.c:17 #2 0x00000000004005f0 in main () at test.c:25 (gdb) f 2 #2 0x00000000004005f0 in main () at test.c:25 25 sleep_print(to); (gdb) l 22 int main () 23 { 24 int to = 100; 25 sleep_print(to); 26 return 0; 27 }
8. print 和 ptype
print 命令可以在我们调试过程中方便地查看(当前栈帧中,要切到对应栈帧中进行打印和设置)变量的值,表达式计算结果,甚至函数计算结果。也可以修改当前内存中的变量值,print 命令可以简写成 p。
p &<val> 来输出变量val的地址,同理,对于一个指针 p *<ptr> 可以输出指针指向的值,若要看指针指向结构体的其它成员,可以使用 p ptr->var 进行显示。
如果在 C++ 对象中,我们可以通过 p this 来显示当前对象的地址,也可以通过 p *this 来列出当前对象的各个成员变量值,也可以使用 p a+b+c 来打印这三个变量的结果值。
假设 func() 是一个可以执行的函数,p func() 命令可以输出该变量的执行结果。
若函数执行失败了,可以通过 p strerror(errno) 来将错误码对应的文字信息打印出来。
print 命令不仅可以输出表达式结果,同时也可以修改变量的值。如 p server.port=6400。
print 输出变量值时可以指定输出格式,命令使用格式为:print /format variable 常见的format有如下几种,完整的格式和用法可以在 gdb 中输入 help x 来查看。
o octal 八进制显示
x hex 十六进制显示
d decimal 十进制显示
u unsigned decimal 无符号十进制显示
t binary 二进制显示
f float 浮点值显示
a address 内存地址格式显示(与十六进制相似)
i instruction 指令格式显示
s string 字符串形式显示
z hex, zero padded on the left 十六进制左侧补0显示
ptype命令含义是"print type",就是输出当前栈帧一个变量的类型。若一个变量是一个结构体,也可以列出一个结构体的各个成员。
示例:
(gdb) bt //打印栈回溯 #0 sleep_print (to=100) at test.c:14 #1 0x00000000004005f0 in main () at test.c:25 (gdb) f 1 //切到标号为1的栈帧 #1 0x00000000004005f0 in main () at test.c:25 25 sleep_print(to); (gdb) p to //打印标号为1的栈帧中变量to的值 $1 = 100 (gdb) p & to //打印to的地址 $2 = (int *) 0x7fffffffda1c (gdb) f 0 //切到0号栈栈帧 #0 sleep_print (to=100) at test.c:14 14 sum = sum + n; (gdb) p to=3 //设置0号栈帧下变量to=3 $3 = 3 (gdb) disable //关闭所有断点 (gdb) c //继续运行,可以发现只执行到了to=3就退出循环了 Continuing. n=2, sum=1 n=3, sum=3 n=4, sum=6 [Inferior 1 (process 180524) exited normally]
9. info 和 thread
(1) info threads 来查看进程当前所有线程信息和这些线程分别阻塞在何处。注意线程编号前面这个星号表示的是当前 gdb 的作用于哪个线程,而不是说标了星号就是主线程。线程编号为1的线程才是主线程。通过 thread <线程标号> 去切换到具体的线程上去,然后输入 bt 就能查看这个线程的调用堆栈。
(2) info args 可以用来查看当前函数的参数值,也是需要先切换到对应栈帧后才能使用,若是多线程,还需要先切到对应的线程。
10. next step until finish return jump
(1) 这是 gdb 调试程序时最常用的几个控制流命令。next 命令简写成 n,意思是让 gdb 调到下一条命令去执行,这里跳转到下一条命令执行不是说一定跳到代码的临近下一行,而是根据程序逻辑,跳转到相应的位置。举个例子,如果当前 gdb 中断在上述代码第 2 行,此时输入 next 命令时,gdb 将调到第6行,因为这里的 if 条件并不满足。
int a = 0; if (a == 9) { print("a is equal to 9.\n"); } int b = 10; print("b = %d.\n", b);
(2) next 命令用调试的术语叫”单步步过“(step over),即遇到函数调用不进入函数体内部而直接跳过;而 step 命令就是”单步步入“(step into),顾名思义,就是遇到函数调用,进入函数内部。step 可简写成 s。当函数的参数也是函数调用时,step 命令会依次进入各个函数。
(3) 在实际调试的时候,在某个函数中调试一会儿后,我们不需要再一步步执行到函数返回处,而是希望直接执行完当前函数并回到上一层调用处,可以使用 finish 命令。
(4) 与 finish 命令类似的还有 return 命令,return 命令作用是结束执行当前函数,还可以指定该函数的返回值,若是没有指定返回值,那么返回的就是一个脏数据。需要注意二者的区别:finish 命令会执行函数到正常退出该函数,而 return 命令是立即结束执行当前函数并返回,也就是说,如果当前函数还有剩余的代码未执行完毕,也不会执行了。
注:我们常用的函数调用方式有 __cdecl、__stdcall,C++ 的非静态成员函数的调用方式是 __thiscall,这些调用方式,函数参数的传递本质上是函数参数的入栈的过程,而这三种调用方式参数的入栈顺序都是从右往左的,如下这段代码中并没有显式标明函数的调用方式,所以采用默认 __cdecl 方式,因此先调用func2()再调用func1()。
可以使用如下文件做debug测试:
#include <stdio.h> int func1(int a, int b) { int c = a + b; return c; } int func2(int p, int q) { int t = q + p; return t; } int func3(int m, int n) { return m + n; } int main() { int c; c = func3(func1(1, 2), func2(3, 4)); printf("c=%d.\n", c); return 0; }
(5) until 的命令,简写成 u,使用 u <line> 指定程序运行到某一行停下来。
(6) jump 会让程序执行流跳转到指定位置执行,可以简写成j,用法 jump <location> location 可以是程序的行号或者函数的地址。如果跳过了某个对象的初始化代码,直接执行操作该对象的代码,那么可能会导致程序崩溃或其他意外行为。如果 jump 跳转到的位置后续没有断点,那么 gdb 会执行完跳转处的代码会继续执行。jump 命令除了跳过一些代码的执行外,还有一个妙用就是可以执行一些我们想要执行的代码,而这些代码在正常的逻辑下可能并不会执行。
11. disassemble
disassemble 命令会输出当前所在函数的汇编指令。要查看某段代码的汇编指令,或是调试一些没有调试信息的发布版程序时只能通过反汇编代码去定位问题,那么 disassemble 命令就派上用场了。这个命令在我们只有程序崩溃后产生 core 文件,且无对应的调试符号时非常有用,我们可以通过分析汇编代码定位一些错误。
gdb 默认反汇编为 AT&T 格式的指令,可以通过 show disassembly-flavor 查看支持的反汇编格式。如果习惯 intel 汇编格式的,用命令 set disassembly-flavor intel 来设置。
示例:
/* //对这段代码反汇编 int func2(int p, int q) { int t = q + p; return t; } */ (gdb) r Breakpoint 1, func2 (p=3, q=4) at test_2.c:11 11 int t = q + p; (gdb) disassemble Dump of assembler code for function func2: 0x0000000000400540 <+0>: push %rbp 0x0000000000400541 <+1>: mov %rsp,%rbp 0x0000000000400544 <+4>: mov %edi,-0x14(%rbp) 0x0000000000400547 <+7>: mov %esi,-0x18(%rbp) => 0x000000000040054a <+10>: mov -0x18(%rbp),%edx 0x000000000040054d <+13>: mov -0x14(%rbp),%eax 0x0000000000400550 <+16>: add %edx,%eax 0x0000000000400552 <+18>: mov %eax,-0x4(%rbp) 0x0000000000400555 <+21>: mov -0x4(%rbp),%eax 0x0000000000400558 <+24>: pop %rbp 0x0000000000400559 <+25>: retq (gdb) set disassembly-flavor Requires an argument. Valid arguments are att, intel. //支持的反汇编格式,上面是att格式,下面是intel格式 (gdb) set disassembly-flavor intel (gdb) disassemble Dump of assembler code for function func2: 0x0000000000400540 <+0>: push rbp 0x0000000000400541 <+1>: mov rbp,rsp 0x0000000000400544 <+4>: mov DWORD PTR [rbp-0x14],edi 0x0000000000400547 <+7>: mov DWORD PTR [rbp-0x18],esi => 0x000000000040054a <+10>: mov edx,DWORD PTR [rbp-0x18] 0x000000000040054d <+13>: mov eax,DWORD PTR [rbp-0x14] 0x0000000000400550 <+16>: add eax,edx 0x0000000000400552 <+18>: mov DWORD PTR [rbp-0x4],eax 0x0000000000400555 <+21>: mov eax,DWORD PTR [rbp-0x4] 0x0000000000400558 <+24>: pop rbp 0x0000000000400559 <+25>: ret End of assembler dump.
12. set args 与 show args
传递命令行参数,在用 gdb 附加程序后,在使用 run 命令之前,使用 set args <参数> 来指定。通过 show args 查看命令行参数是否设置成功。set args 不加任何参数即可清除掉设置好的命令行参数。命令行参数个数是以空格来区分的,若单个命令行参数之间含有空格,可以使用引号将参数包裹起来。
13. watch
watch 可以用来监视一个变量或者一段内存,首先这个变量或内存得在"current context"中的,比如在当前栈帧或代码段中,若此时这个变量还不存在的话是无法添加的。当这个变量或者该内存处的值发送变化时,gdb 就会中断下来。监视某个变量或者某个内存地址会产生一个"watch point"(观察点),可能是通过添加硬件断点(HW breakpoint)来实现的。使用方式是 watch <变量名或内存地址>,一般有以下几种格式:
(1) 整形变量
int i; watch i
(2) 指针类型
char *p; watch p 与 watch *p
(3) watch 一个数组或内存区间
char buf[128]; watch buf
这里是对 buf 的 128 个数据进行了监视。
注意:当设置的观察点是一个局部变量时。局部变量无效后,观察点也会失效。例如在观察点失效时, gdb 可能会提示信息:Watchpoint 2 deleted because the program has left the block in which its expression is valid.
示例:
(gdb) b 8 (gdb) r (gdb) watch buf //首先要确认buf这个变量目前是存在的 Watchpoint 2: buf (gdb) c Continuing. Watchpoint 2: buf Old value = {0 <repeats 64 times>} New value = {0, 0, 0, 0, 4, 0 <repeats 59 times>}
14. display
display 命令监视的变量或者内存地址,每次程序中断下来都会自动输出这些变量或内存的值。例如,假设程序有一些全局变量,每次断点停下来我都希望 gdb 可以自动输出这些变量的最新值,那么使用display <变量名> 设置即可。
示例:
(gdb) b 9 (gdb) c (gdb) r (gdb) display /x $ebx //断点时以16进制格式显示ebx寄存器的值 1: /x $ebx = 0x0 (gdb) display /x $rbp //断点时以16进制格式显示rbp寄存器的值 2: /x $rbp = 0x7fffffffda20 (gdb) c Continuing. Breakpoint 1, main (argc=1, argv=0x7fffffffdb08) at main.c:9 9 int j = i; 1: /x $ebx = 0x0 //遇到断点了,显示这两个寄存器的值 2: /x $rbp = 0x7fffffffda20 (gdb) info display //显示所有display点 Num Enb Expression 1: y /x $ebx 2: y /x $rbp (gdb) disable display 1 //disable一个display (gdb) info display Num Enb Expression 1: n /x $ebx 2: y /x $rbp (gdb) delete display 1 //删除一个display (gdb) info display Num Enb Expression 2: y /x $rbp (gdb) delete display //删除所有的display Delete all auto-display expressions? (y or n) y (gdb) info display There are no auto-display expressions now.
15. dir
让被调试的可执行程序匹配源代码。gcc/g++ 编译出来的可执行程序并不包含完整源码,-g 只是加了一个可执行程序与源码之间的位置映射关系,我们可以通过 dir 命令重新定位这种关系,使用dir SourcePath1:SourcePath2:SourcePath3 表示加一个源文件路径到当前路径的前面,当指定多个路径时使用”:”隔开。SourcePath1、SourcePath2、SourcePath3 指的就是需要设置的源码目录,gdb 会依次去这些目录搜索相应的原文件。
使用"show dir"查看当前设置了哪些源码搜索路径。dir 命令不加参数表示清空当前已设置的源码搜索路径。
示例:
~/origin_tmp/3.gdb $ gcc -g -o main main.c ~/origin_tmp/3.gdb $ mv main.c ../ ~/origin_tmp/3.gdb $ gdb main (gdb) l 1 main.c: No such file or directory. (gdb) dir ~/origin_tmp Source directories searched: /origin_tmp:$cdir:$cwd (gdb) l 1 #include <stdio.h> 2 3 int main(int argc, char **argv) ...
posted on 2022-11-08 11:42 Hello-World3 阅读(642) 评论(0) 编辑 收藏 举报