gdb调试
编译参数
编译时 (-L.):你告诉连接器(Linker)去当前目录找库。
运行时 (Runtime):当你运行 ./main 时,操作系统的动态链接器(ld.so)接管了工作。出于安全和规范考虑,Linux 默认不会在当前目录(.)查找动态库,它只去 /lib, /usr/lib 或配置文件 /etc/ld.so.conf 指定的路径找。
# 1. 编译动态库 libmylib.so
# -g: 生成调试信息 (DWARF)
# -shared -fPIC: 生成位置无关的动态库
g++ -g -O0 -shared -fPIC -o libmylib.so mylib.cpp
# 2. 编译主程序 main
# -L. -lmylib: 链接当前目录下的 mylib 库
# -Wl,-rpath=. : 运行时将当前目录加入库搜索路径
g++ -g -O0 -o main main.cpp -L. -lmylib -Wl,-rpath=.
echo "Compilation finished."
若不使用 -Wl,-rpath 的方式来指定库目录,还可以用以下两种方式:
方法一:使用环境变量(临时生效,无需重新编译)
设置 LD_LIBRARY_PATH 环境变量
export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:.
或者直接写在一行里运行:
LD_LIBRARY_PATH=. ./main
方法二:将库移动到系统目录(不推荐)
sudo cp libmylib.so /usr/lib/
sudo ldconfig
ldd 命令可以查看二进制依赖的所有库:
➜ gdb-demo ldd ./main
linux-vdso.so.1 (0x00007ffcc33f8000)
libmylib.so => ./libmylib.so (0x00007a8bfde9e000)
libstdc++.so.6 => /lib/x86_64-linux-gnu/libstdc++.so.6 (0x00007a8bfdc00000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007a8bfd800000)
libm.so.6 => /lib/x86_64-linux-gnu/libm.so.6 (0x00007a8bfdb17000)
/lib64/ld-linux-x86-64.so.2 (0x00007a8bfdeaa000)
libgcc_s.so.1 => /lib/x86_64-linux-gnu/libgcc_s.so.1 (0x00007a8bfdae9000)
运行 main 后,显示如下:
➜ gdb-demo ./main
Main program started.
Inside dynamic library: Preparing to crash...
[1] 18683 segmentation fault (core dumped) ./main
生成 Core Dump
为了接下来方便使用 nm, objdump 和 gdb,需要让其生成 core 文件。
第一步:检查资源限制 (ulimit) —— 最常见原因
Linux 默认为了防止磁盘被填满,将 core 文件的最大大小限制为 0。 注意:ulimit 的设置仅对当前 Shell 会话有效。 如果你关闭了终端或新开了一个窗口,之前的设置就失效了。
ulimit -c unlimited
再次运行 ulimit -c 确认输出是 unlimited。然后重新运行你的程序 ./main,看是否生成。
第二步:检查 Core Pattern (/proc/sys/kernel/core_pattern) —— 进阶原因
如果 ulimit 已经是 unlimited,但依然没有文件,说明内核把 core dump “劫持” 了。
现代 Linux 发行版(如 Ubuntu, CentOS 7+, Fedora)通常不会直接把 core 文件扔在当前目录,而是通过管道交给一个特定的守护进程(如 apport 或 systemd-coredump)统一管理。
查看内核配置:
cat /proc/sys/kernel/core_pattern
分析输出:
- 情况 A(传统模式): 输出类似
core或core.%e.%p。- 这意味着它应该在当前目录下生成。如果没生成,可能是权限问题(见第三步)。
- 情况 B(管道劫持模式): 输出以
|开头。- 例如 Ubuntu:
|/usr/share/apport/apport %p %s %c ... - 例如 CentOS/Fedora:
|/usr/lib/systemd/systemd-coredump ... - 含义:内核把崩溃信息通过管道(Pipe)直接喂给了后面的程序,而不是写入磁盘。
- 例如 Ubuntu:
解决方案(强行改为生成本地文件): 为了配合你学习 nm 和 objdump 分析,我们需要一个真实的物理文件。你可以临时修改内核参数,强制让它在当前目录生成 core 文件。
执行以下命令(需要 sudo):
# 这里的 pattern 含义是:core.文件名.进程ID
sudo bash -c 'echo "core.%e.%p" > /proc/sys/kernel/core_pattern'
第三步:如果是 systemd 管理的系统(如 CentOS 7+, Ubuntu 20.04+)
如果你的系统使用了 systemd-coredump(即 core_pattern 原本指向 systemd),你的 core dump 其实已经生成了,只是被压缩存放在 /var/lib/systemd/coredump/ 下面。
作为系统程序员,你需要掌握新一代的查看工具:coredumpctl。
列出所有崩溃记录:
coredumpctl list
你应该能看到 main 程序的崩溃记录。
将 Core 文件提取出来: 如果你想用 GDB 或 nm 手动分析,你需要把它从 systemd 的数据库里“倒”出来:
# 将最新的 main 进程的 core dump 保存为文件 core_file
coredumpctl dump main -o core_file
现在你有一个物理文件 core_file 可以用了。
直接调试(快捷方式):
coredumpctl debug main
这会自动启动 GDB 并加载最新的 core dump。
深入解析 Core Dump 并手动定位
Core 文件里到底有什么?
-
Core 文件内容:它主要保存了程序崩溃瞬间的进程内存快照(包括堆、栈、数据段的内容)以及CPU 寄存器状态(RIP/EIP 指令指针、RSP 栈指针等)。
-
符号表位置:符号表(函数名、变量名、行号映射)仍然保留在编译出的二进制文件(
main和libmylib.so)中。 -
GDB 的作用:GDB 启动时,加载
core文件读取内存和寄存器,同时加载二进制文件读取符号表,将内存地址映射回源代码行。
使用 gdb 获取 crash 处的地址
用 gdb 加载 core 文件:
gdb ./main <core-file>
gdb 会输出类似这样的信息
➜ gdb-demo gdb ./main core.main.18683
...
[New LWP 18683]
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
Core was generated by `./main'.
Program terminated with signal SIGSEGV, Segmentation fault.
#0 0x000072c7c37ec19c in crash_function () at mylib.cpp:8
8 *ptr = 42;
假设 GDB 告诉你崩溃地址是 0x000072c7c37ec19c(注意:由于 ASLR 地址随机化,这个地址每次运行都不同)。
Address Space Layout Randomization,地址空间布局随机化
输入 bt (backtrace) 查看栈:
(gdb) bt
#0 0x000072c7c37ec19c in crash_function () at mylib.cpp:8
#1 0x000072c7c37ec1b2 in trigger_crash () at mylib.cpp:12
#2 0x000062bb76cc61c1 in main () at main.cpp:8
#0 栈打印在 0x000072c7c37ec19c 地址出现了错误。
手动定位流程
如果没有源代码,或者想手动验证汇编,你需要找到相对偏移量 (Offset)。
步骤 A: 找到动态库在内存中的加载基址 (Base Address) 在 GDB 中查看内存映射
gdb 中输入:
info proc mappings
输出:
(gdb) info proc mappings
Mapped address spaces:
Start Addr End Addr Size Offset objfile
...
0x72c7c37eb000 0x72c7c37ec000 0x1000 0x0 /home/baominyang/Code/gdb-demo/libmylib.so
0x72c7c37ec000 0x72c7c37ed000 0x1000 0x1000 /home/baominyang/Code/gdb-demo/libmylib.so
0x72c7c37ed000 0x72c7c37ee000 0x1000 0x2000 /home/baominyang/Code/gdb-demo/libmylib.so
0x72c7c37ee000 0x72c7c37ef000 0x1000 0x2000 /home/baominyang/Code/gdb-demo/libmylib.so
0x72c7c37ef000 0x72c7c37f0000 0x1000 0x3000 /home/baominyang/Code/gdb-demo/libmylib.so
看库的第一条输出,./libmylib.so 库的起始地址为 0x72c7c37eb000。
步骤 B: 计算崩溃指令的相对偏移量
0x72c7c37ec19c - 0x72c7c37eb000= 0x119c
注意:
info sharedlibrary显示的是 .text 段的起始,不同工具显示可能略有差异,更准确的是看 RIP 寄存器值减去info proc mappings中该库的起始地址。ELF 文件加载到内存的过程:
- ELF 文件结构:文件开头不是代码,而是 ELF Header、Program Headers 等元数据。
- 加载过程:操作系统加载
.so时,是把整个文件映射到内存(通常按 4KB 对齐)。- nm 的视⻆:
nm显示的0x1159是相对于文件开头 (File Offset 0) 的偏移量。- info sharedlibrary 的视⻆:只显示代码段 (.text) 在内存中的范围,跳过了前面的 ELF Header。
步骤 C: 使用 nm 查看符号
➜ gdb-demo nm -AC libmylib.so | grep crash_function
libmylib.so:0000000000001159 T crash_function()
这里的 1159 是函数入口的静态偏移量。我们的崩溃地址计算出的偏移量大约在 1129,说明崩溃发生在 crash_function 内部 +0x10 (16字节) 左右的地方。
步骤 D: 使用 objdump 反汇编定位汇编指令
# -d: 反汇编, -C: demangle C++符号, -S: 尽可能显示源码
objdump -dC -S libmylib.so
找到 crash_function 部分:
0000000000001159 <crash_function()>:
1159: f3 0f 1e fa endbr64
115d: 55 push %rbp
115e: 48 89 e5 mov %rsp,%rbp
...
1190: 48 c7 45 f8 00 00 00 movq $0x0,-0x8(%rbp)
1197: 00
1198: 48 8b 45 f8 mov -0x8(%rbp),%rax
119c: c7 00 2a 00 00 00 movl $0x2a,(%rax)
分析: 在 119c 这一行(与上面计算的偏移量对上了),汇编指令是 movl $0x2a, (%rax)。 意思是将值 0x2a (十进制 42) 写入 rax 寄存器指向的内存地址。 如果此时 rax 是 0,就会触发 sig 11。
gdb 进阶调试 (更高效)
查看寄存器状态
(gdb) i r rax
rax 0x0 0
i r rax 完整写法是 info registers rax。可以看到 rax 确实是 0 (NULL)。
汇编级调试
不需要 objdump,gdb 自带反汇编。
查看当前函数的汇编:
(gdb) disassemble
Dump of assembler code for function _Z14crash_functionv:
0x000072c7c37ec159 <+0>: endbr64
0x000072c7c37ec15d <+4>: push %rbp
0x000072c7c37ec15e <+5>: mov %rsp,%rbp
0x000072c7c37ec161 <+8>: sub $0x10,%rsp
0x000072c7c37ec165 <+12>: lea 0xe94(%rip),%rax # 0x72c7c37ed000
0x000072c7c37ec16c <+19>: mov %rax,%rsi
0x000072c7c37ec16f <+22>: mov 0x2e62(%rip),%rax # 0x72c7c37eefd8
0x000072c7c37ec176 <+29>: mov %rax,%rdi
0x000072c7c37ec179 <+32>: call 0x72c7c37ec080 <_ZStlsISt11char_traitsIcEERSt13basic_ostreamIcT_ES5_PKc@plt>
0x000072c7c37ec17e <+37>: mov 0x2e5b(%rip),%rdx # 0x72c7c37eefe0
0x000072c7c37ec185 <+44>: mov %rdx,%rsi
0x000072c7c37ec188 <+47>: mov %rax,%rdi
0x000072c7c37ec18b <+50>: call 0x72c7c37ec090 <_ZNSolsEPFRSoS_E@plt>
0x000072c7c37ec190 <+55>: movq $0x0,-0x8(%rbp)
0x000072c7c37ec198 <+63>: mov -0x8(%rbp),%rax
=> 0x000072c7c37ec19c <+67>: movl $0x2a,(%rax)
0x000072c7c37ec1a2 <+73>: nop
0x000072c7c37ec1a3 <+74>: leave
0x000072c7c37ec1a4 <+75>: ret
End of assembler dump.
箭头 => 直接指向了导致崩溃的指令。
源代码与汇编对照 (TUI 模式)
这是 GDB 最强大的功能之一。
输入 layout split (或者先 Ctrl+X, 然后 A)。
- 上半部分显示汇编
- 下半部分显示源代码
- 你可以看到源码
*ptr = 42对应汇编movl ...
layout:用于分割窗口,可以一边查看代码,一边测试。主要有以下几种用法:
layout src:显示源代码窗口
layout asm:显示汇编窗口
layout regs:显示源代码/汇编和寄存器窗口
layout split:显示源代码和汇编窗口
layout next:显示下一个layout
layout prev:显示上一个layout
Ctrl + L:刷新窗口
Ctrl + x,再按1:单窗口模式,显示一个窗口
Ctrl + x,再按2:双窗口模式,显示两个窗口
Ctrl + x,再按a:回到传统模式,即退出layout,回到执行layout之前的调试窗口。
查看局部变量和指针
(gdb) p ptr
$1 = (int *) 0x0
调试 Release 版本 (无符号表/被 strip)
如果 libmylib.so 被 strip 过(去除了符号表),bt 可能只显示 ??,此时:
- 你需要保留一份带符号表的
libmylib.so.debug - 在 gdb中使用
symbol-file libmylib.so.debug加载符号 - 或者设置
set solib-search-path /path/to/debug/libs
gdb 查看及修改内存值
x 命令和字节序
1. x 命令的语法
x 命令(Examine)可以直接对物理内存进行观察,它的语法极其简洁:
x /<n><f><u> <address>
- n (Count): 显示多少个单元。
- f (Format): 显示格式。
x(十六进制,最常用)d(十进制)i(反汇编指令,非常有用)s(字符串)
- u (Unit): 每个单元多大。
b(Byte, 1字节)h(Half-word, 2字节)w(Word, 4字节, 默认值)g(Giant-word, 8字节)
2. 透视字节序 (Little Endian)
假设我们在代码里定义了一个整数:
int magic = 0x12345678;
在 x86_64(以及现代 ARM64)架构下,它们都是 小端序 (Little Endian)。这意味着:低位字节存储在低地址。
我们在 GDB 里看一下:
代码段
(gdb) p &magic
$1 = (int *) 0x7fffffffe4cc
# 方式 A:作为一个 4字节整数 (Word) 查看
(gdb) x/1xw 0x7fffffffe4cc
0x7fffffffe4cc: 0x12345678
GDB 知道你想看一个整数,所以它帮你把字节拼好了,显示出符合人类直觉的
0x12345678。
现在的关键来了,我们拆开看字节:
# 方式 B:作为 4个单字节 (Bytes) 查看
(gdb) x/4xb 0x7fffffffe4cc
0x7fffffffe4cc: 0x78 0x56 0x34 0x12
解读:
- 地址
...cc(最低地址) 存的是0x78(数值的最低位)。- 地址
...cd存的是0x56。- …
- 地址
...cf(最高地址) 存的是0x12(数值的最高位)。
当调试网络协议(通常是 Big Endian,大端序)或者分析二进制文件时,你会经常看到内存里是 78 56 34 12,脑子里要自动拼接成 0x12345678。
3. x 的高级用法:查看指令
如果你没有源代码,x 也是你的反汇编器:
(gdb) x/5i $pc # 查看 PC 指针(当前运行位置)后的5条汇编指令
=> 0x555555555149 <main()+20>: movl $0x2a, (%rax)
0x55555555514f <main()+26>: nop
...
手动救活崩溃程序
回到我们那个空指针崩溃的例子。 场景:程序即将执行 *ptr = 42,但 ptr 是 0x0。正常执行必死无疑。 目标:我们要在 GDB 里强行修改 ptr 的指向,让程序把 42 写到一个安全的地方,然后继续运行,不让它崩溃。
准备工作
在崩溃前的行打断点:
(gdb) b crash_function
Breakpoint 6 at 0x72c7c37ec165: file mylib.cpp, line 5.
(gdb) info break
Num Type Disp Enb Address What
6 breakpoint keep y 0x000072c7c37ec165 in crash_function() at mylib.cpp:5
然后运行 main:
(gdb) run
Starting program: /home/baominyang/Code/gdb-demo/main
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
Main program started.
Breakpoint 6, crash_function () at mylib.cpp:5
5 std::cout << "Inside dynamic library: Preparing to crash..." << std::endl;
(gdb) s
Inside dynamic library: Preparing to crash...
6 int *ptr = nullptr;
(gdb) s
8 *ptr = 42;
程序停在 *ptr = 42; 这一行。
方法一:修改变量值 (set var) —— 最正统的方法
确认当前状态:
(gdb) p ptr
$2 = (int *) 0x0
找个安全的地方: 我们需要一块可写的内存。栈空间通常是安全的。我们可以利用当前的栈顶指针 $rsp,或者定义一个临时变量。 这里我们偷个懒,直接利用 GDB 的强大功能,在被调试程序的内存堆里 malloc 一块内存,然后通过 set var 命令直接操作变量。
(gdb) call (void*)malloc(4)
$3 = (void *) 0x55555556b6c0 <-- GDB 帮你在目标进程里申请了内存!
(gdb) set var ptr = 0x55555556b6c0
(gdb) continue
Continuing.
This line will never be reached.
方法二:修改寄存器 —— 更底层的“硬改”
假设你没有源码符号,变量名 ptr 用不了。你需要直接操作 CPU 寄存器。
看汇编
(gdb) disassemble
Dump of assembler code for function _Z14crash_functionv:
...
0x000072c7c37ec198 <+63>: mov -0x8(%rbp),%rax
=> 0x000072c7c37ec19c <+67>: movl $0x2a,(%rax)
检查寄存器
(gdb) i r rax
rax 0x0 0
修改寄存器: 我们把 rax 改成栈上的一个地址(比如当前的栈指针 rsp 往下一点的地方,暂且当作垃圾桶用)。
(gdb) set $rax = $rsp - 8
运行结果:
(gdb) s
8 *ptr = 42;
(gdb) si
0x00007ffff7fb919c 8 *ptr = 42;
(gdb) set $rax = $rsp - 8
(gdb) si
9 return;
(gdb) c
Continuing.
This line will never be reached.
方法三:直接跳过崩溃指令 (set $pc)
计算下一条指令地址:
(gdb) x/2i $pc
=> 0x7ffff7fb919c <_Z14crash_functionv+67>: movl $0x2a,(%rax)
0x7ffff7fb91a2 <_Z14crash_functionv+73>: nop
需要看汇编确定当前指令有多长,或者直接看下一行的地址。这里下一行是 0x7ffff7fb91a2。
修改指令指针寄存器 (RIP/PC):x86_64 是寄存器 $rip,arm 是寄存器 $pc
打完断点后执行:
(gdb) set $rip = 0x7ffff7fb91a2
(gdb) i r rip
rip 0x7ffff7fb91a2 0x7ffff7fb91a2 <crash_function()+73>
或者更通用的:
(gdb) set $rip = $rip + 6 (跳过6个字节)
完整执行过程:
(gdb) b crash_function
Function "crash_function" not defined.
Make breakpoint pending on future shared library load? (y or [n]) y
Breakpoint 1 (crash_function) pending.
(gdb) run
Starting program: /home/baominyang/Code/gdb-demo/main
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
Main program started.
Breakpoint 1, crash_function () at mylib.cpp:5
warning: Source file is more recent than executable.
5 std::cout << "Inside dynamic library: Preparing to crash..." << std::endl;
(gdb) s
Inside dynamic library: Preparing to crash...
6 int *ptr = nullptr;
(gdb) s
8 *ptr = 42;
(gdb) si
0x00007ffff7fb919c 8 *ptr = 42;
(gdb) set $rip = $rip + 6
(gdb) si
0x00007ffff7fb91a3 9 return;
(gdb) c
Continuing.
This line will never be reached.
s(step):是源码级单步,gdb 会试图执行代码直到下一行 C++ 源码。
si(stepi):是汇编级单步,只执行一条机器指令。