GDB调试全解析:从基础命令到内核调试
1. GDB命令文件
在每次运行GDB时,有些操作是必须的,比如设置sysroot。为了方便,可以将这些命令放在一个命令文件中,每次启动GDB时自动运行。GDB会按以下顺序读取命令:
1. 从 $HOME/.gdbinit 文件读取。
2. 从当前目录的 .gdbinit 文件读取。
3. 从命令行使用 -x 参数指定的文件读取。
不过,出于安全考虑,较新的GDB版本会拒绝加载当前目录的 .gdbinit 文件。要解决这个问题,可以在 $HOME/.gdbinit 文件中添加如下内容:
set auto-load safe-path /
如果不想全局启用自动加载,也可以指定特定目录:
add-auto-load-safe-path /home/chris/myprog
个人更倾向于使用 -x 参数指定命令文件,这样能明确文件位置,避免遗忘。
Buildroot会在 output/staging/usr/share/buildroot/gdbinit 创建一个包含正确sysroot命令的GDB命令文件,例如:
set sysroot /home/chris/buildroot/output/host/usr/arm-buildroot-linux-gnueabi/sysroot
2. GDB常用命令概述
GDB有众多命令,以下是一些常用命令及其功能:
2.1 断点管理命令
| 命令 | 缩写 | 用途 |
|---|---|---|
break <location> | b <location> | 在函数名、行号或行上设置断点,例如 main 、 5 、 sortbug.c:42 |
info breakpoints | i b | 列出所有断点 |
delete breakpoint <N> | d b <N> | 删除指定编号的断点 |
2.2 程序执行控制命令
| 命令 | 缩写 | 用途 |
|---|---|---|
run | r | 将程序的新副本加载到内存并开始运行,但在使用gdbserver进行远程调试时无效 |
continue | c | 从断点处继续执行程序 |
Ctrl-C | - | 停止正在调试的程序 |
step | s | 单步执行一行代码,会进入调用的函数 |
next | n | 单步执行一行代码,跳过函数调用 |
finish | - | 运行到当前函数返回 |
2.3 获取调试信息命令
| 命令 | 缩写 | 用途 |
|---|---|---|
backtrace | bt | 列出调用栈 |
info threads | i th | 显示程序中正在执行的线程信息 |
info sharedlibrary | i share | 显示程序当前加载的共享库信息 |
print <variable> | p <variable> | 打印变量的值,例如 print foo |
list | l | 列出当前程序计数器附近的代码行 |
3. 运行到断点
gdbserver将程序加载到内存并在第一条指令处设置断点,然后等待GDB连接。连接成功后,进入调试会话。但如果立即单步执行,会出现如下错误信息:
Cannot find bounds of current function
这是因为程序在汇编代码处暂停,而汇编代码是为C和C++程序创建运行时环境的。C或C++代码的第一行是 main() 函数。若要在 main() 函数处停止,可以设置断点,然后使用 continue 命令(缩写 c )让gdbserver从程序开始处的断点继续执行,直到 main() 函数:
(gdb) break main
Breakpoint 1, main (argc=1, argv=0xbefffe24) at helloworld.c:8 printf("Hello, world!\n");
(gdb) c
此时可能会看到如下信息:
Reading /lib/ld-linux.so.3 from remote target...
warning: File transfers from remote targets can be slow. Use "set sysroot" to access files locally in
较旧版本的GDB可能会显示:
warning: Could not load shared library symbols for 2 libraries, e.g. /lib/libc.so.6.
这两种情况都是因为忘记设置sysroot,需要回顾前面关于sysroot的部分。
与本地启动程序不同,本地启动只需输入 run 命令。在远程调试会话中输入 run ,可能会看到远程目标不支持该命令的提示,或者在旧版本GDB中程序会无响应。
4. 本地调试
在目标设备上运行本地版本的GDB不如远程调试常见,但也是可行的。除了在目标镜像中安装GDB,还需要未剥离调试信息的可执行文件副本以及对应的源代码。Yocto Project和Buildroot都支持这样做。
4.1 Yocto Project
在 conf/local.conf 文件中添加以下内容,将GDB添加到目标镜像:
IMAGE_INSTALL_append = " gdb"
若要获取要调试的包的调试信息,Yocto Project会构建包含未剥离二进制文件和源代码的调试版本包。可以通过在 conf/local.conf 中添加 <package name>-dbg 来选择性地添加这些调试包,也可以通过添加 dbg-pkgs 到 EXTRA_IMAGE_FEATURES 来安装所有调试包,但这会显著增加目标镜像的大小:
EXTRA_IMAGE_FEATURES = "dbg-pkgs"
源代码会安装到目标镜像的 /usr/src/debug/<package name> 目录,这意味着GDB无需运行 set substitute-path 就能找到源代码。如果不需要源代码,可以在 conf/local.conf 文件中添加以下内容来阻止其安装:
PACKAGE_DEBUG_SPLIT_STYLE = "debug-without-src"
4.2 Buildroot
在Buildroot中,可以通过启用以下选项在目标镜像中安装本地版本的GDB:
BR2_PACKAGE_GDB_DEBUGGER in Target packages | Debugging, profiling and benchmark | Full debugger
为了构建包含调试信息的二进制文件并将其安装到目标镜像中而不进行剥离,需要启用以下两个选项:
BR2_ENABLE_DEBUG in Build options | Build packages with debugging symbols
BR2_STRIP_none in Build options | Strip command for binaries on target
5. 即时调试
有时程序运行一段时间后会出现异常,这时可以使用GDB的 attach 功能进行即时调试,该功能在本地和远程调试会话中都可用。
在远程调试时,需要找到要调试的进程的PID,并使用 --attach 选项将其传递给gdbserver。例如,若PID为 109 ,可以输入以下命令:
# gdbserver --attach :10000 109
Attached; pid = 109
Listening on port 10000
这会使进程像在断点处一样停止,此时可以正常启动交叉GDB并连接到gdbserver。调试完成后,可以使用 detach 命令让程序在不使用调试器的情况下继续运行:
(gdb) detach
Detaching from program: /home/chris/MELP/helloworld/helloworld, process 109
Ending remote debugging.
6. 调试分叉和线程
6.1 调试分叉
当调试的程序发生分叉时,调试会话默认会跟随父进程,这由 follow-fork-mode 控制,其值可以是 parent 或 child ,默认值为 parent 。但目前gdbserver不支持该选项,因此仅在本地调试时有效。如果在使用gdbserver时需要调试子进程,可以修改代码,让子进程在分叉后立即对一个变量进行循环,这样就可以附加一个新的gdbserver会话并设置该变量,使其跳出循环。
6.2 调试线程
在多线程进程中,当一个线程遇到断点时,默认情况下所有线程都会停止。这有助于查看静态变量而不被其他线程修改。当恢复线程执行时,即使是单步执行,所有停止的线程也会重新启动,这可能会导致问题。可以通过 scheduler-locking 参数修改GDB处理停止线程的方式,默认情况下该参数为 off ,将其设置为 on 后,只有在断点处停止的线程会恢复执行,其他线程保持停止状态,直到再次将 scheduler-locking 设置为 off 。gdbserver支持此功能。
7. 核心文件
核心文件会捕获程序失败时的状态,即使不在调试现场,也能通过核心文件获取有价值的信息。当看到 Segmentation fault (core dumped) 时,应深入研究核心文件。
7.1 核心文件的创建
核心文件默认不会创建,只有当进程的核心文件资源限制不为零时才会创建。可以使用 ulimit -c 命令为当前shell更改该限制,若要取消核心文件大小的所有限制,可以输入以下命令:
$ ulimit -c unlimited
默认情况下,核心文件名为 core ,并放置在进程的当前工作目录(即 /proc/<PID>/cwd 指向的目录)。但这种方式存在一些问题,例如难以区分多个同名 core 文件是由哪个程序生成的,当前工作目录可能是只读文件系统、没有足够空间存储核心文件或进程没有写入权限。
7.2 核心文件的命名和放置
有两个文件可以控制核心文件的命名和放置:
- /proc/sys/kernel/core_uses_pid :向其写入 1 会将垂死进程的PID号附加到文件名中,前提是能从日志文件中将PID号与程序名关联起来。
- /proc/sys/kernel/core_pattern :可以使用元字符自定义核心文件的命名模式,默认模式为 core ,可以将其更改为包含以下元字符的模式:
- %p :PID
- %u :转储进程的真实用户ID
- %g :转储进程的真实组ID
- %s :导致转储的信号编号
- %t :转储时间,以自1970年1月1日00:00:00 UTC以来的秒数表示
- %h :主机名
- %e :可执行文件名
- %E :可执行文件的路径名,斜杠 / 替换为感叹号 !
- %c :转储进程的核心文件大小软资源限制
例如,以下模式会将所有核心文件收集到 /corefiles 目录,并以程序名和崩溃时间命名:
# echo /corefiles/core.%e.%t > /proc/sys/kernel/core_pattern
核心转储后,可以在 /corefiles 目录中看到类似 core.sort-debug.1431425613 的文件。
7.3 使用GDB查看核心文件
以下是一个使用GDB查看核心文件的示例:
$ arm-poky-linux-gnueabi-gdb sort-debug /home/chris/rootfs/corefiles/core.sort-debug.1431425613
[...]
Core was generated by `./sort-debug'.
Program terminated with signal SIGSEGV, Segmentation fault.
#0 0x000085c8 in addtree (p=0x0, w=0xbeac4c60 "the") at sort-debug.c:41
41 p->word = strdup (w);
这表明程序在第41行停止。使用 list 命令可以查看附近的代码:
(gdb) list
37 static struct tnode *addtree (struct tnode *p, char *w)
38 {
39 int cond;
40
41 p->word = strdup (w);
42 p->count = 1;
43 p->left = NULL;
44 p->right = NULL;
45
使用 backtrace 命令(缩写 bt )可以查看调用栈:
(gdb) bt
#0 0x000085c8 in addtree (p=0x0, w=0xbeac4c60 "the") at sort-debug.c:41
#1 0x00008798 in main (argc=1, argv=0xbeac4e24) at sort-debug.c:89
从调用栈可以看出, addtree() 函数被传入了空指针,这是一个明显的错误。
8. GDB用户界面
GDB可以通过GDB机器接口(GDB/MI)进行底层控制,该接口可用于将GDB包装在用户界面中或作为更大程序的一部分,大大扩展了可用选项。以下介绍三种适合调试嵌入式目标的用户界面:
8.1 终端用户界面(TUI)
TUI是标准GDB包的可选部分,其主要特点是有一个代码窗口,显示即将执行的代码行和所有断点,相比命令行模式下的 list 命令有显著改进。TUI无需额外设置即可使用,由于是文本模式,因此可以通过SSH终端会话使用,例如在目标设备上本地运行gdb时。大多数交叉工具链都会将GDB配置为支持TUI,只需在命令行中添加 -tui 即可看到效果。
8.2 数据显示调试器(DDD)
DDD是一个简单的独立程序,为GDB提供了图形用户界面,虽然界面控件看起来有些过时,但功能齐全。可以使用 --debugger 选项指定使用工具链中的GDB,并使用 -x 参数指定GDB命令文件的路径,例如:
$ ddd --debugger arm-poky-linux-gnueabi-gdb -x gdbinit sort-debug
DDD的数据窗口是其一大特色,窗口中的项目以网格形式排列,可以随意重新排列。双击指针会将其展开为新的数据项,并通过箭头显示链接。
8.3 Eclipse C开发工具包(CDT)
Eclipse配合CDT插件支持使用GDB进行调试,包括远程调试。如果平时使用Eclipse进行代码开发,那么它是一个不错的调试工具;但如果不经常使用Eclipse,为调试而专门设置可能不值得。配置CDT与交叉工具链配合使用并连接到远程设备的过程较为复杂,可参考相关资料获取更多信息。CDT的调试界面中,右上角窗口显示进程中每个线程的栈帧,右上角还有监视窗口显示变量,中间是代码窗口,显示调试器停止程序的代码行。
9. 内核代码调试
9.1 内核调试概述
可以使用kgdb进行源代码级别的调试,类似于使用gdbserver进行远程调试。还有一个自托管的内核调试器kdb,适用于一些轻量级任务,如查看指令是否执行以及获取调用栈以了解执行路径。此外,内核Oops消息和崩溃信息可以提供很多关于内核异常原因的信息。
9.2 使用kgdb调试内核代码
使用源代码调试器查看内核代码时,要记住内核是一个复杂的系统,具有实时行为,调试难度比应用程序大。单步执行更改内存映射或切换上下文的代码可能会产生奇怪的结果。
kgdb是内核GDB存根的名称,已经成为主线Linux的一部分多年。内核DocBook中有用户手册,也可以在 https://www.kernel.org/doc/htmldocs/kgdb/index.html 找到在线版本。
在大多数情况下,会通过串行接口连接到kgdb,该接口通常与串行控制台共享,因此这种实现方式称为kgdboc(kgdb over console)。它需要一个支持I/O轮询而不是中断的平台tty驱动,因为kgdb在与GDB通信时必须禁用中断。少数平台支持通过USB使用kgdb,也有通过以太网工作的版本,但遗憾的是,这些都没有进入主线Linux。
内核的优化和栈帧也有一些注意事项,内核代码默认假设优化级别至少为 -O1 ,可以在运行 make 之前设置 KCFLAGS 来覆盖内核编译标志。
以下是内核调试所需的配置选项:
CONFIG_DEBUG_INFO is in the menu Kernel hacking | Compile-time checks and compiler options | Compile the kernel with debug info
CONFIG_FRAME_POINTER may be an option for your architecture, and is in the menu Kernel hacking | Compile-time checks and compiler options | Compile the kernel with frame pointers
CONFIG_KGDB is in the menu Kernel hacking | KGDB: kernel debugger
CONFIG_KGDB_SERIAL_CONSOLE is in the menu Kernel hacking | KGDB: kernel debugger | KGDB: use kgdb over the serial console
除了zImage或uImage压缩内核镜像外,还需要ELF对象格式的内核镜像,以便GDB将符号加载到内存中,这个文件是在Linux构建目录中生成的 vmlinux 。在Yocto中,可以请求将其副本包含在目标镜像和SDK中,它作为名为 kernel-vmlinux 的包构建,可以像安装其他包一样进行安装,例如将其添加到 IMAGE_INSTALL 列表中。该文件会被放置在sysroot的 boot 目录中,例如:
/opt/poky/2.2.1/sysroots/cortexa8hf-neon-poky-linux-gnueabi/boot/vmlinux-4.8.12-yocto-standard
在Buildroot中,可以在 output/build/linux-<version string>/vmlinux 目录中找到 vmlinux 文件。
综上所述,GDB提供了丰富的功能和多种调试方式,无论是应用程序调试还是内核代码调试,都能满足不同的需求。通过合理使用这些工具和技术,可以更高效地定位和解决程序中的问题。
10. 调试流程总结
为了更清晰地展示使用GDB进行调试的整体流程,下面给出一个mermaid格式的流程图:
graph LR
classDef startend fill:#F5EBFF,stroke:#BE8FED,stroke-width:2px
classDef process fill:#E5F6FF,stroke:#73A6FF,stroke-width:2px
classDef decision fill:#FFF6CC,stroke:#FFBC52,stroke-width:2px
A([开始调试]):::startend --> B{选择调试方式}:::decision
B -->|本地调试| C(安装GDB及相关调试信息):::process
B -->|远程调试| D(启动gdbserver):::process
C --> E(设置调试环境):::process
D --> E
E --> F{设置断点?}:::decision
F -->|是| G(设置断点位置):::process
F -->|否| H(直接运行程序):::process
G --> H
H --> I{程序异常?}:::decision
I -->|是| J(分析异常原因):::process
I -->|否| K(正常结束调试):::process
J --> L{是否需要即时调试?}:::decision
L -->|是| M(使用attach功能):::process
L -->|否| N(继续常规调试):::process
M --> N
N --> O(查看调用栈、变量值等):::process
O --> P(单步执行或继续执行):::process
P --> I
这个流程图涵盖了从开始调试到处理异常、进行即时调试等一系列步骤,帮助我们更直观地理解调试的整个过程。
11. 不同调试场景对比
下面通过一个表格来对比不同调试场景下的特点和注意事项:
| 调试场景 | 特点 | 注意事项 |
| — | — | — |
| 本地调试 | 直接在目标设备上运行GDB,无需远程连接,调试环境搭建相对简单 | 需要在目标镜像中安装GDB和未剥离调试信息的可执行文件及源代码 |
| 远程调试 | 通过gdbserver在目标设备上运行程序,在开发主机上使用GDB进行调试,可实现跨设备调试 | 要注意设置正确的sysroot,避免出现共享库符号加载问题;gdbserver对部分调试功能支持有限 |
| 内核调试 | 针对内核代码进行调试,可使用kgdb等工具,调试难度大 | 内核是复杂系统,单步执行某些代码可能产生奇怪结果;需要特定的内核配置选项和ELF格式的内核镜像 |
12. 常见问题及解决方法
在使用GDB进行调试的过程中,可能会遇到一些常见问题,下面列出这些问题及相应的解决方法:
12.1 共享库符号加载问题
- 问题描述 :在调试过程中,出现
warning: Could not load shared library symbols for ...的警告信息。 - 解决方法 :检查是否设置了正确的sysroot,可通过在GDB命令文件或GDB会话中使用
set sysroot命令来设置。例如:
set sysroot /home/chris/buildroot/output/host/usr/arm-buildroot-linux-gnueabi/sysroot
12.2 单步执行出错
- 问题描述 :单步执行时出现
Cannot find bounds of current function的错误信息。 - 解决方法 :这通常是因为程序在汇编代码处暂停,可在
main()函数等C或C++代码处设置断点,然后使用continue命令继续执行。示例如下:
(gdb) break main
(gdb) c
12.3 核心文件无法创建
- 问题描述 :程序崩溃但没有生成核心文件。
- 解决方法 :使用
ulimit -c命令设置核心文件资源限制,若要取消所有限制,可输入:
$ ulimit -c unlimited
同时,可检查核心文件的命名和放置设置,确保有足够的空间和权限来创建核心文件。
13. 总结与建议
13.1 总结
GDB作为一款强大的调试工具,提供了丰富的命令和多种用户界面,适用于不同的调试场景,包括应用程序调试、内核代码调试等。通过合理使用GDB的各种功能,如设置断点、查看调用栈、使用即时调试等,可以更高效地定位和解决程序中的问题。
13.2 建议
- 在开始调试前,确保正确设置调试环境,包括安装GDB、配置sysroot、准备好未剥离调试信息的可执行文件和源代码等。
- 对于复杂的程序,合理设置断点可以减少调试时间,提高调试效率。
- 当遇到问题时,充分利用GDB提供的各种命令,如
backtrace、print等,来分析问题的根源。 - 对于内核调试,要仔细阅读相关文档,正确配置内核选项,避免因配置不当导致调试失败。
总之,掌握GDB的使用方法对于开发人员来说是一项重要的技能,通过不断实践和总结经验,可以更好地发挥GDB的作用,提高程序的质量和稳定性。
超级会员免费看
1371

被折叠的 条评论
为什么被折叠?



