作者 | Julia Evans
譯者 | Lv Feng (ucasFL) ? ? ? ? ? 共計翻譯:64 篇 貢獻時間:537 天
大家好!今天,我開始進行我的 ruby 堆疊跟蹤專案[1],我發覺我現在瞭解了一些關於 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>
變數能夠位於的地方有堆、棧或者程式的文字段。全域性變數是程式的一部分。某種程度上,你可以把它們想象成是在編譯的時候分配的。因此,我們可以很容易的找出全域性變數的地址。讓我們來看看,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
所以,我們看到,起始地址 5598a9605000
和 0x5598a9a8f7f0
很像,但並不一樣。哪裡不一樣呢,我們把兩個數相減,看看結果是多少:
(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
的地方,它有型別 31
。31
是什麼?不用擔心,我們也可以在 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 除錯資料,該如何分解:
ruby_current_thread
所指向的記憶體區域24
位元組來得到 stack_size
在上面這個例子中是 131072
(即 128 kb)。
對我來說,這使得除錯資訊的用途更加明顯。如果我們不知道這些所有變數所表示的額外的元資料,那麼我們無法知道儲存在 0x5598ab325b0
這一地址的位元組是什麼。
這就是為什麼你可以為你的程式單獨安裝程式的除錯資訊,因為 gdb
並不關心從何處獲取這些額外的除錯資訊。
DWARF 令人迷惑
我最近閱讀了大量的 DWARF 知識。現在,我使用 libdwarf,使用體驗不是很好,這個 API 令人迷惑,你將以一種奇怪的方式初始化所有東西,它真的很慢(需要花費 0.3 秒的時間來讀取我的 Ruby 程式的所有除錯資訊,這真是可笑)。有人告訴我,來自 elfutils 的 libdw 要好一些。
同樣,再提及一點,你可以檢視 DW_AT_data_member_location
來檢視結構成員的偏移。我在 Stack Overflow 上查詢如何完成這件事,並且得到這個答案[2]。基本上,以下麵這樣一個檢查開始:
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 例子[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中國 榮譽推出