3. GDB启动与退出:启动GDB的多种方式、附加到运行进程、核心转储文件分析
说实话,GDB的启动方式,我见过太多人只会一种——gdb ./a.out。然后呢?遇到线上崩溃,手足无措。今天咱们就把GDB怎么「进门」这件事聊透。
3.1 启动GDB的三种姿势
我个人习惯把启动方式分成三类,你想想看,每种场景对应不同的需求:
方式一:直接启动,加载可执行文件
这是最基础的方式。我刚开始学GDB时就这么用:
gdb ./my_program
# 或者带参数
gdb --args ./my_program --config=prod.conf --port=8080
进了GDB后,你可以用set args重新设置参数。嗯,这里要注意:--args后面的所有内容都会被当作程序参数,包括路径中的空格。
方式二:先启动GDB,再加载文件
有时候你忘了带参数,或者想先看看GDB版本。那就:
gdb
(gdb) file ./my_program
(gdb) set args --config=prod.conf
我在项目中遇到过一种情况——调试脚本里写死了gdb ./a.out,结果换了机器,可执行文件路径变了。后来我改成先gdb再file加载,灵活多了。
方式三:调试core dump文件
这个我重点说说。线上程序崩溃了,你不可能让用户等你attach上去。这时候core dump就是救命稻草:
gdb ./my_program core.12345
或者先启动再加载:
gdb
(gdb) file ./my_program
(gdb) core core.12345
核心要点:调试core dump时,可执行文件必须和生成core时是同一个版本。我踩过这个坑——用新编译的二进制去分析旧core,结果堆栈全是乱的。
3.2 附加到运行中的进程
这是线上调试的杀手锏。说白了,就是GDB「钻进」一个已经在跑的进程里。
按PID附加
gdb -p 12345
# 或者
gdb
(gdb) attach 12345
你可能会问:PID怎么查?ps aux | grep my_program或者pidof my_program都行。
按进程名附加
GDB 7.0之后支持了更人性化的方式:
gdb -p $(pgrep my_program)
我个人习惯写个脚本,因为线上可能有多个同名进程:
#!/bin/bash
PID=$(pgrep -o my_program) # 最旧的那个
if [ -n "$PID" ]; then
gdb -p $PID
else
echo "进程没找到"
fi
注意:附加到进程后,进程会被暂停。你操作完记得用detach让它继续跑,别傻乎乎地quit——那会把进程也杀了。
附加时的权限问题
我曾经在线上遇到过:明明进程在跑,但attach就是失败。为什么?权限不够。Linux默认只允许同用户或root附加。解决方案:
# 临时允许
echo 0 | sudo tee /proc/sys/kernel/yama/ptrace_scope
# 永久修改(/etc/sysctl.d/99-ptrace.conf)
kernel.yama.ptrace_scope = 0
3.3 核心转储文件分析
core dump这东西,说白了就是进程死前的一瞬间,把内存、寄存器、堆栈信息全部拍了个快照。你拿着这个快照,就能还原案发现场。
生成core dump的配置
很多系统默认不生成core文件,你得先打开:
# 查看当前限制
ulimit -c
# 设置为无限制
ulimit -c unlimited
# 永久生效(写入~/.bashrc)
echo "ulimit -c unlimited" >> ~/.bashrc
core文件的命名和位置也很重要。我建议这样配置:
# 让core文件带上进程名和PID
echo "core.%e.%p" | sudo tee /proc/sys/kernel/core_pattern
# 或者存到指定目录
echo "/var/crash/core.%e.%p" | sudo tee /proc/sys/kernel/core_pattern
分析core dump的实战步骤
假设你拿到了一个core文件,别急着打开。先确认几件事:
- 检查文件完整性:
file core.12345,看是不是ELF格式 - 确认二进制版本:
strings core.12345 | grep "GCC:",看编译信息是否匹配 - 加载分析:
gdb ./my_program core.12345
进去之后,我一般先看这三样:
(gdb) bt # 看堆栈,找到崩溃位置
(gdb) info locals # 看局部变量,还原现场
(gdb) frame N # 切换到第N层栈帧,深入分析
我的经验:有时候bt显示的问号(??)特别多,别慌。这通常是因为共享库没加载。用info sharedlibrary看看哪些库没找到,然后set solib-search-path指定路径。
一个真实案例
我曾经分析过一个core dump,bt显示在memcpy里崩溃。乍一看以为是内存越界,但仔细看info registers发现RDI寄存器指向了一个奇怪的地址。后来用x/10gx $rdi查看内存内容,发现是个已经被释放的堆块。嗯,这才是根因——use-after-free。
3.4 退出GDB的正确姿势
退出GDB很简单,但不同场景有不同讲究:
| 命令 | 场景 | 说明 |
|---|---|---|
quit 或 q |
普通退出 | 如果附加了进程,会先detach再退出 |
detach + quit |
附加进程后退出 | 先让进程继续跑,再退出GDB |
kill + quit |
调试子进程时 | 杀掉被调试进程,再退出 |
千万别这么干:调试线上服务时,直接quit。GDB会先发SIGKILL给被调试进程。你想想看,线上服务就这么被你杀了……我曾经见过新手犯这个错,后果很严重。
3.5 本章知识体系
下面这张图,把GDB启动与退出的核心逻辑串起来了。你保存下来,以后遇到相关场景,扫一眼就知道该用哪种方式:
好了,GDB的启动与退出就聊到这儿。记住一句话:启动方式选对,调试事半功倍;退出姿势正确,线上服务不崩。下一节咱们聊聊GDB的常用命令,那才是真正开始「动手」的时候。
课后小练习:找个正在运行的进程(比如你的浏览器),用gdb -p附加进去,看看bt能打出什么。然后记得detach再退出。别把浏览器搞崩了!
可可精品资料库