一、背景
一个程序会经历编码、编译、运行的过程,但所有的开发几乎都不可能是一帆风顺的,总会有些意想不到的错误,这时便需要调试。那么调试器使用的调试信息是从哪里来的呢?答案就是从编译后的文件中来的(依赖编译的时候使用特定的编译选项,如GCC使用“-g”)。
在BPF程序开发过程中,我们也需要这类调试信息,首先LLVM 能够将调试信息附加到生成的 BPF 目标文件中。默认情况下,这是 DWARF 格式。但BPF 使用的是高度简化的版本称为 BTF。我们首先需要将DWARF 转换为 BTF。BPF程序加载到内核后,内核验证器将验证 BTF 数据的正确性并跟踪 BTF 数据包含的数据类型。
DWARF的全称是"Debugging With Attributed Record Formats",遵从GNU FDL授权。由以上述描述可知,DWARF是一种调试文件格式,许多编译器和调试器均使用DWARF来支持源代码级调试。简单来说,DWARF记录了源代码、二进制程序以及它们之间存在的联系。
本文主要从三个方面介绍DWARF:
- DWARF在elf文件存在哪些段,以及每个段的含义;
- 采用具体的实例,介绍了.deubg_str所包含的内容;
- 重点介绍了.debug_info段,其是理解BTF的关键;
二、elf中包含的DWARF段
DWARF在elf文件中共存在12个段。就本文而言,12个段重要的段是:.debug_info段和.deubg_str段,后面也是着重讲解这两个段。这两个段也是libbpf BTF最感兴趣的两个段。
三、.debug_str解析
.debug_str段主要包含了源代码中的各种可读文本。以下面的C代码为例:
struct test_struct{
int testa;
char testb;
}test_s;
int main()
{
return 0;
}
- 首先将该C代码编译成二进制,即gcc -g -o test test.c;
- 运行命令:readelf -ws test,可以得到如下的文本。核心文本包括了test_s(结构体变量)、test_struct(结构体)、testa(结构体成员)、testb(结构体成员)、char(结构体成员类型)等。但是如果变量的名字是单个字符,则不会单独存储。
0x00000000 74657374 5f73006d 61696e00 2f686f6d test_s.main./hom
0x00000010 652f6368 656e6773 68757969 2e637379 e/chengshuyi.csy
0x00000020 2f706168 6f6c6500 74657374 5f737472 /pahole.test_str
0x00000030 75637400 474e5520 4320342e 382e3520 uct.GNU C 4.8.5
0x00000040 32303135 30363233 20285265 64204861 20150623 (Red Ha
0x00000050 7420342e 382e352d 32382920 2d6d7475 t 4.8.5-28) -mtu
0x00000060 6e653d67 656e6572 6963202d 6d617263 ne=generic -marc
0x00000070 683d7838 362d3634 202d6700 74657374 h=x86-64 -g.test
0x00000080 2e630074 65737461 00746573 74620063 .c.testa.testb.c
0x00000090 68617200 har.
四、.debug_info解析
.debug_info包含了DWARF的DIEs,DIE则重在描述数据类型。DIE包含一个tag,指定了DIE所描述的类型。DIE还包含属性列表,用于描述该类型的具体信息。
本节主要详细介绍了两种不同tag的DIE,分别是结构体类型(对应的tag是DW_TAG_structure_type)和引用类型(只介绍DW_TAG_const_type)。理解这两种DIE,非常有助于理解libbpf BTF的去重算法。
结构体类型
下面是一段简单的C语言代码。
struct test{
int a;
char b;
}test;
int main()
{
return 0;
}
- 首先将该C代码编译成二进制,即gcc -g -o test test.c;
- 运行命令:readelf -wi test,可以得到如下文本:
<1><2d>: Abbrev Number: 2 (DW_TAG_structure_type)
<2e> DW_AT_name : (indirect string, offset: 0x1c): test
<32> DW_AT_byte_size : 8
<33> DW_AT_decl_file : 1
<34> DW_AT_decl_line : 1
<35> DW_AT_sibling : <0x4e>
<2><39>: Abbrev Number: 3 (DW_TAG_member)
<3a> DW_AT_name : a
<3c> DW_AT_decl_file : 1
<3d> DW_AT_decl_line : 2
<3e> DW_AT_type : <0x4e>
<42> DW_AT_data_member_location: 0
<2><43>: Abbrev Number: 3 (DW_TAG_member)
<44> DW_AT_name : b
<46> DW_AT_decl_file : 1
<47> DW_AT_decl_line : 3
<48> DW_AT_type : <0x55>
<4c> DW_AT_data_member_location: 4
<2><4d>: Abbrev Number: 0
<1><4e>: Abbrev Number: 4 (DW_TAG_base_type)
<4f> DW_AT_byte_size : 4
<50> DW_AT_encoding : 5 (signed)
<51> DW_AT_name : int
<1><55>: Abbrev Number: 5 (DW_TAG_base_type)
<56> DW_AT_byte_size : 1
<57> DW_AT_encoding : 6 (signed char)
<58> DW_AT_name : (indirect string, offset: 0x75): char
<1><79>: Abbrev Number: 7 (DW_TAG_variable)
<7a> DW_AT_name : (indirect string, offset: 0x1c): test
<7e> DW_AT_decl_file : 1
<7f> DW_AT_decl_line : 4
<80> DW_AT_type : <0x2d>
<84> DW_AT_external : 1
<84> DW_AT_location : 9 byte block: 3 20 10 60 0 0 0 0 0 (DW_OP_addr: 601020)
<1><8e>: Abbrev Number: 0
上述的文字可以转换成如下图所示的图形。可以理解以下几点:
- DW_TAG_structure_type的结构体成员组织方式(通过DW_TAG_member字段);
- 如何确定DW_TAG_variable的具体类型(通过DW_AT_type字段);
引用类型
const int a;
int main()
{
return 0;
}
转换之后,得到如下内容:
<1><4a>: Abbrev Number: 3 (DW_TAG_base_type)
<4b> DW_AT_byte_size : 4
<4c> DW_AT_encoding : 5 (signed)
<4d> DW_AT_name : int
<1><51>: Abbrev Number: 4 (DW_TAG_variable)
<52> DW_AT_name : a
<54> DW_AT_decl_file : 1
<55> DW_AT_decl_line : 1
<56> DW_AT_type : <0x64>
<5a> DW_AT_external : 1
<5a> DW_AT_location : 9 byte block: 3 20 10 60 0 0 0 0 0 (DW_OP_addr: 601020)
<1><64>: Abbrev Number: 5 (DW_TAG_const_type)
<65> DW_AT_type : <0x4a>
上述的文字可以转换成如下图所示的图形。可以理解DW_TAG_const_type存在的形式,