DWARF调试格式的简介(续完)

DWARF调试格式的简介(续完)

2023年7月28日发(作者:)

DWARF调试格式的简介(续完)数组数组类型由⼀个DIE描述,它定义了该数据是以列为主序(就像在Fortan⾥),还是以⾏为主序(就像在C或C++⾥)。该数组的索引由⼀个subrange类型表⽰,这个类型给出了每个维度的上下限。这允许DWARF描述C形式的、总是以0作为最⼩索引的数组;以及在Pascal或Ada中,可以任意值作为上下限的数组。结构体,类,联合,及接⼝⼤多数语⾔允许程序员把数据集中到结构体(在C及C++⾥称为struct,在Pascal⾥称为record)。这个结构体的每个部分通常具有⼀个唯⼀的名字,可能具有不同的类型,并且每个都有⾃⼰的空间。C及C++拥具有union,⽽Pascal拥具有可变(variant)record,它们类似于⼀个结构体,但各部分占据相同的内存位置。Java接⼝具有⼀个C++class特性的⼀个⼦集, 因为它可能仅有抽象⽅法及常量数据成员。虽然每个语⾔有⾃⼰的术语(C++把这些部分称为⼀个类成员,⽽Pascal称它们为域(field)),底层的构造可以描述在DWARF⾥。尊重其传统,DWARF使⽤C/C++术语,同时具有描述struct,union,class及interface的DIE。在这⾥我们将描述类(class)DIE,但其它DIE在本质上具有相同的构造。⽤于⼀个类的DIE是描述这个类每个成员的DIE的⽗亲。每个类有⼀个名字,还可能有其它属性。如果在编译时刻⼀个实例的⼤⼩是已知的,那么它将具有⼀个字节⼤⼩属性。每个这些描述看起来⾮常类似⼀个简单变量的描述,虽然可能有某些额外的属性。例如, C++允许程序员指定⼀个成员是public,private,还是protected。这通过可访问性(accessibility)属性来描述。C及C++允许不是简单变量的⽐特域(bit field)作为⼀个类成员。它们被描述为从这个类实例的开头到这个⽐特域最左侧⽐特的偏移,以及显⽰这个成员占据多少⽐特的⼀个⽐特⼤⼩(bit size)。变量变量通常都相当简单。它们有⼀个名字,代表⼀块可以包含某个类型的⼀个值的内存(或寄存器)。这个变量可以包含的值的类型,以及修改的限制(即,它是否是常量),都由该变量的类型来描述。区分变量的是:该变量保存在何处,及其作⽤域。⼀个变量的作⽤域定义了在这个程序的何处这个变量是已知的,并且在某种程度上,它由该变量在何处声明确定。在C中,在⼀个函数或块中声明的变量具有函数或块作⽤域。那些声明在⼀个函数外的变量具有全局或⽂件作⽤域。这允许在不同的⽂件中定义具有相同名字的、不同的变量,⽽不引起冲突。这也允许不同的函数或编译单元引⽤相同的变量。DWARF使⽤⼀个(file, line,column)三元组记录变量被定义在源⽂件中的何处。DWARF把变量分为3个类别:常量,函数参数,及变量。⼀个常量⽤于描述具有真正命名常量(true named constants)的语⾔,⽐如Ada参数。(C没有把常量⽤作语⾔部分。声明⼀个变量const仅是告诉你,不使⽤⼀个显式的转换,你不能修改这个变量)。⼀个正式的参数代表传递给⼀个函数的值。稍后我们将回到这个话题。某些语⾔,像C或C++(但不包括Pascal),允许声明⼀个变量⽽不定义它。这暗⽰在别的地⽅应该有该变量的⼀个真正的定义,在编译器或调试器可望找到的地⽅。⼀个描述⼀个变量声明的DIE提供了该变量的⼀个描述,但没有告诉调试器它在哪⾥。⼤多数变量具有⼀个描述该变量保存在哪⾥的位置属性。在最简单的情形⾥,⼀个变量保存在内存中,并具有⼀个固定的地址。但许多变量是被动态分配的,⽐如那些声明在⼀个C函数内的,并且定位它们要求某些(通常简单的)计算。例如,⼀个局部变量可能被分配在栈上,定位它可能只是向⼀个框指针(frame pointer)加上⼀个固定的偏移那么简单。在其它情形⾥,这个变量可能保存在⼀个寄存器中。其它变量可能要求稍微复杂的计算来定位数据。作为⼀个C++类成员的⼀个变量可能要求更加复杂的计算,来确定在⼀个派⽣类中基类的位置。位置表达式(Location Expression)DWARF提供了⼀个⾮常通⽤的⽅案来描述如何定位由⼀个变量代表的数据。⼀个DWARF位置表达式包含了告诉⼀个调试器如何定位该数据的⼀连串操作。图7显⽰了3个名为a,b及c的变量的DIE。变量a在内存⾥有⼀个固定的位置,变量b在寄存器0⾥,⽽变量c在当前函数栈框内偏移–12处。虽然a被⾸先声明,描述它的DIE是在所有的函数之后产⽣的。a的实际地址将由链接器填⼊。此外,可能不是⼀个固定地址,但是到这个可执⾏代码载⼊地址的⼀个固定偏移。载⼊器重定位(relocate)了对⼀个可执⾏映像中地址的引⽤,这样,在运⾏时,位置属性包含实际的内存地址。在⼀个⽬标⽂件⾥,位置属性是这个偏移连同⼀个合适的重定位表项(relocationtable entry)。图7. 变量a,b及c的DWARF描述DWARF位置表达式可以包含由⼀个简单栈机器(stack machine)求值的⼀连串操作及值。这可以是⼀个任意复杂的计算,包含种类繁多的算术操作、在该表达式内的测试及跳转、对其它位置表达式的调⽤求值,以及访问处理器的内存或寄存器。甚⾄有操作⽤于描述分裂并保存在不同位置的数据,⽐如⼀个结构体,其中某些数据保存在内存⾥,⽽某些则保存在寄存器中。虽然这巨⼤的灵活性实际上很少使⽤,位置表达式应该允许描述⼀个变量数据的位置,不管这个语⾔的定义如何的复杂,或这个编译器的优化如何的聪明。描述可执⾏代码函数及⼦程序DWARF把返回值的函数及不返回值的⼦例程处理作同⼀个事物的不同变体。稍微偏离其肇始的C的术语,DWARF使⽤⼀个SubprogramDIE描述两者。这个DIE有⼀个名字、⼀个源位置三原体(triplet),及⼀个表⽰这个⼦程序是否是外部的属性,即,在当前编译单元外可见。⼀个Subprogram DIE具有的属性给出了这个⼦程序占据的上下限内存地址,如果⼦程序它是连续的;或者⼀个内存范围列表,如果该函数没有占据⼀组连续的内存地址。低的PC地址被假定为这个例程的⼊⼝,除⾮显式地指定了另⼀个。⼀个函数返回的值由类型属性给出。不返回值的例程(像C的void函数)没有这个属性。DWARF不描述⼀个函数调⽤的约定;这定义在特定架构的应⽤⼆进制接⼝(Application Binary Interface——ABI)中。可能存在能帮助⼀个调试器定位该字程序数据,或找出当前⼦程序调⽤者的属性。返回地址属性是⼀个指明该调⽤者保存地址的位置表达式。框基址(frame base)属性是⼀个计算该函数栈框地址的位置表达式。这些是有⽤的,因为某些编译器有可能执⾏的、最常⽤的优化是:消除显式保存返回地址或框指针(frame pointer)的指令。Subprogram DIE拥有描述这个⼦程序的DIE。由具有variable parameter属性的变量DIE来表⽰可能被传递给⼀个函数的参数。如果这个参数是可选的、或具有⼀个缺省值,这些都由属性来表⽰。这些参数DIE的次序与这个函数的实参列表相同,但中间可能插有额外的DIE,例如,定义由这些参数使⽤的类型。⼀个函数可能定义了可以是局部或全局的变量。这些变量的DIE跟在参数DIE后⾯。许多语⾔允许嵌套词法块(lexical block)。这些由词法块DIE表⽰,它进⽽可能拥有变量DIE,或嵌套的词法块DIE。这⾥是⼀个稍微长些的例⼦。图8a显⽰了strndup.c,GCC中复制⼀个字符串的函数的源代码。图8b列出了为这个⽂件产⽣的DWARF。就像在之前的例⼦中,没有显⽰源代码⾏信息及位置属性。图8a. strndup.c源代码在图8b⾥,DIE <2>显⽰了size_t的定义,它是unsigned int的⼀个typedef。这允许⼀个调试器把形参n的类型显⽰为⼀个size_t,⽽把其值显⽰为⼀个⽆符号整数。DIE <5>描述了函数strndup。它拥有到其兄弟DIE <10>的⼀个指针;接着的所有DIE都是这个SubprogramDIE的孩⼦。该函数返回⼀个描述在DIE<10>中的,指向char的指针。DIE <5>还把该⼦例程描述为外部的、有原型的函数,并给出了该例程的上下限PC值。该例程的形参及局部变量被描述在DIE<6>到<9>中。图8b. strndup.c的DWARF描述编译单元⼤多数有趣的程序包含多个⽂件。构成⼀个程序的每个源⽂件被独⽴编译,然后与系统库链接起来构成这个程序。DWARF把每个单独编译的源⽂件称为⼀个编译单元。每个编译单元的DWARF数据以⼀个Compilation UnitDIE开始。这个DIE包含这个编译单元的通⽤信息,包括⽬录及源⽂件名、使⽤的编程语⾔、⼀个标识这个DWARF数据的产⽣者的字符串,以及到协助定位⾏号及宏信息的DWARF数据节的⼀个偏移。如果该编译单元是连续的(即,它被载⼊⼀块内存中),那么有该单元内存上下限的值。这使得调试器更加容易识别哪个编译单元在⼀个特定内存地址构建代码。如果该编译单元不是连续的,那么由编译器及链接器提供⼀组该代码占据的内存地址。Compilation Unit DIE是所有描述该编译单元的DIE的⽗亲。通常,开始的DIE(多个)将描述数据类型,跟着是全局数据,然后构成这个源⽂件的函数。⽤于变量及函数的DIE出现的次序与这些变量及函数在该源⽂件中出现的次序相同。数据编码从概念上讲,描述⼀个程序的DWARF数据是⼀棵树。每个DIE可能有⼀个兄弟并且包含的若⼲DIE。每个这些DIE有⼀个类型(称为它的TAG)及若⼲属性。每个属性由⼀个属性类型及⼀个值表⽰。不幸的是,这不是⼀个⾮常紧凑的编码。没有压缩的话, DWARF数据是难以处理的。DWARF版本2及3提供了⼏个⽅式来缩⼩这个需要与⽬标⽂件⼀起保存的数据。第⼀个是通过以前序(prefix order)保存“扁平化(flatten)”这棵树。DIE的每个类型被定义为要么有孩⼦,要么没有。如果DIE没有孩⼦,下⼀个DIE是其兄弟。如果DIE可以有孩⼦,那么下⼀个DIE是其第⼀个孩⼦。余下的孩⼦被表⽰为第⼀个孩⼦的兄弟。这样,到兄弟或⼦DIE的链接可以被消除。如果编译器的编写者认为,从⼀个DIE跳到其兄弟,⽽不需要逐个通过其⼦DIE,是有⽤的(例如,跳到在⼀个编译单元⾥的下⼀个函数),那么可以向这个DIE添加⼀个兄弟属性。

