编辑
2025-09-04
记录知识
0

目录

调试什么
示例代码
实践
设置断点
查看寄存器
查看堆栈
查看源码
查看变量
查看内存
打印类型
打印线程
总结

从《gdb调试方法(2)-ptrace》可以看到,ptrace会频繁的下发PTRACE_GETREGS 和 PTRACE_SETREGS ,通过修改pc寄存器,从而使得进程能够被gdb接管,在trace处运行gdb的相关命令。

本文基于简单的程序,使用gdb完成简单的调试工作

调试什么

根据ptrace的理解,我们可以大概总结gdb能够获取哪些内容

  1. 当前寄存器
  2. DWARF调试信息
  3. 函数调用栈
  4. 变量
  5. 函数当前栈
  6. 内存内容
  7. 线程状态
  8. 函数汇编指令

示例代码

为了简单的实现gdb的调试,这里提供一个简易的示例代码如下

#include <stdio.h> static char hello[] = "hello"; int y = 2; void mem() { void *p = malloc(sizeof(char)*10); memcpy(p, hello, strlen(hello) + 1); free(p); } int sum(int x) { int s = x + y; printf("sum=%d\n", s); return s; } int main() { int x = 1; sum(x); mem(); }

实践

为了编译可调试并带有DWARF调试信息,只需要编译时增加g选项即可

gcc example.c -g -o example

然后gdb直接调试即可

# gdb ./example

设置断点

(gdb) b sum Breakpoint 1 at 0x780: file example.c, line 8. (gdb) r Starting program: /root/gdb/example Breakpoint 1, sum (x=1) at example.c:8 8 int s = x + y;

查看寄存器

因为sum的形参是x,所以我们可以看到x0的值就是1

(gdb) i r x0 0x1 1 x1 0xfffffffff498 281474976707736 x2 0xfffffffff4a8 281474976707752 x3 0xaaaaaaaaa7b4 187649984473012 x4 0x0 0 x5 0xc56a615efa85d69 889080589597564265 x6 0xfffff7f87ad8 281474842000088 x7 0x4040100000000000 4629718009122914304 x8 0xffffffffffffffff -1 x9 0xffff 65535 x10 0x800000008000 140737488388096 x11 0x0 0 x12 0xfffff7e1be48 281474840510024 x13 0x0 0 x14 0x0 0 x15 0x6fffff47 1879048007 x16 0xaaaaaaabafa0 187649984540576 x17 0xfffff7e38c68 281474840628328 x18 0x73516240 1934713408 x19 0xaaaaaaaaa7d8 187649984473048 x20 0x0 0 x21 0xaaaaaaaaa660 187649984472672 x22 0x0 0 x23 0x0 0 x24 0x0 0 x25 0x0 0 x26 0x0 0 x27 0x0 0 x28 0x0 0 x29 0xfffffffff2f0 281474976707312 x30 0xaaaaaaaaa7cc 187649984473036 sp 0xfffffffff2f0 0xfffffffff2f0 pc 0xaaaaaaaaa780 0xaaaaaaaaa780 <sum+12> cpsr 0x60000000 [ EL=0 C Z ] fpsr 0x0 0 fpcr 0x0 0

查看堆栈

因为能够获得寄存器,所以可以从x30和x29推算堆栈,然后结合DWARF的信息可以打印如下。

(gdb) bt #0 sum (x=1) at example.c:8 #1 0x0000aaaaaaaaa7cc in main () at example.c:16

查看源码

因为代码存在DWARF信息,所以能够借助DWARF信息查看代码行,如下

(gdb) l 13 int main() 14 { 15 int x = 1; 16 sum(x); 17 } 18

查看变量

DWARF提供了局部变量的偏移值,以及全局变量和静态的固定加载地址。所以借助DAWRF的信息,可以在gdb中查看变量

(gdb) p x $3 = 1 (gdb) p y $4 = 2 (gdb) p hello $5 = "hello" (gdb) p s $6 = 0

查看内存

因为DWARF会帮我们计算好p的值,所以我们可以轻松的通过x打印内存,如下

