gdb 调试

作者:Administrator 发布时间: 2026-06-03 阅读量:1 评论数:0

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, objdumpgdb,需要让其生成 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 文件扔在当前目录,而是通过管道交给一个特定的守护进程(如 apportsystemd-coredump)统一管理。

查看内核配置:

cat /proc/sys/kernel/core_pattern

分析输出:

  • 情况 A(传统模式): 输出类似 corecore.%e.%p
    • 这意味着它应该在当前目录下生成。如果没生成,可能是权限问题(见第三步)。
  • 情况 B(管道劫持模式): 输出以 | 开头。
    • 例如 Ubuntu:|/usr/share/apport/apport %p %s %c ...
    • 例如 CentOS/Fedora:|/usr/lib/systemd/systemd-coredump ...
    • 含义:内核把崩溃信息通过管道(Pipe)直接喂给了后面的程序,而不是写入磁盘

解决方案(强行改为生成本地文件): 为了配合你学习 nmobjdump 分析,我们需要一个真实的物理文件。你可以临时修改内核参数,强制让它在当前目录生成 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 栈指针等)。

  • 符号表位置:符号表(函数名、变量名、行号映射)仍然保留在编译出的二进制文件(mainlibmylib.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 文件加载到内存的过程:

  1. ELF 文件结构:文件开头不是代码,而是 ELF Header、Program Headers 等元数据。
  2. 加载过程:操作系统加载 .so 时,是把整个文件映射到内存(通常按 4KB 对齐)。
  3. nm 的视⻆nm 显示的 0x1159 是相对于文件开头 (File Offset 0) 的偏移量。
  4. 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 可能只显示 ??,此时:

  1. 你需要保留一份带符号表libmylib.so.debug
  2. 在 gdb中使用 symbol-file libmylib.so.debug 加载符号
  3. 或者设置 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,但 ptr0x0。正常执行必死无疑。 目标:我们要在 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):是汇编级单步,只执行一条机器指令

评论