自己動手編寫一個Linux偵錯程式系列之4 ELF檔案格式與DWARF除錯格式
目錄
在上一節中,你已經聽說了DWARF除錯格式,它是程式的除錯資訊,是一種可以更好理解原始碼的方式,而不只是解析程式。今天我們將討論原始碼級除錯資訊的細節,以準備在本教程後面的部分中使用它。
系列索引
- 準備工作
- 斷點的設定
- 暫存器和記憶體
- ELF檔案格式和DWARVF除錯格式
- 原始碼和訊號
- 原始碼級單步
- 原始碼級斷點
- 堆疊解除
- 處理變數
- 高階主題
ELF檔案格式與DWARF格式簡介
ELF和DWARF是你可能沒有聽說過的兩個概念資訊,但可能已經使用很長時間了。 ELF(可執行和可連結格式)是Linux世界中使用最廣泛的物件檔案格式; 它指定了一種儲存二進位制檔案的所有不同部分的方式,如程式碼,靜態資料,除錯資訊和字串。 它還告訴載入程式如何取得二進位制並準備好執行,這涉及二進位制檔案的不同部分應該放置在記憶體中,哪些部分需要根據其他資訊(重定位)等的位置來修復。 我不會在這些帖子中覆蓋更多ELF,但如果你有興趣,可以看看這個漂亮的
DWARF是ELF最常用的除錯資訊格式。它不一定與ELF相關,但兩者是一起發展的,在開發中一起使用也非常好。該格式允許編譯器告訴偵錯程式程式原始碼如何與將執行的二進位制檔案相互關係。該資訊分為不同的ELF部分,每個部分都有自己的資訊來中繼。以下是定義的不同部分,取自於非常詳細的DWARF除錯格式介紹:
.debug_abbrev
.debug_info
部分中使用的縮寫.debug_aranges
記憶體地址和編譯之間的對映.debug_frame
呼叫幀資訊.debug_info
包含DWARF除錯資訊項(DIE)的核心DWARF資料.debug_line
.debug_loc
位置說明.debug_macinfo
巨集描述.debug_pubnames
全域性物件和函式的查詢表.debug_pubtypes
全域性型別的查詢表.debug_ranges
DIE引用的地址範圍.debug_str
.debug_info
使用的字串表.debug_types
型別說明
我們對.debug_line
和.debug_info
部分最感興趣,所以讓我們看看一些DWARF的簡單程式。
1 2 3 4 5 6 |
= 3 ;
|
DWARF debug_line表資訊
如果你使用編譯器(gcc 或 clang
)的-g
選項編譯此程式,並通過dwarfdump
執行結果,則應該看到類似於行號的部分:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
|
第一部分描述部分是關於如何理解下面顯示列表的一些資訊 - 表資訊主行號資料從0x00400670
開始。本質上,它是將一個程式碼記憶體地址對映到一些檔案中的行和列號。 NS
表示該地址標誌著新語句的開始,這通常用於設定斷點或步進。 PE
標記函式開始的結尾,這有助於設定函式入口斷點。 ET
標示翻譯單元的結尾。實際資訊上並不是像這樣編碼的;真正的編碼是一種非常節省空間的程式,可以執行這些程式來建立這個行資訊。
那麼說,我們想在variable.cpp的第4行設定一個斷點,我們該怎麼做?我們查詢與該檔案相對應的條目,然後查詢相關的行條目,查詢與之對應的地址,並在其中設定斷點。在我們的例子中,這是這個條目:
1 |
|
所以我們要在地址0x00400686
設定一個斷點。你可以用你已經寫過的偵錯程式手工完成,如果你想嘗試一下。
相反的工作也是如此。如果我們有一個記憶體位置 - 例如一個程式計數器值,並且想要找出原始碼中的哪個位置,我們只需在行表資訊中找到最接近的對映地址,並從中獲取行。
DWARF debug_info資訊
.debug_info
部分是DWARF的核心。它給了我們有關我們的程式中存在的型別,函式,變數,希望和想要得到的資訊。本節的基本單位是DWARF資訊條目(DWARF Information Entry),簡稱為DIE。 DIE包含一個標籤,告訴您正在表示什麼樣的原始碼級實體,後面是一系列適用於該實體的屬性。這是上面釋出的簡單示例程式的.debug_info
部分:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 |
|
第一個DIE表示一個編譯單元(CU),它基本上是一個原始檔,其中包含所有#includes
,並且這樣解析。以下是它們的含義註釋的屬性:
1 2 3 4 5 6 7 8 9 10 11 12 |
|
其他DIE遵循類似的方案,您可以直觀地看出不同屬性的含義。
現在我們可以嘗試用我們新發現的DWARF知識解決一些實際問題。
使用 DWARF 分析函式
如果我們有一個程式計數器值,並想獲取PC所在函式的資訊。一個簡單的演算法是:
1 2 3 4 5 |
|
這可以用於許多情況,但是在成員函式和行內函數存在的情況下,事情會變得更加困難。 例如,使用行內函數,一旦找到範圍包含我們的PC的函式,我們將需要對該DIE的子項進行遞迴,以檢視是否存在更好匹配的行內函數。我不會在我的偵錯程式程式碼中處理行內函數,但如果你喜歡,你可以新增對此的支援。
如何在函式上設定斷點
再次申明,如果想要支援成員函式,名稱空間等特性可能需要更高階的做法。 對於簡單的函式,您可以在不同的編譯單元中迭代函式,直到找到具有正確名稱的函式。 如果您的編譯器足夠填寫.debug_pubnames
部分,您可以更有效地執行此操作。
一旦找到該函式,您可以在DW_AT_low_pc
給定的記憶體地址上設定一個斷點。 但是,在函式開始時會中斷,但最好在使用者程式碼開始時中斷。 由於行表資訊可以指定指定函式開頭結束的記憶體地址,因此您可以直接在行表中查詢DW_AT_low_pc
的值,然後繼續閱讀,直到找到標記為函式開頭結束的條目。 有些編譯器不會輸出這個資訊,所以另外一個選擇是在該函式的第二行條目給出的地址上設定一個斷點。
假設我們要在我們的示例程式中設定一個斷點。 我們搜尋main函式,並得到這個DIE:
1 2 3 4 5 6 7 8 9 |
|
這告訴我們,函式從0x00400670
開始。 如果我們線上表中檢視,我們得到這個條目:
1 |
|
我們想跳過開頭,所以我們先讀一個條目:
1 |
|
Clang在這個條目中包含了程式碼開頭結束標誌,所以我們知道在這裡停下來,並在地址0x00400676
上設定一個斷點。
如何讀取變數的內容
讀取變數可能非常複雜。 變數是一個難以捉摸的東西,可以在整個函式中存在,可以放在暫存器中,放在記憶體中,還可以被優化,隱藏在角落裡。幸運的是,我們簡單的例子是,很簡單。 如果我們想要讀取變數a的內容,我們來看看它的DW_AT_location
屬性:
1 |
|
這表示區域性變數的記憶體在距離堆疊幀基址的-8
的偏移處。 要找出這個基址的位置,我們來看看包含函式的DW_AT_frame_base
屬性。
1 |
|
在x86上的reg6
是棧幀指標暫存器,由System V x86_64 ABI指定。現在我們讀幀指標的內容,從中減去8,我們已經找到了變數。如果我們想弄明白這個問題,我們需要看看它的型別:
1 2 3 |
|
如果我們在除錯資訊中查詢這個型別,就會得到這個DIE:
1 2 3 4 |
|
這告訴我們型別是一個8位元組(64位)的有符號整數型別,因此我們可以繼續將這些位元組解釋為int64_t
並將其顯示給使用者。
當然,型別可以比這個複雜得多,因為它們必須能夠表達諸如c++型別之類的東西,但這給了你一個關於它們如何工作的基本概念。
回到該棧幀的基址,Clang編譯器可以比較好的跟蹤到幀指標暫存器的幀基址。 最近版本的GCC傾向於喜歡DW_OP_call_frame_cfa
,它涉及解析.eh_frame
ELF部分,這是一個完全不同的文章,在這裡我就不詳述。 如果你使用GCC的DWARF 2版本而不是更新的版本,命令是gcc -gdwarf-2 <原始碼>
那麼它將傾向於輸出位置列表,這更容易閱讀:
1 2 3 4 5 |
|
上面列表根據程式計數器的位置給出不同的位置。 這個例子是說,如果PC在DW_AT_low_pc
處於0x0
的偏移量的情況下,那麼棧幀基地址是從暫存器7中儲存的值加偏移量8,如果它在0x1
到0x4
之間,那麼它的偏移距離一樣都是16,等等。
總結一下
這節包含了很多DWARF資訊需要好好吸收一下才行。不要擔心!有個好訊息,就是在接下來的幾個章節中,我們將有一個庫幫我們完成最麻煩的工作。瞭解了DWARF的概念,特別是在出現問題或希望支援一些DWARF庫的情況下,仍然有用。
如果您想了解更多關於DWARF的資訊,那麼你可以在此獲取標準文件。 在撰寫本文時,DWARF 5剛剛被髮布,但DWARF 4更受歡迎。
說明
自己動手實踐一下
本節內容是整個系列最枯燥的一章,全篇都是在講述DWARF除錯格式的內容。我們可以使用編譯器gcc
或者clang
編譯原始碼時在生成的可執行檔案中產生除錯資訊,並使用DWARF相關的工具dwarfdump
檢視和解析可執行檔案ELF檔案格式中的除錯資訊。
使用gcc
的命令可以生成dwarf格式的除錯資訊
gcc -g <原始碼>
編譯生成dwarf除錯格式的資訊 原始碼使用的是文章的例子。
使用gcc編譯之後,可以使用readelf檢視可執行檔案中的Seciton資訊1
2
3
4
5
6
int
main() {
long
a
=
3
;
long
b
=
2
;
long
c
=
a
+
b;
a
=
4
;
}
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 |
|
可以看出其種有譯文中最重要的兩個Section,.debug_line
和.debug_info
gcc -gdwarf-2 <原始碼>
編譯生成 DWARF 2 版本除錯格式的資訊 與上面的命令類似,只是格式版本略有不同
使用dwarfdump
可以檢視生成的可執行檔案的除錯資訊
dwarfdump -a <程式>
檢視程式中所有debug開頭的除錯資訊 由於資訊量比較大,就不貼圖了dwarfdump -l <程式>
檢視程式中除錯資訊的debugline資訊dwarfdump -i <程式>
檢視程式中除錯資訊的debuginfo資訊dwarfdump -p <程式>
檢視程式中除錯資訊的debug_pubnames資訊