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>
變數能夠位於的地方有 堆 、 棧 或者程序的 文本段 。全局變數是程序的一部分。某種程度上,你可以把它們想像成是在編譯的時候分配的。因此,我們可以很容易的找出全局變數的地址。讓我們來看看,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
- 讀 8 位元組(以小端的格式,因為是在 x86 上)
- 得到答案!
在上面這個例子中是 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
本文轉載來自 Linux 中國: https://github.com/Linux-CN/archive