對於以下程序:
int bar(int c, int d) { int e = c + d; return e; } int foo(int a, int b) { return bar(a, b); } int main(void) { foo(2, 3); return 0; }
在編譯時加上-g選項,用objdump反匯編時可以把C代碼和匯編代碼穿插起來顯示:
反匯編的結果很長以下是截取要分析的部分:
整個程序的執行過程是main調用foo, foo調用bar, 用gdb跟蹤程序的執行,直到bar函數中的int e = c + d;語句執行完畢准備返回時,這時在gdb中打印函數棧幀。
disassemble可以反匯編當前函數或者指定的函數,單獨用disassemble是反匯編當前函數,如果disassemble后邊跟函數名或地址則反匯編指定的函數。
s(step)命令可以一行代碼一行代碼的單步調試,而si命令可以一條指令一條指令的單步調試。bt 列出調用棧
info registers可以顯示所有寄存器的當前值。在gdb中表示寄存器名時前面要加個$,例如p $esp命令查看esp寄存器的值(上圖沒有展示該命令),在上例中esp寄存器的值是0xbff1c3f4,所以x/20 $esp命令查看內存中從0xbff1c3f4地址開始的20個32位數。在執行程序時,操作系統為進程分配一塊棧空間來存儲函數棧幀,esp寄存器總是指向棧頂,,在x86平台上這個棧是從高地址向低地址增長的,每次調用一個函數都要分配一個棧幀來存儲參數和局部變量,現在我們分析這些數據是怎么存儲的,根據gdb的輸出結果圖示如下:
途中每個小方格占4個字節,例如b:3這個方格的內存地址是0xbf822d20~0xbf822d23。我們從main函數的這里開始看起:
要調用函數foo先要把參數准備好,第二個參數保存在esp+4所指向的內存位置,第一個參數保存在esp所指向的內存位置,可見參數是從右往左一次壓棧的。然后執行call指令,這個指令有兩個作用:
1. foo函數調用完之后要返回call的下一條指令繼續執行,所以把call的下一條指令的地址0x80483e9壓棧,同時把esp的值 減4,esp的值現在是0xbf822d18。
2. 修改程序計數器eip, 跳轉到foo函數的開頭執行。
現在看foo函數的匯編代碼:
首先將ebp寄存器的值壓棧,同時把esp的值再減4,esp的值現在是0xbf822d14,然后把這個值傳送給ebp寄存器。換句話說就是把原來ebp的值保存在棧上,然后又給ebp賦了新值。在每個函數的棧幀中,ebp指向棧底,esp指向棧頂,在函數執行過程中esp隨着壓棧和出棧操作隨時變化,而ebp是不動的,函數的參數和局部變量都是通過ebp的值加上一個偏移量來訪問的,例如foo函數的參數a和b分別通過ebp+8和ebp+12來訪問,所以下面的指令把參數a和b再次壓棧,為調用bar函數做准備,然后把返回地址壓棧,調用bar函數:
現在看bar函數的指令:
這次又把foo函數的ebp壓棧保存,然后給ebp賦了新值,指向bar函數棧幀的棧底,通過ebp+8和ebp+12分別可以訪問參數c和d。bar函數還有一個局部變量e,可以通過ebp-4來訪問。所以后面幾條指令的意思是把參數c和d取出來存在寄存器中做加法,add指令的計算結果保存在eax寄存器中,再把eax寄存器存回局部變量e的內存單元。
現在可以解釋為什么在gdb中可以用bt命令和frame命令查看每個棧幀上的參數和局部變量了:如果我當前在bar函數中,我可以通過ebp找到bar函數的參數和局部變量,也可以找到foo函數的ebp保存在棧上的值,有個foo函數的ebp,又可以找到它的參數和局部變量,也可以找到main函數的ebp保存在棧上的值,因此各函數的棧幀通過保存在棧上的ebp的值串起來了。現在看bar函數的返回命令:
bar函數有一個int型的返回值,這個返回值是通過eax寄存器傳遞的,所以首先把e的值讀到eax寄存器中。然后執行leave指令,這個指令是函數開頭的push %ebp和mov %esp, %ebp的逆操作:
1. 把ebp的值賦給esp,現在esp的值是0xbf822d04。
2. 現在esp所指向的棧頂保存着foo函數棧幀的ebp,把這個值恢復給ebp,同時esp增加4,現在esp的值是0xbf822d08。
最后是ret指令,它是call指令的逆操作:
1. 現在esp所指向的棧頂保存着返回地址,把這個值恢復給eip,同時esp增加4,現在esp的值是0xbf822d0c。
2. 修改了程序計數器eip,因此跳轉到返回地址0x80483c2繼續執行。
地址0x80483c2處是foo函數的返回指令:
重復同樣的過程,就又返回到了main函數。注意函數調用和返回過程中的這些規則:
1. 參數壓棧傳遞,並且是從右向左依次壓棧。
2. ebp 總是指向棧幀的棧底。
3. 返回值通過 eax 寄存器傳遞。
這些規則並不是體系結構所強加的, ebp 寄存器並不是必須這么用,函數的參數和返回值也不是必須這么傳,只是操作系統和編譯器選擇了以這樣的方式實現C代碼中的函數調用,這稱為Calling Convention,除了Calling Convention之外,操作系統還需要規定許多C代碼和二進制指令之間的接口規范,統稱為ABI(Application Binary Interface)。