一种DWARF格式C语言调试信息分析方法
2014-10-20林广栋黄光红耿锐
林广栋 黄光红 耿锐
摘要:DWARF格式是一种常见的调试信息格式,它以节点作为存储调试信息的基本单元。BWDSP系列芯片的调试系统使用一种自主可控的算法分析C语言的DWARF调试信息。该方法首先读取.debug_abbrev节区,获得节点的缩略信息。然后读取.debug_info节区,获取调试信息节点属性的取值,并把这些调试信息存储为内部数据结构。该算法已经在BWDSP系列芯片的调试系统中得到成功的使用,实践验证了其可行性与正确性。
关键词:DWARF;调试信息;调试系统;BWDSP
中图分类号:TP368.1 文献标识码:A 文章编号:1009-3044(2014)25-5825-09
A Method to Analyze DWARF Format C Language Debugging Information
LIN Guang-dong, HUANG Guang-hong, GENG Rui
(NO. 38th Research Institute of China Electronic Technology Group Corporation, Hefei 230088, China)
Abstract: DWARF format is a widely used debugging information format. It uses entries as basic element to store debugging information. The debugger system of BWDSP develops an innovative algorithm to analyze DWARF debugging information of C language. Firstly, the algorithm read .debug_abbrev section to retrieve abbreviation information of entry. Then the algorithm analyzes .debug_info section to get value of attribute of entries, and finally stores the extracted information in local data structures. The algorithm has been applied successfully in debugger system of BWDSP and has been proved to be applicable and corrective.
Key words: DWARF; debugging information; debugger system; BWDSP
BWDSP系列芯片是中国电子科技集团公司第38所自主研发的一系列高性能通用DSP,包括单核、双核等多个型号,受国家十二五“核高基”科技专项支持。BWDSP系列芯片拥有自主开发的调试系统,该调试系统使用自主开发的算法分析调试信息。该调试系统支持的调试信息格式包括DWARF、STABS等等,
DWARF是一种常用的调试信息格式,它包括DWARF1、DWARF2、DWARF3三个版本。其中DWARF2格式是使用最广泛,定义最标准的DWARF格式。BWDSP芯片调试系统支持对DWARF2版本调试信息的分析。
DWARF格式中,行号调试信息在.debug_line节区存储,而高级语言的源文件、函数、变量、类型等调试信息在.debug_info节区中存储。该文主要介绍对高级语言调试信息的分析,提出的算法主要针对C语言调试信息。
.debug_info节区中,调试信息以节点的形式存在。节点可以存储一个源文件的调试信息、一个变量的调试信息、一个函数的调试信息等等。节点之间存在兄弟或父子的关系,一个源文件的调试信息节点形成一个调试信息树。若被调试文件由多个源文件联合编译生成,则.debug_info节区会包含多个源文件的调试信息,这些源文件的调试信息树构成一个调试信息森林数据结构。
.debug_info节区中的节点有不同的类型和格式,但大多数节点的类型和格式是相同的,为了节省存储空间,DWARF在.debug_abbrev节区中定义了所有节点的类型和格式。.debug_info节区中存储节点调试信息时,只需要引用.debug_abbrev节区中存储的节点类型格式等信息即可,然后只存储节点的取值即可。所以,分析.debug_abbrev节区是分析.debug_info节区的先决条件。
本文将详细介绍DWARF格式C语言调试信息的格式以及在BWDSP系列芯片中应用的一种自主可控的解析方法和存储数据结构。
1 DWARF格式C语言调试信息简介
DWARF格式高级语言调试信息以树的形式存在,所有调试信息都可以用树中的节点表示。一个节点用一个整数来表示其类型。DWARF2中预定义的节点类型有如表1所示。
每个节点表示一种调试信息。例如,DW_TAG_compile_unit表示源文件调试信息,DW_TAG_variable表示变量,DW_TAG_subprogram表示函数,DW_TAG_typedef表示自定义类型等等。每个节点有若干组属性,这些属性描述该节点的特点。例如,常见的DW_TAG_compile_unit节点的属性及属性的格式如图1所示。该节点描述源文件的调试信息,每个源文件都有一个该类型的节点描述。
图1 DW_TAG_compile_unit类型节点的属性及属性的格式
每个预定义的TAG具有的属性并不是固定的。例如,常见的节点DW_TAG_compile_unit具有上述几种属性,但对不同的源文件DW_TAG_compile_unit节点也可以具有其他属性。所以,在表示一个节点的调试信息时,必须把它具有什么属性也表示出来。DWARF2中预定义的属性值如表2所示。
表2 DWARF格式常见属性类型及属性格式
[属性名\&属性含义\&属性常用格式\&DW_AT_name\&节点描述的调试信息的名字,如文件名、函数名、变量名、类型名等等。\&DW_FORM_string\&DW_AT_high_pc\&一段程序的开始地址\&DW_FORM_addr\&DW_AT_low_pc\&一段程序的结束地址\&DW_FORM_addr\&DW_AT_encoing\&基类型的编码形式\&DW_FORM_data1\&DW_AT_decl_file\&节点描述的调试信息定义的文件号\&DW_FORM_data1\&DW_AT_decl_line\&节点描述的调试信息定义的行号\&DW_FORM_data1
DW_FORM_data2
DW_FORM_data4\&DW_AT_type\&类型索引\&DW_FORM_ref4\&DW_AT_sibling\&兄弟节点的位置\&DW_FORM_ref4\&DW_AT_byte_size\&调试信息以字节为单位的大小,如类型的大小、变量的大小等等\&DW_FORM_data1\&DW_AT_bit_size\&调试信息以位为单位的大小,如结构体元素的大小\&DW_FORM_data1\&DW_AT_bit_offset\&调试信息以位为单位的位置,如结构体元素的位置\&DW_FORM_data1\&DW_AT_comp_dir\&源文件的编译目录,这里指编译器编译源文件时的工作目录\&DW_FORM_string\&]
节点的每种属性都要有一个确定的取值,该属性的取值可以有多种格式,有4字节整数、2字节整数、字符串、LEB128数、节区内的偏移值等等。LEB128数是DWARF定义的一种压缩存储数据方式,它可以描述任意范围的数据,但又可以占用较少的空间。DWARF2中属性的常见格式如表3所示。
表3 DWARF格式中属性常用格式及其内部存储结构
[格式名\&格式含义\&存储该格式的数据结构类型\&DW_FORM_addr\&32位绝对地址\&unsigned int\&DW_FORM_block1\&首先是一个1字节的长度信息,其后则是长度在0-255字节之间的二进制信息块,该信息块的长度由第一个字节指定\&shared_ptr
unsigned int len;\&DW_FORM_block2\&首先是一个2字节的长度信息,其后则是长度在0-65535字节之间的二进制信息块,该信息块的长度由前2个字节指定\&shared_ptr
unsigned int len;\&DW_FORM_block4\&首先是一个4字节的长度信息,其后则是连续的二进制信息块,该信息块的长度由前4个字节指定\&shared_ptr
unsigned int len;\&DW_FORM_data1\&1字节大小的无符号立即数\&unsigned char\&DW_FORM_data2\&2字节大小的无符号立即数\&unsigned short\&DW_FORM_data4\&4字节大小的无符号立即数\&unsigned int\&DW_FORM_string\&以\0字符结束的字符串\&string\&DW_FORM_ref4\&4字节大小的立即数,表示相对于该源文件调试信息节点的偏移\&unsigned int\&DW_FORM_sdata\&有符号型LEB128数\∫\&DW_FORM_udata\&无符号型LEB128数\&unsigned int\&]
除属性外,节点还有一个默认的属性:是否有子节点。一般,调试信息按其从属关系确定子节点。例如源文件调试信息DW_TAG_compile_unit节点的子节点是该源文件中的函数调试信息DW_TAG_subprogram节点。DW_TAG_subprogram节点的子节点包括函数参数、在该函数中定义的局部变量等等。DW_TAG_compile_unit节点的子节点还包括在该源文件中定义的全局变量节点、类型节点等等。不同类型节点之间的从属关系见表4。
表4 DWARF格式中常见的父子节点类型
[节点类型\&可能的子节点类型\&DW_TAG_compile_unit\&DW_TAG_subprogram
DW_TAG_base_type
DW_TAG_pointer_type
DW_TAG_array_type
DW_TAG_structure_type
DW_TAG_typedef
DW_TAG_variable\&DW_TAG_structure_type\&DW_TAG_member\&DW_TAG_subprogram\&DW_TAG_formal_parameter
DW_TAG_variable
DW_TAG_pointer_type
DW_TAG_array_type
DW_TAG_structure_type
DW_TAG_typedef
DW_TAG_lexical_block\&DW_TAG_array_type\&DW_TAG_subrange_type\&]
一般,一个被调试文件的调试信息中有很多节点,这些节点的类型大多数是相同的。例如,若一个可执行文件由若干个源文件编译而成,其调试信息中必然有若干个DW_TAG_compile_unit节点,这些节点的属性类型、属性的格式一般是相同的。又比如,一般一个源文件中会有若干个函数,则该源文件调试信息的DW_TAG_compile_unit节点必然有很多子节点是DW_TAG_subprogram节点,这些子节点的属性类型、属性格式也相同。如果把这些相同类型节点的属性类型及其格式都显式地表示出来,会造成很多存储空间的浪费。因此,DWARF2规定,所有节点的类型、属性类型、属性格式、是否有子节点等信息都定义在.debug_abbrev节区内。而.debug_info节区内只记录各节点的属性的具体取值。.debug_abbrev节区中定义的节点类型、属性类型等信息称为节点的缩略信息。例如,对上述DW_TAG_compile_unit节点,该节点的类型、属性的类型、属性的格式等信息存储在.debug_abbrev节区内。属于该类型的节点可以有若干个,它们各属性的具体取值存储在.debug_info节区中。图1是一个DW_TAG_compile_unit类型节点及其属性类型、属性格式在.debug_abbrev节区中的定义,而图2则是该类型节点在.debug_info节区中的具体取值。
图2 DW_TAG_compile_unit在.debug_info节区中的具体取值
2 DWARF格式调试信息分析方法
2.1 .debug_abbrev节区分析方法
根据DWARF格式调试信息的特点,读取DWARF调试信息的第一步是读取.debug_abbrev节区内各节点的缩略信息。首先定义读取.debug_abbrev节区需要的数据结构。下面是存储属性类型及属性格式的数据结构。
typedef struct _AttAbbrev
{
unsigned int at;//属性类型
unsigned int form;//属性格式
}AttAbbrev;
AttAbbrev结构体可以存储一个属性的类型及格式信息。在此基础上,定义存储节点缩略信息的数据结构。
typedef struct _TagAbbrev
{
unsigned int abbCode;//节点缩略信息编号
unsigned int tag;//节点类型
unsigned char haschildren;//该节点是否有子节点
vector
}TagAbbrev;
TagAbbrev结构体可以存储节点的类型、属性类型、属性的格式、有无子节点等信息。
typedef struct _CompilationUnitTags
{
unsigned int offset;//一个源文件的所有节点的缩略信息在.debug_abbrev节区中的偏移
vector
}CompUnitTags;
CompUnitTags结构体可以存储一个源文件的所有节点缩略信息。
在.debug_abbrev节区中,节点类型、属性类型、属性格式类型都以无符号LEB128数的格式存储。它们存储顺序依次为:节点的缩略信息编号、节点的类型、若干组属性类型和属性格式类型定义。若一组属性类型和属性格式类型都为0,标志着一个节点的属性信息结束。每个源文件的节点缩略信息连续存储,节点缩略信息编号从1开始,连续递增。不同的源文件也可以共享节点缩略信息。若一个节点的缩略信息编号为0,标志着一个源文件的节点缩略信息的结束。.debug_abbrev节区中的信息结构如图3示意。
分析.debug_abbrev节区的算法流程图如图4所示。
2.2 .debug_info节区分析方法
一个节点的属性及其值用如下数据结构表示。
typedef struct _EntryAtt
{
unsigned int att;//属性标识,标志着属性的类型,如DW_AT_name,DW_AT_type等等
unsigned int form;//属性格式,标志着属性的形式,如DW_FORM_data1等等
//以下为属性内容,这几种形式的属性内容只能取其一。
string str;//字符串形式的属性
shared_ptr
unsigned int len;//数据块属性长度。block和len必须同时赋值。
unsigned long long u8;//存储64位无符号数
long long i8;//存储64位有符号数
unsigned int u4;//存储32位无符号数
unsigned short u2;//存储16位无符号数
unsigned char u1;//存储8位无符号数
}EntryAtt;
其中shared_ptr
EntryAtt数据结构可以存储一个属性的类型、格式及取值。由于该结构体需要存储各种格式的属性值,所以它必须支持存储所有DWARF2可能属性格式。各种属性格式的存储数据结构见表3。但由于一个属性的格式只有一种,所以该结构体中存储属性值的元素只能使用一个。
存储一个节点的调试信息的数据结构如下:
typedef struct _Entry
{unsigned int uCpIdx;//该节点调试信息所在源文件的编号索引
unsigned int uEtIdx;//该节点调试信息在该源文件所有节点中的编号索引
unsigned int tag;//标志节点的类型,如DW_TAG_variable等等
unsigned int offset;//该节点调试信息在.debug_info节区中的偏移
unsigned int depth;//该节点在DWARF格式调试信息树中的深度
unsigned int abbrevCode;//节点缩略信息号
bool done;//标志一个节点是否已经被分析过
bool hasChildren;//是否有孩子
vector
}Entry;
Entry数据结构中,除DWARF格式中定义的一些节点调试信息外,还有一些帮助分析调试信息的元素,如uCpIdx、uEtIdx、depth、done等等。其中uCpIdx记录该节点所属源文件调试信息的索引号,uEtIdx是该节点在其所在源文件中的索引号,depth表示该节点在DWARF调试信息树中的深度,done标记该节点是否已经被分析完成,offset标志该节点在.debug_info节区中的偏移。
一个源文件的所有节点调试信息记录在一个数据结构中:
typedef struct _CompUnit
{unsigned int offset;//一个源文件的调试信息在.debug_info节区的开始位置
unsigned int endoffset;//一个源文件的调试信息在.debug_info节区的结束位置
unsigned int abbOffset;//一个源文件的节点缩略信息在.debug_abbrev节区中的位置
unsigned int len;// 一个源文件的调试信息在.debug_info节区的长度
unsigned short version; //调试信息版本
unsigned char potSize;//程序中地址的大小,32位计算机中为4,64位计算机中为8
vector
}CompUnit;
同样,源文件的调试信息数据结构CompUnit中,除DWARF格式规定的信息外,还有一些为方便解析调试信息而加入的额外信息,如offset、endoffset。Offset表示一个源文件的节点调试信息在.debug_info节区中的开始位置,endoffset表示一个源文件的节点调试信息在.debug_info节区中的结束位置。
.debug_info节区中,源文件的节点调试信息是连续存放的。例如,若一个可执行文件由a.c和b.c编译而成,则a.c的调试信息在.debug_info中存放为连续的二进制块,b.c的调试信息也存储为连续的二进制块,两者不会交叉。
每个源文件的调试信息以一个源文件头开始。该源文件头不是节点,其格式与节点的格式不同。源文件头按顺序包含如下信息:
1) 一个4字节的长度信息len,该长度表示.debug_info节区中为该源文件产生的调试信息的长度,这里的长度不包含存放该长度信息的4字节;
2) 一个2字节的版本信息,该版本信息该源文件调试信息的DWARF格式版本号;
3) 一个4字节的偏移,该偏移指该源文件调试信息的节点缩略信息在.debug_abbrev节区中的位置;
4) 一个1字节的地址大小。例如,对于32位计算机,该值为4,对于64位计算机,该值为8。
源文件调试信息头仅仅给出与该源文件与DWARF存储格式有关的一些信息。对每一个源文件,都会有一个DW_TAG_compile_unit类型节点,该节点会给出关于该节点更详细的调试信息。一般,DW_TAG_compile_unit节点是该源文件调试信息的第一个节点,并且是该源文件DWARF调试信息节点树的根节点。
源文件调试信息头之后就是一系列的DWARF调试信息节点值。每个节点以一个LEB128数开始,这个数记录该节点的缩略信息编号。调试信息分析程序可以从.debug_abbrev节区中根据该缩略信息编号找到该节点类型、属性类型、属性格式等信息。该LEB128缩略信息编号之后就是该节点各属性的取值。若该节点缩略信息中记录该节点有子节点,则.debug_info节区中此后的节点为该节点的子节点,这些节点在调试信息树中的深度比本节点深一层。若遇到一个节点的缩略信息编号为0,表示子节点结束,此后的节点在调试信息树的上一层。.debug_info节区的内容格式如图5示意。
.debug_info节区分析算法流程如图6所示。
3 实验结果
DWDSP系列芯片调试系统的DWARF调试信息分析算法在分析调试信息的过程中,不断通过向日志文件中写入调试信息来记录每一步的数据。通过这种方法可以验证解析过程中每一步的正确性,方便追踪错误调试信息的来源。当所有调试信息都得到解析并处理完成后,输出获得的最终调试信息,与源代码进行验证,可以验证调试信息分析算法的正确性。如下为一个示例性的源文件。
int fun(int a)
{return a*a;
}
int main()
{typedef struct _stu
{int a;
char * b;
}stu;
char c='z';
stu s;
s.a=10;
s.b=&c;
return fun(s.a);
}
经过调试信息解析算法分析处理后,得到的最终调试信息如图5所示。可见,解析得到的最终调试信息与源代码一致,算法处理过程正确。该文提出的解析算法经过大量源代码测试,已经证实该算法是正确有效的算法。
4 结束语
本文介绍了高级语言的DWARF2格式调试信息。该文还着重介绍了BWDSP系列芯片调试系统使用的DWARF格式调试信息分析算法。该算法首先分析.debug_abbrev节区,读取调试信息节点的缩略信息。然后该算法分析.debug_info节区,读取各节点的值。该文还介绍了该算法使用的主要数据结构。该算法在BWDSP芯片的调试系统中已经得到应用,经过实际使用,该算法的正确性已经得到确保。该算法还通过输出日志的方式验证分析过程每一步的正确性。通过日志文件最后一步输出的调试信息,可以验证该算法的正确性。
参考文献:
[1] Unix International Programming Languages SIG. DWARF Debugging Information Format. UNIX International.
[2] Julia Menapace, Jim Kingdon, David MacKenzie. The “stabs” debug format. Free Software Foundation.
[3] 黄光红,刘冠南.可配置多核处理器的调试器模块化分层设计[J].单片机与嵌入式系统应用,2014,14(7):13-15.
[4] 余锋林,刘小明,朱艳,鲍华.BWDSP100集成开发环境设计与实现[J].中国集成电路,2012,21(6):25-29.