調試器工做原理之(三):研究調試信息

這是調試器的工做原理系列文章的第三篇。閱讀這篇文章以前應當先閱讀第一篇第二篇html

調試信息linux

現代編譯器可以將有着各類縮進或嵌套的程序流程、各類數據類型的變量的高級語言代碼轉換爲一大堆稱之爲機器碼的 0/1 數據,這麼作的惟一目的是儘量快的在目標 CPU 上運行程序。一般來講一行 C 語言代碼可以轉換爲若干條機器碼。變量被分散在機器碼中的各個部分,有的在堆棧中,有的在寄存器中,或者直接被優化掉了。數據結構與對象在機器碼中甚至不「存在」,它們只是用於將數據按必定的結構編碼存儲進緩存。git

那麼調試器怎麼知道,當你須要在某個函數入口處暫停時,程序要在哪停下來呢?它怎麼知道當你查看某個變量值時,它怎麼找到這個值?答案是,調試信息。程序員

編譯器在生成機器碼時同時會生成相應的調試信息。調試信息表明了可執行程序與源代碼之間的關係,並以一種提早定義好的格式,同機器碼存放在一塊兒。過去的數年裏,人們針對不一樣的平臺與可執行文件發明了不少種用於存儲這些信息的格式。不過咱們這篇文章不會講這些格式的歷史,而是將闡述這些調試信息是如何工做的,因此咱們將專一於一些事情,好比 DWARF。DWARF 現在十分普遍的用做 Linux 和類 Unix 平臺上的可執行文件的調試格式。github

ELF 中的 DWARFweb

調試器工做原理之(三):研究調試信息調試器工做原理之(三):研究調試信息

根據它的維基百科 所描述,雖然 DWARF 是同 ELF 一同設計的(DWARF 是由 DWARF 標準委員會推出的開放標準。上文中展現的圖標就來自這個網站。),但 DWARF在理論上來講也能夠嵌入到其餘的可執行文件格式中。編程

DWARF 是一種複雜的格式,它吸取了過去許多年各類不一樣的架構與操做系統的格式的經驗。正是由於它解決了一個在任何平臺與 ABI (應用二進制接口)上爲任意高級語言產生調試信息這樣棘手的難題,它也必須很複雜。想要透徹的講解 DWARF 僅僅是經過這單薄的一篇文章是遠遠不夠的,說實話我也並無充分地瞭解 DWARF 到每個微小的細節,因此我也不能十分透徹的講解 (若是你感興趣的話,文末有一些可以幫助你的資源。建議從 DWARF 教程開始上手)。這篇文章中我將以淺顯易懂的方式展現 DWARF,以說明調試信息是如何實際工做的。緩存

ELF 文件中的調試部分數據結構

首先讓咱們看看DWARF 處在 ELF 文件中的什麼位置。ELF< 定義了每個生成的目標文件中的每一節。 節頭表section header table 聲明並定義了每一節及其名字。不一樣的工具以不一樣的方式處理不一樣的節,例如鏈接器會尋找鏈接器須要的部分,調試器會查找調試器須要的部分。架構

咱們本文的實驗會使用從這個 C 語言源文件構建的可執行文件,編譯成 tracedprog2:

#include <stdio.h>

void do_stuff(int my_arg)、
{
    int my_local = my_arg + 2;
    int i;

    for (i = 0; i < my_local; ++i)
        printf("i = %d/n", i);
}

int main()
{
    do_stuff(2);
    return 0;
}

使用 objdump -h 命令檢查 ELF 可執行文件中的節頭section header,咱們會看到幾個以 .debug_ 開頭的節,這些就是 DWARF 的調試部分。

26 .debug_aranges 00000020  00000000  00000000  00001037
                 CONTENTS, READONLY, DEBUGGING
27 .debug_pubnames 00000028  00000000  00000000  00001057
                 CONTENTS, READONLY, DEBUGGING
28 .debug_info   000000cc  00000000  00000000  0000107f
                 CONTENTS, READONLY, DEBUGGING
29 .debug_abbrev 0000008a  00000000  00000000  0000114b
                 CONTENTS, READONLY, DEBUGGING
