寧可十年不將軍, 不可一日不拱卒。 |
和我之前學習的主題一樣, 曾經挑戰多次, 但都無功而返。突然這幾天又被我想到 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。
需要自己 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 hello 會發現:
這是因為該程式使用的 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。
作者 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。
//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 被整個清掉了。所以才用了這麼迂迴的手法。
分析 elf 有個 readelf 工具, 類似地, 觀察 dwarf 也有一個 dwarfdump 工具, readelf 算熟悉, dwarfdump 就很陌生了, 我今天 (20220630) 才第一次安裝這個工具, 和 elf 不同的是, dwarf 的資訊是壓縮過的, 所以無法用 hexdump 來直接觀察 dwarf, 還是借助 dwarfdump 會比較容易。對於 elf 我會使用 readelf, hexdump 來交叉比對, dwarf 看起來就難纏多了。
fig 3 的圖應該是我看過最具象化 elf 的圖了。初步比較, 我覺得 dwarf 比較複雜。
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 了。
有了使用上的理解, 之後看相關的程式碼, 就可以知道這是怎麼辦到的, 揭開除錯器神秘的面紗。Writing a Linux Debugger 系列文一次解除我 debugger/ptrace 2 個疑惑。
最後在我寫下這篇文章時, 我對 dwarfdump 的內容以達「略懂」, 沒有一開始覺得那麼難了, 也覺得 dwarf 很了不起, 為了 debug, 紀錄了相當多的資訊。
ref:
沒有留言:
張貼留言
使用 google 的 reCAPTCHA 驗證碼, 總算可以輕鬆留言了。
我實在受不了 spam 了, 又不想讓大家的眼睛花掉, 只好放棄匿名留言。這是沒辦法中的辦法了。留言的朋友需要有 google 帳號。