1. 程式人生 > >C語言函式呼叫過程的彙編分析

C語言函式呼叫過程的彙編分析

轉自:   http://www.cnblogs.com/xiaojianliu/articles/8733560.html

 

下面一段C程式:

複製程式碼

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程式碼和彙編程式碼穿插起來顯示,這樣C程式碼和彙編程式碼的對應關係看得更清楚。反彙編的結果很長,以下只列出我們關心的部分。

複製程式碼

$ gcc main.c -g
$ objdump -dS a.out
...
08048394 <bar>:
int bar(int c, int d)
{
8048394: 55 push %ebp
8048395: 89 e5 mov %esp,%ebp
8048397: 83 ec 10 sub $0x10,%esp
int e = c + d;
804839a: 8b 55 0c mov 0xc(%ebp),%edx
804839d: 8b 45 08 mov 0x8(%ebp),%eax
80483a0: 01 d0 add %edx,%eax
80483a2: 89 45 fc mov %eax,-0x4(%ebp)
return e;
80483a5: 8b 45 fc mov -0x4(%ebp),%eax
}

80483a8: c9 leave
80483a9: c3 ret
080483aa <foo>:
294int foo(int a, int b)
{
80483aa: 55 push %ebp
80483ab: 89 e5 mov %esp,%ebp
80483ad: 83 ec 08 sub $0x8,%esp
return bar(a, b);
80483b0: 8b 45 0c mov 0xc(%ebp),%eax
80483b3: 89 44 24 04 mov %eax,0x4(%esp)
80483b7: 8b 45 08 mov 0x8(%ebp),%eax
80483ba: 89 04 24 mov %eax,(%esp)
80483bd: e8 d2 ff ff ff call 8048394 <bar>
}

80483c2: c9 leave
80483c3: c3 ret
080483c4 <main>:
int main(void)
{
80483c4: 8d 4c 24 04 lea 0x4(%esp),%ecx
80483c8: 83 e4 f0 and $0xfffffff0,%esp
80483cb: ff 71 fc pushl -0x4(%ecx)
80483ce: 55 push %ebp
80483cf: 89 e5 mov %esp,%ebp
80483d1: 51 push %ecx
80483d2: 83 ec 08 sub $0x8,%esp
foo(2, 3);
80483d5: c7 44 24 04 03 00 00 movl $0x3,0x4(%esp)
80483dc: 00
80483dd: c7 04 24 02 00 00 00 movl $0x2,(%esp)
80483e4: e8 c1 ff ff ff call 80483aa <foo>
return 0;
80483e9: b8 00 00 00 00 mov $0x0,%eax
}

80483ee: 83 c4 08 add $0x8,%esp
80483f1: 59 pop %ecx
80483f2: 5d pop %ebp
80483f3: 8d 61 fc lea -0x4(%ecx),%esp
80483f6: c3 ret
...

複製程式碼

要檢視編譯後的彙編程式碼,其實還有一種辦法是gcc -S main.c,這樣只生成彙編程式碼main.s,而不生成二進位制的目標檔案。

disassemble可以反彙編當前函式或者指定的函式,單獨用disassemble命令是反彙編當前函式,如果disassemble命令後面跟函式名或地址則反彙編指定的函式。

在執行程式時,作業系統為程序分配一塊棧空間來儲存函式棧幀, esp暫存器總是指向棧頂,在x86平臺上這個棧是從高地址向低地址增長的,我們知道每次呼叫一個函式都要分配一個棧幀來儲存引數和區域性變數,現在我們詳細分析這些資料在棧空間的佈局:

圖中每個小方格表示4個位元組的記憶體單元,例如b: 3這個小方格佔的記憶體地址是0xbff1c420~0xbff1c423。

從main函式的這裡開始看起:

複製程式碼

foo(2, 3);
80483d5: c7 44 24 04 03 00 00 movl $0x3,0x4(%esp)
80483dc: 00
80483dd: c7 04 24 02 00 00 00 movl $0x2,(%esp)
80483e4: e8 c1 ff ff ff call 80483aa <foo>
return 0;
80483e9: b8 00 00 00 00 mov $0x0,%eax

複製程式碼

要呼叫函式foo先要把引數準備好,第二個引數儲存在esp+4指向的記憶體位置,第一個引數儲存在esp指向的記憶體位置,可見引數是從右向左依次壓棧的。然後執行call指令,這個指令有兩個作用:

1. foo函式呼叫完之後要返回到call的下一條指令繼續執行,所以把call的下一條指令的地

址0x80483e9壓棧,同時把esp的值減4,esp的值現在是0xbff1c418。