30 .debug_line   0000006b  00000000  00000000  000011d5
                 CONTENTS, READONLY, DEBUGGING
31 .debug_frame  00000044  00000000  00000000  00001240
                 CONTENTS, READONLY, DEBUGGING
32 .debug_str    000000ae  00000000  00000000  00001284
                 CONTENTS, READONLY, DEBUGGING
33 .debug_loc    00000058  00000000  00000000  00001332
                 CONTENTS, READONLY, DEBUGGING

每一個節的第一個數字表明瞭該節的大小,最後一個數字表明瞭這個節開始位置距離 ELF 的偏移量。調試器利用這些信息從可執行文件中讀取節。

如今讓咱們看看一些在 DWARF 中查找有用的調試信息的實際例子。

查找函數

調試器的最基礎的任務之一,就是當咱們在某個函數處設置斷點時,調試器須要可以在入口處暫停。爲此,必須爲高級代碼中的函數名稱與函數在機器碼中指令開始的地址這二者之間創建起某種映射關係。

爲了獲取這種映射關係,咱們能夠查找 DWARF中的 .debug_info 節。在咱們深刻以前,須要一點基礎知識。DWARF 中每個描述類型被稱之爲調試信息入口(DIE)。每一個 DIE 都有關於它的類型、屬性之類的標籤。DIE 之間經過兄弟節點或子節點相互鏈接,屬性的值也能夠指向其它的 DIE。

運行如下命令:

objdump --dwarf=info tracedprog2

輸出文件至關的長,爲了方便舉例咱們只關注這些行(從這裏開始,無用的冗長信息我會以 (...)代替,方便排版):

<1><71>: Abbrev Number: 5 (DW_TAG_subprogram)
    <72>   DW_AT_external    : 1
    <73>   DW_AT_name        : (...): do_stuff
    <77>   DW_AT_decl_file   : 1
    <78>   DW_AT_decl_line   : 4
    <79>   DW_AT_prototyped  : 1
    <7a>   DW_AT_low_pc      : 0x8048604
    <7e>   DW_AT_high_pc     : 0x804863e
    <82>   DW_AT_frame_base  : 0x0      (location list)
    <86>   DW_AT_sibling     : <0xb3>

<1><b3>: Abbrev Number: 9 (DW_TAG_subprogram)
    <b4>   DW_AT_external    : 1
    <b5>   DW_AT_name        : (...): main
    <b9>   DW_AT_decl_file   : 1
    <ba>   DW_AT_decl_line   : 14
    <bb>   DW_AT_type        : <0x4b>
    <bf>   DW_AT_low_pc      : 0x804863e
    <c3>   DW_AT_high_pc     : 0x804865a
    <c7>   DW_AT_frame_base  : 0x2c     (location list)

上面的代碼中有兩個帶有 DW_TAG_subprogram 標籤的入口,在 DWARF 中這是對函數的指代。注意,這是兩個節的入口,其中一個是 do_stuff 函數的入口,另外一個是主(main)函數的入口。這些信息中有不少值得關注的屬性,但其中最值得注意的是 DW_AT_low_pc。它表明了函數開始處程序指針的值(在 x86 平臺上是 EIP)。此處 0x8048604 表明了 do_stuff 函數開始處的程序指針。下面咱們將利用 objdump -d 命令對可執行文件進行反彙編。來看看這塊地址中都有什麼:

08048604 <do_stuff>:
 8048604:       55           push   ebp
 8048605:       89 e5        mov    ebp,esp
 8048607:       83 ec 28     sub    esp,0x28
 804860a:       8b 45 08     mov    eax,DWORD PTR [ebp+0x8]
 804860d:       83 c0 02     add    eax,0x2
 8048610:       89 45 f4     mov    DWORD PTR [ebp-0xc],eax
 8048613:       c7 45 (...)  mov    DWORD PTR [ebp-0x10],0x0
 804861a:       eb 18        jmp    8048634 <do_stuff+0x30>
 804861c:       b8 20 (...)  mov    eax,0x8048720
 8048621:       8b 55 f0     mov    edx,DWORD PTR [ebp-0x10]
 8048624:       89 54 24 04  mov    DWORD PTR [esp+0x4],edx
 8048628:       89 04 24     mov    DWORD PTR [esp],eax
 804862b:       e8 04 (...)  call   8048534 <printf@plt>
 8048630:       83 45 f0 01  add    DWORD PTR [ebp-0x10],0x1
 8048634:       8b 45 f0     mov    eax,DWORD PTR [ebp-0x10]
 8048637:       3b 45 f4     cmp    eax,DWORD PTR [ebp-0xc]
 804863a:       7c e0        jl     804861c <do_stuff+0x18>
 804863c:       c9           leave
 804863d:       c3           ret

顯然,0x8048604 是 do_stuff 的開始地址,這樣一來,調試器就能夠創建函數與其在可執行文件中的位置間的映射關係。

查找變量

假設咱們當前在 do_staff 函數中某個位置上設置斷點停了下來。咱們想經過調試器取得 my_local 這個變量的值。調試器怎麼知道在哪裏去找這個值呢?很顯然這要比查找函數更爲困難。變量可能存儲在全局存儲區、堆棧、甚至是寄存器中。此外,同名變量在不一樣的做用域中可能有着不一樣的值。調試信息必須可以反映全部的這些變化,固然,DWARF 就能作到。

我不會逐一去將每一種可能的情況,但我會以調試器在 do_stuff 函數中查找 my_local 變量的過程來舉個例子。下面咱們再看一遍 .debug_info 中 do_stuff 的每個入口,此次連它的子入口也要一塊兒看。

<1><71>: Abbrev Number: 5 (DW_TAG_subprogram)
    <72>   DW_AT_external    : 1
    <73>   DW_AT_name        : (...): do_stuff
    <77>   DW_AT_decl_file   : 1
    <78>   DW_AT_decl_line   : 4
    <79>   DW_AT_prototyped  : 1
    <7a>   DW_AT_low_pc      : 0x8048604
    <7e>   DW_AT_high_pc     : 0x804863e
    <82>   DW_AT_frame_base  : 0x0      (location list)
    <86>   DW_AT_sibling     : <0xb3>
 <2><8a>: Abbrev Number: 6 (DW_TAG_formal_parameter)
    <8b>   DW_AT_name        : (...): my_arg
    <8f>   DW_AT_decl_file   : 1
    <90>   DW_AT_decl_line   : 4
    <91>   DW_AT_type        : <0x4b>
    <95>   DW_AT_location    : (...)       (DW_OP_fbreg: 0)
 <2><98>: Abbrev Number: 7 (DW_TAG_variable)
    <99>   DW_AT_name        : (...): my_local
    <9d>   DW_AT_decl_file   : 1
    <9e>   DW_AT_decl_line   : 6
    <9f>   DW_AT_type        : <0x4b>
    <a3>   DW_AT_location    : (...)      (DW_OP_fbreg: -20)
<2><a6>: Abbrev Number: 8 (DW_TAG_variable)
    <a7>   DW_AT_name        : i
    <a9>   DW_AT_decl_file   : 1
    <aa>   DW_AT_decl_line   : 7
    <ab>   DW_AT_type        : <0x4b>
    <af>   DW_AT_location    : (...)      (DW_OP_fbreg: -24)

看到每一個入口處第一對尖括號中的數字了嗎?這些是嵌套的等級,在上面的例子中,以 <2> 開頭的入口是以 <1> 開頭的子入口。所以咱們得知 my_local 變量(以 DW_TAG_variable 標籤標記)是 do_stuff 函數的局部變量。除此以外,調試器也須要知道變量的數據類型,這樣才能正確的使用與顯示變量。上面的例子中 my_local 的變量類型指向另外一個 DIE <0x4b>。若是使用 objdump 命令查看這個 DIE 的話,咱們會發現它是一個有符號 4 字節整型數據。

而爲了在實際運行的程序內存中查找變量的值,調試器須要使用到 DW_AT_location 屬性。對於 my_local 而言,是 DW_OP_fbreg: -20。這個代碼段的意思是說 my_local 存儲在距離它所在函數起始地址偏移量爲 -20 的地方。

do_stuff 函數的 DW_AT_frame_base 屬性值爲 0x0 (location list)。這意味着這個屬性的值須要在 location list 中查找。下面咱們來一塊兒看看。

