歡迎光臨
每天分享高質量文章

gdb 如何工作? | Linux 中國

最近,我使用 gdb 來查看我的 Ruby 程式,所以,我們將對一個 Ruby 程式運行 gdb 。
— Julia Evans


致謝
編譯自 | https://jvns.ca/blog/2016/08/10/how-does-gdb-work/ 
 作者 | Julia Evans
 譯者 | Lv Feng (ucasFL) ? ? ? ? ? 共計翻譯:64 篇 貢獻時間:537 天

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

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

獲取全域性變數

下麵展示瞭如何獲取全域性變數 ruby_current_thread 的地址:

  1. $ sudo gdb -p 2983

  2. (gdb) p & ruby_current_thread

  3. $2 = (rb_thread_t **) 0x5598a9a8f7f0 <ruby_current_thread>

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

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

  1. $ sudo cat /proc/2983/maps | grep bin/ruby

  2. 5598a9605000-5598a9886000 r-xp 00000000 00:32 323508                     /home/bork/.rbenv/versions/2.1.6/bin/ruby

  3. 5598a9a86000-5598a9a8b000 r--p 00281000 00:32 323508                     /home/bork/.rbenv/versions/2.1.6/bin/ruby

  4. 5598a9a8b000-5598a9a8d000 rw-p 00286000 00:32 323508                     /home/bork/.rbenv/versions/2.1.6/bin/ruby

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

  1. (gdb) p/x 0x5598a9a8f7f0 - 0x5598a9605000

  2. $4 = 0x48a7f0

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

  1. sudo nm /proc/2983/exe | grep ruby_current_thread

  2. 000000000048a7f0 b ruby_current_thread

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

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

解取用指標

  1. (gdb) p ruby_current_thread

  2. $1 = (rb_thread_t *) 0x5598ab3235b0

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

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

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

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

描述結構

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

  1. (gdb) x/40b ruby_current_thread

  2. 0x5598ab3235b0: 16  -90 55  -85 -104    85  0   0

  3. 0x5598ab3235b8: 32  47  50  -85 -104    85  0   0

  4. 0x5598ab3235c0: 16  -64 -55 115 -97 127 0   0

  5. 0x5598ab3235c8: 0   0   2   0   0   0   0   0

  6. 0x5598ab3235d0: -96 -83 -39 115 -97 127 0   0

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

  1. (gdb) p *(ruby_current_thread)

  2. $8 = {self = 94114195940880, vm = 0x5598ab322f20, stack = 0x7f9f73c9c010,

  3.    stack_size = 131072, cfp = 0x7f9f73d9ada0, safe_level = 0,    raised_flag = 0,

  4.    last_status = 8, state = 0, waiting_fd = -1, passed_block = 0x0,

  5.    passed_bmethod_me = 0x0, passed_ci = 0x0,    top_self = 94114195612680,

  6.    top_wrapper = 0, base_block = 0x0, root_lep = 0x0, root_svar = 8, thread_id =

  7.    140322820187904,

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

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

  1. DW_AT_name                  "rb_thread_struct"

  2. DW_AT_byte_size             0x000003e8

  3. DW_TAG_member

  4.  DW_AT_name                  "self"

  5.  DW_AT_type                  <0x00000579>

  6.  DW_AT_data_member_location  DW_OP_plus_uconst 0

  7. DW_TAG_member

  8.  DW_AT_name                  "vm"

  9.  DW_AT_type                  <0x0000270c>

  10.  DW_AT_data_member_location  DW_OP_plus_uconst 8

  11. DW_TAG_member

  12.  DW_AT_name                  "stack"

  13.  DW_AT_type                  <0x000006b3>

  14.  DW_AT_data_member_location  DW_OP_plus_uconst 16

  15. DW_TAG_member

  16.  DW_AT_name                  "stack_size"

  17.  DW_AT_type                  <0x00000031>

  18.  DW_AT_data_member_location  DW_OP_plus_uconst 24

  19. DW_TAG_member

  20.  DW_AT_name                  "cfp"

  21.  DW_AT_type                  <0x00002712>

  22.  DW_AT_data_member_location  DW_OP_plus_uconst 32

  23. DW_TAG_member

  24.  DW_AT_name                  "safe_level"

  25.  DW_AT_type                  <0x00000066>

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

  1. < 1><0x00000031>    DW_TAG_typedef

  2.                      DW_AT_name                  "size_t"

  3.                      DW_AT_type                  <0x0000003c>

  4. < 1><0x0000003c>    DW_TAG_base_type

  5.                      DW_AT_byte_size             0x00000008

  6.                      DW_AT_encoding              DW_ATE_unsigned

  7.                      DW_AT_name                  "long unsigned int"

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

如果我們有了 DWARF 除錯資料,該如何分解:

☉ 查看 ruby_current_thread 所指向的記憶體區域
☉ 加上 24 位元組來得到 stack_size
☉ 讀 8 位元組(以小端的格式,因為是在 x86 上)
☉ 得到答案!

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

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

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

DWARF 令人迷惑

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

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

  1. dwarf_whatform(attrs[i], &form, &error);

  2.    if (form == DW_FORM_data1 || form == DW_FORM_data2

  3.        form == DW_FORM_data2 || form == DW_FORM_data4

  4.        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 例子[3],我們可以知道,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[5] 譯者:ucasFL 校對:wxy

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

赞(0)

分享創造快樂

© 2020 知識星球   网站地图