调试二进制程序时,经常要借助GDB工具,跟踪程序的执行流程,获取程序执行时变量的值,以发现问题所在。GDB能得到这些信息,是因为编译程序时,编译器保存了相应的信息。Linux下的可执行程序和链接库一般为ELF格式(Executable and Linking Format),调试信息以DWARF格式保存。
查看ELF文件信息
新建文件main.c
内容如下
1 |
|
通过gcc编译, -g
表示添加调试信息
1 |
|
通过readelf命令ELF文件的Section headers
1 |
|
编译生成的可执行文件中有34个Section, 其中的debug_*
几个ELF头部代表程序的DWARF格式的调试信息
查看DWARF信息
查看debug_line Section
debug_line
Section中记录了二进制程序的指令地址对应源代码的位置。可以通过readelf -wl main
查看
1 |
|
注意这里面的Line Number Statements
,里面的每一行都标明了一条指定的地址和对应的源代码再文件中的位置。
[0x000000c8] Special opcode 8: advance Address by 0 to 0x64a and Line by 3 to 4
这一行表示指定地址为0x64a
的指令对应源代码在文件中的第4行
对debug_line
section处理后,可以得到指令地址到源代码位置的转换表,可以通过readelf -wL main
查看处理后的结果(指令地址和源代码的对应关系)。
1 |
|
其中每行都指明了文件名、第X行、指令地址。
查看debug_info section
通过readelf -wi main
可以读取debug_info
section的内容。debug_info section是DWARF的核心内容,其他一些如debug_str
等都是为了加快查找/压缩空间而使用的。
1 |
|
这里是由一个个的称为DIE(Debuging Information Entity)的单元来表示的,其中TAG表示DIE的类型,如DW_TAG_compile_unit中包含了编译时的参数,源文件,目录等。这里只关注其中DW_TAG_subsystem,即函数信息。
DW_AT_name: add
: 函数名为addDW_AT_low_pc: 0x64a
: 函数对应的初始PC地址DW_AT_high_pc: 0x1a
: 函数结束时PC地址为0x64a + 0x1aDW_AT_frame_base
: 表达函数的栈帧基址(frame base),函数参数和局部变量的存储位置会以相对栈帧基址的偏移给出。
在DW_TAG_subprogram后面是函数中变量的信息 DW_TAB_formal_parameter
表示这个DIE代表的时函数的参数
DW_AT_name: a
: 参数名为aDW_AT_type: 0x62
: 参数类型DW_AT_location : 2 byte block: 91 5c (DW_OP_fbreg: -36)
: 表示存储在函数栈帧基址偏移-24
的地方,
有了这些信息,GDB就可以根据当前执行的指令地址得到对应的源代码文件位置、当前函数名以及当前函数中的参数/局部变量/全局变量的信息。
调试符号单独保存
GDB允许将调试信息保存在单独的文件中。因为调试信息占用的空间会很大,甚至远超过程序本身。很多系统会将调试信息剥离到单独的文件中,需要调试时再安装,以节约存储空间。
剥离出调试信息
通过objcopy可以将ELF文件的调试信息提取到单独的文件中
1 |
|
然后通过strip去除文件中的调试信息
1 |
|
寻找带有调试信息的文件路径
GDB支持使用下面两种方式来寻找调试信息所在的文件
通过build-id
指定
1 |
|
在.note.gnu.build-id
中记录了build-id为536cc8d42fa3ed672abc427d4a683313fb902b6b
,GDB会尝试从
/usr/lib/debug/.build-id/53/6cc8d42fa3ed672abc427d4a683313fb902b6b.debug
文件中读取调试信息。
在/usr/lib/debug
目录中,以build-id的头两位为子目录名,后面的几位+.debug
为文件名。
通过debug_link
指定
通过debug_link
指定文件名。如指定为main.debug,则GDB会在下面三个路径下寻找debug文件
- /usr/bin/main.debug
- /usr/bin/.debug/main.debug
- /usr/lib/debug/usr/bin/main.debug
可通过objcopy
命令将debug_link添加到程序中
1 |
|
然后可以看到ELF头部已有gnu_debuglink
1 |
|