2004年3月31日 星期三

Who Call Me?

誰叫我? 有用過 gdb 對 core 檔作 bt (backtrace) 嗎? 所謂 backtrace 是用來回溯檢查函式呼叫的關係, 以便了解是由那一個函式呼叫出問題的函式. 尢其是在許多錯綜複雜的龐大程式碼中, backtrace 是相當有用的 debug 技巧. 而這個題目則是用來討論如何在程式執行中作 backtrace.

在實作這個技術前有兩個關鍵點要先解決:

1. 如何取得此 function 返回位址.
2. 如何依據返回位址查知函式名稱.
關於第一點, 必須先了解堆疊(Stack) 和函式呼叫的處理關係. 堆疊是一個後進先出(Last-In-First-Out)的資料結構. 當呼叫某個函式時, 相關的暫存器(Register)就會被存入堆疊. 而當函式返回時便會從堆疊裡取回返回位址以便回到原來呼叫的下一個指令繼續執行. 至於暫存器(Register), 其中 EIP 是 Instruction Pointer, 用來指出 CPU 將要執行指令的位址. ESP 暫存器則是用來指向目前堆壘的位址.

我們先寫個小程式來觀察可行性.

----------- test.c -----------
void test()
{

}

int main()
{

test();
}
------------------------------

[tim@localhost whocallme]$ gcc -o test test.c
[tim@localhost whocallme]$ gdb ./test
GNU gdb 5.3-25mdk (Mandrake Linux)
Copyright 2002 Free Software Foundation, Inc.
GDB is free software, covered by the GNU General Public License, and you are
welcome to change it and/or distribute copies of it under certain
conditions.
Type "show copying" to see the conditions.
There is absolutely no warranty for GDB. Type "show warranty" for details.
This GDB was configured as "i586-mandrake-linux-gnu"...
(gdb) b test
Breakpoint 1 at 0x804832f
(gdb) r
Starting program: /home/tim/research/whocallme/test

Breakpoint 1, 0x0804832f in test ()
(gdb) info reg
eax 0x0 0
ecx 0x1 1
edx 0x4014fe50 1075117648
ebx 0x4014e9a0 1075112352
esp 0xbffff698 0xbffff698
ebp 0xbffff698 0xbffff698
esi 0x40013880 1073821824
edi 0xbffff6f4 -1073744140
eip 0x804832f 0x804832f

(gdb) disas test
Dump of assembler code for function test:
0x804832c : push %ebp
0x804832d : mov %esp,%ebp
0x804832f : pop %ebp
0x8048330 : ret
End of assembler dump.

ebp 暫存器值是 0xbffff698, 也就是原來的堆疊位址. 我們知道在呼叫函式時(call) CPU 會將返回位址存入堆疊, 因此可以從 ebp 暫存器的位址裡面找到我們需要的返回位址:

(gdb) p/x *0xbffff698
$1 = 0xbffff6a8

別忘了, 一進入此函式時 push $ebp 已被執行, 因此堆疊位址已被減 4, 所以要取得正確的值還得把 4 加回去才行:

(gdb) p/x *(0xbffff698+4)
$2 = 0x8048346

這個值應該就是 test() 正確的返回位址, 來檢查看看:

(gdb) disas main
Dump of assembler code for function main:
0x8048331
: push %ebp
0x8048332 : mov %esp,%ebp
0x8048334 : sub $0x8,%esp
0x8048337 : and $0xfffffff0,%esp
0x804833a : mov $0x0,%eax
0x804833f : sub %eax,%esp
0x8048341 : call 0x804832c
0x8048346 : leave
0x8048347 : ret
0x8048348 : nop
0x8048349 : nop
0x804834a : nop
0x804834b : nop
0x804834c : nop
0x804834d : nop
0x804834e : nop
0x804834f : nop
End of assembler dump.

果然在 call 完後的下個指令是位於 0x8048346, 也就是 test() 返回位址.
接下來我們就用 C 和一些 assembly 配合來實作.

------------- test-1.c ------------------
void test()
{
unsigned long *stack;
asm ("movl %%ebp, %0\n"
printf("ret address = 0x%x\n", *(stack+1));

}

int main()
{

test();
}
-----------------------------------------

[tim@localhost whocallme]$ ./test-1
ret address = 0x8048394
[tim@localhost whocallme]$ gdb ./test-1
(gdb) disas main
Dump of assembler code for function main:
0x804837f
: push %ebp
0x8048380 : mov %esp,%ebp
0x8048382 : sub $0x8,%esp
0x8048385 : and $0xfffffff0,%esp
0x8048388 : mov $0x0,%eax
0x804838d : sub %eax,%esp
0x804838f : call 0x804835c
0x8048394 : leave
0x8048395 : ret
0x8048396 : nop

第一個關鍵點目前已解決, 再來要想想怎麼要能夠依記憶體位址查知所處的函式名稱呢?

更多詳細的內容請看 Who Call Me? 一文.

沒有留言: