深入探索GDB调试:从基础到高级应用
1. GDB基础与远程调试
当gdbserver检测到来自主机的连接时,会输出如下信息:
Remote debugging from host 192.168.1.1
我们可以从
<data-directory>/python
将Python命令脚本(如
tp.py
)加载到GDB中并使用这些命令,示例如下:
(gdb) source tp.py
(gdb) tp search
这里,
tp
是跟踪点命令的名称,
search
是
bsdiff
中的一个递归函数名。若要查看GDB搜索Python命令脚本的目录,可执行以下命令:
(gdb) show data-directory
GDB对Python的支持还可用于调试Python程序。与标准的Python调试器
pdb
不同,GDB能深入了解CPython的内部机制,甚至可将Python代码注入到正在运行的Python进程中,这为创建强大的调试工具提供了可能,例如Facebook的Python 3内存分析器(https://github.com/facebookincubator/memory-analyzer)。
2. 本地调试
在目标设备上运行本地版本的GDB虽不如远程调试常见,但也是可行的。除了在目标镜像中安装GDB,还需将待调试的可执行文件的未剥离副本及相应的源代码安装到目标镜像中。Yocto Project和Buildroot都支持这一操作。
2.1 Yocto Project
在
conf/local.conf
中添加以下内容,将gdb添加到目标镜像:
EXTRA_IMAGE_FEATURES ?= "tools-debug dbg-pkgs"
若要调试特定包,需要该包的调试信息。Yocto Project会构建包的调试变体,其中包含未剥离的二进制文件和源代码。可通过在
conf/local.conf
中添加
<package name>-dbg
来选择性地将这些调试包添加到目标镜像,或者像上述那样将
dbg-pkgs
添加到
EXTRA_IMAGE_FEATURES
来安装所有调试包,但要注意这会显著增加目标镜像的大小。
源代码会安装在目标镜像的
/usr/src/debug/<package name>
目录下,这意味着GDB无需运行
set substitute-path
即可找到它。若不需要源代码,可在
conf/local.conf
文件中添加以下内容来阻止其安装:
PACKAGE_DEBUG_SPLIT_STYLE = "debug-without-src"
2.2 Buildroot
在Buildroot中,可通过启用以下选项来在目标镜像中安装本地版本的GDB:
-
BR2_PACKAGE_GDB_DEBUGGER
(位于Target packages | Debugging, profiling and benchmark | Full debugger)
要构建带有调试信息的二进制文件并将其安装到目标镜像而不进行剥离,需启用以下第一个选项并禁用第二个选项:
-
BR2_ENABLE_DEBUG
(位于Build options | Build packages with debugging symbols)
-
BR2_STRIP_strip
(位于Build options | Strip target binaries)
本地调试在嵌入式设备上并不常见,因为额外的源代码和调试符号会使目标镜像变得臃肿。
3. 即时调试
有时,程序运行一段时间后会出现异常,此时可使用GDB的
attach
功能进行即时调试,该功能在本地和远程调试会话中均可用。
在远程调试时,需找到待调试进程的PID,并使用
--attach
选项将其传递给gdbserver。例如,若PID为109,可输入以下命令:
# gdbserver --attach :10000 109
Attached; pid = 109
Listening on port 10000
这会使进程暂停,就像遇到断点一样,之后可正常启动交叉GDB并连接到gdbserver。调试完成后,可使用以下命令分离调试器,让程序继续运行:
(gdb) detach
Detaching from program: /home/chris/MELP/helloworld/helloworld, process 109
Ending remote debugging.
4. 调试分叉和线程
当调试的程序发生分叉时,调试会话会跟随父进程还是子进程,这由
follow-fork-mode
控制,其值可以是
parent
或
child
,默认值为
parent
。不过,当前版本(10.1)的gdbserver不支持此选项,因此该功能仅适用于本地调试。若使用gdbserver时确实需要调试子进程,可修改代码,使子进程在分叉后立即对一个变量进行循环,这样就有机会为其附加一个新的gdbserver会话,然后设置该变量使其跳出循环。
在多线程进程中,当一个线程遇到断点时,默认行为是所有线程都会暂停。多数情况下,这有助于查看静态变量而不被其他线程修改。但当恢复线程执行时,即使是单步执行,所有暂停的线程都会启动,这可能会引发问题。可通过
scheduler-locking
参数修改GDB处理暂停线程的方式,该参数默认关闭,若将其设置为
on
,则只有在断点处暂停的线程会恢复执行,其他线程保持暂停,直到将
scheduler-locking
关闭。gdbserver支持此功能。
5. 核心文件
核心文件能捕获程序崩溃时的状态,即使在程序出现问题时不在调试现场,也可通过核心文件获取有价值的信息。当看到
Segmentation fault (core dumped)
时,应深入研究核心文件。
核心文件默认不会创建,只有当进程的核心文件资源限制不为零时才会生成。可使用
ulimit -c
为当前shell更改该限制,若要移除核心文件大小的所有限制,可执行以下命令:
$ ulimit -c unlimited
默认情况下,核心文件名为
core
,并放置在进程的当前工作目录(由
/proc/<PID>/cwd
指向)中,但这种方式存在一些问题。一是当设备上有多个名为
core
的文件时,难以确定每个文件由哪个程序生成;二是进程的当前工作目录可能位于只读文件系统中,可能没有足够空间存储核心文件,或者进程可能没有写入权限。
有两个文件可控制核心文件的命名和放置位置。第一个是
/proc/sys/kernel/core_uses_pid
,向其中写入
1
会将垂死进程的PID号追加到文件名中,只要能从日志文件中将PID号与程序名关联起来,这会有一定帮助。
更有用的是
/proc/sys/kernel/core_pattern
,它能让你对核心文件有更多的控制。默认模式为
core
,但可将其更改为由以下元字符组成的模式:
| 元字符 | 含义 |
| ---- | ---- |
| %p | 进程ID |
| %u | 转储进程的真实用户ID |
| %g | 转储进程的真实组ID |
| %s | 导致转储的信号编号 |
| %t | 转储时间,以自1970年1月1日00:00:00 +0000 (UTC) 以来的秒数表示 |
| %h | 主机名 |
| %e | 可执行文件名 |
| %E | 可执行文件的路径名,斜杠 (/) 替换为感叹号 (!) |
| %c | 转储进程的核心文件大小软资源限制 |
也可使用以绝对目录名开头的模式,将所有核心文件集中到一个地方。例如,以下模式会将所有核心文件放入
/corefiles
目录,并以程序名和崩溃时间命名:
# echo /corefiles/core.%e.%t > /proc/sys/kernel/core_pattern
核心转储后,可在
/corefiles
目录下看到类似以下的文件:
# ls /corefiles
core.sort-debug.1431425613
使用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()
函数被传入了一个空指针。
6. GDB用户界面
GDB可通过GDB机器接口(GDB/MI)进行底层控制,该接口可用于将GDB封装在用户界面中或作为更大程序的一部分,极大地扩展了可用选项的范围。以下介绍三种适合调试嵌入式目标的GDB前端。
6.1 终端用户界面(TUI)
TUI是标准GDB包的可选部分,其主要特点是有一个代码窗口,能显示即将执行的代码行以及所有断点,相比命令行模式下的
list
命令有明显改进。TUI的优势在于无需额外设置即可使用,由于它是文本模式,可通过SSH终端会话使用,例如在目标设备上本地运行gdb时。大多数交叉工具链都会将GDB配置为支持TUI,只需在命令行中添加
-tui
即可。
6.2 数据显示调试器(DDD)
DDD是一个简单的独立程序,能以最小的麻烦为GDB提供图形用户界面,尽管其UI控件看起来有些过时,但功能完备。可使用
--debugger
选项指定DDD使用工具链中的GDB,并使用
-x
参数指定GDB命令文件的路径,示例如下:
$ ddd --debugger arm-poky-linux-gnueabi-gdb -x gdbinit sort-debug
其数据窗口是一个很棒的特性,其中的项以网格形式显示,可随意重新排列。双击指针会将其展开为一个新的数据项,并以箭头显示链接。
7. Visual Studio Code集成GDB调试
Visual Studio Code是微软推出的流行开源代码编辑器,由于它是用TypeScript编写的Electron应用程序,因此比Eclipse等成熟的IDE更轻量级、响应更迅速。通过其庞大用户社区贡献的扩展,它为多种语言提供了丰富的语言支持(如代码补全、跳转到定义等)。可使用CMake和C/C++扩展将远程交叉GDB调试集成到Visual Studio Code中。
7.1 安装Visual Studio Code
在Ubuntu Linux系统上,可使用snap轻松安装Visual Studio Code:
$ sudo snap install --classic code
7.2 安装工具链
使用Yocto为Raspberry Pi 4构建SDK,该SDK将包含针对Raspberry Pi 4的64位Arm核心的工具链。具体步骤如下:
1. 导航到克隆Yocto目录的上一级目录。
2. 加载
build-rpi
构建环境:
$ source poky/oe-init-build-env build-rpi
-
编辑
conf/local.conf,使其包含以下内容:
MACHINE ?= "raspberrypi4-64"
IMAGE_INSTALL_append = " gdbserver"
EXTRA_IMAGE_FEATURES ?= "ssh-server-openssh debug-tweaks"
debug-tweaks
特性可消除对root密码的需求,便于使用
scp
和
ssh
等命令行工具将新构建的二进制文件从主机部署到目标设备。
4. 构建Raspberry Pi 4的开发镜像:
$ bitbake core-image-minimal-dev
-
使用Etcher将
tmp/deploy/images/raspberrypi4-64/core-image-minimal-dev-raspberrypi4-64.wic.bz2镜像写入microSD卡,并在Raspberry Pi 4上启动。 -
将Raspberry Pi 4通过以太网连接到本地网络,使用
arp-scan查找其IP地址,后续配置CMake进行远程调试时会用到该地址。 - 构建SDK:
$ bitbake -c populate_sdk core-image-minimal-dev
需注意,切勿在生产镜像中使用
debug-tweaks
,对于OTA软件更新,自动化的CI/CD管道至关重要,要确保开发镜像不会意外泄露到生产环境。
构建完成后,在
poky/build-rpi/tmp/deploy/sdk
目录下会有一个自解压安装程序
poky-glibc-x86_64-core-image-minimal-dev-aarch64-raspberrypi4-64-toolchain-3.1.5.sh
,可使用该程序在任何Linux开发机器上安装新构建的SDK:
$ ./poky-glibc-x86_64-core-image-minimal-dev-aarch64-raspberrypi4-64-toolchain-3.1.5.sh
每次在新的shell会话中使用SDK时,需加载环境设置脚本:
$ . /opt/poky/3.1.5/environment-setup-aarch64-poky-linux
7.3 安装CMake
在Ubuntu Linux上安装CMake,可执行以下命令:
$ sudo apt update
$ sudo apt install cmake
7.4 创建Visual Studio Code项目
创建一个名为
hellogdb
的Visual Studio Code项目,步骤如下:
$ mkdir hellogdb
$ cd hellogdb
$ mkdir src build
$ code .
最后一个
code .
命令将启动Visual Studio Code并打开
hellogdb
目录,同时会创建一个隐藏的
.vscode
目录,其中包含项目的
settings.json
和
launch.json
文件。
7.5 安装Visual Studio Code扩展
需安装以下Visual Studio Code扩展,以便使用SDK中的工具链进行代码的交叉编译和调试:
- C/C++ by Microsoft
- CMake by twxs
- CMake Tools by Microsoft
可点击Visual Studio Code窗口左侧边缘的扩展图标,在市场中搜索并安装这些扩展。
7.6 配置CMake
为了使用工具链对
hellogdb
项目进行交叉编译,需填充
CMakeLists.txt
和
cross.cmake
文件,具体步骤如下:
1. 将
MELP/Chapter19/hellogdb/CMakeLists.txt
复制到
hellogdb
项目文件夹。
2. 在Visual Studio Code中,点击窗口左上角的资源管理器图标打开资源管理器侧边栏。
3. 点击资源管理器侧边栏中的
CMakeLists.txt
查看文件内容,注意项目名称定义为
HelloGDBProject
,目标板的IP地址硬编码为
192.168.1.128
,将其修改为Raspberry Pi 4的IP地址并保存文件。
4. 展开
src
文件夹,点击资源管理器侧边栏中的新建文件图标,在
hellogdb
项目的
src
目录下创建一个名为
main.c
的文件,粘贴以下代码并保存:
#include <stdio.h>
int main() {
printf("Hello CMake\n");
return 0;
}
-
将
MELP/Chapter19/hellogdb/cross.cmake复制到hellogdb项目文件夹。 -
点击资源管理器侧边栏中的
cross.cmake查看文件内容,注意其中定义的sysroot_target和tools路径指向安装SDK的/opt/poky/3.1.5目录,且CMAKE_C_COMPILE、CMAKE_CXX_COMPILE和CMAKE_CXX_FLAGS变量的值直接来源于SDK附带的环境设置脚本。
7.7 配置项目构建设置
配置
hellogdb
项目的
settings.json
文件,使其使用
CMakeLists.txt
和
cross.cmake
进行构建,步骤如下:
1. 在Visual Studio Code中打开
hellogdb
项目,按下
Ctrl + Shift + P
打开命令面板。
2. 在命令面板中输入
>settings.json
,从选项列表中选择
Preferences: Open Workspace Settings (JSON)
。
3. 编辑
.vscode/settings.json
文件,使其内容如下:
{
"cmake.sourceDirectory": "${workspaceFolder}",
"cmake.configureArgs": [
"-DCMAKE_TOOLCHAIN_FILE=${workspaceFolder}/cross.cmake"
],
"C_Cpp.default.configurationProvider": "ms-vscode.cmake-tools"
}
注意
cmake.configureArgs
中对
cross.cmake
的引用。
4. 再次按下
Ctrl + Shift + P
打开命令面板,输入
>CMake: Delete Cache and Configuration
并执行。
5. 点击Visual Studio Code窗口左侧边缘的CMake图标打开CMake侧边栏。
6. 点击CMake侧边栏中的
HelloGDBProject
二进制文件进行构建。若配置正确,输出面板的内容应如下所示:
[main] Building folder: hellogdb HelloGDBProject
[build] Starting build
[proc] Executing command: /usr/bin/cmake --build /home/frank/hellogdb/build --config Debug --target HelloGDBProject -- -j 14
[build] [100%] Built target HelloGDBProject
[build] Build finished with exit code 0
7.8 配置远程调试启动设置
创建
launch.json
文件,以便将
HelloGDBProject
二进制文件部署到Raspberry Pi 4并在Visual Studio Code中进行远程调试,步骤如下:
1. 点击Visual Studio Code窗口左侧边缘的运行图标打开运行侧边栏。
2. 点击运行侧边栏中的
Create a launch.json file
,选择
C++ (GDB/LLDB)
环境。
3. 提示选择C/C++调试配置类型时,选择
Default Configuration
。
4. 在
.vscode/launch.json
的
"(gdb) Launch"
配置中添加或编辑以下字段:
"program": "${workspaceFolder}/build/HelloGDBProject",
"miDebuggerServerAddress": "192.168.1.128:10000",
"targetArchitecture": "aarch64",
"miDebuggerPath": "/opt/poky/3.1.5/sysroots/x86_64-pokysdk-linux/usr/bin/aarch64-poky-linux/aarch64-poky-linux-gdb"
-
将
miDebuggerServerAddress中的192.168.1.128替换为Raspberry Pi 4的IP地址并保存文件。 -
在
main.c的main()函数体的第一行设置断点。 -
点击运行侧边栏中的
build_and_debug – Utility将HelloGDBProject二进制文件发送到Raspberry Pi 4并使用gdbserver启动。若Raspberry Pi 4和launch.json文件设置正确,输出面板的内容应如下所示:
[main] Building folder: hellogdb build_and_debug
[build] Starting build
[proc] Executing command: /usr/bin/cmake --build /home/frank/hellogdb/build --config Debug --target build_and_debug -- -j 14
[build] [100%] Built target HelloGDBProject
[build] Process ./HelloGDBProject created; pid = 552
[build] Listening on port 10000
点击Visual Studio Code窗口左上角的
(gdb) Launch
按钮,GDB应会命中在
main.c
中设置的断点,输出面板会出现类似以下的行:
[build] Remote debugging from host 192.168.1.69, port 44936
通过以上步骤,我们详细介绍了GDB调试的各个方面,从基础的远程调试、本地调试,到高级的即时调试、线程和分叉调试,以及核心文件的使用,还重点介绍了如何在Visual Studio Code中集成GDB进行远程调试,希望能帮助你更高效地进行程序调试。
深入探索GDB调试:从基础到高级应用
8. 总结与流程梳理
为了更清晰地展示整个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(安装Visual Studio Code):::process
B --> C(安装工具链):::process
C --> D(安装CMake):::process
D --> E(创建Visual Studio Code项目):::process
E --> F(安装扩展):::process
F --> G(配置CMake):::process
G --> H(配置项目构建设置):::process
H --> I(配置远程调试启动设置):::process
I --> J([完成调试准备]):::startend
同时,我们将前面提到的各个操作步骤进行一个汇总,以表格形式呈现,方便大家对比和查阅:
| 操作步骤 | 详细内容 | 命令示例 |
| ---- | ---- | ---- |
| 安装Visual Studio Code | 使用snap在Ubuntu Linux系统上安装 |
sudo snap install --classic code
|
| 安装工具链 | 使用Yocto为Raspberry Pi 4构建SDK,包含一系列环境设置和构建操作 | 1.
source poky/oe-init-build-env build-rpi
2. 编辑
conf/local.conf
3.
bitbake core-image-minimal-dev
4. 写入镜像到SD卡并启动
5. 查找IP地址
6.
bitbake -c populate_sdk core-image-minimal-dev
7. 运行安装程序 |
| 安装CMake | 在Ubuntu Linux上更新并安装 |
sudo apt update
sudo apt install cmake
|
| 创建项目 | 创建
hellogdb
项目目录和子目录,启动VS Code |
mkdir hellogdb
cd hellogdb
mkdir src build
code .
|
| 安装扩展 | 安装C/C++、CMake、CMake Tools扩展 | 在VS Code扩展市场搜索并安装 |
| 配置CMake | 复制文件、修改IP地址、创建
main.c
文件 | 1. 复制
CMakeLists.txt
2. 修改IP地址
3. 创建
main.c
并写入代码
4. 复制
cross.cmake
|
| 配置项目构建设置 | 编辑
settings.json
文件,删除缓存并构建 | 1.
Ctrl + Shift + P
打开命令面板
2. 编辑
settings.json
3. 删除缓存
4. 点击CMake侧边栏构建 |
| 配置远程调试启动设置 | 创建
launch.json
文件,设置参数、断点并启动调试 | 1. 点击运行图标创建
launch.json
2. 选择配置类型
3. 编辑字段并替换IP地址
4. 设置断点
5. 点击
build_and_debug – Utility
6. 点击
(gdb) Launch
|
9. 常见问题及解决方法
在使用GDB进行调试的过程中,可能会遇到一些常见问题,下面为大家列举并给出相应的解决方法:
-
问题1:gdbserver连接失败
-
可能原因
:网络问题、目标设备防火墙限制、PID错误等。
-
解决方法
:检查网络连接是否正常,确保目标设备和主机在同一网络中;检查目标设备的防火墙设置,开放相应的端口;确认PID是否正确。
-
问题2:核心文件未生成
-
可能原因
:核心文件资源限制为零、文件系统权限问题等。
-
解决方法
:使用
ulimit -c unlimited
设置核心文件大小无限制;检查文件系统是否为只读,确保进程有写入权限。
-
问题3:VS Code调试时无法命中断点
-
可能原因
:代码和调试信息不匹配、
launch.json
配置错误等。
-
解决方法
:确保代码和调试信息是最新的,重新构建项目;检查
launch.json
中的参数设置,特别是
program
、
miDebuggerServerAddress
等字段。
10. 高级调试技巧拓展
除了前面介绍的基本调试方法,还有一些高级调试技巧可以进一步提升调试效率。
-
条件断点
:在GDB中,可以设置条件断点,只有当满足特定条件时,断点才会触发。例如,在
main.c
中,我们可以设置当某个变量的值为特定值时才触发断点:
(gdb) break main.c:10 if variable == 10
这样,当程序执行到
main.c
的第10行,并且
variable
的值为10时,程序才会暂停。
-
日志记录
:在调试过程中,可以使用GDB的日志记录功能,将调试信息输出到文件中,方便后续分析。例如:
(gdb) set logging on
(gdb) set logging file debug.log
这样,后续的调试信息都会记录在
debug.log
文件中。
11. 不同场景下的调试策略
根据不同的应用场景,我们可以采用不同的调试策略。
-
嵌入式设备调试
:由于嵌入式设备资源有限,尽量减少不必要的调试信息和代码,避免增加设备负担。可以优先使用远程调试,将大部分调试工作放在主机上进行。
-
多线程程序调试
:对于多线程程序,要特别注意线程之间的同步和竞争问题。可以使用
scheduler-locking
参数来控制线程的执行,避免多个线程同时修改共享资源导致的问题。
-
长时间运行程序调试
:对于长时间运行的程序,即时调试功能就显得尤为重要。可以在程序出现异常时,及时使用
attach
功能进行调试,找出问题所在。
12. 持续学习与实践
GDB调试是一个不断学习和实践的过程。随着技术的不断发展,GDB的功能也在不断更新和完善。建议大家持续关注GDB的官方文档和社区论坛,了解最新的功能和使用技巧。同时,多进行实践,通过实际项目来巩固所学的知识,提高调试能力。
在实际项目中,可能会遇到各种各样的问题,不要害怕失败,每次调试都是一次学习的机会。通过不断地尝试和总结,相信你会逐渐成为一名调试高手,能够快速准确地定位和解决程序中的问题。
希望本文能够为你提供一个全面的GDB调试指南,帮助你在程序开发和调试的道路上更加顺利。如果你在调试过程中遇到任何问题,欢迎在评论区留言,我们一起探讨和解决。
超级会员免费看
13

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