$ objdump --dwarf=loc tracedprog2

tracedprog2:     file format elf32-i386

Contents of the .debug_loc section:

    Offset   Begin    End      Expression
    00000000 08048604 08048605 (DW_OP_breg4: 4 )
    00000000 08048605 08048607 (DW_OP_breg4: 8 )
    00000000 08048607 0804863e (DW_OP_breg5: 8 )
    00000000 <End of list>
    0000002c 0804863e 0804863f (DW_OP_breg4: 4 )
    0000002c 0804863f 08048641 (DW_OP_breg4: 8 )
    0000002c 08048641 0804865a (DW_OP_breg5: 8 )
    0000002c <End of list>

咱們須要關注的是第一列(do_stuff 函數的 DW_AT_frame_base 屬性包含 location list 中 0x0 的偏移量。而 main 函數的相同屬性包含 0x2c 的偏移量,這個偏移量是第二套地址列表的偏移量)。對於調試器可能定位到的每個地址,它都會指定當前棧幀到變量間的偏移量,而這個偏移就是經過寄存器來計算的。對於 x86 平臺而言,bpreg4 指向 esp,而 bpreg5 指向 ebp。

讓咱們再看看 do_stuff 函數的頭幾條指令。

08048604 <do_stuff>:
 8048604:       55          push   ebp
 8048605:       89 e5       mov    ebp,esp
 8048607:       83 ec 28    sub    esp,0x28
 804860a:       8b 45 08    mov    eax,DWORD PTR [ebp+0x8]
 804860d:       83 c0 02    add    eax,0x2
 8048610:       89 45 f4    mov    DWORD PTR [ebp-0xc],eax

只有當第二條指令執行後,ebp 寄存器才真正存儲了有用的值。固然,前兩條指令的基址是由上面所列出來的地址信息表計算出來的。一但 ebp 肯定了,計算偏移量就十分方便了,由於儘管 esp 在操做堆棧的時候須要移動,但 ebp 做爲棧底並不須要移動。

究竟咱們應該去哪裏找 my_local 的值呢?在 0x8048610 這塊地址後, my_local 的值通過在 eax 中的計算後被存在了內存中,從這裏開始咱們才須要關注 my_local 的值。調試器會利用 DW_OP_breg5: 8 這個棧幀來查找。咱們回想下,my_local 的 DW_AT_location 屬性值爲 DW_OP_fbreg: -20。因此應當從基址中 -20 ,同時因爲 ebp 寄存器須要 +8,因此最終結果爲 ebp - 12。如今再次查看反彙編代碼,來看看數據從 eax 中被移動到哪裏了。固然,這裏 my_local 應當被存儲在了 ebp - 12 的地址中。

查看行號

當咱們談到在調試信息尋找函數的時候,咱們利用了些技巧。當調試 C 語言源代碼並在某個函數出放置斷點的時候,咱們並不關注第一條「機器碼」指令(函數的調用準備工做已經完成而局部變量尚未初始化)。咱們真正關注的是函數的第一行「C 代碼」。

這就是 DWARF 徹底覆蓋映射 C 源代碼中的行與可執行文件中機器碼地址的緣由。下面是 .debug_line 節中所包含的內容,咱們將其轉換爲可讀的格式展現以下。

$ objdump --dwarf=decodedline tracedprog2

tracedprog2:     file format elf32-i386

Decoded dump of debug contents of section .debug_line:

CU: /home/eliben/eli/eliben-code/debugger/tracedprog2.c:
File name           Line number    Starting address
tracedprog2.c                5           0x8048604
tracedprog2.c                6           0x804860a
tracedprog2.c                9           0x8048613
tracedprog2.c               10           0x804861c
tracedprog2.c                9           0x8048630
tracedprog2.c               11           0x804863c
tracedprog2.c               15           0x804863e
tracedprog2.c               16           0x8048647
tracedprog2.c               17           0x8048653
tracedprog2.c               18           0x8048658