2. 修改程式計數器eip,跳轉到foo函式的開頭執行。

現在看foo函式的彙編程式碼:

int foo(int a, int b)
{
80483aa: 55 push %ebp
80483ab: 89 e5 mov %esp,%ebp
80483ad: 83 ec 08 sub $0x8,%esp

push %ebp指令把ebp暫存器的值壓棧,同時把esp的值減4。 esp的值現在是0xbff1c414,下一條指令把這個值傳送給ebp暫存器。這兩條指令合起來是把原來ebp的值儲存在棧上,然後又給ebp賦了新值。在每個函式的棧幀中, ebp指向棧底,而esp指向棧頂,在函式執行過程中esp隨著壓棧和出棧操作隨時變化,而ebp是不動的,函式的引數和區域性變數都是通過ebp的值加上一個偏移量來訪問,例如foo函式的引數a和b分別通過ebp+8和ebp+12來訪問。所以下面的指令把引數a和b再次壓

棧,為呼叫bar函式做準備,然後把返回地址壓棧,呼叫bar函式:

複製程式碼

return bar(a, b);
80483b0: 8b 45 0c mov 0xc(%ebp),%eax
80483b3: 89 44 24 04 mov %eax,0x4(%esp)
80483b7: 8b 45 08 mov 0x8(%ebp),%eax
80483ba: 89 04 24 mov %eax,(%esp)
80483bd: e8 d2 ff ff ff call 8048394 <bar>

複製程式碼

現在看bar函式的指令:

複製程式碼

int bar(int c, int d)
{
8048394: 55 push %ebp
8048395: 89 e5 mov %esp,%ebp
8048397: 83 ec 10 sub $0x10,%esp
int e = c + d;
804839a: 8b 55 0c mov 0xc(%ebp),%edx
804839d: 8b 45 08 mov 0x8(%ebp),%eax
80483a0: 01 d0 add %edx,%eax
80483a2: 89 45 fc mov %eax,-0x4(%ebp)

複製程式碼

這次又把foo函式的ebp壓棧儲存,然後給ebp賦了新值,指向bar函式棧幀的棧底,通過ebp+8和ebp+12分別可以訪問引數c和d。 bar函式還有一個區域性變數e,可以通過ebp-4來訪問。所以後面幾條指令的意思是把引數c和d取出來存在暫存器中做加法,計算結果儲存在eax暫存器中,再把eax暫存器存回區域性變數e的記憶體單元。

在gdb中可以用bt命令和frame命令檢視每層棧幀上的引數和區域性變數,現在可以解釋它的工作原理了:如果我當前在bar函式中,我可以通過ebp找到bar函式的引數和區域性變數,也可以找到foo函式的ebp儲存在棧上的值,有了foo函式的ebp,又可以找到它的引數和區域性變數,也可以找到main函式的ebp儲存在棧上的值,因此各層函式棧幀通過儲存在棧上的ebp的值串起來了。

現在看bar函式的返回指令:

return e;
80483a5: 8b 45 fc mov -0x4(%ebp),%eax
}
80483a8: c9 leave
80483a9: c3 ret

bar函式有一個int型的返回值,這個返回值是通過eax暫存器傳遞的,所以首先把e的值讀到eax暫存器中。然後執行leave指令,這個指令是函式開頭的push %ebp和mov %esp,%ebp的逆操作:

1. 把ebp的值賦給esp,現在esp的值是0xbff1c404。

2. 現在esp所指向的棧頂儲存著foo函式棧幀的ebp,把這個值恢復給ebp,同時esp增加4, esp的值變成0xbff1c408。

最後是ret指令,它是call指令的逆操作:

1. 現在esp所指向的棧頂儲存著返回地址,把這個值恢復給eip,同時esp增加4, esp的值變成0xbff1c40c。

2. 修改了程式計數器eip,因此跳轉到返回地址0x80483c2繼續執行。

地址0x80483c2處是foo函式的返回指令:

80483c2: c9 leave
80483c3: c3 ret

重複同樣的過程,又返回到了main函式。注意函式呼叫和返回過程中的這些規則:

1. 引數壓棧傳遞,並且是從右向左依次壓棧。

2. ebp總是指向當前棧幀的棧底。

3. 返回值通過eax暫存器傳遞。

這些規則並不是體系結構所強加的, ebp暫存器並不是必須這麼用,函式的引數和返回值也不是必須這麼傳,只是作業系統和編譯器選擇了以這樣的方式實現C程式碼中的函式呼叫,這稱為Calling Convention, Calling Convention是作業系統二進位制介面規範(ABI, Application BinaryInterface)的一部分。