(gdb) u 12 mem () at example.c:12 12 free(p); (gdb) x/10c p 0xaaaaaaabc6b0: 104 'h' 101 'e' 108 'l' 108 'l' 111 'o' 0 '\000' 0 '\000' 0 '\000' 0xaaaaaaabc6b8: 0 '\000' 0 '\000'

同样的,也可以使用dump直接打印区域的内存,只要我们自行计算好变量起始地址和结束即可。
首先我们对mem函数反汇编

(gdb) disassemble/m Dump of assembler code for function mem: 9 { 0x0000aaaaaaaaa894 <+0>: stp x29, x30, [sp, #-32]! 0x0000aaaaaaaaa898 <+4>: mov x29, sp 10 void *p = malloc(sizeof(char)*10); => 0x0000aaaaaaaaa89c <+8>: mov x0, #0xa // #10 0x0000aaaaaaaaa8a0 <+12>: bl 0xaaaaaaaaa720 <malloc@plt> 0x0000aaaaaaaaa8a4 <+16>: str x0, [sp, #24] 11 memcpy(p, hello, strlen(hello) + 1); 0x0000aaaaaaaaa8a8 <+20>: adrp x0, 0xaaaaaaabb000 0x0000aaaaaaaaa8ac <+24>: add x0, x0, #0x10 0x0000aaaaaaaaa8b0 <+28>: bl 0xaaaaaaaaa700 <strlen@plt> 0x0000aaaaaaaaa8b4 <+32>: add x0, x0, #0x1 0x0000aaaaaaaaa8b8 <+36>: mov x2, x0 0x0000aaaaaaaaa8bc <+40>: adrp x0, 0xaaaaaaabb000 0x0000aaaaaaaaa8c0 <+44>: add x1, x0, #0x10 0x0000aaaaaaaaa8c4 <+48>: ldr x0, [sp, #24] 0x0000aaaaaaaaa8c8 <+52>: bl 0xaaaaaaaaa6f0 <memcpy@plt> 12 free(p); 0x0000aaaaaaaaa8cc <+56>: ldr x0, [sp, #24] 0x0000aaaaaaaaa8d0 <+60>: bl 0xaaaaaaaaa760 <free@plt> 13 } 0x0000aaaaaaaaa8d4 <+64>: nop 0x0000aaaaaaaaa8d8 <+68>: ldp x29, x30, [sp], #32 0x0000aaaaaaaaa8dc <+72>: ret End of assembler dump.

如果我们想取出void* p的内存,可以看到sp+24存放了malloc的地址,而传入malloc的参数是0xa,如下

10 void *p = malloc(sizeof(char)*10); => 0x0000aaaaaaaaa89c <+8>: mov x0, #0xa // #10 0x0000aaaaaaaaa8a0 <+12>: bl 0xaaaaaaaaa720 <malloc@plt> 0x0000aaaaaaaaa8a4 <+16>: str x0, [sp, #24]

然后解析malloc返回地址,如下

(gdb) x/gx $sp + 24 0xfffffffff2e8: 0x0000aaaaaaabc6b0

所以可以直接将其dump出来,如下

(gdb) u 12 mem () at example.c:12 12 free(p); (gdb) dump binary memory test.bin 0x0000aaaaaaabc6b0 0x0000aaaaaaabc6b0+10

此时可以看到test.bin就是我想要查看的内存内容 hello

# hexdump -C test.bin 00000000 68 65 6c 6c 6f 00 00 00 00 00 |hello.....| 0000000a

打印类型

打印类型也是非常常用的命令,借助DWARF可以帮我们默认解析好变量的类型,所以可以直接打印,如下

(gdb) ptype y type = int

打印线程

多线程程序调试时,可以通过threads来切换线程,并单独控制某个线程,打印线程的命令如下

(gdb) i threads Id Target Id Frame * 1 process 392716 "example" mem () at example.c:12

总结

到这里,gdb的基本简单调试的方法已经说明了,可以发现,gdb调试绝大部分情况下还是依赖DWARF调试信息,如果有这个信息,那么调试会变得非常简单,如果没有这个信息,那么就得人为的计算,如果不辅助一下插桩的技巧,或者标记点,那么调试计算会变得很麻烦,但是总而言之,调试的时候,尽可能得需要保证二进制包含DWARF调试信息。