GDB调试用法

GDB(GNU Debugger)是Linux/Unix平台上最通用的交互式进程调试器,覆盖C/C++/Rust/Go/Java等。在运维和内核排查中,它是看进程到底卡在哪的终极工具。


1.GDB在工具链中的定位

层次            工具                 解决什么问题
──────────────  ────────────────────  ─────────────────────────────────
静态分析        gcc -Wall -O0        编译期抓错
内存检查        valgrind             悬垂指针/内存泄漏
性能剖析        perf / valgrind-callgrind  热点函数
交互式调试      GDB                 运行时逐指令/变量/堆栈
崩溃分析        GDB + core dump      事后还原崩溃现场
多线程调试      GDB + 信号           数据竞争/死锁/竞态
内核调试        GDB + kgdb           内核panic/驱动bug
远程调试        GDB + gdbserver      目标机没有GDB(嵌入式/ARM)

GDB = “把进程冻住, 让你看到任意时刻的任意变量/堆栈/内存


2.启动GDB的三种姿势

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 1. 直接启动(进程还没跑)
gdb ./nginx
(gdb) run # 内部启动
(gdb) start # 等价于 run + 断在主函数

# 2. attach到运行中进程(★运维最常用)
gdb -p 12345 # 把PID 12345冻住
gdb -p 12345 -batch -ex "thread apply all bt" # 非交互抓全线程堆栈

# 3. 分析core dump(事后)
ulimit -c unlimited # 允许生成core
gdb ./nginx /path/core.12345
# 或:
coredumpctl gdb nginx # systemd系统用这个

# 4. 远程调试(嵌入式/ARM目标机)
gdb --remote=192.168.1.100:1234 # 目标机跑 gdbserver 12345 ./app

3.编译时配合GDB

1
2
3
4
5
6
7
8
9
# ★ 必须带 -g, 否则GDB看不到变量/行号
gcc -g -O0 -o app app.c
# -g: 生成DWARF调试信息
# -O0: 不优化(优化后变量可能被寄存器化/消除, GDB显示混乱)
# 生产调试折中: -O1 -g (保留行号但有一定优化)

# CMake项目:
cmake -DCMAKE_BUILD_TYPE=Debug .. # 自动加 -g -O0
cmake -DCMAKE_BUILD_TYPE=RelWithDebInfo .. # -O2 -g (生产+调试)

4.核心命令速查表(按使用频率排序)

命令                  作用                         备注
────────────────────  ────────────────────────────  ──────────────────────────
run / r               启动/继续                     带参数: run -- -e flag
break / b 函数名       下断点(函数入口)              b main / b 123(行号)
break / b 文件:行      下断点(指定文件行)            b nginx.c:456
break / b 条件         条件断点                      b read fd==3
watch / rwatch        监视变量改变(写/读+写)        watch count
info breakpoints / i b  查看所有断点
delete / d 编号       删除断点
continue / c           继续运行到下一断点
next / n               单步(不进入函数)
step / s               单步(进入函数)
finish                 跑到当前函数返回
backtrace / bt         打印当前调用堆栈
frame N / f N          切到第N帧(看那个帧的局部变量)
print / p 表达式       打印变量/表达式
p *ptr                 解引用指针
p (struct T*)0x1000   按类型解释内存
print /x addr         十六进��显示
list / l               显示当前源码(前后10行)
info locals            当前帧所有局部变量
info args              当前帧参数
info registers / i r   所有CPU寄存器
info threads           所有线程
thread N               切到线程N
info proc mappings     进程内存映射(=cat /proc/pid/maps)
x/Nc addr             反汇编查看(addr)
x/10i $pc             当前PC位置反汇编10条
x/4xw 0x7fff...       4个word(32bit)十六进制
disassemble / disas    反汇编当前函数
attach / a PID        附加到进程
detach / d            脱离进程(进程继续跑)
kill / k               杀掉被调试进程
quit / q              退出GDB
help cmd              查某命令帮助

5.实战场景

场景1:进程卡住/响应慢/抓堆栈

1
2
3
4
5
6
7
8
9
10
11
# 不杀进程, 只抓一次全线程堆栈后继续
gdb -p 12345 -batch -ex "thread apply all bt full" 2>&1 | tee /tmp/bt.txt

# 输出示例:
# Thread 23 (Thread 0x7f3a...):
# #0 0x00007f3a... in futex_wait_cancelable ()
# #1 0x00007f3a... in pthread_cond_wait ()
# #2 0x000055... in NIOSSL_thread_acquire () at ssl.c:1234
# ...

# 分析: 23个线程都卡在pthread_cond_wait → 上游没数据/锁没释放

生产环境注意:

