到目前为止,你已经偶尔听说过矮人,调试信息,一种无需解析就能理解源代码的方法。今天我们将详细介绍源代码级调试信息,为本指南后面的使用做准备。系列文章索引这些链接会随着后续文章的发布而逐渐生效。PrepareEnvironmentBreakpointsRegistersandMemoryElvesanddwarvesSourceCodeandSignalsSourceCodeandSignalsSourceCodeLevelSteppingSourceCodeLevelBreakpointsCallStackUnwindingReadingVariablesNextStepELF和DWARF介绍ELF和DWARF可能有两个你没有听说过,但可能其中大部分是一直在使用的组件。ELF(ExecutableandLinkableFormat,可执行和可链接格式)是Linux系统中使用最广泛的目标文件格式;它指定了一种存储二进制文件所有不同部分的方法,例如代码、静态数据、调试信息和字符串。它还告诉加载程序如何加载二进制文件并准备执行,包括二进制文件的不同部分应该放在内存中的什么位置,哪些位需要根据其他组件的位置进行固定(重新分配),等等。我不会在这些博客文章中过多介绍ELF,但如果您有兴趣,可以查看这个漂亮的信息图或标准。DWARF是ELF常用的调试信息格式。它不必绑定到ELF,但两者已经共同发展并且可以很好地协同工作。这种格式允许编译器告诉调试器原始源代码如何与正在执行的二进制文件相关联。此信息分为不同的ELF部分,每个部分都链接到自己的信息。下面不同部分的定义,信息取自这个稍微过时但非常重要的DWARF调试格式介绍:(DWARFInformationEntries)(DIEs)coreDWARFdata.debug_linelinenumberprogram.debug_loclocationdescription.debug_macinfomacrodescription.debug_pubnamesglobalobjectandfunctionlookuptable.debug_pubtypesglobaltypelookuptable.debug_rangesreferenceaddressrangeofDIEs.debug_str.debug_info使用的字符串列表.debug_typestypedescription我们最关心的是.debug_line和.debug_info部分,我们来看一下DWARF信息对于一个简单的程序。intmain(){longa=3;longb=2;longc=a+b;a=4;}DWARF行表如果你用-g选项编译这个程序,然后将结果传递给dwarfdump执行,在行号你应该看到这样的部分:.debug_line:linenumberinfoforasinglecuSourcelines(fromCU-DIEat.debug_infooffset0x0000000b):NSnewstatement,BBnewbasicblock,ETendoftextsequencePEprologueend,EBepiloguebeginIS=valISAnumber,DI=valdiscriminatorvalue[lno,col=DIpurath"EBIS"0x00[4006,0]NSuri:"/home/simon/play/MiniDbg/examples/variable.cpp"0x00400676[2,10]NSPE0x0040067e[3,10]NS0x00400686[4,14]NS0x0040068a[4,16]0x0040068e[4,10]0x00400692[5,7]NS0x0040069a[6,1]NS0x0040069c[6,1]NSET前几行是一些关于如何解释转储的信息——主行号数据从以0x00400670开头的行开始。实际上,这是代码内存地址到文件中行号和列号的映射。NS表示该地址标志着一条新语句的开始,常用于设置断点或单步执行。PE表示函数序言结束(LCTT译注:在汇编语言中,函数序言是程序的前几行代码,用于准备函数中使用的堆栈和寄存器),对设置很有帮助函数断点。ET表示翻译单元结束。信息实际上并不是这样编码的;真正的编码是一种非常节省空间的排序过程,可以执行它来构建这些行信息。那么,假设我们要在variable.cpp的第4行设置断点,我们该怎么做呢?我们寻找文件对应的入口,然后寻找对应的行入口,寻找对应的地址,并在那里设置断点。在我们的例子中,条目是:0x00400686[4,14]NS假设我们想在地址0x00400686处设置一个断点。如果您想尝试一下,可以使用您编写的调试器手动完成。反之亦然。如果我们已经有了一个内存地址——比如说,一个程序计数器值——并且想找到它在源代码中的位置,我们只需要从行表信息中查找最近的映射地址并从中获取行号。DWARF调试信息.debug_info部分是DWARF的核心。它为我们提供了有关程序中存在的类型、函数、变量、希望和梦想的信息。这部分的基本单元是DWARF信息条目(DWARFInformationEntry),我们亲切地称之为DIEs。DIE由一个标记组成,该标记告诉您表示的是什么源代码级实体,后面是该实体的属性列表。这是我上面展示的简单事例程序的.debug_info部分:.debug_infoCOMPILE_UNIT:<0><0x0000000b>DW_TAG_compile_unitDW_AT_producerclangversion3.9.1(tags/RELEASE_391/final)DW_AT_languageDW_LANG_C_plus_plusDW_AT_name/super/secret/path/MiniDbg/examples/variable.cppDW_AT_stmt_list0x00000000DW_AT_comp_dir/super/secret/path/MiniDbg/buildDW_AT_low_pc0x00400670DW_AT_high_pc0x0040069cLOCAL_SYMBOLS:<1><0x0000002e>DW_TAG_subprogramDW_AT_low_pc0x00400670DW_AT_high_pc0x0040069cDW_AT_frame_baseDW_OP_reg6DW_AT_namemainDW_AT_decl_file0x00000001/super/secret/path/MiniDbg/examples/variable.cppDW_AT_decl_line0x00000001DW_AT_type<0x00000077>DW_AT_externalyes(1)<2><0x0000004c>DW_TAG_variableDW_AT_locationDW_OP_fbreg-8DW_AT_nameaDW_AT_decl_file0x00000001/super/secret/path/MiniDbg/examples/variable.cppDW_AT_decl_line0x00000002DW_AT_type<0x0000007e><2><0x0000005a>DW_TAG_variableDW_AT_locationDW_OP_fbreg-16DW_AT_namebDW_AT_decl_file0x00000001/super/secret/path/MiniDbg/examples/variable.cppDW_AT_decl_line0x00000003DW_AT_type<0x0000007e><2><0x00000068>DW_TAG_variableDW_AT_locationDW_OP_fbreg-24DW_AT_namecDW_AT_decl_file0x00000001/super/secret/path/MiniDbg/examples/variable.cppDW_AT_decl_line0x00000004DW_AT_type<0x0000007e><1><0x00000077>DW_TAG_base_typeDW_AT_nameintDW_AT_encodingDW_ATE_signedDW_AT_byte_size0x00000004<1><0x0000007e>DW_TAG_base_typeDW_AT_namelongintDW_AT_encodingDW_ATE_signedDW_AT_byte_size0x00000008***个DIE表示一个编译单元(CU),实际上是一个包括了所有#includes和类似语句的源文件以下是带有含义注释的属性:DW_AT_producerclangversion3.9.1(tags/RELEASE_391/final)<--生成二进制文件的编译器DW_AT_languageDW_LANG_C_plus_plus<--原始编程语言DW_AT_name/super/secret/path/MiniDbg/examples/variable。cpp<--CU代表的文件名DW_AT_stmt_list0x00000000<--跟踪CU的行表偏移DW_AT_comp_dir/super/secret/path/MiniDbg/build<--编译目录DW_AT_low_pc0x00400670<--CU的代码开始DW_AT_high_pc0x0040069c<--CU代码末尾的其他DIE遵循类似的模式,您可能可以推断出不同属性的含义。现在我们可以尝试根据我们新发现的DWARF知识解决一些实际问题。我目前在哪个职能部门?假设我们有一个程序计数器值,想知道我们当前在哪个函数中。解决这个问题的一个简单算法:函数或内联。如果有内联,一旦我们找到一个作用域包括我们的程序计数器(PC)的函数,我们需要递归遍历该DIE的所有子项,以检查是否有一个内联函数可以更好地匹配。在我的代码中,我不处理该调试器的内联,但您可以根据需要添加该功能。如何在函数上设置断点?同样,这取决于您是否要支持成员函数、名称空间等。对于简单的函数,您只需要遍历不同编译单元中的函数,直到找到合适的名称。如果您的编译器能够填充.debug_pubnames部分,您可以更有效地执行此操作。一旦找到该函数,就可以在DW_AT_low_pc给定的内存地址处设置断点。虽然这会在函数序言处中断,但更适合在用户代码处中断。由于行表信息可以指定序言结束的内存地址,所以只需要在行表中查找DW_AT_low_pc的值,然后读取直到标记为序言结束的条目。有些编译器不输出这些信息,所以另一种方法是在函数第二行入口指定的地址处设置断点。Supposewewanttosetabreakpointinthemainfunctionofourexampleprogram.我们查找名为main的函数,获取到它的DIE:<1><0x0000002e>DW_TAG_subprogramDW_AT_low_pc0x00400670DW_AT_high_pc0x0040069cDW_AT_frame_baseDW_OP_reg6DW_AT_namemainDW_AT_decl_file0x00000001/super/secret/path/MiniDbg/examples/variable.cppDW_AT_decl_line0x00000001DW_AT_type<0x00000077>DW_AT_externalyes(1)这告诉我们函数从0x00400670开始。如果我们在行表中查找这个,我们可以得到条目:0x00400670[1,0]NSuritD:"/pathexamples/variable.cpp"我们想跳过序言,所以我们又读了一个条目:0x00400676[2,10]NSPEClang在这个条目中包含序言结束标记,所以我们知道要在这里停止,并设置一个断点在地址0x00400676。如何读取变量的内容?读取变量可能非常复杂。它们是难以捉摸的东西,可能会在整个函数中移动、保存在寄存器中、放置在内存中、被优化掉、隐藏在角落等。幸运的是,我们的简单示例非常简单。如果我们想读取变量a的内容,我们需要查看它的DW_AT_location属性:DW_AT_locationDW_OP_fbreg-8这告诉我们内容存储在距堆栈帧底部偏移-8处。Tofindthestackframebase,welookuptheDW_AT_frame_baseattributeoftheenclosingfunction.DW_AT_frame_baseDW_OP_reg6FromtheSystemVx86_64ABIwecanknowthatreg6istheframepointerregisterinx86.Nowwereadthecontentsoftheframepointer,subtract8fromit,andfindourvariable.如果我们知道它具体是什么,我们还需要看它的类型:<2><0x0000004c>DW_TAG_variableDW_AT_nameaDW_AT_type<0x0000007e>如果我们在调试信息中查找该类型,我们得到下面的DIE:<1><0x0000007e>DW_TAG_base_typeDW_AT_namelongintDW_AT_encodingDW_ATE_signedDW_AT_byte_size0x00000008这tellsusthatthetypeisan8-byte(64-bit)signedinteger,sowecangoaheadandparsethosebytesintoanint64_tanddisplayittotheuser.Ofcourse,typescanbemuchmorecomplicatedthanthat,sincetheyneedtobeabletorepresentC++-liketypes,butthisgivesyouabasicideaof??howtheywork.Goingbacktotheframebaseagain,Clangcankeeptrackoftheframebaseviatheframepointerregister.RecentversionsofGCCtendtouseDW_OP_call_frame_cfa,whichincludesparsingthe.eh_frameELFsection,whichisacompletelydifferentarticlethatIwon'tbewriting.如果你告诉GCC使用DWARF2而不是最近的版本,它会倾向于输出位置列表,这更便于阅读:DW_AT_frame_baselow-off:0x00000000addr0x00400696high-off0x00000001addr0x00400697>DW_OP_breg7+8low-off:0x00000001addr0x00400697high-off0x00000004addr0x0040069a>DW_OP_breg7+16low-off:0x00000004addr0x0040069ahigh-off0x00000031addr0x004006c7>DW_OP_breg6+16low-off:0x00000031addr0x004006c7high-off0x00000032addr0x004006c7>DW_OP_breg6+16low-off:0x00000031addr0x004006c7high-off0x00000032addr0x004006c7>DW_OP_breggivesalistofdifferentpositionsdependingonwheretheprogramcounterislocated.Thisexampletellsusthatiftheprogramcounterisatoffset0x0inDW_AT_low_pc,thentheframebaseisatoffset8fromthevalueheldinregister7,andifitisbetween0x1and0x4,thentheframeThebaseisatanoffsetof16fromthesameposition,andsoon.TakeabreakThere'salotofinformationforyourbraintodigesthere,butthegoodnewsisthatinthenextfewarticleswe'llusealibrarytodothehardworkforus.Understandingtheconceptsisstillhelpful,especiallyifthereisabugoryouwanttosupportsomeDWARFconceptsthatarenotimplementedbytheDWARFlibraryyouareusing.IfyouwanttoknowmoreaboutDWARF,youcangetitsstandardfromhere.Atthetimeofwritingthisblog,DWARF5hasjustbeenreleased,butDWARF4ismoregenerallysupported.