對於編譯器全端的訓練, 剩下 debugger (好啦, linker 嚴格來說還沒完全練過一遍),
debugger 是我覺得最難的, 我有想過目標設定在 dos debug 那種即可 (無 symbol
debugger), 但還是不知道怎麼開始, 甚至去看 msdos 的 source code, 裡頭有 debug,
不過是用組合語言寫的, 決定先撤退。
和我之前學習的主題一樣, 曾經挑戰多次, 但都無功而返。突然這幾天又被我想到
debugger 這個主題, 再次找了一些資料, 終於找到有關 linux debugger 開發的資料,
幸運的是還有簡體中文翻譯 - 开发一个 Linux 调试器,
原文是: Writing a Linux Debugger。
是不是在 linux 我倒是沒那麼在意, debugger 都好,
但如果是 linux debugger 那更好。
這個門檻就低一點, 站在巨人的肩膀, 使用 linux/ptrace 來寫 debugger,
不用和硬體 debug 暫存器搏鬥。不過 ptrace 也不是那麼好學就是, 我曾經挑戰多次,
一樣無功而返。
ptrace 和 debugger 一起學習很適合, 之前學習 ptrace 就只想做一件事情,
改變程式的變數值印出來, 這不就是 debugger 做的事情嗎!
這篇算是這個教學文的學習指引, 也就是學習這系列文的學習文, 有點繞舌, 如果
Writing a Linux Debugger 有難倒你, 希望我這篇文章能幫助你學習這個主題。
先把程式編譯起來, 需要額外 2 個 library, libelfin, linenoise。
descent@debian-vm:minidbg$ ls ext/
libelfin linenoise
需要自己 git clone 出來:
libelfin 需要切到 fbreg branch
origin https://github.com/TartanLlama/libelfin.git (fetch)
origin https://github.com/TartanLlama/libelfin.git (push)
descent@debian-vm:libelfin$ git branch
* fbreg
linenoise
origin https://github.com/antirez/linenoise.git (fetch)
origin https://github.com/antirez/linenoise.git (push)
編譯 minidbg
cmake .
make
make VERBOSE=1 # 可以看到編譯指令
編譯好 minidbg 之後會再需要一個 debug 程式, 寫個 hello world 吧! 編譯之後,
然後執行 ./minidbg h 會發現:
list 1 error message
Hello worlddescent@debian-vm:minidbg$ ./minidbg h
terminate called after throwing an instance of 'dwarf::format_error'
what(): unknown compilation unit version 5
Aborted
這是因為該程式使用的 libelfin 只支援 DWARFv4, gcc 11 是使用 DWARFv5,
改用 gcc-gdwarf-4 h.c -o h 來編譯要除錯的程式即可,
這樣就會使用 DWARFv4 的版本; 不過 source code 是用 -gdwarf-2 來編譯測試程式。
而為了搭配設定中斷點, 最後我使用
gcc h.c -no-pie -gdwarf-2 -o h 來編譯要除錯的程式。
minidbg 使用範例請參考 list 2。
list 2. minidbg
0 minidbg h
1 minidbg> break main
2 Set breakpoint at address 0x1148
3 minidbg> cont
4 hello, 10
5 Got signal Unknown signal -911728400
6 minidbg> cont
作者 Sy Brand 有說明 -no-pie 的影響, 並介紹了 personality()。大概原因是這樣,
用
objdump 看到的 main 位址很有可能不是被載入執行的位址, 如果是這樣,
設置的中斷點就可能不是預期的位置, 而 execl 不是自己寫的,
我們不知道 execl 是不是真的把 main load 到 objdump 看到的位址
(這衍生另外一個問題, execl 把程式 load 到那個位址,
有辦法查得到嗎?), 所以才有 -no-pie 或是 personality(),
我自己嘗試過在 linux 把 elf 執行檔 load 起來並執行, 但沒有成功,
卡在一些地方, 我的目標是想把 elf 執行檔
load 任何位址都可以執行, 但其中有部份我還沒克服, 所以止步到某個步驟,
在 linux 要克服蠻多問題, 在 bare-metal 環境我是有成功,
uefi loader 載入 os kernel 就是這麼做的。
Sy Brand 有提到可以看 /proc/[pid]/maps 的第一行位址來當做載入位址。
使用 gdb 測試, gdb 不管有沒有 -no-pie, 都可以正確設定中斷點,
gdb 找得到執行檔被真正載入的位址, 我用 strace (strace -o g.txt gdb abcdef)
觀察 gdb, 可惜沒找出什麼方向,
gdb 在執行 run 指令之後才會呼叫 ptrace。
(gdb) b main
Breakpoint 1 at 0x1172: file h.c, line 11.
gdb 設定中斷點一樣是顯示在 0x1172,
但最後卻可以正確設定在 0x555555555172 的位址。
//child
personality(ADDR_NO_RANDOMIZE);
execute_debugee(prog);
如果不用 -no-pie 編譯, elf 執行檔會被載入到某個未知位址,
我想了一個很特別的方式, 找出 elf 被載入到的 main 位址,
list 22 會印出 main addree, 就可以得知被載入
的真正位址, 和 objdump (list 23) 果然不同。
可以看到, 被載入到 0x555555555163,
而不是 0x1163。
流程是這樣: 執行到 list 22 L12 會卡在 while(1),
使用 kill -s SIGSTOP 240757 stop list 22 的執行檔,
這時候 minidbg 又可以繼續執行, list 25 L6 就可以看到
main 開始的 8 byte data。
我本來一開始是沒有設計 list 22 L12 while(1), cont 讓程式跑完再使用
memory read 指令, 但是只讀到 0xffffffffffffffff, 可能為了資安問題,
這個 process 被整個清掉了。所以才用了這麼迂迴的手法。
list 22 h.c
1 #include <stdio.h>
2 int abc123 = 5;
3
4 void func123()
5 {
6 int mn789 = 8;
7 printf("hello c, abc123: %d, mn789: %d\n", abc123, mn789);
8 }
9 int main(int argc, char *argv[])
10 {
11 printf("main: %p\n", main);
12 while(1);
13 func123();
14 return abc123;
15 }
list 23 objdump -d h
1 0000000000001163 <main>:
2 int main(int argc, char *argv[])
3 {
4 1163: 55 push %rbp
5 1164: 48 89 e5 mov %rsp,%rbp
6 1167: 48 83 ec 10 sub $0x10,%rsp
list 25 md
1 descent@debian64:minidbg$ ./minidbg h
2 h child pid: 240757
3 minidbg> c
4 regs.rip: 7ffff7fd5090
5 main: 0x555555555163
6 minidbg> m r 0x555555555163
7 0x55 0x48 0x89 0xe5 0x48 0x83 0xec 0x10
8 minidbg>
分析 elf 有個 readelf 工具, 類似地, 觀察 dwarf 也有一個 dwarfdump 工具,
readelf 算熟悉, dwarfdump 就很陌生了, 我今天 (20220630)
才第一次安裝這個工具, 和 elf 不同的是, dwarf 的資訊是壓縮過的,
所以無法用 hexdump 來直接觀察 dwarf, 還是借助
dwarfdump 會比較容易。對於 elf 我會使用 readelf,
hexdump 來交叉比對, dwarf 看起來就難纏多了。
fig 3 的圖應該是我看過最具象化 elf 的圖了。初步比較, 我覺得 dwarf 比較複雜。
[elf101-64_pages-to-jpg-0001]
fig 3. from https://github.com/corkami/pics/raw/master/binary/elf101/
elf101-64.pdf
dwarf 比我想的難很多, 之前的基礎派不太上用場, 大概是重新學習的難度。
在快速掃過 10 篇文章之後, 我慢慢有了怎麼開始學習的計畫:
在初步階段, 需要用 objdump 來觀察到設定的位址, 然後使用 break
指令來設定中斷點, 先不要把 dwarf 搞進來, 光使用 ptrace 就夠複雜了。
對於 dwarf 安排了另外的學習方式, 我已經很擅長拆開整個複雜的東西來學習,
畢竟主要還是要學習 debugger, 沒有 dwarf 還是可以作到的, 只要會用 ptrace 即可。
這部份就是 Writing a Linux Debugger Part 3: Registers and memory 之前的部份,
之後就會開始講到 dwarf, 會變得很複雜, 先放一邊。
測試了「設定中斷點」和「繼續執行」和「讀暫存器」以及「讀寫記憶體位址」
這些指令。和 list 2 不同的是, 我增加了縮寫, break 打 b 就可以,
cont 打 c 就可以, 類似 gdb 的指令。
測試時需要 list 3 的資訊, 才能做這個驗證, main 位址在 0x401122,
所以 list 5 L3 就是把中斷點設定在 main。另外我把 h 的 pid 印出來,
ps 可以看到 h 的狀態是 t+, 處於停止或是被追蹤的狀態。
再來打 c 讓 h 停在 main, 程式起來是停在 main, 要怎麼驗證?
我想到的方式是看 cs:rip, 所以來看一下 rip, list 5 L8 的指令,
顯示的是 0x401123, 差了 1, 不知道是怎麼回事?
可能是這樣, main 現在被換成 0xcc (int 3), 所以 rip 指向 0x401123
是下一個要執行的位址。如果我說錯, 請打我臉 (不是真的打我臉)。
好, 看起來真的停在中斷點上, 再來看記憶體的內容, 要看哪裡, 看 main 好了,
list 5 L6 的指令下了之後可以看到 20ec8348e58948cc,
有沒發現和 list 3 L5 ~ L6, L7 很像,
除了 0x55 變成 0xcc, 也說明之前下的中斷點真的把 0x55 改成 0xcc 了。
list 3. objdump -Sd h
1 0000000000401122 <main>:
2 #include <stdio.h>
3 int main(int argc, char *argv[])
4 {
5 401122: 55 push %rbp
6 401123: 48 89 e5 mov %rsp,%rbp
7 401126: 48 83 ec 20 sub $0x20,%rsp
8 40112a: 89 7d ec mov %edi,-0x14(%rbp)
9 40112d: 48 89 75 e0 mov %rsi,-0x20(%rbp)
10 int a;
11 a = 5;
12 401131: c7 45 fc 05 00 00 00 movl $0x5,-0x4(%rbp)
13 printf("hello c\n");
14 401138: 48 8d 3d c5 0e 00 00 lea 0xec5(%rip),%rdi #
402004 <_IO_stdin_used+0x4>
15 40113f: e8 ec fe ff ff callq 401030 <puts@plt>
16 return a;
17 401144: 8b 45 fc mov -0x4(%rbp),%eax
18 }
19 401147: c9 leaveq
20 401148: c3 retq
21 401149: 0f 1f 80 00 00 00 00 nopl 0x0(%rax)
list 5 minidbg 測試過程
1 descent@debian64:minidbg$ ./minidbg h
2 h child pid: 231719
3 minidbg> b 0x401122
4 Set breakpoint at address 0x401122
5 minidbg> c
6 minidbg> m r 0x401122
7 20ec8348e58948cc
8 minidbg> r r rip
9 401123
有了使用上的理解, 之後看相關的程式碼, 就可以知道這是怎麼辦到的,
揭開除錯器神秘的面紗。Writing a Linux Debugger
系列文一次解除我 debugger/ptrace 2 個疑惑。
最後在我寫下這篇文章時, 我對 dwarfdump 的內容以達「略懂」,
沒有一開始覺得那麼難了, 也覺得 dwarf 很了不起,
為了 debug, 紀錄了相當多的資訊。
ref:
‧ LCTT 是“Linux 中国”(https://linux.cn/)的翻译组,负责从国外优秀媒体翻
译
Linux 相关的技术、资讯、杂文等内容。
‧ DWARF, 调试信息存储格式
‧ dwarf2调试信息格式——chapter1,2
‧ Dwarf2 Exception Handler HOWTO
‧ Writing a Debugger
‧ 中文試譯:How debuggers work: Part 3 – Debugging information
blog 原文:
https://descent-incoming.blogspot.com/2022/07/debugger.html