第⼆个压缩数据的⽅案是使⽤缩略语。虽然DWARF在产⽣哪个DIE及属性⽅⾯允许⾼度的灵活性,⼤多数编译器仅产⽣有限的⼀组DIE,它们都具有相同的⼀组属性。作为保存这个DIE的TAG值及属性-值对的替代,仅保存⼀个缩略语表的⼀个索引,后跟属性码。每个缩略语给出该标签的值——⼀个表⽰该DIE是否有孩⼦的标记,并带有所期望值的类型的⼀组属性。图9显⽰了⽤于图8b中的形参DIE的缩略语。图8中的DIE <6>实际上如图⽰那样编码 。这以增加⼀些复杂性作为代价,显著减少了需要保存的数据量。编码的项还包括该⽂件及⾏的值,它们没有显⽰在图8b⾥。图9. 缩写项及编码形式DWARF版本3允许从⼀个编译单元引⽤保存在另⼀个编译单元或共享库中的DWARF数据的特性较少使⽤。许多编译器为每个编译单元产⽣相同的缩略语表或基本类型,不管这个编译单元是否真正使⽤所有这些缩略语或类型。这些可以保存在⼀个共享库⾥,并由每个编译单元援引,⽽不是复制在每个编译单元⾥。其它DWARF数据⾏号表DWARF⾏表(linetable)包含了源代码⾏(⽤于⼀个程序的可执⾏部分)与包含对应机器代码的内存之间的映射。在最简单的形式中,这可以被看做⼀个矩阵,其中⼀列包含内存地址,⽽另⼀列包含源代码三元组(⽂件,⾏及列)。如果你希望在特定的⼀⾏上设置⼀个断点,这个表向你给出保存这个断点的内存地址。相反,如果你的程序在内存的某个位置上有⼀个缺陷(⽐如,使⽤⼀个坏的指针),你可以查看最接近这个内存地址的源代码⾏。DWARF通过添加传送⼀个程序额外信息的列进⾏扩展。当⼀个编译器优化这个程序时,它可能移动或删除指令。⼀个给定源代码语句的代码可能没有保存为⼀个机器指令序列,⽽可能是分散的,并插⼊了附近其它语句的指令。识别代表⼀个函数prolog的结尾,或epilog的开始的代码是有⽤的。这样调试器可以在载⼊⼀个函数的所有实参之后,或在这个函数返回之前停⽌。某些处理器可以执⾏多个指令集,因此有另⼀个列表⽰在指定的机器位置保存了哪个集。正如你可能想象的,如果这个表以每条机器指令⼀⾏来保存,它将是巨⼤的。DWARF通过把它编码为⼀个称作⾏号程序的指令序列来压缩这个数据。这些指令由⼀个简单的有限状态机解释来重新构建完整的⾏号表。这个有限状态机使⽤⼀组缺省值初始化。通过执⾏⾏号程序的⼀个或多个操作码产⽣这个⾏号表中的每⾏。通常这些操作码是相当简单的:例如,向机器地址或⾏号添加⼀个值,设置列号,或设置⼀个标记表⽰该内存地址代表⼀个源语句开始、函数prolog结束、或函数epilog开始。⼀组特别的操作码把最常⽤的操作(递增内存地址,及递增或递减源代码⾏号)合并⼊⼀个单操作码(a single opcode)。最后,如果该⾏号表的⼀⾏有与前⾯的⾏相同的源代码三元组, 那么在⾏号程序中不为该⾏产⽣指令。图10列出了strndup.c的⾏号程序。注意仅保存了代表⼀个语句开始指令的机器地址。在这个代码中编译器不能识别基本块,函数prolog的结尾或epilog的开始。在⾏号程序中,这个表仅编码为31个字节。称这为⼀个⾏号程序(line numberprogram)有点⽤词不当。这个程序描述的⽐⾏号要多得多,⽐如指令集、基本块的开始、函数prolog的结尾等。图10. strndup.c的⾏号表宏信息⼤多数调试器很难显⽰并调试带有宏的代码。⽤户查看原始的,带有这些宏的源⽂件,⽽代码对应这些宏产⽣的结果。DWARF包括了定义在这个程序中的宏的描述。这是相当初级的信息,但可以被⼀个调试器⽤于显⽰⼀个宏的值,或有可能把这个宏翻译回源语⾔。调⽤框信息每个处理器有某种特定的⽅式调⽤函数以及传递实参,这通常定义在ABI⾥。在最简单的情形中,对于每个函数这都是相同的,并且调试器确切知道如何找到实参的值及函数的返回地址。对于某些处理器,依赖于该函数如何写,可能有不同的调⽤序列,例如,如果实参数⽬多于⼀个特定的值,取决于操作系统,可能有不同的调⽤序列。编译器将尝试优化这个调⽤序列来使得代码既⼩⼜快。⼀个常⽤的优化是,当有⼀个简单的、不调⽤任何其它函数的函数(⼀个叶⼦函数)时,让它使⽤调⽤者的栈框,⽽不是构建它⾃⼰的。另⼀个优化可能是消除⼀个指向当前调⽤框的寄存器。在这个调⽤过程中,某些寄存器可能被保留,其它则不会。尽管让调试器推敲出调⽤序列或优化的所有可能的排列是可能的,但这既枯燥⼜容易出错。优化及调试器的⼀个⼩修改就可能不能在栈中移动到调⽤函数。DWARF调⽤框信息(Call Frame Information——CFI)向调试器提供了⾜够的关于⼀个函数如何被调⽤的信息,因此它可以定位该函数的每个实参、定位当前调⽤框,以及定位调⽤函数的调⽤框。这个信息被调试器⽤来“回滚栈”,定位前⼀个函数、该函数被调⽤的位置,以及传递的值。类似⾏号表,CFI被编码为⼀个将被解释产⽣⼀个表的指令序列。在这个表中,包含代码的每个地址对应⼀⾏。第⼀列包含机器地址,⽽随后的列包含在该地址指令执⾏时机器寄存器的值。类似于⾏号表,如果如果构建这个表,它将是巨⼤的。幸运的是,两个机器指令间的改变⾮常⼩,因此CFI编码相当紧凑。ELF节虽然DWARF被定义为,允许它与任何⽬标⽂件格式⼀起使⽤,它最通常与ELF⼀起使⽤。每个不同类型的DWARF数据保存在它们⾃⼰的节⾥。所有这些节的名字都以".debug_"开始。为了提升效率,⼤多数DWARF数据的引⽤使⽤到该编译单元数据开头的⼀个偏移。这避免了重定位这个调试数据,可以加速程序的载⼊及调试。ELF节及它们的内容是.debug_abbrev ⽤在.debug_info节的缩写.debug_aranges 内存地址与编译单元之间的⼀个映射.debug_frame 调⽤框信息.debug_info 包含DIE的核⼼DWARF数据.debug_line ⾏号程序.debug_loc 宏描述.debug_macinfo 全局对象及函数的⼀个查找表.debug_pubnames 全局对象及函数的⼀个查找表.debug_pubtypes 全局类型的⼀个查找表.debug_ranges DIE所援引的地址范围.debug_str 由.debug_info使⽤的字符串表总结现在你应该了解了——DWARF简明扼要的解释。嗯,也不是很简明扼要。DWARF调试信息的基本概念是简单的。⼀个程序被描述为⼀棵树,所带的节点以⼀个紧凑的语⾔及机器⽆关的⽅式表⽰源代码中的函数、数据及类型。⾏表提供了可执⾏指令与产⽣它们的源代码之间的映射。CFI描述了如何回滚栈。同样,在DWARF中也有相当多微妙的地⽅,考虑到要为⼤范围的程序语⾔及不同的机器架构表达许多不同的微细差别。DWARF未来的⽅向是提⾼对优化代码的描述,这样调试器可以更好地在由先进的编译器优化产⽣的代码中⾏进。完整的DWARF版本3标准在DWARF⽹站可以免费下载()。还有⼀个⽤于DWARF相关问题及讨论的邮件列表。在⽹站上还有注册这个邮件列表的指引。致谢我想感谢Sun Microsystems的ChrisQuenelle,HP前雇员Ron Brender,感谢他们关于这篇⽂章的意见与建议。同样感谢SusanHeimlich,她给出了很多编辑的建议。使⽤GCC产⽣DWARF使⽤gcc产⽣DWARF⾮常简单。只要指定–g选项产⽣调试信息。可以使⽤带有-h选项的objump来显⽰ELF节。$ gcc –g –c strndup.c$ objdump –h up.o: file format elf32-i386Sections:Idx Name Size VMA LMA File off Algn0 .text 0000007b 00000000 00000000 00000034 2**2 CONTENTS,ALLOC, LOAD, RELOC, READONLY, CODE1 .data 00000000 00000000 00000000 000000b0 2**2 CONTENTS, ALLOC, LOAD, DATA2 .bss 00000000 00000000 00000000 000000b0 2**2 ALLOC3 .debug_abbrev 00000073 00000000 00000000 000000b0 2**0 CONTENTS, READONLY, DEBUGGING4 .debug_info 00000118 00000000 00000000 00000123 2**0 CONTENTS,RELOC, READONLY, DEBUGGING5 .debug_line 00000080 00000000 00000000 0000023b 2**0 CONTENTS, RELOC, READONLY, DEBUGGING6 .debug_frame 00000034 00000000 00000000 000002bc 2**2 CONTENTS, RELOC, READONLY, DEBUGGING7 .debug_loc 0000002c 00000000 00000000 000002f0 2**0 CONTENTS,READONLY, DEBUGGING8 .debug_pubnames 0000001e 00000000 00000000 0000031c 2**0 CONTENTS,RELOC, READONLY, DEBUGGING9 .debug_aranges 00000020 00000000 00000000 0000033a 2**0 CONTENTS, RELOC, READONLY,DEBUGGING10 .comment 0000002a 00000000 00000000 0000035a 2**0 CONTENTS,READONLY11 .-stack 00000000 00000000 00000000 00000384 2**0 CONTENTS,READONLY使⽤Readelf 打印DWARFReadelf可以显⽰及解码在⼀个⽬标⽂件或可执⾏⽂件中的DWARF数据。这些选项是-w displayall DWARF sections-w[liaprmfFso] display specific sectionsl line tablei debug infoa abbreviation tablep public namesr rangesm macro tablef debug frame (encoded)F debug frame (decoded)s string tableo location lists列出的DWARF,即使最⼩的程序也是相当多的,因此把readelf的输出重定向到⼀个⽂件,然后使⽤less或⼀个编辑器,⽐如vi,来浏览这个⽂件,是个好主意。

发布者:admin,转转请注明出处:http://www.yc00.com/news/1690512638a361856.html

相关推荐

发表回复

评论列表(0条)

  • 暂无评论

联系我们

400-800-8888

在线咨询: QQ交谈

邮件:admin@example.com

工作时间:周一至周五,9:30-18:30,节假日休息

关注微信