[匯編與C語言關系]1.函數調用


  對於以下程序:

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)。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM