看板 C_and_CPP 關於我們 聯絡資訊
對於編譯器全端的訓練, 剩下 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 -- 紙上得來終覺淺,絕知此事要躬行。 -- ※ 發信站: 批踢踢實業坊(ptt.cc), 來自: 116.89.131.215 (臺灣) ※ 文章網址: https://www.ptt.cc/bbs/C_and_CPP/M.1671885594.A.FFC.html
hella: 推 12/24 21:16
NCKUchemRx: 看了1/2 先推個 12/25 07:08
enthos: 我是只到手刻elf header(FORTH),可以執行但格式有錯. 12/26 01:08
真厲害, 難得看到有研究 forth 的, 有沒有相關心得可以分享?
SmallHanley: 推 12/26 11:53
lc85301: 推 12/27 12:08
eopXD: 推推推 12/27 14:18
IhateOGC: 太神了 01/01 00:15
※ 編輯: descent (180.217.141.102 臺灣), 01/01/2023 00:26:20
handsome616: 謝謝 01/01 18:05