本周花了幾天的時間來研究怎么在 breakpad [1, 2] 中加入打印函數參數的功能,以期其產生的 callstack 更具可讀性,方便定位崩潰原因。
現代 ELF 中的調試信息基本是以 DWARF 格式為主了,因此這幾天的研究也主要將時間花在了理解 DWARF 這貨是怎么工作上,感嘆要把東西做到極致真是件繁瑣而細致的事情。關於 DWARF,網上能找到的相關介紹真心不多,估計也是因為真正需要和它打交道的人真是太少了,在這種情況下最有用最權威的,當然還是官方的文檔了,好在我現在也不必把整個 DWARF 的所有細節都給搞明白,且用且看先把需要用到的東西理一理唄。
LEB128編碼
LEB128(little endian base 128) 是 DWARF 讀寫數據使用的一種變長整型編碼格式,該編碼格式的理論基礎與哈夫曼編碼相似:相對常用的小整數用較少的位數來表示,大的整數用較長的編碼來表示,形式上看 LEB128 分為有符號與無符號兩種版本,但在實現上其本質是相同的。
LEB128 以7個 bit 為一個編碼單元放在一個 byte 中,從低放到高,該字節的最高位用於表示當前 byte 是否是當前數據的最后一個 byte,0 表示是最后一個字節,1表示還有其它的字節,所以對於小於等於 2 ^ 7 - 1 的整數,只要一個字節就可以表示,大於 2 ^ 7 - 1 又小於等於 2 ^ 14 - 1的整數則需要2個字節,如此類推。
// 原始數據 -> 二進制表示 -> leb128表示
7 -> 00000111 -> 00000111
771 -> 00000011 00000011 - > 00000011 10000011[更正:此處正確的編碼應該是:00000110 10000011,多謝網友 @寶刀未老 的指正]
因此對於無符號整型, LEB128 的編碼過程可以簡單用如下偽代碼來表示:
void encode_uleb128(value, output)
{
do {
byte = value & 0x7f; // get lower 7 bits of the input.
value >>= 7;
if (value) byte |= 0x80; // need more byte
*output = byte;
output++;
} while (value);
}
至於有符號整數,它的編碼原理與實現和無符號本質是一樣的,不同之處在於有符號的整形需要一個符號位,因此有符號的 leb128 也需要加入符號位,這個符號位就設在了最后一個字節的第二高位上:
void encode_sleb128(value, output)
{
more = true;
do {
byte = value & 0x7f;
value >>= 7; // 有符號數移位,如果 value 是負數,高位補1.
if (value == 0 && (byte & ox40) == 0 // value 是正數,且當前 byte 的符號位沒被占
|| value == -1 && (byte & 0x40) ) // value 是負數,且當前 byte 的符號位已經設置
{
more = false;
}
else
{
byte |= 0x80; // need more byte.
}
*output = byte;
output++;
} while (more);
}
可見整個編碼過程十分簡單明了,解碼只是編碼的逆過程,這里從略。
DWARF 中調試信息的組織
DWARF 中的調試信息被放在一個叫作 .debug_info 的段中,該段與 DWARF 中其它的段類似,可以看成是一個表格狀的結構,表中每一條記錄叫作一個 DIE(debugging information entry), 一個 DIE 由一個 tag 及 很多 attribute 組成,其中 tag 用於表示當前的 DIE 的類型,類型指明當前 DIE 用於描述什么東西,如函數,變量,類型等,而 attribute 則是一對對的 key/value 用於描述其它一些信息,在 linux 下我們可以用如下命令來查看 ELF 中的調試信息:
// file: debug_info.c
#include <stdio.h>
void func(int arg)
{
int i = 0;
int local = arg + 42;
while (i < local)
{
printf("i = %d\n", i++);
}
}
int main()
{
func(23);
return 0;
}
-bash-3.00$ gcc -o the_executable -g debug_info.c
-bash-3.00$ readelf --debug-dump=info the_executable
得到如下信息:
The section .debug_info contains:
Compilation Unit @ 0:
Length: 342
Version: 2
Abbrev Offset: 0
Pointer Size: 8
<0><b>: Abbrev Number: 1 (DW_TAG_compile_unit)
DW_AT_stmt_list : 0
DW_AT_high_pc : 0x4004fe
DW_AT_low_pc : 0x4004a8
DW_AT_producer : GNU C 3.4.5 20051201 (Red Hat 3.4.5-2)
DW_AT_language : 1 (ANSI C)
DW_AT_name : debug_info.c
DW_AT_comp_dir : /home/miliao/code/snippet
<1><6f>: Abbrev Number: 2 (DW_TAG_base_type)
DW_AT_name : (indirect string, offset: 0x0): long unsigned int
DW_AT_byte_size : 8
DW_AT_encoding : 7 (unsigned)
// skip some the output.
<1><eb>: Abbrev Number: 4 (DW_TAG_subprogram)
DW_AT_sibling : <138>
DW_AT_external : 1
DW_AT_name : func
DW_AT_decl_file : 1
DW_AT_decl_line : 4
DW_AT_prototyped : 1
DW_AT_low_pc : 0x4004a8
DW_AT_high_pc : 0x4004e9
DW_AT_frame_base : 0 (location list)
<2><10d>: Abbrev Number: 5 (DW_TAG_formal_parameter)
DW_AT_name : arg
DW_AT_decl_file : 1
DW_AT_decl_line : 3
DW_AT_type : <c9>
DW_AT_location : 2 byte block: 91 6c (DW_OP_fbreg: -20)
// skip some of the output
其中以2個尖括號開始的行表示一個 DIE 的開始,第一行可以看成前面說的 tag,接下來的行表示眾多的 attribute。
樹型結構的序列化存儲
由前面的描述,我們知道 DIE 結構在物理上是以一個數組的形式存放在了一塊,但實際在邏輯上它們是樹狀的,將一棵樹序列化存儲有很多的方式,DWARF 的實現是這樣的:按先序訪問這棵樹,把節點按訪問順序依次存儲,那怎么來表示這些節點間的父子關系呢?
樹中每一個節點設置一個 hasChild flag, 該 flag 如為 true,則表示該節點有子節點,且子節點緊跟着當前節點依次存放,直到遇到一個空節點。因此對於每一個節點來說,它要么是前一個節點的子節點,要么是前一個節點的兄弟節點,就看前一個節點的 hasChild 是否為 true。
舉個粟子,如下形狀的一棵樹:
按前面的算法描述,我們可以得到以下序列化的結果:
<A, true>
<B, true>
<D, false>
<E, false>
<NULL>
<C, true>
<F, false>
<NULL>
<NULL>
顯然,反序列化就只是一個深度優先,不斷回溯的過程,和某些找路徑的算法有些相似。
數據壓縮
因為調試信息是嵌入在可執行文件當中的,因此調試信息數據量的大小對最后可執行文件的大小有顯著的影響,如果你有注意過編譯程序時加-g
與不加-g
,最后得到的程序大小有什么不同,你就明白我的意思。因此對調試信息進行適當壓縮是很有意義的,而就目前的結果來看,哪怕最后進行了壓縮,調試信息的數據在體積上還是輕松超過了程序的代碼與數據,若是不進行壓縮。。。
DWARF 為對數據進行壓縮采取了兩方面的措施,其一前面已經講了,就是用leb128對數據進行編碼及把樹序列化從而省去節點指針的開銷,另一個措施則是減少 DIE 中 attribute 的數據量,這個怎么做呢? 雖然設計上 DWARF 允許每個 DIE 中可以有不同的 attribute,從而可以極度靈活地來描述各種信息,但在實際的應用中,各個 DIE 的 attribute 數量上是非常少而且非常固定的,比如說描述函數的 DIE 中,它們含有的 attribute 在數量與種類上很多是一樣的,只是 value 不同,想像一下如果每一個 DIE 中都保存一份相同的 key,那豈不是太浪費?
所以,DWARF 引進了一個叫作 abbreviation 的東西, 每個 DIE 中包含一個索引,該索引指向一個 abbreviation,該 abbreviation 指明該 DIE 是否有兒子節點,及都有哪些 attribute,而 DIE 中就只存了各個 attribute 的值。
換一句話說,這個做法其實就是把 DIE 中的 key 給抽出來放到abbreviation 中,DIE 則只保存相對應的 value,因此 abbreviation 功能上看就類似個書簽索引之類的東西,指導你怎么去解析 DIE 中的數據,舉個粟子:
<2><10d>: Abbrev Number: 5 (DW_TAG_formal_parameter)
DW_AT_name : arg
DW_AT_decl_file : 1
DW_AT_decl_line : 3
DW_AT_type : <c9>
DW_AT_location : 2 byte block: 91 6c (DW_OP_fbreg: -20)
該 DIE 實際上是存儲為如下這樣子:
05 'arg\0' 01 03 000000c9 916c 00
其中05是該 DIE 對應的abbreviation 的編號,這條 abbreviation 長成如下樣子:
5 DW_TAG_formal_parameter [no children]
DW_AT_name DW_FORM_string
DW_AT_decl_file DW_FORM_data1
DW_AT_decl_line DW_FORM_data1
DW_AT_type DW_FORM_ref4
DW_AT_location DW_FORM_block1
所以我們知道,DIE 中 'arg\0' 是 DW_AT_name 這個 attribute 的值,類型是 DW_FORM_string,01 對應 DW_AT_decl_file 這個 attribute, 類型是 DW_FORM_data1,如此類推。因為類型可以從 abbreviation 中獲取,而每一個類型的數據長度又是確定的,因此 DIE 中的數據也就可以順利解析了。
DWARF Expression
DWARF 表達式是一個基礎於棧的簡單程序語言,主要用來描述怎么去計算一個數值或地址。這個語言非常的簡單,具體來說,一個表達式由一系列的指令組成,解釋表達式的過程就是執行這些指令的過程,而執行指令就是根據該指令及其相應的操作數(如果存在)執行具體的動作,然后把得到的結果放到棧上,等所有指令都執行完了,棧頂的元素就是這個表達式返回的結果。
棧中元素的大小與當前機器的地址長度一樣,至於指令,則主要包括如下四類:
-
Literal Encoding, 字面意思來看,該類指令做的事情很簡單:直接把操作數壓入棧中,如 DW_OP_lit0 ~ DW_OP_lit31, 這幾個指令,執行后會分別往棧上壓入 0 ~ 31 這些數字,執行 DW_OP_addr 則把該指令的操作數(一個地址)壓到棧上,DW_OP_const1u~DW_OP_const8u 則分別表示往棧上壓入 一個 1 ~ 8 個字節的無符號整數,等等。
-
Register Based Addressing, 這類的指令需要讀取寄存器,再把得到的數值與操作數作某些運算后壓入棧中,比如:DW_OP_breg0, DW_OP_breg1, ..., DW_OP_breg31,這幾個指令都跟着一個 signed LEB128 的操作數,執行這些指令則要求從相應的寄存器(reg 0, reg 1, ..) 取出一個值與該指令的操作數相加,然后把得到的結果壓到棧上。
-
Stack operations, 這類指令表示直接操作當前棧上的元素,如 DW_OP_dup,該指令用來把當前棧頂上的元素再次壓入棧中,DW_OP_drop 則表示把當前棧頂的元素從棧中移除,也就 Pop.
-
Arithmetic And Logical Operations, 這類的指令也是用於操作棧上的元素,但這些操作主要與一些算術邏輯運算相關,如 DW_OP_abs,該指令用來把當前棧頂的元素 Pop 出來,把其當作有符號數,取絕對值后再壓回棧中。同理的指令還有諸如 DW_OP_and, DW_OP_div, 等等。
-
Control flow operations, 這一類指令數量非常少,只有6個,分別是 DW_OP_le, DW_OP_ge, DW_OP_eq, DW_OP_lt, DW_OP_gt, DW_OP_ne,它們的作用是取出當前棧頂的前兩元素作相應的比較操作(如,<=, >=),把得到布爾值壓回棧中。
-
空指令, DW_OP_nop,該指令什么事情也不作。
DWARF 表達式在 debug info 中是廣泛存在的,主要用來描述參數地址,變量地址等,因此幾乎處處都有它的身影,因此讀懂這些指令對理解調試信息是至關重要的,好在這個語言並不復雜,甚至解釋起來都還算簡單,只是考慮到相應指令數量不小,具體寫代碼實現起來還是得多參考參考 DWARF 的手冊,反正到現在我都還沒耐心去做完這件事情,根據需要慢慢來吧,攤手。
【引用】
http://www.dwarfstd.org/doc/Dwarf3.pdf
http://www.cs.dartmouth.edu/~sergey/cs108/2010/Debugging using DWARF.pdf
http://dwarfstd.org/doc/Debugging using DWARF-2012.pdf