1
2
3
4
5
# gdb attach会STW(Stop The World)冻结进程几百ms~几秒
# 生产服务: 短暂卡顿可接受; 不能接受的用 perf record(无STW)
# 或用 eu-stack (elfutils, 轻量, 只抓一次堆栈)
eu-stack -c 12345
eu-stack -f pthread -c 12345 # 抓所有线程

场景2:SIGSEGV/core dump分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 1. 让进程能生成core
ulimit -c unlimited
echo "/tmp/core.%e.%p" > /proc/sys/kernel/core_pattern # 或 /etc/systemd/coredump.conf

# 2. 程序崩溃后, 找到core文件
ls -la /tmp/core.*
# 或 systemd系统:
coredumpctl list
coredumpctl info nginx 12345 # 自动找到core并显示

# 3. GDB分析
gdb ./nginx /tmp/core.nginx.12345
(gdb) bt full # 全堆栈+所有局部变量
(gdb) frame 5 # 切到第5帧
(gdb) p *ptr # 看崩溃时指针值
(gdb) p *req # 看请求结构体内容
(gdb) x/16xw $rsp # 看栈顶16个word(找参数)

典型崩溃堆栈解读:

#0  0x000055... in ngx_http_handler () at http.c:892
    r = 0x7f3a...
    r->method = 2
#1  0x000055... in ngx_http_do_run () at http.c:2001
#2  0x000055... in ngx_epoll_process_events () at events.c:845
#3  0x000055... in main () at main.c:380

→ 第892行: r->method = 2(GET) 但 r 本身可能是悬垂指针
→ p r 看地址是否在合法范围
→ info proc mappings 确认堆/栈/so段范围

场景3:死锁检测

1
2
3
4
5
6
7
8
9
10
11
gdb -p 12345 -batch -ex "thread apply all bt" 2>&1 | grep -A3 "pthread_mutex_lock\|__lll_lock"
# 或交互式:
(gdb) thread apply all bt
# 看两个线程:
# Thread 5: pthread_mutex_lock(mutexA) → waiting
# Thread 7: pthread_mutex_lock(mutexB) → waiting
# 而 Thread 5 持 mutexB, Thread 7 持 mutexA → 经典AB-BA死锁

# 用GDB条件断点辅助定位:
(gdb) break pthread_mutex_lock if __gthread_mutexkind == PTHREAD_MUTEX_RECURSIVE
(gdb) run

场景4:修改运行时变量(热修)

1
2
3
4
5
6
7
8
9
10
# 不重启进程, 临时改一个计数器/开关
gdb -p 12345
(gdb) p some_global_flag
$1 = 0
(gdb) set some_global_flag = 1 # 直接改
(gdb) c # 继续, 进程带着新值跑
(gdb) detach

# 危险: 改了结构体大小/指针域可能导致后续segfault
# 适用: 改bool/int枚举等无副作用的字段

场景5:跟踪系统调用

1
2
3
4
5
6
7
8
9
10
11
12
13
# GDB跟踪(比strace更"深"——能看到调用链)
(gdb) break read
(gdb) commands
> silent
> print fd, buf, count
> continue
> end
(gdb) run
# 每次read都被断, 打印参数, 自动继续(不中断流程)

# 或直接用条件断点:
(gdb) break write if fd==3 && count > 65536
# 只在写fd=3且buffer>64K时停

6.GDB脚本/批量操作(自动化)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 单条命令(免交互):
gdb -batch -ex "bt" -ex "info locals" -ex "quit" ./app

# 脚本文件(gdb.txt):
# 内容:
# break main
# run
# print argc
# print argv
# info registers
# quit

gdb -x gdb.txt ./app

# CI/CD中自动抓core:
gdb -batch \
-ex "handle SIGSEGV stop print" \
-ex "bt 20" \
-ex "info registers rdi rsi rdx rcx" \
./app /path/core.12345 2>&1 | tee crash.log

7.DWARF调试信息与strip的关系

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 生产二进制通常strip过(去掉调试信息, 体积减半):
file /usr/sbin/nginx
# /usr/sbin/nginx: ELF 64-bit, stripped

# strip后GDB只能看函数名(动态符号), 看不到局部变量/行号
# 解决:
# 1. 保留调试符号包(Debian: debuginfod / RHEL: *-debuginfo包)
sudo apt install nginx-common-dbgsym # Debian/Ubuntu
sudo dnf install nginx-debuginfo # RHEL/CentOS

# 2. 或编译时保留 -g, 部署时再strip(符号放单独文件)
gcc -g -o app app.c
objcopy --only-keep-debug app app.debug # 提取调试段
strip app # 去掉
# 之后GDB: put app.debug next to app, GDB自动找

