Linux中國

gdb 如何工作?

大家好!今天,我開始進行我的 ruby 堆棧跟蹤項目,我發覺我現在了解了一些關於 gdb 內部如何工作的內容。

最近,我使用 gdb 來查看我的 Ruby 程序,所以,我們將對一個 Ruby 程序運行 gdb 。它實際上就是一個 Ruby 解釋器。首先,我們需要列印出一個全局變數的地址:ruby_current_thread

獲取全局變數

下面展示了如何獲取全局變數 ruby_current_thread 的地址:

$ sudo gdb -p 2983
(gdb) p & ruby_current_thread
$2 = (rb_thread_t **) 0x5598a9a8f7f0 <ruby_current_thread>

變數能夠位於的地方有 heap stack 或者程序的 文本段 text 。全局變數是程序的一部分。某種程度上,你可以把它們想像成是在編譯的時候分配的。因此,我們可以很容易的找出全局變數的地址。讓我們來看看,gdb 是如何找出 0x5598a9a87f0 這個地址的。

我們可以通過查看位於 /proc 目錄下一個叫做 /proc/$pid/maps 的文件,來找到這個變數所位於的大致區域。

$ sudo cat /proc/2983/maps | grep bin/ruby
5598a9605000-5598a9886000 r-xp 00000000 00:32 323508                     /home/bork/.rbenv/versions/2.1.6/bin/ruby
5598a9a86000-5598a9a8b000 r--p 00281000 00:32 323508                     /home/bork/.rbenv/versions/2.1.6/bin/ruby
5598a9a8b000-5598a9a8d000 rw-p 00286000 00:32 323508                     /home/bork/.rbenv/versions/2.1.6/bin/ruby

所以,我們看到,起始地址 5598a96050000x5598a9a8f7f0 很像,但並不一樣。哪裡不一樣呢,我們把兩個數相減,看看結果是多少:

(gdb) p/x 0x5598a9a8f7f0 - 0x5598a9605000
$4 = 0x48a7f0

你可能會問,這個數是什麼?讓我們使用 nm 來查看一下程序的符號表。

sudo nm /proc/2983/exe | grep ruby_current_thread
000000000048a7f0 b ruby_current_thread

我們看到了什麼?能夠看到 0x48a7f0 嗎?是的,沒錯。所以,如果我們想找到程序中一個全局變數的地址,那麼只需在符號表中查找變數的名字,然後再加上在 /proc/whatever/maps 中的起始地址,就得到了。

所以現在,我們知道 gdb 做了什麼。但是,gdb 實際做的事情更多,讓我們跳過直接轉到…

解引用指針

(gdb) p ruby_current_thread
$1 = (rb_thread_t *) 0x5598ab3235b0

我們要做的下一件事就是解引用 ruby_current_thread 這一指針。我們想看一下它所指向的地址。為了完成這件事,gdb 會運行大量系統調用比如:

ptrace(PTRACE_PEEKTEXT, 2983, 0x5598a9a8f7f0, [0x5598ab3235b0]) = 0

你是否還記得 0x5598a9a8f7f0 這個地址?gdb 會問:「嘿,在這個地址中的實際內容是什麼?」。2983 是我們運行 gdb 這個進程的 ID。gdb 使用 ptrace 這一系統調用來完成這一件事。

好極了!因此,我們可以解引用內存並找出內存地址中存儲的內容。有一些有用的 gdb 命令,比如 x/40w 變數x/40b 變數 分別會顯示給定地址的 40 個字/位元組。

描述結構

一個內存地址中的內容可能看起來像下面這樣。可以看到很多位元組!

(gdb) x/40b ruby_current_thread
0x5598ab3235b0: 16  -90 55  -85 -104    85  0   0
0x5598ab3235b8: 32  47  50  -85 -104    85  0   0
0x5598ab3235c0: 16  -64 -55 115 -97 127 0   0
0x5598ab3235c8: 0   0   2   0   0   0   0   0
0x5598ab3235d0: -96 -83 -39 115 -97 127 0   0

這很有用,但也不是非常有用!如果你是一個像我一樣的人類並且想知道它代表什麼,那麼你需要更多內容,比如像這樣:

