背景:
在做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
Assembly: 0x000000360ae00af3: CALL 0x360ae01120
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/附件位址:
http://files.cnblogs.com/quixotic/fdb.rar