1. 前言
工作中时不时会用到 GDB 来定位软件 bug,编写此文档只是为了方便查阅相关用法,并不会详述相关细节。因此本文档更像是一本书的目录而不是一整本书。
本文档所述仅供参考,请以 gdb help 查询到的用法为准。
2. 为何要使用 GDB?
当软件运行异常时,需要我们能快速定位软件 bug 并进行修复,GDB 这一工具正是为此而生。比一步步地加日志更高效!
3. 如何使用 GDB?
请自行安装 GDB,安装完成之后可自行通过下列命令查阅 GDB 的用法:
$ gdb
(gdb) help
也可自行通过GDB 官网查阅相关信息。
4. 常用命令一览表
命令 | 缩写 | 功能 |
---|---|---|
help | h | 查看帮助文档 |
info | i | 查看正在调试的程序的相关信息 |
break | b | 新建断点 |
run | r | 开始调试 |
quit | q | 终止调试 |
continue | c | 继续运行直到断点或者程序结束 |
next | n | 单步调试:按行执行代码,不进入函数内部 |
step | s | 单步调试:按语句执行代码,会进入函数内部 |
finish | fin | 正常运行当前函数直至结束 |
return | ret | 直接结束当前函数并返回 |
list | l | 查看代码 |
backtrace | bt | 查看调用栈 |
where | 作用同 backtrace ,查看调用栈 | |
attach | 调试正在运行的程序 | |
frame | f | 查看调用栈当前帧信息或者选择指定帧 |
p | 查看变量值 | |
x | 查看内存地址中的值 | |
display | 自行选择 print 或者 x 命令来查看表达式的值 | |
set | 设置变量值或更改 GDB 环境设置 | |
show | 查看 GDB 环境设置 | |
delete | d | 删除断点或者其他 GDB 对象 |
clear | 删除指定位置处的所有断点 | |
disable | 禁用断点 | |
enable | 启用断点 | |
enable | 启用断点 | |
save | 保存断点或其他 GDB 信息到文件中 | |
source | 加载文件中保存的 GDB 信息 | |
watch | 设置监控点,当监控对象的值被改变时中断程序 | |
rwatch | 设置监控点,当监控对象的值被读取时中断程序 | |
awatch | 设置监控点,当监控对象的值被改变或被读取时中断程序 | |
handle | 设置信号处理模式 | |
disassemble | 查看汇编代码 |
5. 如何在可执行文件中加入调试信息?
GCC 默认生成 release 版本,生成的可执行文件不包含调试信息。为了使用 GDB 能直接调试可执行文件,需要在编译时使用 GCC 的-g 选项告知 GCC 在可执行文件中加入调试信息。
$ gcc main.c -o main -g
6. 如何启动调试?
6.1. 如何启动新的进程来调试?
$ gdb 可执行文件名
(gdb) break main
(gdb) run
6.2. 如何在调试时给程序传递命令行参数?
方法一,在启动 GDB 命令时加入命令行参数:
$ gdb --args 可执行文件名 [参数1] [参数2] ...
(gdb) break main
(gdb) run
方法二,在启动 GDB 命令之后加入命令行参数:
$ gdb 可执行文件名
(gdb) set args [参数1] [参数2] ...
(gdb) break main
(gdb) run
6.3. 如何调试正在运行的进程?
先使用 ps 命令查询进程 ID,在启动 GDB 使用 attach 到进程 ID (pid)命令来进行调试:
$ ps -ef | grep 可执行文件名 | grep -v grep | awk '{print $2}'
$ gdb
(gdb) attach pid
(gdb) run
7. 怎么调试不带调试信息的可执行文件?
当可执行文件或者动态库文件为 release 版本时,文件中不带符号表信息,需要手动加载符号表信息。
7.1. 场景一:启动 GDB 时指定符号表文件
$ gdb --symbol=符号表文件名或者debug版本可执行文件名 --exec=release版本可执行文件名
(gdb) break main
(gdb) run
7.2. 场景二:启动 GDB 后加载符号表文件
$ ps -ef | grep 可执行文件名 | grep -v grep | awk '{print $2}'
$ gdb
(gdb) file 符号表文件名
(gdb) attach 进程ID
(gdb) run
7.3. 场景三:启动 GDB 后加载动态库符号表文件
$ gdb
(gdb) info sharedlibrary
(gdb) add-symbol-file 符号表文件名 符号表地址(上一条命令查询到的对应动态库文件的起始地址)
(gdb) run
8. 如何终止调试?
场景一,进程已中断,直接使用 quit 命令退出即可。
场景二,进程正在运行,按 Ctrl+C 中断进程,然后使用 quit 命令退出即可。
9. 如何管理断点?
使用断点可以让进程在我们期望的地方中断,以便进行调试。
9.1. 如何新建断点?
场景一,在指定源文件的指定行新建断点,例:
(gdb) break main.c:123
场景二,在进入函数处新建断点,例:
(gdb) break main
场景三,在指定源文件指定函数指定标签处新建断点,例:
(gdb) break factorial.c:fact:the_top
(gdb) break -source factorial.c -function fact -label the_top
场景四,新建条件断点,例:
(gdb) break main if argc > 1
(gdb) break myfunc if i % (j + 3) != 0
(gdb) break test.c:34 if (x & y) == 1
(gdb) break test.c:180 if (p_str == NULL && i < 0)
场景五,在调用栈中新建断点,用于使进程在调用栈指定帧处中断,例:
(gdb) backtrace
(gdb) frame 6
(gdb) break
9.2. 如何运行直至遇到断点?
场景一,启动了 GDB,未启动进程,使用 run 命令。
场景二,进程已中断,运行到下一个断点处使用 continue 命令。
(gdb) run
(gdb) continue
9.3. 如何查看现有断点?
使用 info breakpoints 命令或者缩写命令查看断点:
(gdb) info breakpoints
(gdb) i b
9.4. 如何删除断点?
场景一,删除指定编号断点,例:
(gdb) info breakpoints
(gdb) delete 1
场景二,删除所有断点,例:
(gdb) delete
9.5. 如何禁用和启用断点?
禁用断点使用 disable 命令,可禁用指定编号的单个(多个)断点、禁用指定编号范围内的多个断点、禁用所有断点,例:
(gdb) disable 1
(gdb) disable 1 2
(gdb) disable 4-10
(gdb) disable
启用断点使用 enable 命令,可启用指定编号的单个断点、启用指定编号范围内的多个断点、启用所有断点、启用指定断点后只命中指定次数、启用指定断点后只命中一次、启用指定断点后只命中一次且命中后自动删除断点,例:
(gdb) enable 1
(gdb) enable 4-10
(gdb) enable
(gdb) enable 1 5
(gdb) enable 1 once
(gdb) enable 1 delete
9.6. 如何保存和加载断点?
保存断点至文件使用 save breakpoints 命令,例:
(gdb) save breakpoints gdb.cfg
从文件加载断点使用 source 命令,例:
(gdb) source gdb.cfg
10. 如何调试?
10.1. 如何中断正在运行的进程?
GDB 调试时,若进程正在运行,可按 Ctrl+C 中断进程。
10.2. 如何进入函数?
使用 step 命令,可一次执行多步,默认执行一步,例:
(gdb) step
(gdb) step 5
10.3. 如何逐行调试?
使用 next 命令,不进入函数执行,可一次执行多步,默认执行一步,例:
(gdb) next
(gdb) next 5
10.4. 如何从当前函数返回?
场景一,执行函数到正常退出函数,使用 finish 命令,例:
(gdb) finish
场景二,立即结束执行当前函数并返回,可指定返回值,使用 return 命令,例:
(gdb) return 10
10.5. 如何查看变量值?
可通过 print/p 命令来查看变量值,使用:p[/输出类型] [变量名/表达式]。输出类型如下所示:
/x 以十六进制的形式打印出整数。
/d 以有符号、十进制的形式打印出整数。
/u 以无符号、十进制的形式打印出整数。
/o 以八进制的形式打印出整数。
/t 以二进制的形式打印出整数。
/f 以浮点数的形式打印变量或表达式的值。
/c 以字符形式打印变量或表达式的值。
场景一,打印普通变量的值:
(gdb) p var1
(gdb) p/x var2
(gdb) p/d (short)(0xfec0)
场景二,打印结构体变量或结构体指针变量指定成员的值:
(gdb) p var1.member1
(gdb) p/d var2->member2
场景三,打印结构体变量或者结构体指针变量所有成员的值:
(gdb) p var1
(gdb) p *var2
场景四,打印最近一次打印过的变量的值:
(gdb) p
场景五,查看内存地址中的值,可使用 x 命令,通过 help 命令自行查询其用法,例:
(gdb) help x
(gdb) x/10xb 0xffffaa7fb790
10.6. 如何修改变量值?
可通过 info locals 查看临时变量列表,通过 set 命令设置变量值(若变量名与 set 子命令名冲突可加上 var 关键字),如:
(gdb) info locals
(gdb) set x = 2
(gdb) set var x = 4
10.7. 如何显示源代码?
可通过 list 命令查看源代码,自行通过 help list 命令查看其详细用法,例:
(gdb) help list
(gdb) list
(gdb) list -
(gdb) list 100, 200
(gdb) list 100,
(gdb) list , 200
(gdb) list func
(gdb) list main.c:50
10.8. 如何查看调用栈?
可通过 backtrace 或者 where 命令查看调用栈信息,可通过 frame 命令查看当前帧或者切换到指定帧,然后可以通过 print 命令查看当前帧的临时变量,例:
(gdb) backtrace
(gdb) where
(gdb) frame
(gdb) frame 6
(gdb) print x
10.9. 如何调试多线程程序?
场景一,显示当前可调式的所有线程:
(gdb) info threads
场景二,切换当前调试的线程为指定 ID 的线程:
(gdb) thread 6
场景三,让新建断点在所有线程中生效:
(gdb) break test.c:10 thread all
场景四,让指定线程执行 GDB 命令,thread apply ID1 ID2 command,例:
(gdb) thread apply 1 2 3 step
场景五,设置现场调度模式,set scheduler-locking off|on|step,在使用 step 或者 continue 命令调试当前被调试线程的时候,其他线程也是同时执行的。off 不锁定任何线程,也就是所有线程都执行,这是默认值。 on 只有当前被调试程序会执行。 step 在单步的时候,除了 next 过一个函数的情况以外,只有当前线程会执行。例:
(gdb) set scheduler-locking off
场景六,如何查看锁的持有者。可通过 print/t 命令来查看,例如 _owner = 12345 说明当前锁的持有者的 pid 为 12345,例:
(gdb) p *(pthread_mutex_t*)[锁地址]
11. 如何进行远程调试?
在目标开发板上安装好 gdbserver 后,使用命令启动新的程序或者调试正在运行的程序:
$ gdbserver 192.168.1.34:54321 test
$ gdbserver 192.168.1.34:54321 --attach pid
在 PC 端:
$ gdb
(gdb) target remote 192.168.1.34:54321
12. 如何分析 coredump 文件?
为了让程序在异常退出时可生成 coredump 文件,在终端上执行命令:
$ ulimit -c unlimited
使用 GDB 分析 coredump 文件,使用 gdb [exec file] [core file] 命令行格式打开 gdb 和生成的 core 文件,然后即可像正常调试一样查看对应信息:
$ gdb ./a.out core
(gdb) backtrace
(gdb) frame 6
(gdb) list
(gdb) info locals
(gdb) print x