zoukankan      html  css  js  c++  java
  • DWARF调试格式的简介

    DWARF调试格式的简介

    Michael J. Eager, Eager Consulting

    Feb, 2007

    翻译:吴晖

    2012年2月

    如果我们可以编写确保能正确工作且永远不需要调试的程序,这将非常美妙。在梦想成真之前,通常的编程周期还将是包括:编写一个程序,编译它,执行它,然后可怕的、灾难般的调试。然后重复以上步骤,直到程序如期工作。

    通过插入打印各种感兴趣变量的值的代码来调试程序是可能的。事实上,在某些情形里,比如调试内核驱动,这可能是最好的方法。存在有允许你一条接一条指令单步执行可执行程序,并以二进制显示寄存器及内存的低级调试器。

    但使用一个源代码级的调试器要容易得多,它允许你单步执行一个程序的源代码,设置断点,打印变量值,并且可能有其它一些功能,比如在调试器里允许你调用你程序中的一个函数。问题是如何协调两个完全不同的程序,编译器及调试器,使程序可以被调试。

    从源代码翻译到可执行文件

    把一个程序从人类可读的形式编译到一个处理器执行的二进制形式的过程是相当复杂的,但它本质上包括了持续地把源代码转换到越来越简单的形式,在每一步丢弃信息,直到最后产生包含简单操作、寄存器、内存地址,及处理器真正理解的二进制值的序列。最终,处理器实际上不在乎你是否使用面向对象编程、模板,还是智能指针;它仅能理解,在有限寄存器及包含二进制值的内存位置上的,非常简单的一组操作。

    在一个编译器读入并解析一个程序的源代码时,它收集关于这个程序的各种信息,比如声明或使用一个变量或函数的行的行号。语义分析扩展这个信息的细节,比如变量及函数实参的类型。优化可能到处移动这个程序的部分、合并类似的片段、展开内联函数,或移除不需要的部分。最终,代码生成(code generation)接受该程序的这个内部表达形式,产生实际的机器指令。通常,在这个机器代码上有另一个遍(pass)执行被称为“窥孔(peephole)”的优化,它可能进一步重排或修改代码,例如,消除重复指令。

    总而言之。编译器的任务是接受精心制作的、可理解的源代码。把它转换为高效但本质上难以理解的机器语言。构建紧凑、快速的代码这个目标,编译器实现得越好,结果越有可能难以理解。

    在这个翻译过程中,编译器收集关于这个程序的信息,在后面调试这个程序时,这些信息将是有用的。要做好这点有两个挑战。第一个是,在这个处理的后半部分,编译器要把对该程序做出的修改关联到程序员写的原始源代码,可能很困难。例如,窥孔优化可能移除了一条指令,因为在由一个C++模板具现(instantiation)中的一个内联函数产生的代码里,它可以切换一个测试的次序。在优化了这个程序后,优化器要把它处理的低级代码与原始的源代码联系起来,就相当困难了。

    第二个挑战是如何足够详细地描述可执行程序,以及它与源原始代码的关系,使得一个调试器可以向程序员提供有用的信息。同时,描述必须足够简洁,这样它不会占据大量的空间,或要求可观的处理器时间来解释。这正是DWARF调试格式要解决的:它是可执行程序与源代码关系的一个紧凑的表示,以一个对于调试器的处理合理有效的方式。

    调试过程

    当一个程序员在一个调试器下运行一个程序时,他或她可能希望进行某些通用的操作。这些中最常用的是设置一个断点,通过指定行号或一个函数名,在源代码的一个特定地点停止调试器。当击中这个断点时,程序员通常会显示局部或全局变量的值,或者函数的实参。在有多个执行路径的情形下,显示调用栈使得程序员了解程序如何到达断点。在检查了这个信息后,程序员可以要求调试器继续执行测试的程序。

    在调试中有另外一些有用的操作。例如,一行一行地单步通过一个程序,进入或跳过调用的函数,可能是有帮助的。在一个模板或内联函数的每个实例上设置一个断点,对于C++程序调试是重要的。在一个函数结束前停止是有用的,这样可以显示或改变返回值。有时,程序员可能希望绕过一个函数的执行,返回一个已知的值,而不是这个函数将(可能不正确地)算出的值。

    仍有有用的、与操作相关的数据。例如,显示一个变量的类型可以避免在源文件中查看这个类型。以不同的格式显示一个变量的值,或者以一个指定的格式显示一个内存或寄存器,也是有帮助的。

    有某些操作可以被称为先进的调试功能:例如,为了可以调试多线程程序,或保存在只写内存的程序,可能希望一个调试器(或其它某些程序分析工具)记录代码的特定节(section)是否已经执行。某些调试器允许程序员在正在测试的程序里调用函数。在不远的过去,调试被优化的程序被视为一个先进的特性。

    一个调试器的任务是尽可能以自然、可理解的方式,向程序员提供执行程序的一个概观,同时允许对其执行进行多样各种不同的控制。这意味着在本质上,调试器必须逆向许多编译器精心制作的变换,把程序的数据及状态转换回,这个程序源代码里程序员原来使用的措辞(terms)。

    对一个调试数据格式,像DWARF,的挑战是,使得这成为可能,甚至容易。

    调试格式

    有几种调试格式:stabs,COFF,PECOFF,OMF,IEEE695,及3个版本的DWARF,它们是一些常用的格式。我不准备深入描述这些格式。这里的目的仅是提到它们,以引入DWARF调试格式。

    Stabs的名字来自symbol table strings(符号表字符串),因为一开始,在Unix的a.out目标文件的符号表里,调试数据保存为字符串。Stabs以文本字符串编码一个程序的信息。起初相当简单,随着时间的推移,stabs演进为一个相当复杂,偶尔令人困惑及很少一致的(less-than-consistent)调试格式。Stabs不是标准化的,也没有很好的文档[1]。SunMicrosystems对stabs做了若干扩展。GCC做了其它的扩展,同时尝试逆向设计(reverse engineer)Sun的扩展。不管怎么说,stabs仍然在广泛使用。

    COFF代表CommonObject File Format(通用目标文件格式),肇始于Unix System V Release 3。COFF格式定义了基本的调试信息,但因为COFF包括了对具名节(named section)的支持,各种各样不同的调试格式,比如stabs,已经用于COFF。COFF最重要的问题是,虽然它的名字中有通用(Common)字样,在每个使用这个格式的架构上,它是不相同的。COFF有许多变种,包括XCOFF(用在IBM RS/6000上),ECOFF(用在MIPS及Alpha上),以及Windows PECOFF。这些变种的文档在不同程度上可以获得,但目标模块的格式及调试信息都不是标准化的。

    PECOFF是MicrosoftWindows从Windows 95开始使用的目标模块格式。它基于COFF格式,包含COFF调试数据,以及Microsoft自己私有的CodeView或CV4调试数据格式。关于这个调试格式的文档粗浅而且难以获取。

    OMF代表ObjectModule Format(目标模块格式),它是在CP/M,DOS及OS/2系统,以及少数嵌入式系统使用的目标文件格式。OMF为调试器定义了公共名及行号信息,也可以包含MicrosoftCV,IBM PM,AIX格式的调试数据。OMF仅为调试器提供最基本的支持。

    IEEE695是MicrotecResearch及HP在1980年代后期,为嵌入式环境联合开发的一个标准的目标文件及调试格式。在1990年,它成为一个IEEE标准。它是一个非常灵活的规范,目标是可用于几乎所有的机器架构。调试格式是块结构的,比其它格式更好地对应于源代码的构造。虽然它是一个IEEE标准,在许多方面,IEEE695更像私有的格式。虽然原始的标准能容易地从IEEE获取,Microtec制作了若干扩展以支持C++及优化代码,对此很少文档记载。从来没有修改这个IEEE标准来包括任何Microtec做出的、及其它的改变。

    DWARF的简史[2]

    DWARF 1 ─ Unix SVR4 sdb及PLSIG

    DWARF起源于Bell实验室在1980年代中期开发的Unix System V Release 4 (SVR4)中的C编译器及sdb调试器。编程语言特别兴趣小组(The Programming Languages Special Interest Group——PLSIG),UnixInternational(UI)的一部分,在1989年把由SVR4产生的DWARF记载为DWARF1。虽然原始的DWARF有几个明显的缺点,但最显著的是它不那么紧凑,PLSIG决定仅以最低限度的修改来标准化SVR4格式。它被明智地在嵌入节(embeddedsector)里采用了,时至今日它仍然在使用,特别是对于小的处理器。

    DWARF 2 ─ PLSIG

    PLSIG继续开发解决DWARF一些问题的扩展,并制作文档,其中最重要的是减少数据产生的数量。并添加了对新语言的支持,比如前途远大的C++。在1990年,DWARF2作为一个标准草案发布。

    作为多米诺理论的现实例子,在PLSIG发布了标准草案后不久,在Motorola的88000微处理器里发现了致命的缺陷。Motorola终止了这个处理器,结果导致了Open88的消亡,一个使用88000开发计算机的公司联合体。反过来,Open88是Unix International的一个支持者,PLSIG的一个赞助商,这导致UI的解散。在UI结束时,PLSIG所留下只是一个邮件列表,及保存了DWARF 2标准草案各种版本的各个ftp站点。一个最终标准从来没发布。

    因为Unix International的消失及PLSIG的解散,几个组织各自决定扩展DWARF 1及2。某些这些扩展特定于一个架构,但其它可能适用于任何架构。不幸,在这些扩展上,不同的组织没有一起工作。这些扩展的文档通常是零散的或者难以获取。或者作为一个GCC开发者可能提出的,追根溯源(tongue firmlyin cheek),这些扩展有良好的文档:你所要做的就是阅读编译器的源代码。DWARF遵循在COFF方面是良好的,并且正在成为分歧实现(divergent implementations)的一个集合,而不是成为一个工业标准。

    DWARF 3 ─ 自由标准组织(Free Standards Group)

    尽管在PLSIG邮件列表(在UI解散后,它在X/Open(后来的开放组)下生存下来)上,有关于DWARF的几个在线讨论,直到1999年末,少有动力来修改(甚至完成)这个文档。在那个时候,在扩展DWARF为HP/Intel IA64架构提供更好的支持,以及更好地归档由C++程序使用的ABI方面,令人感兴趣。这两部分工作被分开了,作者接任为百劫余生的DWARF委员会主席。

    在长达18个月的开发工作及DWARF 3一个规范草案的创建之后,标准化击中了一个可以被称为软补丁(a soft patch)的东西。该委员会(特别的,本作者)希望确保这个DWARF标准容易获取,并确保避免由这个标准的多个来源所导致的分歧。在2003年,DWARF委员会成为自由标准组织(Free Standards Group)里的DWARF工作组。在2005年早期,DWARF 3标准的积极开发及说明重新开始,目标是解决在这个标准里所有已知的问题。在10月,发布一个公开的送审稿以征求公众意见,而DWARF3标准的最终版本在2006年1月发布。在自由标准组织与开源代码开发实验室(Open Source Development Labs——OSDL)合并为Linux基金会后,DWARF委员会回到独立的状态,并创建了自己的网站dwarfstd.org

    DWARF概览

    大多数现代编程语言是块结构的:每个实体(例如,一个类定义或一个函数)被包含在另一个实体中。在一个C程序里,每个文件可能包含多个数据定义、多个变量定义,及多个函数。在每个C函数里,可能有几个数据定义,后跟可执行语句。一个语句进而可以是包含数据定义及可执行语句的复合语句。这构成了词法作用域(lexical scopes),其中名字仅在定义它们的作用域中已知。为了在一个程序里找出一个特定符号的定义,你首先在当前作用域中查找,然后在依次的封装作用域里,直到找到这个符号。在不同作用域中,相同的名字可能有多个定义。编译器非常自然地在内部把一个程序表示为一棵树。

    DWARF遵循这个模型,因为它也是块结构的。在DWARF里每个描述性的实体(entity)(除了最顶层描述源文件的项(entry))被包含在一个父项(parententry)中,并且可能包含子实体(children entities)。如果一个节点包含多个实体,它们都是相互关联的兄弟。一个程序的DWARF描述是一个数结构,它类似于编译器的内部树,其中每个节点可以具有孩子或兄弟。这些节点可能表示类型,变量,或函数。这是一个紧凑的格式,它仅提供描述一个程序某一方面所需要的信息。这个格式可以一个统一的形式扩展,这样一个调试器可以识别并忽略一个扩展,即使它可能不能理解其含义。(这比其它大多数调试格式,在尝试读入修改后的数据时,调试器不可避免地被搞糊涂了的情形,要好得多)。DWARF也被设计为可扩展地描述几乎任何任何机器架构上的过程编程语言,而不是仅限于在有限范围架构上的,描述一个语言或一个语言的一个版本。

    虽然DWARF最常见与ELF目标文件格式关联,它并不依赖于这个目标文件格式。它可以并且已经用于其它目标文件格式。所需要的一切是,在这个目标文件或可执行文件里,构成DWARF数据的不同数据节是可识别的。DWARF不会复制包含在这个目标文件里的信息,比如标记处理器架构,或者文件是bigendian,还是littleendian格式。

    调试信息项(Debugging InformationEntry——DIE)

    标签及属性

    在DWARF里基本的描述项是调试信息项(DebuggingInformation Entry——DIE)。一个DIE有一个标签,它指明了这个DIE描述什么及一个填入了细节并进一步描述该项的属性列表。一个DIE(除了最顶层的)被一个父DIE包含(或者说拥有),并可能有兄弟DIE或子DIE。属性可能包含各种值:常量(比如一个函数名),变量(比如一个函数的起始地址),或对另一个DIE的引用(比如一个函数的返回值类型)。

    图1显示了C的经典hello.c程序的DWARF描述的一个简化的图形化表示。最顶层的DIE表示编译单元。它有两个“孩子”,第一个是描述main的DIE,而第二个描述了基础类型int,这是main返回值的类型。子程序(subprogram)DIE是编译单元(compilation unit)DIE的一个孩子,而基础类型(base type)DIE被子程序DIE中的类型属性引用。我们还会谈及一个DIE“拥有”或“包含”子DIE。



    [1] 在1992年,作者写了一篇全面的,描述了由Sun Microsytems的编译器产生的stabs的文档。很可惜,它没有广泛地传播开来。

    [2]  名字DWARF某种程度是一个双关语,因为它与ELF目标文件格式一同开发。这个名字可能是“Debugging With Attributed RecordFormats(使用属性化记录格式调试)”的首字母缩写词,输入在在任何DWARF标准中都没有提到。

     


    图1:DWARF数据的图形化表示

    DIE类型

    DIE可以被分为两个一般的类型。即描述数据,包括数据类型;及描述函数及其他可执行代码。

    描述数据及类型

    大多数编程语言具有复杂的数据描述。存在若干内置数据类型、指针、各种数据结构,及通常构建新的数据类型的方法。因为DWARF的目的是用于各种各样的语言,它抽象出了基本的概念,并提供了可以用于所有所支持语言的一个表示法。主要的类型是直接构建在硬件上的基本类型。其它数据类型构造为这些基本类型的集合(collections)或合成(compositions).

    基本类型

    每个编程语言定义了几个基本的标量(scalar)数据类型。例如,C和Java定义了int及double。但Java提供了这些类型的一个完整的定义,C仅指定了某些通用的特性,允许编译器选择最适合目标处理器的实际规格。某些语言:像Pascal,允许定义新的基本类型,例如,一个可以保存0到100之间值的整数类型。Pascal没有指定这如何实现,一个编译器可能把这实现为单个字节,另一个可能使用一个16比特整数,第三个可能把所有整数类型实现为32比特值,不管怎么定义它们。

    使用DWARF版本1及其他调试格式,编译器及调试器被假定共享关于一个int是16,32或甚至64比特的一个共同的理解。当相同的硬件可以支持不同大小的整数,或不同的编译器对相同的目标处理器有不同的实现决定时,这变得不合适。这些假定,通常没有文档记录,使得在不同编译器或调试器,甚至同一个工具不同的版本间很难兼容。

    在简单数据类型与它们如何在目标机器硬件上实现之间,DWARF基本类型提供了最低级的映射。对于java及C,这明确了int的定义,并且甚至允许在同一个程序里使用不同的定义。图2a显示了描述在一个典型32位处理器上int的DIE。属性指明了名字(int)、一个编码(有符号二进制整数),及字节数(4)。图2b显示了在16位处理器上的int一个类似定义。(在图2里,我们使用了在DWARF标准中定义的标签及属性名,而不是上面使用的更随便的名字。标签的名字带有前缀DW_TAG,而属性的名字带有前缀DW_AT)。

    图2a. 在32位处理器上的int基本类型


    图2b. 在16位处理器上的int基本类型

    基本类型允许编译器描述几乎任何一个编程语言标量类型与其在处理器上实际实现间的映射。图3描述了一个保存在4字节字高16位的16比特整数值。在这个基本类型里,有一个指明这个值是16比特宽,且到高位比特的偏移是0的比特大小属性。(这是一个真实生活的例子,取自Pascal一个在栈上在一个字的高半部分传递16比特整数的实现)。


    图3. 保存在一个32位字高16位的16比特字类型

    DWARF基本类型允许描述若干不同的编码,包括地址、字符、定点(fixed point)、浮点,及组合十进制(packeddecimal),除了二进制整数之外。但仍然有一点二义性:例如,没有指定一个浮点数的实际编码;这由硬件实际支持的编码来确定。在一个遵循IEEE754标准,支持32位及64位浮点值的处理器里,依赖于值的大小,由“float”表示的编码是不同的。

    类型合成

    一个指定(named)变量由一个DIE描述,它有各种属性,其中之一是对一个类型定义的引用。图4描述了名为x的int变量。(目前,我们将忽略通常包含在一个描述一个变量的DIE中的其它信息)。


    图4. “int x”的DWARF描述

    int的基本类型把int描述为一个占4字节的有符号二进制整数。x的DW_TAG_variable给出了它的名字及一个类型属性,这个属性援引基本类型DIE。为了清晰起见,在这个及后面的例子里,DIE被依次标记;在真实的DWARF数据里,一个DIE的引用是到这个DIE所在编译单元起始的偏移。这些引用可以援引先定义的DIE,就像在图4中那样,或后面定义的DIE。一旦我们为int构建了一个基本类型DIE,在相同编译单元中的任何变量可以引用这个DIE[1].

     
     


    通过复合,DWARF使用基本类型构造其它数据类型定义。一个新的类型被构建为另一个类型的修改。例如,图5显示了一个指向典型32位机器上一个int的指针。这个DIE定义了一个指针类型,指明其大小是4个字节,进而引用int基本类型。其它DIE描述了const或volatile属性,C++引用类型,或C restrict类型。这些类型DIE可以串接起来描述更复杂的数据类型,比如“constchar **argv”,这个类型描述在图6中。



    [1] 某些编译器在每个编译单元的开头定义了一组公用的类型定义。其它仅为在这个程序中真正引用的类型生成定义。两者都是有效的。

    图5. “int *px”的DWARF描述

    图6

  • 相关阅读:
    ADT中通过Android SDK Manager去安装x86的image时无法下载
    javadoc 生成文档注释
    char类型的字节数
    win7下JDK环境变量设置方法
    artTemplate模板引擎
    输入与输出
    后台弹出JS类
    数据库主键的设计和思考
    Windows下批处理执行MySQL脚本文件
    You know元音字母吗?
  • 原文地址:https://www.cnblogs.com/feng9exe/p/7977219.html
Copyright © 2011-2022 走看看