背景:
在做XXX編譯器檢證時經常需要區分是代碼端錯誤,還是編譯器端錯誤,因此對代碼進行調試是必不可少的。但是狗日的甲方並沒有提供對應的調試器XXXDB,而用GDB調試XXX生成的可執行程序很不穩定,經常出現異常,干脆自己動手,寫mini調試器,順便學習一下開發一個調試器到底需要哪些知識。
目標:
GDB一共有十幾萬行代碼,95%的功能都用不上。三個最基本的功能:“單步”、“斷點”、“查看變量”即可滿足日常工作中的大部分需求。並且基於學習、分享的初衷,我盡量把代碼控制在千行左右,足夠簡單,足夠傻瓜,最關鍵的是,老夫沒那么時間啊。
預備知識:
先簡單解釋下調試器的基本原理。
假設調試器進程為A,被調試程序的進程為B. 如果要實現“單步”、“斷點” 和“查看變量”三種基本功能,那也就意味着A進程必須要擁有三種操控B進程的能力:
1 A可以暫停B進程的執行
2 A可以恢復B進程的執行
3 A可以在任意時刻查看B進程的內存及寄存器
顯然,所謂“斷點”就是在某個特定“時刻”暫停B進程的執行;所謂“單步”就是先恢復B進程的執行“一小會兒”,然后立刻暫停;所謂watch變量,就是查看特定內存或者某個寄存器,不管啥變量都只能存在這倆地方。
問題是,如果你是進程B,你不會覺得很不踏實么,居然有人可以這么樣將你玩弄在鼓掌之中,你在他面前根本就是完全透明,毫無秘密,任人蹂躪。很顯然,不應該有這么苦逼的事情發生。或者說,一個普通的用戶進程不可能僅通過什么絢爛的編程技巧來做到這一點,再或者說,這必須是操作系統提供的“能力”。
認識到這一點很重要,也就是說如果是linux,那就應該是某些神奇的系統調用,如果是windows,那就應該是某些擁有又臭又長參數的API,如果你的操作系統沒提供這樣的接口,那你就不要想了(僅限於二進制代碼,基於虛擬機的,解釋器的不算)。 windows下的不知道也暫時不關心,linux下的就是“ptrace”,32位/64位都是它。 因為第一篇文章嘛,只是簡單解釋下,而且后面要說的還有很多,所以我就不詳細介紹了,關於ptrace的資料你可以參考
原版:
http://www.linuxjournal.com/article/6100
http://www.linuxjournal.com/node/6210/print
中文版:
http://www.kgdb.info/gdb/playing_with_ptrace_part_i/
http://www.kgdb.info/gdb/playing_with_ptrace_part_ii/
但是有一個關鍵點需要仔細說明一下,進程A怎么通過ptrace讓進程B暫停? 這么說吧,首先進程A通過ptrace可以改寫B進程空間的任意地址的內容,當然也就能改寫B進程的機器指令,比如下面的超白痴C代碼
1 //test.c
2 int main()
3 {
4 return 0;
5 }
先編譯 gcc test.c -o test,然后用objdump -d test 反匯編下
1 0000000000400474 <main>:
2 400474: 55 push %rbp
3 400475: 48 89 e5 mov %rsp,%rbp
4 400478: b8 00 00 00 00 mov $0x0,%eax
5 40047d: c9 leaveq
6 40047e: c3 retq
7 40047f: 90 nop
main函數一共6條指令,
第一條在 0x400474處,1個字節,內容是"0x55", 意思是 push %rbp
第二條在 0x400475處,3個字節,內容是"0x48 0x89 0xe5", 意思是 mov %rsp,%rbp
...省略...
如果我想B進程在第3行暫停,或者說在第3行設置一個斷點,那么在進程B運行到第3行之前,進程A通過ptrace修改進程B內存空間0x400478處, 將第一個字節(0xb8)修改成(0xcc),那么進程B運行到第三行自動就暫停了。為啥?因為0xcc就是INT 3 指令,先show一些官方文檔吧:
==============================================
Opcode Instruction Description
CC INT3 Interrupt 3—trap to debugger
CD ib INT imm8 Interrupt vector numbered by immediate byte
CE INTO Interrupt 4—if overflow flag is 1
Intel® Itanium® Architecture Software Developer’s Manual
Volume 2: System Architecture
==============================================
==============================================
The INT 3 instruction generates a special one byte opcode (CC) that is
intended for calling the
debug exception handler. (This one byte form is valuable because it can
be used to replace the
first byte of any instruction with a breakpoint, including other one
byte instructions, without
over-writing other code).
Intel Architecture Software Developer’s Manual
Volume 2:Instruction Set Reference
================================================
看不懂沒關系,原理很簡單,0xcc就是“暫停”(Trap)指令,並且它只有一個字節。64位下的機器指令的長度不等,比如上面的6條指令就有1,3,5幾種,但是最小必須是1,也就是說INT 3是最短的一條指令,那它就能覆蓋到任意一條指令的最開始部分,比如,把它覆蓋到0x400478處,
第4行 400478: b8 00 00 00 00 mov $0x0,%eax
就變成了
第4行 400478: cc 00 00 00 00 mov $0x0,%eax
除了第一個“操作符”變了,其他的“操作數”都沒變 ,當B進程執行到0x400478處時,它就會暫停,然后將控制權交給父進程,也就是A,然后A干完它想干的事情,比如查查寄存器,看看內存啥的,再把B的0x400478處改回來,於是又變成了
第4行 400478: b8 00 00 00 00 mov $0x0,%eax
進程內存一點兒沒變,但是這時候指令寄存器(SP? IP? 反正好幾種叫法)已經指向下一條指令了,也就是b8后面的00,為啥?因為b8以前cc,單字節指令,執行過了,ip往前挪了一個字節,於是指向00了,所以A進程通過ptrace把指令寄存器-1,於是又指向了b8,一切如常,繼續執行。
ok,總結一下。
假設你想設置幾個斷點,那么首先確定好位置,比如0x400474, 0x400478,0x40047e,然后流程如下:
================================================
a 保存位置的第一個字節,然后修改位置的第一個字節為0xcc(INT 3)
b 繼續B進程
c B進程遇到斷點暫停,將控制權交還A進程
d A進程將斷點位置的第一個字節改回來,將指令寄存器-1,繼續B進程,轉入步驟b.
================================================
假設你想單步執行,在能設置斷點基礎上,流程如下:
================================================
a 將斷點設在下一條指令處,繼續B進程
b B進程遇到斷點暫停,轉入a步驟
================================================
瞧瞧,原來單步執行就是不停的在下一條指令前設斷點啊...
后記:
在上面的內容中,我屏蔽了很多細節,比如:
1 “下一條指令”,假設你在0x400475處
第3行 400475: 48 89 e5 mov %rsp,%rbp
第4行 400478: b8 00 00 00 00 mov $0x0,%ea
顯然,下一條指令在0x400478處,也就是3個字節之后,問題是你怎么知道要去跳
過“3”個字節,為啥不是2個,不是1個?很顯然因為0x400475指令的內容“48
89 e5”告訴你這條指令有3個字節長。它怎么告訴你的?“48 89 e5”這6個字母
里面一個“3”都沒有。
2 “B進程將控制權交還給A”,B怎么就還給A了?B與A到底通過什么樣的方式
來交互?進程間交還還是線程間交互?
3 到目前為止,操作的都是機器碼,我能停在0x400475處有什么用?我需要的
是能停在 "int i = 0;"處。換句話說,如何建立機器碼與源代碼之間的關系。
實現:
在參考文獻的鏈接中,提供了關於ptrace的C代碼示例。不過這種有歷史的東西,肯定有一大堆封裝好的庫。這里我用的python的封裝,python-ptrace。
python-ptrace本身提供了一個gdb.py,800行左右代碼。基本上局部了簡單的單線程匯編代碼調試能力。不過,我的目標是提供源代碼級的調試功能,而且還要限制在千行左右,gdb就有點大了,自己簡單寫搭了個框架,200行,先實現了匯編碼的單步執行,慢慢擴展。
當前要執行的匯編代碼,效果如下:
================================================
In [6]: run fdb.py ../test/test
fdb: step
fdb: command:step params:[]
fdb: a_step
Assembly: 0x000000360ae00af0: MOV RDI, RSP
fdb: step
fdb: command:step params:[]
fdb: a_step
Assembly: 0x000000360ae00af3: CALL 0x360ae01120
fdb: step
fdb: command:step params:[]
fdb: a_step
Assembly: 0x000000360ae00af8: MOV R12, RAX
================================================
具體源碼在附件,但是首先,它依賴一些第三方庫,其次它只支持64 位,linux,再次,它是python實現的,再次,我剛開了個頭。
cd /usr/tmp/luqi/python-ptrace-0.6.3
fdb.py ../test/test
fdb: step
后面我會繼續解釋上面的一些細節,進一步補充理論,也會深入到具體代碼實現,作為一個開頭,這次的內容已經很多,歡迎有這方面經驗的兄弟一起交流,因為,其實我也有很多不明白的地方想要找高人請教。
參考文獻:
互聯網上關於調試器的內容並不多,先貢獻一個精品
http://eli.thegreenplace.net/2011/01/23/how-debuggers-work-part-1/
http://eli.thegreenplace.net/2011/01/27/how-debuggers-work-part-2-breakpoints/
http://eli.thegreenplace.net/2011/02/07/how-debuggers-work-part-3-debugging-information/
附件地址: