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

目录

什么是DWARF
DWARF包含什么
如何提取dwarf
strip
如何找到strip后的二进制入口
如何调试一个不带符号的程序
总结

一个程序是否方便调试的关键在于调试信息是否完善,本文基于此简单介绍一下什么是DWARF调试信息

什么是DWARF

Debugging With Attributed Record Formats 是很多编译器使用的调试信息格式,他能够轻松的使得代码能够进行源码级的调试,DWARF的标准文档如下

https://dwarfstd.org/doc/DWARF5.pdf

DWARF包含什么

当我们编译要一个程序时,如果带g和不带g,那么就可以对比的了解到DWARF在ELF中是如何存在的,如下

gcc example.c -g -o example_with_dwarf gcc example.c -o example_without_dwarf

此时我们对比其section即可,如下

readelf -S example_with_dwarf readelf -S example_without_dwarf

对比情况如下

image.png

可以看到,dwarf实际上是新增了如下段

  1. .debug_aranges
  2. .debug_info
  3. .debug_abbrev
  4. .debug_line
  5. .debug_str

这些段的内容就是提供了这个二进制可进行源码级调试的辅助内容,主要包含如下:

  1. 函数名
  2. 变量计算
  3. 变量类型
  4. 源码行
  5. 源码文件路径

通过dwarfdump可以解析这些段的实际内容,如下

# dwarfdump -a example_with_dwarf .debug_info COMPILE_UNIT<header overall offset = 0x00000000>: < 0><0x0000000b> DW_TAG_compile_unit DW_AT_producer GNU C17 10.3.0 -mlittle-endian -mabi=lp64 -g -fasynchronous-unwind-tables -fstack-protector-strong -fstack-clash-protection DW_AT_language DW_LANG_C99 DW_AT_name example.c DW_AT_comp_dir /root/gdb DW_AT_low_pc 0x00000894 DW_AT_high_pc <offset-from-lowpc>180 DW_AT_stmt_list 0x00000000 LOCAL_SYMBOLS: < 1><0x0000002d> DW_TAG_base_type DW_AT_byte_size 0x00000008 DW_AT_encoding DW_ATE_unsigned DW_AT_name long unsigned int ...... .debug_line: line number info for a single cu Source lines (from CU-DIE at .debug_info offset 0x0000000b): NS new statement, BB new basic block, ET end of text sequence PE prologue end, EB epilogue begin IS=val ISA number, DI=val discriminator value <pc> [lno,col] NS BB ET PE EB IS= DI= uri: "filepath" 0x00000894 [ 9, 1] NS uri: "/root/gdb/example.c" ...... .debug_str name at offset 0x00000000, length 13 is 'long long int' name at offset 0x0000000e, length 4 is 'main' name at offset 0x00000013, length 22 is 'long long unsigned int' name at offset 0x0000002a, length 13 is 'unsigned char' name at offset 0x00000038, length 9 is 'example.c' name at offset 0x00000042, length 18 is 'short unsigned int' name at offset 0x00000055, length 123 is 'GNU C17 10.3.0 -mlittle-endian -mabi=lp64 -g -fasynchronous-unwind-tables -fstack-protector-strong -fstack-clash-protection' name at offset 0x000000d1, length 5 is 'hello' name at offset 0x000000d7, length 9 is 'short int' name at offset 0x000000e1, length 9 is '/root/gdb' .debug_aranges COMPILE_UNIT<header overall offset = 0x00000000>: < 0><0x0000000b> DW_TAG_compile_unit DW_AT_producer GNU C17 10.3.0 -mlittle-endian -mabi=lp64 -g -fasynchronous-unwind-tables -fstack-protector-strong -fstack-clash-protection DW_AT_language DW_LANG_C99 DW_AT_name example.c DW_AT_comp_dir /root/gdb DW_AT_low_pc 0x00000894 DW_AT_high_pc <offset-from-lowpc>180 DW_AT_stmt_list 0x00000000 arange starts at 0x00000894, length of 0x000000b4, cu_die_offset = 0x0000000b arange end .debug_frame

如何提取dwarf

在linux操作系统发行中,例如debian系列,默认提供软件包同名的ddeb,这个ddeb包含的是该程序的可调试信息,也就是dwarf信息文件。

我们以example_with_dwarf为例子,首先将其的dwarf调试信息拿出来,如下

objcopy --only-keep-debug example_with_dwarf example.debug

然后将example_with_dwarf的debug信息去掉,如下

objcopy --strip-debug example_with_dwarf

此时gdb调试example_with_dwarf是没有符号的,如下

# gdb ./example_with_dwarf Reading symbols from ./example_with_dwarf... (No debugging symbols found in ./example_with_dwarf) (gdb) list No symbol table is loaded. Use the "file" command.

但是我们可以手动将dwarf加载进来,如下

(gdb) add-symbol-file example.debug add symbol table from file "example.debug" (y or n) y Reading symbols from example.debug... (gdb) list 11 memcpy(p, hello, strlen(hello) + 1); 12 free(p); 13 } 14 15 int sum(int x) 16 { 17 int s = x + y; 18 printf("sum=%d\n", s); 19 mem(); 20 return s;

这样就使得了不带dwarf信息的二进制,能够轻易的加载分离的dwarf文件,从而支持方便的源码级的调试

strip

通常情况下,debian package会调用strip来进行进一步裁剪,而不是如下仅仅裁剪debug info

strip program objcopy --strip-debug program

这也就意味着strip后的程序会天然丢掉.symtab.strtab。此时在调试操作系统时,需要注意strip的二进制会比仅删除debug info的二进制更难调试。其原因是.symtab.strtab存放的是链接和调试用的符号表。如下解释

如果一个程序是仅删掉debug info的,那么因为.symtab.strtab存在,所以还是能够下发断点,因为我们输入的函数名字能够正常的被gdb找到偏移,如下

(gdb) l No symbol table is loaded. Use the "file" command. (gdb) b mem Breakpoint 1 at 0x8a0 (gdb) r Starting program: /root/gdb/example_with_dwarf Breakpoint 1, 0x0000aaaaaaaaa8a0 in mem ()

因为dwarf提供的额外信息不存在的,所以取而代之的是代码运行时的地址,但是此时程序还是能够被调试的。

而通过strip的应用程序,如果运行gdb,那么可能无法正常通过名字下发断点,如下

# strip example_with_dwarf # gdb ./example_with_dwarf (gdb) l No symbol table is loaded. Use the "file" command. (gdb) b main Function "main" not defined. Breakpoint 1 (main) pending. (gdb) b mem Function "mem" not defined. Breakpoint 2 (mem) pending. (gdb) r Starting program: /root/gdb/example_with_dwarf sum=3 [Inferior 1 (process 756218) exited normally]

可以看到,我们在gdb下发的是函数名,但是因为缺少了.symtab.strtab,自然就找不到对应名字的函数地址了。

如何找到strip后的二进制入口

根据上面说的,如果一个程序strip之后无法通过函数名找到地址了,那么怎么定位和调试呢。 其实可以通过计算来找到函数的入口,如下
当程序strip之后,我们没办法给main下断点

(gdb) b main Function "main" not defined.

但是程序总有入口的,这个入口在elf头的entry上,在gdb中,可以读取真实的偏移后的entry,需要先加载一次程序,如下

(gdb) r Starting program: /root/gdb/example sum=3 [Inferior 1 (process 830429) exited normally] (gdb) info files Symbols from "/root/gdb/example". Local exec file: `/root/gdb/example', file type elf64-littleaarch64. Entry point: 0xaaaaaaaaa780

可以看到此程序的入口是0xaaaaaaaaa780,所以对这个地址下断点即可

(gdb) b *0xaaaaaaaaa780 Breakpoint 1 at 0xaaaaaaaaa780 (gdb) r Starting program: /root/gdb/example Breakpoint 1, 0x0000aaaaaaaaa780 in ?? ()

这是实际的函数入口,值得注意的是,这并不是main函数,而是_start函数,接下来就可以做一下黑客操作了。

如何调试一个不带符号的程序

根据上面说的,在操作系统中,大量的程序是被strip过的,这种情况下,我们无法简单的调试他,因为函数名就对应不了地址,通过人来计算很难实现。

但是通常说不带符号的程序,应该是编译没有带g标志的程序,同时也没有被strip的程序。

那么这种程序,如何调试呢?

这种情况其实很简单,根据之前的文章我们知道,程序是否易调试的主要原因是dwarf的加载,那么这个问题就是如果没有dwarf信息的加载,如何自行通过寄存器推理计算来调试某个函数。下面通过一个简单的sum来实践一下

函数如下

int sum(int x) { int s = x + y; printf("sum=%d\n", s); return s; }

那么如果在没有dwarf信息帮助的前提下,获取x,y,s三个值呢。 那么就是通过看寄存器和反汇编完成,我们可以对sum函数打下断点,然后反汇编查看,如下

(gdb) b sum Breakpoint 1 at 0x8f4 (gdb) r Starting program: /root/gdb/example Breakpoint 1, 0x0000aaaaaaaaa8f4 in sum () (gdb) disassemble Dump of assembler code for function sum: 0x0000aaaaaaaaa8e0 <+0>: stp x29, x30, [sp, #-48]! 0x0000aaaaaaaaa8e4 <+4>: mov x29, sp 0x0000aaaaaaaaa8e8 <+8>: str w0, [sp, #28] 0x0000aaaaaaaaa8ec <+12>: adrp x0, 0xaaaaaaabb000 0x0000aaaaaaaaa8f0 <+16>: add x0, x0, #0x18 => 0x0000aaaaaaaaa8f4 <+20>: ldr w0, [x0] 0x0000aaaaaaaaa8f8 <+24>: ldr w1, [sp, #28] 0x0000aaaaaaaaa8fc <+28>: add w0, w1, w0 0x0000aaaaaaaaa900 <+32>: str w0, [sp, #44] 0x0000aaaaaaaaa904 <+36>: ldr w1, [sp, #44] 0x0000aaaaaaaaa908 <+40>: adrp x0, 0xaaaaaaaaa000 0x0000aaaaaaaaa90c <+44>: add x0, x0, #0x9e8 0x0000aaaaaaaaa910 <+48>: bl 0xaaaaaaaaa770 <printf@plt> 0x0000aaaaaaaaa914 <+52>: ldr w0, [sp, #44] 0x0000aaaaaaaaa918 <+56>: ldp x29, x30, [sp], #48 0x0000aaaaaaaaa91c <+60>: ret End of assembler dump.

可以看到,根据汇编对照代码分析,y的值应该在0xaaaaaaabb018 所以直接读取验证即可

0xaaaaaaabb018 <y>: 0x00000002

而x的值应该在sp+28,读取验证即可

(gdb) x/wx $sp+28 0xfffffffff30c: 0x00000001

此时s的值被存放到sp+44,所以我们先调整栈帧,然后打印即可

(gdb) b *0x0000aaaaaaaaa904 Breakpoint 2 at 0xaaaaaaaaa904 (gdb) c Continuing. Breakpoint 2, 0x0000aaaaaaaaa904 in sum () (gdb) x $sp+44 0xfffffffff31c: 0x00000003

至此,也就完成了一个没有符号的前提下,简单调试一个函数。其本质还是反汇编理解代码并修改内存而已。相比于没有dwarf信息的程序调试而言,更加枯燥无味。

总结

本文基于调试信息进行了一个简单的介绍,从而能够更清晰的理解gdb调试的技巧。如果一个程序没有dwarf信息,那么调试它则需要通过分析栈区,分析汇编从而理解,这种理解通常只能是片段逻辑分析。
而如果一个程序有dwarf信息,那么使用gdb调试就会如鱼得水,非常方便。能够进行整体代码行为的分析。
如果一个程序被strip后,按照个人经验,不建议强行分析,即使计算了内存地址,也有可能无法访问,投入产出不成正比。