前言
在我们编译iOS项目的时候会生成macho可执行文件。
项目编译期间会生成.o文件的时同时会生成一个DWARF的调试文件,放到__DWARF符号段中(这些都是Macho的的概念,有空我会专门再写一篇Macho)
。
今天主要讲讲DWARF调试文件到底在做什么。
DWARF概览
DWARF:(Debugging With Attributed RecordFormats 使用属性化记录格式调试),通俗点说就是把我们的代码按一定规则搞成有结构的信息。
DWARF是块结构的。在DWARF里每个描述性的实体(entity)(除了最顶层描述源文件的项(entry))被包含在一个父项(parententry)中,并且可能包含子实体(children entities)。如果一个节点包含多个实体,它们都是相互关联的兄弟。
简单点来说DWARF描述是一个树结构,它类似于编译器的内部树,其中每个节点可以具有孩子或兄弟。这些节点可能表示类型,变量,或函数
。
DWARF被设计为可扩展地描述几乎任何任何机器架构上的过程编程语言,而不是仅限于在有限范围架构上的,描述一个语言或一个语言的一个版本。
虽然DWARF最常见与ELF目标文件格式关联,它并不依赖于这个目标文件格式。它可以并且已经用于其它目标文件格式。所需要的一切是,在这个目标文件或可执行文件里,构成DWARF数据的不同数据节是可识别的。DWARF不会复制包含在这个目标文件里的信息,比如标记处理器架构.
DWARF 内部结构
标签 属性
在DWARF里基本的描述项是调试信息项(DebuggingInformation Entry——DIE)
。一个DIE有一个标签,它指明了这个DIE描述什么及一个填入了细节并进一步描述该项的属性列表。一个DIE(除了最顶层的)被一个父DIE包含(或者说拥有),并可能有兄弟DIE或子DIE。属性可能包含各种值:常量(比如一个函数名),变量(比如一个函数的起始地址),或对另一个DIE的引用(比如一个函数的返回值类型)。
我们看一个例子:上图显示了C的经典hello.c程序的DWARF描述的一个简化的图形化表示。最顶层的DIE表示编译单元(compilation Unit)。它有两个“孩子”,第一个是描述main的DIE(Subprogram),而第二个描述了基础类型int(Base Type),这是main返回值的类型。子程序(subprogram DIE)是编译单元(compilation unit DIE)的一个孩子,而基础类型(base type DIE)DIE被子程序DIE中的类型属性引用。我们还会谈及一个DIE“拥有”或“包含”子DIE。
DIE类型
DIE可以被分为两个一般的类型。即描述数据,包括数据类型
;及描述函数及其他可执行代码
。
描述数据及类型
大多数编程语言具有复杂的数据描述。存在若干内置数据类型、指针、各种数据结构,及通常构建新的数据类型的方法
。因为DWARF的目的是用于各种各样的语言,它抽象出了基本的概念,并提供了可以用于所有所支持语言的一个表示法。主要的类型是直接构建在硬件上的基本类型。其它数据类型构造为这些基本类型的集合(collections)或合成(compositions)
.
基本类型
每个编程语言定义了几个基本的标量(scalar)数据类型。例如,C和Java定义了int及double。
image.png对于java及C,这明确了int的定义,并且甚至允许在同一个程序里使用不同的定义。上图显示了描述在一个典型32位处理器上和16位处理器int的DIE。属性指明了名字(int)、一个编码(有符号二进制整数),及字节数(4)。我们使用了在DWARF标准中定义的标签及属性名,而不是上面使用的更随便的名字。标签的名字带有前缀DW_TAG,而属性的名字带有前缀DW_AT(attitude缩写)
。
类型合成
一个指定(named)变量由一个DIE描述,它有各种属性,其中之一是对一个类型定义的引用。
int的基本类型把int描述为一个占4字节的有符号二进制整数。
x的DW_TAG_variable给出了它的名字及一个类型属性,这个属性援引基本类型DIE
。为了清晰起见,在这个及后面的例子里,DIE被依次标记;在真实的DWARF数据里,一个DIE的引用是到这个DIE所在编译单元起始的偏移。这些引用可以援引先定义的DIE,就像在图4中那样,或后面定义的DIE。一旦我们为int构建了一个基本类型DIE,在相同编译单元中的任何变量可以引用这个DIE.通过组合,DWARF使用基本类型构造其它数据类型定义。一个新的类型被构建为另一个类型的修改。
再来看个指针类型的DWARF的描述:
image.png
变量
变量通常都相当简单。它们有一个名字,代表一块可以包含某个类型的一个值的内存(或寄存器)
。这个变量可以包含的值的类型
,以及修改的限制(即,它是否是常量),都由该变量的类型来描述。
区分变量的是:该变量保存在何处,及其作用域。一个变量的作用域定义了在这个程序的何处这个变量是已知的,并且在某种程度上,它由该变量在何处声明确定。
DWARF使用一个file, line,column)三元组
记录变量被定义在源文件中的何处。
DWARF把变量分为3个类别:常量,函数参数,及变量。一个常量用于描述具有真正命名常量(true named constants)的语言,比如Ada参数。(C没有把常量用作语言部分。声明一个变量const仅是告诉你,不使用一个显式的转换,你不能修改这个变量)。一个正式的参数代表传递给一个函数的值。
位置表达式(Location Expression)
DWARF提供了一个非常通用的方案来描述如何定位由一个变量代表的数据。一个DWARF位置表达式包含了告诉一个调试器如何定位该数据的一连串操作。
下图显示了3个名为a,b及c的变量的DIE
。变量a在内存里有一个固定的位置
,变量b在寄存器0里
,而变量c在当前函数栈框内偏移–12处
。虽然a被首先声明,描述它的DIE是在所有的函数之后产生的。a的实际地址将由链接器填入。
一个完整程序DWARF的描述
前面的铺垫差不多了,我们可以看看完整的程序DWARF描述是什么样子的。
这是一个strndup.c文件
DWARF的描述后
完整的类文件描述.png
如图
DIE <2>显示了size_t
的定义,它是unsigned int的一个typedef
。这允许一个调试器把形参n的类型显示为一个size_t
,而把其值显示为一个无符号整数
。DIE <5>描述了函数strndup
。它拥有到其兄弟DIE <10>的一个指针
;接着的所有DIE都是这个Subprogram DIE的孩子
。该函数返回一个描述在DIE<10>中的,指向char的指针
。DIE <5>还把该子例程描述为外部的、有原型的函数
,并给出了该例程
的上下限PC值。该例程的形参及局部变量
被描述在DIE<6>到<9>中
.
DWARF描述的总结
基本的DWARF的描述的差不多了,写这个文章主要的。
总结下,每个源文件的编译都是由编译单元开始
Compilation Unit DIE是所有描述该编译单元的DIE的父亲。通常,开始的DIE(多个)将描述数据类型,跟着是全局数据,然后构成这个源文件的函数。用于变量及函数的DIE出现的次序与这些变量及函数在该源文件中出现的次序相同的。
写这篇文章主要是在探究Macho执行文件
编译过程中遇到的,写这篇文章是为了让我们更好理解项目在编译过程中生成.o文件
的时同时会生成一个DWARF的调试文件
.这个调试文件到底做了些什么。也能便于我们更好的理解Macho的编译过程。
网友评论