4.6.3. 使用 GDB 调试应用
4.6.3.1. GDB 概述
GDB(GNU Debugger)是一个强大的开源调试工具,用于调试 C、C++、Fortran 等程序。它允许开发者在程序运行时暂停,查看变量的值、检查程序的控制流、修复错误等。GDB 是开源软件,广泛应用于 Linux 和其他类 Unix 操作系统,且与 GCC 编译器紧密集成。
GDB 主要用于调试用户空间程序。它最初是由 Richard Stallman 和 The GNU Project 开发的,旨在为开源软件提供强大且免费的调试工具。下面是 GDB 的发展历史:
1986年:GDB 的第一个版本发布。它是由 Stallman 在其工作的早期阶段开发的,主要是为了调试 GNU 项目的软件,尤其是 GCC(GNU 编译器)编译的程序。
1990年代:随着开源软件和 GNU 项目的发展,GDB 被广泛应用到各种开发项目中。它支持多种编程语言(如 C、C++、Fortran 等),并逐步支持更多的操作系统和平台。
2000年代:GDB 增加了更多功能,如多线程调试、远程调试、与硬件相关的调试(例如使用 JTAG 接口)等。
2010年代至今:GDB 继续得到活跃维护,增加了对现代硬件架构的支持(如 ARM、RISC-V 等),并进一步扩展了远程调试和嵌入式系统调试的功能。GDB 的功能仍在不断扩展,逐步成为世界上最强大的调试工具之一。
4.6.3.2. GDB 的主要功能
GDB 的主要功能有如下几个方面:
程序执行控制
可以启动程序并控制其执行(单步执行、跳过、暂停等)。
支持在程序的不同位置(例如:行号或函数)设置断点。
变量检查
可以在程序运行时检查和修改变量的值,帮助开发者追踪程序状态。
堆栈跟踪
当程序崩溃或发生异常时,GDB 可以显示程序的调用堆栈(backtrace),帮助开发者定位问题源。
动态调试
支持动态加载库和函数,可以在程序运行时对其进行调试。
源代码级调试
可以通过源代码(如 C 或 C++ 的源代码)进行调试,显示变量、函数调用和执行位置。
多平台支持
支持多种架构(如 x86、ARM 等),以及跨平台调试。
4.6.3.3. 常用的 GDB 调试命令
常用的 GDB 调试命令汇总如下:
| 命令 | 说明 |
|---|---|
l (list) |
显示当前代码行的上下文,每次显示 10 行代码。可以指定行号或函数名。 |
r (run) |
运行程序。没有断点时直接运行,有断点时从第一个断点处开始运行。 |
b (breakpoint) <行号> |
在指定的行号处设置断点。 |
b <源文件>:<函数名> |
在指定源文件中的函数首行设置断点。 |
b <源文件>:<行号> |
在指定源文件中的某一行设置断点。 |
info b |
查看当前断点的信息,包括命中次数等。 |
d (delete) <断点编号> |
删除指定的断点,不能使用行号删除断点。若当前没有跳出过gdb,则断点的编号会持续累加。 |
d breakpoints |
删除所有断点。 |
disable b (breakpoints) |
禁用所有断点。 |
enable b (breakpoints) |
启用所有断点。 |
disable b (breakpoint) <编号> |
禁用指定编号的断点。 |
enable b (breakpoint) <编号> |
启用指定编号的断点。 |
enable breakpoint |
启用指定断点,使其生效。 |
n (next) |
逐过程执行,跳过函数内部。 |
s (step) |
逐语句执行,进入函数内部执行。 |
bt (backtrace) |
显示当前调用堆栈,查看函数调用的过程。 |
set var |
修改变量的值。 |
p (print) <变量名> |
打印指定变量的值。 |
display <变量名> |
跟踪并显示某个变量的值,每次停下来时都会显示该变量的当前值。 |
undisplay <变量名> |
取消先前设置的变量跟踪。 |
until <行号> |
跳转至指定行号,执行完指定区间代码后停下来。 |
finish |
执行当前函数至返回,然后停在函数调用处。 |
c (continue) |
从当前断点继续执行,直到下一个断点处停止。 |
注: 括号里面是该指令的全称。
GDB 详细说明可以参考官方文档 GDB: The GNU Project Debugger。
4.6.3.4. GDB 调试的具体方法介绍
编写测试程序
在 PC 端编写应用程序,交叉编译时加 -g 选项编译为可调试二进制文件:
示例 demo.c:
#include <stdio.h>
int main()
{
int a = 0;
char i = 0;
for(i =0; i <10; i++)
{
a = i + 1;
int b = 10/a;
printf("b = %d\n",b);
}
return 0;
}
编译
注意,编译的时候需要指定编译工具链为 X5 BSP 所使用的 opt/arm-gnu-toolchain-11.3.rel1-x86_64-aarch64-none-linux-gnu/bin/aarch64-none-linux-gnu-gcc,为了方便调用,建议在 .bashrc 中创建别名:
alias arm_gcc='/opt/arm-gnu-toolchain-11.3.rel1-x86_64-aarch64-none-linux-gnu/bin/aarch64-none-linux-gnu-gcc'
alias arm_gdb='/opt/arm-gnu-toolchain-11.3.rel1-x86_64-aarch64-none-linux-gnu/bin/aarch64-none-linux-gnu-gdb'
具体编译过程如下:
$ arm_gcc -g demo.c -o gdb_demo
$ ls
demo.c gdb_demo
$ file gdb_demo
gdb_demo: ELF 64-bit LSB executable, ARM aarch64, version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux-aarch64.so.1,
for GNU/Linux 3.7.0, with debug_info, not stripped
最终生成了所需要的 gdb_demo。
GDB 调试过程
将 gdb_demo 、demo.c 传到单板上(如 /userdata 目录),然后就可以进行 gdb 调试:
root@buildroot:/userdata# chmod +x gdb_demo
root@buildroot:/userdata# gdb gdb_demo
GNU gdb (GDB) 13.2
Copyright (C) 2023 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.
Type "show copying" and "show warranty" for details.
This GDB was configured as "aarch64-buildroot-linux-gnu".
Type "show configuration" for configuration details.
For bug reporting instructions, please see:
<https://www.gnu.org/software/gdb/bugs/>.
Find the GDB manual and other documentation resources online at:
<http://www.gnu.org/software/gdb/documentation/>.
For help, type "help".
Type "apropos word" to search for commands related to "word"...
Reading symbols from gdb_demo...
# 从 line 1 开始显示 demo.c 的 10 行代码
(gdb) l 1
warning: Source file is more recent than executable.
1 #include <stdio.h>
2
3 int main()
4 {
5 int a = 0;
6 char i = 0;
7 for(i =0; i <10; i++)
8 {
9 a = i + 1;
10 int b = 10/a;
# 运行代码直到遇到断点
(gdb) r
Starting program: /userdata/gdb_demo
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/libthread_db.so.1".
b = 10
b = 5
b = 3
b = 2
b = 2
b = 1
b = 1
b = 1
b = 1
b = 1
[Inferior 1 (process 2581) exited normally]
# 在 demo.c 的第 10 行设置断点
(gdb) b 10
Breakpoint 1 at 0x4005e8: file demo.c, line 10.
# 查看当前设置的断点
(gdb) info b
Num Type Disp Enb Address What
1 breakpoint keep y 0x00000000004005e8 in main at demo.c:10
(gdb) r
Starting program: /userdata/gdb_demo
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/libthread_db.so.1".
Breakpoint 1, main () at demo.c:10
10 int b = 10/a;
# 监控变量 'b' 的变化
(gdb) display b
1: b = 65535
# 继续运行
(gdb) c
Continuing.
b = 10
Breakpoint 1, main () at demo.c:10
10 int b = 10/a;
1: b = 10
(gdb) c
Continuing.
b = 5
Breakpoint 1, main () at demo.c:10
10 int b = 10/a;
1: b = 5
(gdb) c
Continuing.
b = 3
Breakpoint 1, main () at demo.c:10
10 int b = 10/a;
1: b = 3
# 退出 gdb 调试
(gdb) q
gdb 分析 Core Dump 文件
通过 gdb,我们还可以有效地分析程序崩溃时的状态,快速定位问题并进行修复。
编写一个测例模拟 crash:
#include <stdio.h>
int main() {
printf("Program will now crash due to null pointer dereferencing.\n");
// 创建一个空指针
int *ptr = NULL;
// 尝试解引用空指针
*ptr = 10; // 空指针解引用会导致崩溃
return 0;
}
编译:
arm_gcc -g crash_example.c -o crash_example
其中 arm_gcc 是在 .bashrc 中创建的编译工具的别名:
# ~/.bashrc
alias arm_gcc='/opt/arm-gnu-toolchain-11.3.rel1-x86_64-aarch64-none-linux-gnu/bin/aarch64-none-linux-gnu-gcc'
将编译产物传入单板中,进行测试:
1) 执行文件,触发 crash
root@buildroot:/userdata# ./crash_example
Program will now crash due to null pointer dereferencing.
Segmentation fault (core dumped)
2)取出生成的 Core dump 文件
X5设备中生成的 Core dump 文件存放在 /userdata/log/coredump 目录中,将其复制到 /userdata 目录进行分析:
root@buildroot:/userdata/log/coredump# ls
core-crash_example-2685-36246
root@buildroot:/userdata/log/coredump# cp core-crash_example-2685-36246 ../../
3)运行 gdb 分析 Core dump 文件
# 开始 gdb 分析
root@buildroot:/userdata# gdb crash_example core-crash_example-2685-36246
GNU gdb (GDB) 13.2
Copyright (C) 2023 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.
Type "show copying" and "show warranty" for details.
This GDB was configured as "aarch64-buildroot-linux-gnu".
Type "show configuration" for configuration details.
For bug reporting instructions, please see:
<https://www.gnu.org/software/gdb/bugs/>.
Find the GDB manual and other documentation resources online at:
<http://www.gnu.org/software/gdb/documentation/>.
For help, type "help".
Type "apropos word" to search for commands related to "word"...
Reading symbols from crash_example...
[New LWP 2685]
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/libthread_db.so.1".
Core was generated by `./crash_example'.
Program terminated with signal SIGSEGV, Segmentation fault.
#0 0x00000000004005e4 in main () at crash_example.c:11
11 *ptr = 10; // 空指针解引用会导致崩溃
# 显示报错行附件的代码
(gdb) l 10
5 printf("Program will now crash due to null pointer dereferencing.\n");
6
7 // 创建一个空指针
8 int *ptr = NULL;
9
10 // 尝试解引用空指针
11 *ptr = 10; // 空指针解引用会导致崩溃
12
13 return 0;
14 }
# 显示当前调用堆栈
(gdb) bt
#0 0x00000000004005e4 in main () at crash_example.c:11
# 显示当前所有变量值
(gdb) info locals
ptr = 0x0
4.6.3.5. 常见问题
GDB 调试过程中一般常见以下问题:
| 问题 | 描述 | 可能原因 | 解决方案 |
|---|---|---|---|
| 无法启动 GDB 或连接到目标程序 | 启动 GDB 时,出现启动失败或无法连接到正在调试的进程 | GDB 与目标程序架构不匹配,缺少足够权限,目标程序没有符号信息 | 确保 GDB 与目标架构匹配,使用 sudo 权限运行 GDB,确保编译时使用 -g 生成调试符号 |
| 调试符号缺失 | GDB 提示找不到符号,无法查看变量值或栈信息 | 编译时未包含调试符号,优化级别过高,导致无法映射源代码和机器代码 | 确保编译时使用 -g 选项生成调试符号,使用较低优化级别(如 -O0)编译 |
| GDB 无法加载目标程序的符号 | 调试器无法加载目标程序的符号,导致无法查看变量或栈信息 | GDB 找不到正确的符号文件,或者符号文件与程序版本不匹配 | 手动加载符号文件,使用 set solib-search-path 指定共享库符号文件的路径 |
| 调试多线程程序时无法正确切换线程 | 在多线程程序中,GDB 无法正确识别或切换线程 | GDB 与多线程程序的同步问题,未启用多线程调试支持,目标程序未正确链接线程库 | 使用 info threads 查看线程信息,使用 thread <id> 切换线程,确保编译时启用了多线程支持 |
| 断点不生效或被跳过 | 设置的断点不生效,或者程序执行时跳过断点 | 断点设置位置无效,代码优化导致目标行未执行,动态库符号信息丢失 | 使用 info breakpoints 查看断点状态,重新编译时使用较低的优化级别,确保加载共享库符号 |
| 在调试内核或裸机程序时遇到问题 | 使用 GDB 调试内核或裸机程序时,出现连接问题或调试器无法加载符号 | 内核或裸机程序缺少调试符号,裸机程序中 GDB 与目标程序的连接不稳定 | 在编译时启用调试符号,使用适当的远程调试方法(如串口、JTAG)连接目标 |
| GDB 与目标程序版本不匹配 | 在调试过程中,GDB 提示符号文件不匹配或无法找到符号 | GDB 使用的符号文件与目标程序的版本不一致 | 确保 GDB 使用正确的符号文件,与目标程序版本匹配 |
| 远程调试时网络连接问题 | 使用 GDB 进行远程调试时,连接不稳定或无法建立连接 | 网络配置错误、防火墙阻止调试端口,目标系统调试代理未正确启动 | 检查网络配置,确保防火墙未阻止调试端口,确保 GDBserver 正确启动并监听正确的端口 |
| 内存泄漏或数据不一致 | 调试过程中出现内存泄漏或数据不一致的错误 | 代码中存在未初始化的变量、越界访问,编译优化或调试符号丢失导致变量值无法追踪 | 使用 valgrind 检查内存泄漏,确保编译时使用调试选项,避免高优化级别 |
4.6.3.6. 参考文献
GDB: The GNU Project Debugger gdb Debugging Full Example (Tutorial): ncurses