文章目录
1. 简介
ABI Dumper 是 一种转储包含 DWARF 调试信息的 ELF 对象的 ABI 的工具。
该工具旨在与 ABI Compliance Checker 工具一起使用,用于跟踪 C/C++ 库或内核模块的 ABI 更改:https://github.com/lvc/abi-compliance-checker
2. 社区
官方参考:https://github.com/lvc/abi-dumper
最新版本:1.2(发布时间2020-11-20)
许可协议:LGPL-2.1
开发语言:perl
主作者:Andrey Ponomarenko
更新时间:2021-6-11
3. 依赖
- Perl 5
- Elfutils (eu-readelf)
- GNU Binutils
- Universal Ctags (https://github.com/universal-ctags/ctags)
- Vtable Dumper >= 1.1 (https://github.com/lvc/vtable-dumper)
- ABI Compliance Checker >= 2.2 (https://github.com/lvc/abi-compliance-checker)
- GCC C++
4. 使用
4.1. 用法
ABI Dumper 1.2
Dump ABI of an ELF object containing DWARF debug info
Copyright (C) 2019 Andrey Ponomarenko's ABI Laboratory
License: GNU LGPL 2.1
Usage: abi-dumper [options] [object]
Example:
abi-dumper libTest.so -o ABI.dump
abi-dumper Module.ko.debug -o ABI.dump
More info: abi-dumper --help
要求so/ko需要包含DWARF调试信息
4.2. 测试
4.2.1. hello文件
创建目录h,在其中新建hello.h和hello.c如下:
1)hello.h
#ifndef __HELLO_H__
#define __HELLO_H__
void hello(char *what);
#endif
2)hello.c
#include <stdio.h>
#include "hello.h"
void hello(char *what)
{
if (what == NULL) {
return;
}
printf("hello %s\n", what);
}
4.2.2. so文件
在h目录中执行如下命令,生成带有调试信息的so文件:
gcc --share -fPIC -g -o libhello.so hello.c
4.2.3. dump文件
在h目录中执行如下命令,生成dump文件:
abi-dumper libhello.so -o ABI.dump
在执行上述命令,会给出如下提示:
- -lver NULL
指定模块的版本号,用于后续版本间的abi比较 - -Og
如果不使用其他优化选项,请考虑将“-Og”与“-g”一起使用。 完全没有“-O”选项,一些收集对调试有用的信息的编译器传递根本不会运行,因此“-Og”可能会带来更好的调试体验。
对于高版本的gcc带-Og后,abi-dumper在解析调试信息时报错如下:(需完善)
ABI.dump文件内容如下:
$VAR1 = {
'ABI_DUMPER_VERSION' => '1.2',
'ABI_DUMP_VERSION' => '3.5',
'Arch' => 'x86_64',
'GccVersion' => '11.1.1',
'Headers' => {},
'Language' => 'C',
'LibraryName' => 'libhello.so',
'LibraryVersion' => '1',
'MissedOffsets' => '1',
'MissedRegs' => '1',
'NameSpaces' => {},
'Needed' => {
'libc.so.6' => 1
},
'Sources' => {},
'SymbolInfo' => {
'148' => {
'Header' => undef,
'Line' => '4',
'Param' => {
'0' => {
'name' => 'what',
'type' => '102'
}
},
'Return' => '1',
'ShortName' => 'hello'
}
},
'SymbolVersion' => {},
'Symbols' => {
'libhello.so' => {
'hello' => 1
}
},
'Target' => 'unix',
'TypeInfo' => {
'1' => {
'Name' => 'void',
'Type' => 'Intrinsic'
},
'102' => {
'BaseType' => '107',
'Name' => 'char*',
'Size' => '8',
'Type' => 'Pointer'
},
'107' => {
'Name' => 'char',
'Size' => '1',
'Type' => 'Intrinsic'
}
},
'UndefinedSymbols' => {
'libhello.so' => {
'_ITM_deregisterTMCloneTable' => 0,
'_ITM_registerTMCloneTable' => 0,
'__cxa_finalize@GLIBC_2.2.5' => 0,
'__gmon_start__' => 0,
'printf@GLIBC_2.2.5' => 0
}
},
'WordSize' => '8'
};
其实就是perl的嵌套散列表。
5. 原理
5.1. 符号表
对于so文件,abi-dumper通过eu-readelf -s xxx.so
读取符号表信息:
符号表文件说明,可以参考abi-compliance-checker文章的“符号表说明”一节。
abi-dumper只提取了有下述特征的符号:
- Bind 中包含
WEAK、GLOBAL、LOCAL
的符号 - Type 中包含
FUNC、IFUNC、GNU_IFUNC、TLS\OBJECT、COMMON
的符号 - Vis 中包含
DEFAULT、PROTECTE
的符号
5.2. DWARF
DWARF:Debugging With Arbitrary Record Formats 使用任意记录格式进行调试。
参考文章:http://www.dwarfstd.org/doc/Debugging using DWARF-2012.pdf
5.2.1. Overview 概述
大多数现代编程语言都是块结构的:每个实体(例如类定义或函数)都包含在另一个实体中。 C 程序中的每个文件可能包含多个
数据定义、多个变量定义和多个函数。在每个 C 函数中,可能有多个数据定义,后跟可执行语句。语句可以是复合语句,而复合语句又可以包含数据定义和可执行语句。这会创建词法范围,其中名称仅在定义它们的范围内是已知的。要在程序中查找特定符号的定义,首先要查看当前作用域,然后在连续的封闭作用域中查找,直到找到该符号。在不同的范围内可能有多个相同名称的定义。编译器很自然地在内部将程序表示为一棵树。
DWARF 遵循此模型,因为它也是块结构的。 DWARF 中的每个描述实体(除了描述源文件的最顶层条目)都包含在父条目中,并且可能包含子实体。如果一个节点包含多个实体,它们都是兄弟,彼此相关。程序的 DWARF 描述是一个树结构,类似于编译器的内部树,其中每个节点都可以有子节点或兄弟节点。节点可以表示类型、变量或函数。这是一种紧凑格式,其中仅提供描述程序某个方面所需的信息。该格式以统一的方式可扩展,因此调试器可以识别并忽略扩展,即使它可能不理解其含义。 (这比大多数其他调试格式的情况要好得多,在这种情况下,调试器在尝试读取无法识别的数据时会陷入致命的困惑。)DWARF 还被设计为可扩展以描述任何机器架构上的几乎任何过程编程语言,而不是被绑定到只在有限的架构范围内描述一种语言或一种语言版本。
虽然 DWARF 最常与 ELF 目标文件格式相关联,但它独立于目标文件格式。它可以并且已经与其他目标文件格式一起使用。所需要的只是组成 DWARF 数据的不同数据部分在目标文件或可执行文件中是可识别的。 DWARF 不会复制目标文件中包含的信息,例如识别处理器体系结构或文件是否以 bigendian 或 littleendian 格式编写。
5.2.2. Debugging Information Entry (DIE) 调试信息条目
5.2.2.1. Tags and Attributes 标签和属性
DWARF 中的基本描述实体是调试信息条目 (DIE)。 DIE 有一个标签,它指定 DIE 描述的内容以及填充详细信息并进一步描述实体的属性列表。 DIE(除了最上面的)包含在父 DIE 中或由父 DIE 拥有,并且可能有兄弟 DIE 或子 DIE。属性可能包含各种值:常量(例如函数名)、变量(例如函数的起始地址)或对另一个 DIE 的引用(例如函数返回值的类型)。
图 1 显示了 C 的经典 hello.c 程序及其 DWARF 描述的简化图形表示。最上面的 DIE 代表编译单元。它有两个“孩子”,第一个是描述 main 的 DIE,第二个描述基类型 int,它是 main 返回值的类型。子程序 DIE 是编译单元 DIE 的子程序,而基类型 DIE 由子程序 DIE 中的 Type 属性引用。我们还讨论“拥有”或“包含”子 DIE 的 DIE。
5.2.2.2. Types of DIEs 的类型
DIE 可以分为两种一般类型。 描述数据的那些包括数据类型和描述函数和其他可执行代码的那些。
5.2.3. Describing Data and Types 描述数据和类型
大多数编程语言都有复杂的数据描述。 有许多内置数据类型、指针、各种数据结构,以及通常创建新数据类型的方法。 由于 DWARF 旨在用于多种语言,因此它抽象出基础知识并提供可用于所有受支持语言的表示。 直接构建在硬件上的主要类型是基本类型。 其他数据类型被构造为这些基本类型的集合或组合。
5.2.3.1. Base Types 基本类型
每种编程语言都定义了几种基本的标量数据类型。例如,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 标准中定义的标签和属性名称,而不是图 1 中使用的更非正式的名称。标签名称都以 DW_TAG 为前缀,属性名称以 DW_AT 为前缀。)
基本类型允许编译器描述编程语言标量类型与其在处理器上实际实现的方式之间的几乎任何映射。图 3 描述了一个 16 位整数值,它存储在一个四字节字的高 16 位中。在此基本类型中,有一个位大小属性,指定该值是 16 位宽,与零的高阶位的偏移量。
除了二进制整数之外,DWARF 基本类型还允许描述许多不同的编码,包括地址、字符、定点、浮点和压缩十进制。仍然存在一些歧义:例如,未指定浮点数的实际编码;这是由硬件实际支持的编码决定的。在遵循IEEE754标准的同时支持32bit和64bit浮点值的处理器中,“float”表示的编码根据值的大小而不同。
5.2.3.2. Type Composition 类型构成
命名变量由具有多种属性的 DIE 描述,其中之一是对类型定义的引用。图 4 描述了一个名为 x 的整数变量。 (目前我们将忽略通常包含在描述变量的 DIE 中的其他信息。)
int 的基本类型将其描述为占用四个字节的有符号二进制整数。 x 的 DW_TAG_variable DIE 给出了它的名称和一个类型属性,它指的是基本类型 DIE。为清楚起见,在本示例和以下示例中按顺序标记了 DIE;在实际的 DWARF 数据中,对 DIE 的引用是从可以找到 DIE 的编译单元开始的偏移量。引用可以是先前定义的 DIE,如图 4 所示,也可以是稍后定义的 DIE。一旦我们为 int 创建了一个基类型 DIE,同一编译中的任何变量都可以引用同一个 DIE。
DWARF 使用基本类型通过组合构造其他数据类型定义。新类型是作为另一种类型的修改而创建的。例如,图 5 显示了一个指向我们典型 32 位机器上的 int 的指针。此 DIE 定义了一个指针类型,指定其大小为四个字节,并依次引用 int 基类型。其他 DIE 描述了 const 或 volatile 属性、C++ 引用类型或 C 限制类型。这些类型的 DIE 可以链接在一起以描述更复杂的数据类型,例如图 6 中描述的“const char **argv”。
5.2.3.3. Array 数组
数组类型由 DIE 描述,该 DIE 定义数据是按列主序(如 Fortan)还是按行主序(如 C 或 C++)存储。 数组的索引由给出每个维度的下限和上限的子范围类型表示。 这允许 DWARF 描述 C 风格的数组,它们总是以零作为最低索引,以及 Pascal 或 Ada 中的数组,它们可以具有任何上下界值。
5.2.3.4. Structures, Classes, Unions, and Interfaces 结构、类、联合和接口
大多数语言允许程序员将数据组合成结构(在 C 和 C++ 中称为结构,在 C++ 中称为类,在 Pascal 中称为记录)。结构的每个组件通常都有一个唯一的名称,并且可能有不同的类型,并且每个组件都占据自己的空间。 C 和 C++ 具有联合,Pascal 具有类似于结构的变体记录,只是组件占用相同的内存位置。 Java 接口具有 C++ 类的属性子集,因为它可能只有抽象方法和常量数据成员。
尽管每种语言都有自己的术语(C++ 将类成员称为组件,而 Pascal 将它们称为字段),但底层组织可以在 DWARF 中进行描述。忠实于其传统,DWARF 使用 C/C++/Java 术语并具有描述结构、联合、类和接口的 DIE。我们将在这里描述 DIE 类,但其他类具有基本相同的组织。
一个类的 DIE 是描述每个类成员的 DIE 的父级。每个类都有一个名称和可能的其他属性。如果实例的大小在编译时已知,那么它将具有字节大小属性。这些描述中的每一个看起来都非常像一个简单变量的描述,尽管可能有一些额外的属性。例如,C++ 允许程序员指定成员是公共的、私有的还是受保护的。这些是用可访问性属性描述的。
C 和 C++ 允许位域作为不是简单变量的类成员。这些是用从类实例的开头到位字段的最左位的位偏移量和表示成员占用多少位的位大小来描述的。
5.2.3.5. Variables 变量
变量通常非常简单。它们有一个名称,表示可以包含某种值的一块内存(或寄存器)。变量可以包含的值的类型以及对如何修改它的限制(例如,它是否为 const)由变量的类型描述。
变量的区别在于其值的存储位置和作用域。变量的范围定义了程序中已知变量的位置,并且在某种程度上取决于变量的声明位置。在 C 中,在函数或块中声明的变量具有函数或块作用域。在函数外声明的那些具有全局或文件作用域。这允许在不同文件中定义具有相同名称的不同变量而不会发生冲突。它还允许不同的函数或编译引用相同的变量。 DWARF 文档,其中变量在源文件中使用(文件、行、列)三元组进行声明。
DWARF 将变量分为三类:常量、形式参数和变量。常量用于将真正命名常量作为语言一部分的语言,例如 Ada 参数。 (C 语言中没有常量。声明一个变量 const 只是说你不能在不使用显式转换的情况下修改变量。)形式参数表示传递给函数的值。稍后我们会回到这个话题。
某些语言,如 C 或 C++(但不是 Pascal),允许在不定义变量的情况下声明变量。这意味着应该在其他地方有变量的真实定义,希望在编译器或调试器可以找到的地方。描述变量声明的 DIE 提供了变量的描述,而没有实际告诉调试器它在哪里。
大多数变量都有一个位置属性,用于描述变量的存储位置。在最简单的情况下,变量存储在内存中并具有固定地址。但是许多变量,例如在 C 函数中声明的变量,是动态分配的,定位它们需要一些(通常是简单的)计算。例如,一个局部变量可能被分配在堆栈上,定位它可能就像给一个帧指针添加一个固定的偏移量一样简单。在其他情况下,变量可以存储在寄存器中。其他变量可能需要更复杂的计算来定位数据。作为 C++ 类成员的变量可能需要更复杂的计算来确定基类在派生类中的位置。
5.2.3.6. Location Expressions 位置表达式
DWARF 提供了一个非常通用的方案来描述如何定位由变量表示的数据。 DWARF 位置表达式包含一系列操作,这些操作告诉调试器如何定位数据。图 7 显示了名为 a、b 和 c 的三个变量的 DIE。变量 a 在内存中具有固定位置,变量 b 在寄存器 0 中,变量 c 位于当前函数堆栈帧内的偏移量 –12 处。尽管 a 是首先声明的,但在所有函数之后生成描述它的 DIE。 a 的实际位置将由链接器填充。
DWARF 位置表达式可以包含由简单堆栈机器计算的一系列运算符和值。这可以是任意复杂的计算,在表达式中具有广泛的算术运算、测试和分支、评估其他位置表达式的调用以及对处理器的存储器或寄存器的访问。甚至还有一些操作用于描述被拆分并存储在不同位置的数据,例如一些数据存储在内存中,一些数据存储在寄存器中的结构。
尽管这种极大的灵活性在实践中很少使用,但无论语言定义多么复杂或编译器的优化多么巧妙,位置表达式都应该允许描述变量数据的位置。
5.2.4. Describing Executable Code 描述可执行代码
5.2.4.1. Functions and Subprograms 函数和子程序
DWARF 将返回值的函数和不返回值的子例程视为同一事物的变体。 DWARF 稍微偏离了 C 术语的根源,用子程序 DIE 描述了两者。该 DIE 具有名称、源位置三元组和指示子程序是否是外部的,即在当前编译之外可见的属性。
子程序 DIE 的属性给出了子程序占用的低和高内存地址(如果它是连续的)或一个内存范围列表(如果函数不占用一组连续的内存地址)。除非明确指定另一个地址,否则假定低 PC 地址是例程的入口点。
函数返回的值由 type 属性给出。不返回值的子程序(如 C void 函数)不具有此属性。 DWARF 没有描述函数的调用约定;在特定架构的应用程序二进制接口 (ABI) 中定义的。可能有一些属性可以帮助调试器定位子程序的数据或找到当前子程序的调用者。返回地址属性是一个位置表达式,用于指定调用者地址的存储位置。帧基属性是一个位置表达式,用于计算函数的堆栈帧的地址。这些很有用,因为编译器可能执行的一些最常见的优化是消除显式保存返回地址或帧指针的指令。
子程序 DIE 拥有描述子程序的 DIE。可以传递给函数的参数由具有可变参数属性的可变 DIE 表示。如果参数是可选的或具有默认值,则这些参数由属性表示。参数的 DIE 与函数的参数列表的顺序相同,但可能散布着其他 DIE,例如,用于定义参数使用的类型。
函数可以定义局部变量或全局变量。这些变量的 DIE 遵循参数 DIE。许多语言允许嵌套词法块。这些由词法块 DIE 表示,而词法块 DIE 又可能拥有变量 DIE 或嵌套的词法块 DIE。
这是一个稍长一些的例子。图 8a 显示了 strndup.c 的源代码,这是 gcc 中的一个复制字符串的函数。图 8b 列出了为此文件生成的 DWARF。与前面的示例一样,未显示源行信息和位置属性。在图 8b 中,DIE <2> 显示了 size_t 的定义,它是 unsigned int 的类型定义。这允许调试器将形式参数 n 的类型显示为 size_t,同时将其值显示为无符号整数。 DIE <5> 描述了函数 strndup。它有一个指向它的兄弟 DIE <10> 的指针;以下所有 DIE 都是子程序 DIE 的子程序。该函数返回一个指向 char 的指针,如 DIE <10> 中所述。 DIE <5> 还将子例程描述为外部和原型,并给出例程的低 PC 值和高 PC 值。例程的形式参数和局部变量在 DIE <6> 到 <9> 中描述。
5.2.4.2. Compilation Unit 编译单位
最有趣的程序由多个文件组成。组成程序的每个源文件都是独立编译的,然后与系统库链接在一起组成程序。 DWARF 将每个单独编译的源文件称为一个编译单元。
每个编译单元的 DWARF 数据都以编译单元 DIE 开头。此 DIE 包含有关编译的一般信息,包括源文件的目录和名称、使用的编程语言、标识 DWARF 数据生产者的字符串,以及 DWARF 数据部分的偏移量,以帮助定位行号和宏信息。
如果编译单元是连续的(即,它被一块加载到内存中),那么该单元的低内存地址和高内存地址都有值。这使得调试器更容易识别哪个编译单元在特定内存地址创建了代码。如果编译单元不连续,则代码占用的内存地址列表由编译器和链接器提供。
编译单元 DIE 是所有描述编译单元的 DIE 的父级。通常,第一个 DIE 将描述数据类型,然后是全局数据,然后是构成源文件的函数。变量和函数的 DIE 与它们在源文件中出现的顺序相同。
5.2.4.3. Data encoding 数据编码
从概念上讲,描述程序的 DWARF 数据是一棵树。每个 DIE 可能有一个兄弟姐妹,也可能有几个孩子 DIE。每个 DIE 都有一个类型(称为它的 TAG)和许多属性。每个属性由属性类型和值表示。不幸的是,这不是一种非常密集的编码。如果没有压缩,DWARF 数据是笨重的。
DWARF 提供了多种方法来减少需要与目标文件一起保存的数据的大小。第一种是通过按前缀顺序保存树来“展平”树。每种类型的 DIE 都被定义为是否有子节点。如果 DIE 不能有子级,则下一个 DIE 是其兄弟级。如果 DIE 可以有孩子,那么下一个 DIE 就是它的第一个孩子。其余的孩子被表示为第一个孩子的兄弟姐妹。这样,可以消除与同级或子 DIE 的链接。如果编译器编写者认为能够从一个 DIE 跳转到它的兄弟 DIE 而无需遍历其每个子 DIE(例如,跳转到编译中的下一个函数)可能会很有用,那么可以添加一个兄弟属性到DIE。
压缩数据的第二种方案是使用缩写。尽管 DWARF 在生成 DIE 和属性方面具有很大的灵活性,但大多数编译器只生成有限的 DIE 集,所有 DIE 都具有相同的属性集。不是存储 TAG 的值和属性值对,而是仅存储缩写表中的索引,然后是属性代码。每个缩写都给出了 TAG 值、一个指示 DIE 是否有子元素的标志,以及一个具有它期望的值类型的属性列表。图 9 显示了图 8b 中使用的形式参数 DIE 的缩写。图 8 中的 DIE <6> 实际上是如图所示编码的。这显着减少了需要以增加复杂性为代价而保存的数据量
不太常用的是 DWARF 版本 3 和 4 的功能,它们允许从一个编译单元引用存储在另一个编译单元或共享库中的 DWARF 数据。许多编译器为每次编译生成相同的缩写表和基本类型,与编译是否实际使用所有缩写或类型无关。这些可以保存在共享库中并被每个编译单元引用,而不是在每个编译单元中复制。
5.2.5. Other DWARF Data 其它数据
5.2.5.1. Line Number Table 行号表
DWARF 行表包含包含程序可执行代码的内存地址与对应于这些地址的源代码行之间的映射。在最简单的形式中,这可以看作是一个矩阵,其中一列包含内存地址,另一列包含该地址的源三元组(文件、行和列)。如果您想在特定行设置断点,该表会为您提供存储断点指令的内存地址。相反,如果您的程序在内存中的某个位置出现错误(例如,使用了错误的指针),您可以查找最接近内存地址的源代码行。
DWARF 已通过添加列来扩展此功能,以传达有关程序的附加信息。当编译器优化程序时,它可能会移动或删除指令。给定源语句的代码可能不会存储为机器指令序列,而是可能与附近其他源语句的指令分散和交错。标识代表函数序言的代码的结尾或结语的开头可能很有用,以便调试器可以在函数的所有参数都已加载或函数返回之前停止。一些处理器可以执行多个指令集,因此还有另一列指示哪个指令集存储在指定的机器位置。
正如您可能想象的那样,如果该表为每条机器指令存储一行,那将是巨大的。 DWARF 通过将其编码为称为行号程序的指令序列来压缩此数据。这些指令由一个简单的有限状态机解释以重新创建完整的行号表。
有限状态机用一组默认值初始化。行号表中的每一行都是通过执行行号程序的一个或多个操作码生成的。操作码通常非常简单:例如,向机器地址或行号添加一个值,设置列号,或设置一个标志,表示内存地址代表源语句的开始,结束函数序言,或函数结语的开始。一组特殊的操作码将最常见的操作(增加内存地址和增加或减少源行号)组合成一个操作码。
最后,如果行号表的一行与前一行具有相同的源三元组,则行号程序中不会为该行生成指令。图 10 列出了 strndup.c 的行号程序。请注意,仅存储表示语句开始指令的机器地址。编译器未识别此代码中的基本块、序言的结尾或函数的结语的开头。该表在行号程序中仅以 31 个字节编码。
5.2.5.2. Macro Information 宏信息
大多数调试器都很难显示和调试具有宏的代码。 用户看到带有宏的原始源文件,而代码对应于生成的任何宏。
DWARF 包括对程序中定义的宏的描述。 这是非常基本的信息,但可以被调试器用来显示宏的值或可能将宏翻译成相应的源语言。
5.2.5.3. Call Frame Information 调用帧信息
每个处理器都有特定的调用函数和传递参数的方式,通常在 ABI 中定义。在最简单的情况下,这对于每个函数都是相同的,并且调试器确切地知道如何找到函数的参数值和返回地址。
对于某些处理器,根据函数的编写方式,可能会有不同的调用序列,例如,如果有超过一定数量的参数。根据操作系统的不同,可能会有不同的调用顺序。编译器将尝试优化调用序列,使代码更小、更快。一个常见的优化是有一个简单的函数,它不调用任何其他函数(叶函数),使用其调用者堆栈帧而不是创建自己的堆栈帧。另一种优化可能是消除指向当前调用帧的寄存器。有些寄存器可能会在整个调用过程中被保留,而另一些则不会。虽然调试器可能会在调用序列或优化中拼凑出所有可能的排列,但它既乏味又容易出错。优化中的一个小变化和调试器可能不再能够将堆栈遍历到调用函数。
DWARF 调用帧信息 (CFI) 为调试器提供了有关如何调用函数的足够信息,以便它可以定位函数的每个参数、定位当前调用帧以及定位调用函数的调用帧。调试器使用此信息来“展开堆栈”,定位前一个函数、调用函数的位置以及传递的值。
与行号表一样,CFI 被编码为一系列指令,这些指令被解释为生成一个表。对于包含代码的每个地址,该表中有一行。第一列包含机器地址,而后续列包含执行该地址处的指令时机器寄存器的值。就像行号表一样,如果这个表真的被创建了,它会很大。幸运的是,两条机器指令之间的变化很小,因此 CFI 编码非常紧凑。
5.2.5.4. Variable length data 变长数组
在整个 DWARF 中使用整数值来表示从偏移到数据部分到数组或结构大小的所有内容。 在大多数情况下,不可能对这些值的大小进行限制。 在经典数据结构中,这些值中的每一个都将使用默认整数大小表示。 由于大多数值只能用几位表示,这意味着数据主要由零组成。
DWARF 定义了一个可变长度的整数,称为 Little Endian Base 128(LEB128 或更常见的 ULEB 表示无符号值,SLEB 表示有符号值),它压缩这些整数值。 由于低位包含数据,高位由全零或 1 组成,因此 LEB 值会切断值的低七位。 如果其余位全为零或一(符号扩展位),则这是编码值。 否则,将高位设置为 1,输出该字节,然后继续接下来的 7 个低位。
5.2.5.5. Shrinking DWARF data 收缩数据
与 DWARF 版本 1 等未编码格式相比,DWARF 使用的编码方案显着减少了调试信息的大小。不幸的是,对于许多程序,编译器生成的调试数据量可能变得非常大,通常比可执行代码大得多和数据。
DWARF 提供了进一步减少调试数据大小的方法。 DWARF 调试数据中的大多数字符串实际上是对单独的 .debug_str 部分的引用。生成此部分时可以消除重复的字符串。潜在地,链接器可以将来自多个编译的 .debug_str 部分合并到一个较小的字符串部分中。
许多程序包含在每个编译单元中重复的声明。例如,在每次编译中可能会重复描述许多(可能数千个)C++ 模板函数声明的调试数据。这些重复的描述可以保存在单独的编译单元中唯一命名的节中。链接器可以使用 COMDAT(公共数据)技术来消除重复的部分。
许多程序引用了大量包含许多类型定义的包含文件,导致 DWARF 数据包含这些类型的数千个 DIE。编译器可以通过仅为编译中实际使用的类型生成 DWARF 来减少此数据的大小。使用 DWARF 版本 4,类型定义可以保存到单独的 .debug_types 部分。编译单元包含一个 DIE,它引用这个单独的类型单元和这些类型的唯一 64 位签名。链接器可以识别定义相同类型单元的编译并消除重复项。
5.3. 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 使用的字符串表 |
.debug_types | 类型说明 |
libhello.so的段信息通过eu-readelf -S libhello.so
进行查看,如下所示:
可以通过eu-readelf
的-w
或--debug-dump[=SECTION]
来查看不同DWARF段的内容:
abi-dumper中主要使用了line、loc、info三个段,下面分别libhello.so中这几个段的内容。
5.3.1. .debug_line段
readelf --debug-dump=line libhello.so
如下,和eu-readelf格式上有一些差异:
5.3.2. .debug_loc段
libhello.so中没有此段
5.3.3. .debug_info段
eu-readelf --debug-dump=info libhello.so
如下:
DWARF section [26] '.debug_info' at offset 0x31a8:
[Offset]
Compilation unit at offset 0:
Version: 5, Abbreviation section offset: 0, Address size: 8, Offset size: 4
Unit type: compile (1)
[ c] compile_unit abbrev: 3
producer (strp) "GNU C17 11.1.1 20210428 (Red Hat 11.1.1-1) -mtune=generic -march=x86-64 -g -fPIC"
language (data1) C11 (29)
name (line_strp) "hello.c"
comp_dir (line_strp) "/mnt/hgfs/projects/linux/abi-test/h"
low_pc (addr) +0x0000000000001109 <hello>
high_pc (data8) 51 (+0x000000000000113c <_fini>)
stmt_list (sec_offset) 0
[ 2e] base_type abbrev: 1
byte_size (data1) 8
encoding (data1) unsigned (7)
name (strp) "long unsigned int"
[ 35] base_type abbrev: 1
byte_size (data1) 4
encoding (data1) unsigned (7)
name (strp) "unsigned int"
[ 3c] base_type abbrev: 1
byte_size (data1) 1
encoding (data1) unsigned_char (8)
name (strp) "unsigned char"
[ 43] base_type abbrev: 1
byte_size (data1) 2
encoding (data1) unsigned (7)
name (strp) "short unsigned int"
[ 4a] base_type abbrev: 1
byte_size (data1) 1
encoding (data1) signed_char (6)
name (strp) "signed char"
[ 51] base_type abbrev: 1
byte_size (data1) 2
encoding (data1) signed (5)
name (strp) "short int"
[ 58] base_type abbrev: 4
byte_size (data1) 4
encoding (data1) signed (5)
name (string) "int"
[ 5f] base_type abbrev: 1
byte_size (data1) 8
encoding (data1) signed (5)
name (strp) "long int"
[ 66] pointer_type abbrev: 2
byte_size (implicit_const) 8
type (ref4) [ 6b]
[ 6b] base_type abbrev: 1
byte_size (data1) 1
encoding (data1) signed_char (6)
name (strp) "char"
[ 72] const_type abbrev: 5
type (ref4) [ 6b]
[ 77] subprogram abbrev: 6
external (flag_present) yes
name (strp) "printf"
decl_file (data1) stdio.h (2)
decl_line (data2) 332
decl_column (data1) 12
prototyped (flag_present) yes
type (ref4) [ 58]
declaration (flag_present) yes
sibling (ref4) [ 8f]
[ 88] formal_parameter abbrev: 7
type (ref4) [ 8f]
[ 8d] unspecified_parameters abbrev: 8
[ 8f] pointer_type abbrev: 2
byte_size (implicit_const) 8
type (ref4) [ 72]
[ 94] subprogram abbrev: 9
external (flag_present) yes
name (strp) "hello"
decl_file (data1) hello.c (1)
decl_line (data1) 4
decl_column (data1) 6
prototyped (flag_present) yes
low_pc (addr) +0x0000000000001109 <hello>
high_pc (data8) 51 (+0x000000000000113c <_fini>)
frame_base (exprloc) [ 0] call_frame_cfa
call_all_tail_calls (flag_present) yes
[ ae] formal_parameter abbrev: 10
name (strp) "what"
decl_file (data1) hello.c (1)
decl_line (data1) 4
decl_column (data1) 18
type (ref4) [ 66]
location (exprloc) [ 0] fbreg -24
简单说明:
- 中括号开始的行表示一个 DIE 的开始,第一行可以看成前面说的 tag,接下来的行表示众多的 attribute。
- abbrev后面的数字表示TAG的缩写编号,在.debug_abbrev段中存放对应关系
下面分析函数void hello(char *what);
在.debug_info段中的表示:
[ 94] subprogram abbrev: 9
函数作为子程序
external (flag_present) yes
函数采用extern修饰(默认)
name (strp) "hello"
子程序名字为“hello”
decl_file (data1) hello.c (1)
声明在hello.c文件中,括号中的1表示.debug_line段中文件的序号
decl_line (data1) 4
decl_column (data1) 6
声明位置为第4行,第6列(见下图)
prototyped (flag_present) yes
函数原型(非调用)
函数没有type属性,说明该函数没有返回值
low_pc (addr) +0x0000000000001109 <hello>
high_pc (data8) 51 (+0x000000000000113c <_fini>)
frame_base (exprloc) [ 0] call_frame_cfa
call_all_tail_calls (flag_present) yes
[ ae] formal_parameter abbrev: 10
形式参数(函数hello如果有形参,形参DIE紧接着子程序的DIE)
name (strp) "what"
形参名为“what”
decl_file (data1) hello.c (1)
decl_line (data1) 4
decl_column (data1) 18
形参位置在hello.c文件的第4行,第18列
type (ref4) [ 66]
类型为[66],即下面的指针类型,大小为8个字节,指向有符号char类型
location (exprloc) [ 0] fbreg -24
……
[ 66] pointer_type abbrev: 2
byte_size (implicit_const) 8
type (ref4) [ 6b]
[ 6b] base_type abbrev: 1
byte_size (data1) 1
encoding (data1) signed_char (6)
name (strp) "char"
readelf --debug-dump=info libhello.so
如下,和eu-readelf格式上有一些差异:
Contents of the .debug_info section:
Compilation Unit @ offset 0x0:
Length: 0xbb (32-bit)
Version: 5
Unit Type: DW_UT_compile (1)
Abbrev Offset: 0x0
Pointer Size: 8
<0><c>: Abbrev Number: 3 (DW_TAG_compile_unit)
<d> DW_AT_producer : (indirect string, offset: 0x12): GNU C17 11.1.1 20210428 (Red Hat 11.1.1-1) -mtune=generic -march=x86-64 -g -fPIC
<11> DW_AT_language : 29 (C11)
<12> DW_AT_name : (indirect line string, offset: 0x0): hello.c
<16> DW_AT_comp_dir : (indirect line string, offset: 0x8): /mnt/hgfs/projects/linux/abi-test/h
<1a> DW_AT_low_pc : 0x1109
<22> DW_AT_high_pc : 0x33
<2a> DW_AT_stmt_list : 0x0
<1><2e>: Abbrev Number: 1 (DW_TAG_base_type)
<2f> DW_AT_byte_size : 8
<30> DW_AT_encoding : 7 (unsigned)
<31> DW_AT_name : (indirect string, offset: 0x0): long unsigned int
<1><35>: Abbrev Number: 1 (DW_TAG_base_type)
<36> DW_AT_byte_size : 4
<37> DW_AT_encoding : 7 (unsigned)
<38> DW_AT_name : (indirect string, offset: 0x5): unsigned int
<1><3c>: Abbrev Number: 1 (DW_TAG_base_type)
<3d> DW_AT_byte_size : 1
<3e> DW_AT_encoding : 8 (unsigned char)
<3f> DW_AT_name : (indirect string, offset: 0x8b): unsigned char
<1><43>: Abbrev Number: 1 (DW_TAG_base_type)
<44> DW_AT_byte_size : 2
<45> DW_AT_encoding : 7 (unsigned)
<46> DW_AT_name : (indirect string, offset: 0x63): short unsigned int
<1><4a>: Abbrev Number: 1 (DW_TAG_base_type)
<4b> DW_AT_byte_size : 1
<4c> DW_AT_encoding : 6 (signed char)
<4d> DW_AT_name : (indirect string, offset: 0x8d): signed char
<1><51>: Abbrev Number: 1 (DW_TAG_base_type)
<52> DW_AT_byte_size : 2
<53> DW_AT_encoding : 5 (signed)
<54> DW_AT_name : (indirect string, offset: 0x76): short int
<1><58>: Abbrev Number: 4 (DW_TAG_base_type)
<59> DW_AT_byte_size : 4
<5a> DW_AT_encoding : 5 (signed)
<5b> DW_AT_name : int
<1><5f>: Abbrev Number: 1 (DW_TAG_base_type)
<60> DW_AT_byte_size : 8
<61> DW_AT_encoding : 5 (signed)
<62> DW_AT_name : (indirect string, offset: 0x99): long int
<1><66>: Abbrev Number: 2 (DW_TAG_pointer_type)
<67> DW_AT_byte_size : 8
<67> DW_AT_type : <0x6b>
<1><6b>: Abbrev Number: 1 (DW_TAG_base_type)
<6c> DW_AT_byte_size : 1
<6d> DW_AT_encoding : 6 (signed char)
<6e> DW_AT_name : (indirect string, offset: 0x94): char
<1><72>: Abbrev Number: 5 (DW_TAG_const_type)
<73> DW_AT_type : <0x6b>
<1><77>: Abbrev Number: 6 (DW_TAG_subprogram)
<78> DW_AT_external : 1
<78> DW_AT_name : (indirect string, offset: 0xa2): printf
<7c> DW_AT_decl_file : 2
<7d> DW_AT_decl_line : 332
<7f> DW_AT_decl_column : 12
<80> DW_AT_prototyped : 1
<80> DW_AT_type : <0x58>
<84> DW_AT_declaration : 1
<84> DW_AT_sibling : <0x8f>
<2><88>: Abbrev Number: 7 (DW_TAG_formal_parameter)
<89> DW_AT_type : <0x8f>
<2><8d>: Abbrev Number: 8 (DW_TAG_unspecified_parameters)
<2><8e>: Abbrev Number: 0
<1><8f>: Abbrev Number: 2 (DW_TAG_pointer_type)
<90> DW_AT_byte_size : 8
<90> DW_AT_type : <0x72>
<1><94>: Abbrev Number: 9 (DW_TAG_subprogram)
<95> DW_AT_external : 1
<95> DW_AT_name : (indirect string, offset: 0x80): hello
<99> DW_AT_decl_file : 1
<9a> DW_AT_decl_line : 4
<9b> DW_AT_decl_column : 6
<9c> DW_AT_prototyped : 1
<9c> DW_AT_low_pc : 0x1109
<a4> DW_AT_high_pc : 0x33
<ac> DW_AT_frame_base : 1 byte block: 9c (DW_OP_call_frame_cfa)
<ae> DW_AT_call_all_tail_calls: 1
<2><ae>: Abbrev Number: 10 (DW_TAG_formal_parameter)
<af> DW_AT_name : (indirect string, offset: 0x86): what
<b3> DW_AT_decl_file : 1
<b4> DW_AT_decl_line : 4
<b5> DW_AT_decl_column : 18
<b6> DW_AT_type : <0x66>
<ba> DW_AT_location : 2 byte block: 91 68 (DW_OP_fbreg: -24)
<2><bd>: Abbrev Number: 0
<1><be>: Abbrev Number: 0
通过上面可以看出,readelf相比较eu-readelf导出的信息更符合DWARF一节中所述的术语。
6. 后语
本文只是简单介绍了abi-dumper解析c程序的信息,对于c++程序,还需要解析vtable信息(基于vtable-dumper工具),更多的功能读者可以自行解锁!