(gdb) p *(ruby_current_thread)
$8 = {self = 94114195940880, vm = 0x5598ab322f20, stack = 0x7f9f73c9c010,
    stack_size = 131072, cfp = 0x7f9f73d9ada0, safe_level = 0,    raised_flag = 0,
    last_status = 8, state = 0, waiting_fd = -1, passed_block = 0x0,
    passed_bmethod_me = 0x0, passed_ci = 0x0,    top_self = 94114195612680,
    top_wrapper = 0, base_block = 0x0, root_lep = 0x0, root_svar = 8, thread_id =
    140322820187904,

太好了。現在就更加有用了。gdb 是如何知道這些所有域的,比如 stack_size ?是從 DWARF 得知的。DWARF 是存儲額外程序調試數據的一種方式,從而像 gdb 這樣的調試器能夠工作的更好。它通常存儲為二進位的一部分。如果我對我的 Ruby 二進位文件運行 dwarfdump 命令,那麼我將會得到下面的輸出:

(我已經重新編排使得它更容易理解)

DW_AT_name                  "rb_thread_struct"
DW_AT_byte_size             0x000003e8
DW_TAG_member
  DW_AT_name                  "self"
  DW_AT_type                  <0x00000579>
  DW_AT_data_member_location  DW_OP_plus_uconst 0
DW_TAG_member
  DW_AT_name                  "vm"
  DW_AT_type                  <0x0000270c>
  DW_AT_data_member_location  DW_OP_plus_uconst 8
DW_TAG_member
  DW_AT_name                  "stack"
  DW_AT_type                  <0x000006b3>
  DW_AT_data_member_location  DW_OP_plus_uconst 16
DW_TAG_member
  DW_AT_name                  "stack_size"
  DW_AT_type                  <0x00000031>
  DW_AT_data_member_location  DW_OP_plus_uconst 24
DW_TAG_member
  DW_AT_name                  "cfp"
  DW_AT_type                  <0x00002712>
  DW_AT_data_member_location  DW_OP_plus_uconst 32
DW_TAG_member
  DW_AT_name                  "safe_level"
  DW_AT_type                  <0x00000066>

所以,ruby_current_thread 的類型名為 rb_thread_struct,它的大小為 0x3e8 (即 1000 位元組),它有許多成員項,stack_size 是其中之一,在偏移為 24 的地方,它有類型 3131 是什麼?不用擔心,我們也可以在 DWARF 信息中查看。

< 1><0x00000031>    DW_TAG_typedef
                      DW_AT_name                  "size_t"
                      DW_AT_type                  <0x0000003c>
< 1><0x0000003c>    DW_TAG_base_type
                      DW_AT_byte_size             0x00000008
                      DW_AT_encoding              DW_ATE_unsigned
                      DW_AT_name                  "long unsigned int"

所以,stack_size 具有類型 size_t,即 long unsigned int,它是 8 位元組的。這意味著我們可以查看該棧的大小。

如果我們有了 DWARF 調試數據,該如何分解:

  1. 查看 ruby_current_thread 所指向的內存區域
  2. 加上 24 位元組來得到 stack_size
  3. 讀 8 位元組(以小端的格式,因為是在 x86 上)
  4. 得到答案!

在上面這個例子中是 131072(即 128 kb)。

對我來說,這使得調試信息的用途更加明顯。如果我們不知道這些所有變數所表示的額外的元數據,那麼我們無法知道存儲在 0x5598ab325b0 這一地址的位元組是什麼。

這就是為什麼你可以為你的程序單獨安裝程序的調試信息,因為 gdb 並不關心從何處獲取這些額外的調試信息。

DWARF 令人迷惑

我最近閱讀了大量的 DWARF 知識。現在,我使用 libdwarf,使用體驗不是很好,這個 API 令人迷惑,你將以一種奇怪的方式初始化所有東西,它真的很慢(需要花費 0.3 秒的時間來讀取我的 Ruby 程序的所有調試信息,這真是可笑)。有人告訴我,來自 elfutils 的 libdw 要好一些。

同樣,再提及一點,你可以查看 DW_AT_data_member_location 來查看結構成員的偏移。我在 Stack Overflow 上查找如何完成這件事,並且得到這個答案。基本上,以下面這樣一個檢查開始:

dwarf_whatform(attrs[i], &form, &error);
    if (form == DW_FORM_data1 || form == DW_FORM_data2
        form == DW_FORM_data2 || form == DW_FORM_data4
        form == DW_FORM_data8 || form == DW_FORM_udata) {

繼續往前。為什麼會有 800 萬種不同的 DW_FORM_data 需要檢查?發生了什麼?我沒有頭緒。

不管怎麼說,我的印象是,DWARF 是一個龐大而複雜的標準(可能是人們用來生成 DWARF 的庫稍微不兼容),但是我們有的就是這些,所以我們只能用它來工作。

我能夠編寫代碼並查看 DWARF ,這就很酷了,並且我的代碼實際上大多數能夠工作。除了程序崩潰的時候。我就是這樣工作的。

展開棧路徑

在這篇文章的早期版本中,我說過,gdb 使用 libunwind 來展開棧路徑,這樣說並不總是對的。

有一位對 gdb 有深入研究的人發了大量郵件告訴我,為了能夠做得比 libunwind 更好,他們花費了大量時間來嘗試如何展開棧路徑。這意味著,如果你在程序的一個奇怪的中間位置停下來了,你所能夠獲取的調試信息又很少,那麼你可以對棧做一些奇怪的事情,gdb 會嘗試找出你位於何處。

gdb 能做的其他事

我在這兒所描述的一些事請(查看內存,理解 DWARF 所展示的結構)並不是 gdb 能夠做的全部事情。閱讀 Brendan Gregg 的昔日 gdb 例子,我們可以知道,gdb 也能夠完成下面這些事情:

  • 反彙編
  • 查看寄存器內容

在操作程序方面,它可以:

  • 設置斷點,單步運行程序
  • 修改內存(這是一個危險行為)

了解 gdb 如何工作使得當我使用它的時候更加自信。我過去經常感到迷惑,因為 gdb 有點像 C,當你輸入 ruby_current_thread->cfp->iseq,就好像是在寫 C 代碼。但是你並不是在寫 C 代碼。我很容易遇到 gdb 的限制,不知道為什麼。

知道使用 DWARF 來找出結構內容給了我一個更好的心智模型和更加正確的期望!這真是極好的!

via: https://jvns.ca/blog/2016/08/10/how-does-gdb-work/

作者:Julia Evans 譯者:ucasFL 校對:wxy

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


本文轉載來自 Linux 中國: https://github.com/Linux-CN/archive

對這篇文章感覺如何?

太棒了
0
不錯
0
愛死了
0
不太好
0
感覺很糟
0
雨落清風。心向陽

    You may also like

    Leave a reply

    您的郵箱地址不會被公開。 必填項已用 * 標註

    此站點使用Akismet來減少垃圾評論。了解我們如何處理您的評論數據

    More in:Linux中國