很容易就能夠看出其中 C 源代碼與反彙編代碼之間的對應關係。第 5 行指向do_stuff 函數的入口,0x8040604。第 6 行,指向 0x804860a ,正是調試器在調試 do_stuff 函數時須要停下來的地方。這裏已經完成了函數調用的準備工做。上面的這些信息造成了行號與地址間的雙向映射關係。

  • 當在某一行設置斷點的時候,調試器會利用這些信息去查找相應的地址來作斷點工做(還記得上篇文章中的 int 3 指令嗎?)
  • 當指令形成段錯誤時,調試器會利用這些信息來查看源代碼中發生問題的行。

libdwarf - 用 DWARF 編程

儘管使用命令行工具來得到 DWARF 頗有用,但這仍然不夠易用。做爲程序員,咱們但願知道當咱們須要這些調試信息時應當怎麼編程來獲取這些信息。

天然咱們想到的第一種方法就是閱讀 DWARF 規範並按規範操做閱讀使用。有句話說的好,分析 HTML 應當使用庫函數,永遠不要手工分析。對於 DWARF 來講正是如此。DWARF 比 HTML 要複雜得多。上面所展現出來的只是冰山一角。更糟糕的是,在實際的目標文件中,大部分信息是以很是緊湊的壓縮格式存儲的,分析起來更加複雜(信息中的某些部分,例如位置信息與行號信息,在某些虛擬機下是以指令的方式編碼的)。

因此咱們要使用庫來處理 DWARF。下面是兩種我熟悉的主要的庫(還有些不完整的庫這裏沒有寫)

  1. BFD (libbfd),包含了 objdump (對,就是這篇文章中咱們一直在用的這貨),ld(GNU 鏈接器)與 as(GNU 編譯器)。BFD 主要用於 GNU binutils
  2. libdwarf ,同它的哥哥 libelf 一同用於 Solaris 與 FreeBSD 中的調試信息分析。

相比較而言我更傾向於使用 libdwarf,由於我對它瞭解的更多,而且 libdwarf 的開源協議更開放(LGPL 對比 GPL)。

由於 libdwarf 自己至關複雜,操做起來須要至關多的代碼,因此我在這不會展現全部代碼。你能夠在 這裏 下載代碼並運行試試。運行這些代碼須要提早安裝 libelfand 與 libdwarf ,同時在使用鏈接器的時候要使用參數 -lelf 與 -ldwarf。

這個示例程序能夠接受可執行文件並打印其中的函數名稱與函數入口地址。下面是咱們整篇文章中使用的 C 程序通過示例程序處理後的輸出。

$ dwarf_get_func_addr tracedprog2
DW_TAG_subprogram: 'do_stuff'
low pc  : 0x08048604
high pc : 0x0804863e
DW_TAG_subprogram: 'main'
low pc  : 0x0804863e
high pc : 0x0804865a

libdwarf 的文檔很棒,若是你花些功夫,利用 libdwarf 得到這篇文章中所涉及到的 DWARF 信息應該並不困難。

結論與計劃

原理上講,調試信息是個很簡單的概念。儘管實現細節可能比較複雜,但通過了上面的學習我想你應該瞭解了調試器是如何從可執行文件中獲取它須要的源代碼信息的了。對於程序員而言,程序只是代碼段與數據結構;對可執行文件而言,程序只是一系列存儲在內存或寄存器中的指令或數據。但利用調試信息,調試器就能夠將這二者鏈接起來,從而完成調試工做。

此文與這系列的前兩篇,一同介紹了調試器的內部工做過程。利用這裏所講到的知識,再敲些代碼,應該能夠完成一個 Linux 中最簡單、基礎但也有必定功能的調試器。

下一步我並不肯定要作什麼,這個系列文章可能就此結束,也有可能我要講些堆棧調用的事情,又或者講 Windows 下的調試。大家有什麼好的點子或者相關材料,能夠直接評論或者發郵件給我。

參考

via: http://eli.thegreenplace.net/2011/02/07/how-debuggers-work-part-3-debugging-information

做者:Eli Bendersky 譯者:YYforymj 校對:wxy

本文由 LCTT 原創編譯,Linux中國 榮譽推出

原文來自:https://linux.cn/article-8579-1.html

本文地址:http://www.linuxprobe.com/debugger-principle-information.html

相關文章
相關標籤/搜索