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文件,别急着打开。先确认几件事:

  1. 检查文件完整性:file core.12345,看是不是ELF格式
  2. 确认二进制版本:strings core.12345 | grep "GCC:",看编译信息是否匹配
  3. 加载分析: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 ./program gdb → file加载 gdb -p PID attach PID pgrep + gdb -p gdb prog core gdb → core加载 bt / info locals 退出方式 quit / q detach + quit kill + quit 核心原则:根据场景选择启动方式,退出时注意进程状态

好了,GDB的启动与退出就聊到这儿。记住一句话:启动方式选对,调试事半功倍;退出姿势正确,线上服务不崩。下一节咱们聊聊GDB的常用命令,那才是真正开始「动手」的时候。

课后小练习:找个正在运行的进程(比如你的浏览器),用gdb -p附加进去,看看bt能打出什么。然后记得detach再退出。别把浏览器搞崩了!

可可精品资料库