# 3. 内核调试:
sudo apt install linux-image-$(uname -r)-dbgsym
# 或自建符号链接:
gdb /usr/lib/debug/boot/vmlinux-$(uname -r)

8.多线程调试要点

# 多线程GDB的核心命令:
info threads              # 列出所有线程
thread 3                  # 切到线程3
thread apply all bt       # 所有线程都打印堆栈
thread apply 1,3,5 bt    # 指定线程
schedule-lock 3           # 冻结线程3(多核CPU场景)
non-stop                  # 非停止模式(只有命中线程停, 其他继续)
step / c 只影响当前线程

坑:
• attach多线程进程时, GDB会STW所有线程(生产慎用)
• 多线程下 bt 默认只打印当前线程, 要加 thread apply all
• race condition难以复现 → 用 -g -O0 编译 + GDB + 条件断点
或直接用 thread-sanitizer(gcc -fsanitize=thread), 不靠GDB


9.与strace/ltrace的关系

工具        看什么                侵入性     性能损失     适合
──────────  ────────────────────  ─────────  ────────────  ──────────────────
GDB         用户态: 变量/堆栈/指令  高(STW)   高            定位bug/逻辑错
strace      系统调用+参数+返回值    中         中(慢2~10x)  看进程做了什么syscall
ltrace      库函数调用              中         中            看libc/glibc调用链
perf        采样/计数(内核+用户)    低         低(<2%)      性能热点
valgrind    内存错误(插桩)          高         非常高(20x+)  内存泄漏/越界
eu-stack    一次性抓堆栈            中         低(几百ms)   生产轻量诊断

实战组合拳:

1. 服务慢/卡 → eu-stack / GDB抓堆栈(轻量) → 定位到函数
2. 需要看变量值 → GDB attach + 条件断点
3. 怀疑系统调用 → strace -p PID -e trace=read,write,open
4. 怀疑内存 → valgrind --tool=memcheck ./app(不能跑生产)
5. 崩溃 → core + GDB bt(事后)

10.辅助工具/插件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# GDB内置Python(可以写GDB脚本):
(gdb) python
> import gdb
> print(gdb.selected_frame().name())
> print(gdb.selected_inferior().read_register("rip"))
> exit
(gdb) end

# pwndbg(逆向/安全场景):
pip install pwndbg
# 增强: ctx命令(一键显示寄存器+堆栈+内存)

# 内核调试(kgdb):
# 1. 内核编译: make KCONFIG=... kgdb_debug
# 2. 触发: echo g > /proc/ksyms (或breakpoint + panic)
# 3. 宿主: gdb vmlinux → target remote /dev/ttyUSB0
# 用于: 内核panic分析/驱动开发/内核模块调试

11.施场景结合

场景                              GDB用法
────────────────────────────────  ───────────────────────────────────────
Nginx worker hang              gdb -p <worker_pid> -batch -ex "thread apply all bt"
Redis 内存异常                 gdb -p <redis_pid> -ex "print rax" -ex "detach"
                              (或直接用 redis-cli INFO memory)
Java进程(Tomcat)              gdb -p <pid> 看JNI native crash
                              更常用: jstack -l <pid> (JVM自带堆栈)
                              或: jcmd <pid> Thread.print
内核panic                    crash 工具(Linux Kernel Debuggers) 而非GDB
                              或: GDB + kgdb串口
嵌入式(ARM板)               宿主机: gdb multi-app.elf
                              目标机: gdbserver :1234 multi-app
                              或: JTAG + OpenOCD + GDB
容器内调试                   docker run -it --cap-add=SYS_PTRACE image \
                              gdb -p 1 ./app
                              (需要SYS_PTRACE capability, 默认容器没有)

12.速查

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
# 编译
gcc -g -O0 -o app app.c

# 启动调试
gdb ./app
(gdb) break main
(gdb) run

# 看进程
gdb -p 12345 -batch -ex "thread apply all bt full"

# core分析
gdb ./nginx /tmp/core.nginx.999
(gdb) bt full
(gdb) frame 3
(gdb) p *ptr
(gdb) x/4xw 0x7fff...

# 条件断点
(gdb) break read if fd==3
(gdb) break nginx.c:456 if r->method==2

# 单步
(gdb) n # 下一行(不进函数)
(gdb) s # 下一行(进函数)
(gdb) f # 跑到当前函数返回
(gdb) c # 继续到下一断点

# 看变量
(gdb) p x
(gdb) p *s->name
(gdb) x/16xw $rsp # 看栈
(gdb) p/s "hello" # 字符串打印

# 辅助
(gdb) info registers
(gdb) info proc mappings
(gdb) disassemble $pc-16, $pc+16