c函式的引數傳遞過程
基礎知識
函式呼叫的本質將在這裡得到闡明。首先請讀者理解堆疊的操作。函式和堆疊的關係密切,這是因為:c語言程式通過堆疊把引數從函式外部傳入到函式內部。此外,在堆疊中劃分區域來容納函式的內部變數。
呼叫push和pop指令的時候,暫存器
esp用於指向棧頂的位置--棧頂總是棧中位址最小的位置。
push執行的結果,esp總是減少,pop則增加。對於c程式預設的呼叫方式,堆疊總是呼叫方把引數反序(從右到左)地壓入堆疊中,被呼叫方把堆疊復原(這些我們會在後面見到)。這些引數對齊到機器字長,16位、32位、64位cpu下分別對齊到2、4、8個位元組。這種呼叫是c編譯器
預設的c方式。
函式呼叫規則
在乙個編寫高階語言的程式設計師的觀念中,函式(或者沒有返回值的過程)是必不可少的基礎單元。c語言的程式完全由函式構成,所有的**都在某乙個函式中。pascal區分函式和過程,但是本質依然是類似的。對計算機硬體而言,這種區分毫無必要,因為cpu只關心一條一條的指令,並不關心它們是以怎樣的結構組織的。
call指令和ret指令只是為了呼叫的方便而已,絕不是函式存在的絕對證據。即使我們僅僅使用jmp並自己操作堆疊,也一樣可以實現函式的功能。因此,一種高階語言如何實現函式呼叫,並沒有法律的約束,所以出現了各種不同的函式呼叫規則。
但是毫無疑問,如果乙個第三方提供的函式要能被使用,那麼必須有約定的函式呼叫規則。
函式呼叫規則指的是呼叫者和被呼叫函式間傳遞引數及返回引數的方法,在windows上,常用的有pascal方式、winapi方式(_stdcall)、c方式(_cdecl)。
_cdecl c呼叫規則:
(1)引數從右到左進入堆疊;
(2)在函式返回後,呼叫者要負責清除堆疊,所以這種呼叫常會生成較大的可執行程式。
_stdcall又稱為winapi,其呼叫規則:
(1)引數從右到左進入堆疊;
(2)被呼叫的函式在返回前自行清理堆疊,所以生成的**比cdecl小。
pascal呼叫規則:
pascal呼叫規則主要用在win16函式庫中,現在基本不用。
(1)引數從左到右進入堆疊;
(2)被呼叫的函式在返回前自行清理堆疊。
(3)不支援可變引數的函式呼叫。
此外,在windows核心中還常見有快速呼叫方式(_fastcall);在c++編譯的**中有this call方式(_thiscall)。這些會在後面的章節中詳細闡明。
技術細節
在用c語言所寫的程式中,堆疊用於傳遞函式引數。寫乙個簡單的函式如下:
void myfunction(int a,int b)
這是標準的c函式呼叫方式。其過程是:
呼叫者把引數反序地壓入堆疊中。
呼叫函式。
呼叫者把堆疊清理復原。
這就是c編譯器預設的_cdecl方式,而windows api一般採用的_stdcall則是被呼叫者恢復堆疊(可變引數函式呼叫除外)。
至於返回值都是寫入eax中,然後返回的。
在windows中,不管哪種呼叫方式都是返
回值放在eax中,然後返回。外部從eax中得到返回值。
_cdecl方式下被呼叫函式需要做以下一些事情。
(1)儲存ebp。ebp總是被我們用來儲存這個函式執行之前的esp的值。執行完畢之後,我們用ebp恢復esp;同時,呼叫此函式的上層函式也用ebp做同樣的事情。所以先把ebp壓入堆疊,返回之前彈出,避免ebp被我們改動。
(2)儲存esp到ebp中。
上面兩步的**如下:
;儲存ebp,並把esp放入ebp中,此時ebp與esp同;都是這次函式呼叫時的棧頂
push ebp
mov ebp,esp
(3)在堆疊中騰出乙個區域用來儲存區域性變數,這就是常說的所謂區域性變數是儲存在棧空間中的。方法是:把esp減少乙個數值,這樣就等於壓入了一堆變數。要恢復時,只要把esp恢復成ebp中儲存的資料就可以了。
(4)儲存ebx、esi、edi到堆疊中,函式呼叫完後恢復。
對應的**如下:
;把esp往下移動乙個範圍,等於在堆疊中放出一片新;的空間用來存區域性變數
sub esp,0cch
push ebx
;下面儲存三個暫存器:ebx、esi、edi
push esi
push edi
(5)把區域性變數區域初始化成全0cccccccch。0cch實際是int 3指令的機器碼,這是乙個斷點中斷指令。因為區域性變數不可能被執行,如果執行了,必然程式有錯,這時發生中斷來提示開發者。這是vc編譯debug版本的特有操作。相關**如下:
lea edi,[ebp-0cch] ;本來是要mov edi,ebp-0cch,但是mov不支援-操作;所以對ebp-0cch取內容,而lea把內容的位址,也就
;是ebp-0cch載入到edi中。目的是把儲存區域性變數
;的區域(從ebp-0cch開始的區域)初始化成全
;部0cccccccch
mov ecx,33h
mov eax,0cccccccch
rep stos dword ptr [edi] ;串寫入
(6)然後做函式裡應該做的事情。引數的獲取是ebp+8位元組為第乙個引數,ebp+12為第二個引數,依次增加。ebp+4位元組處是要返回的位址。
(7)恢復ebx、esi、edi、esp、ebp,最後返回。**如下:
pop edi;恢復edi、esi、ebx
pop esi
pop ebx
mov esp,ebp
;恢復原來的ebp和esp,讓上乙個調
;用的函式正常使用
pop ebp
ret
為了簡單起見,我的函式沒有返回值。如果要返回值,函式應該在返回之前,把返回值放入eax中。外部通過eax得到返回值。
**分析
用vc 2003編譯debug版本,完整的反彙編**如下:
void myfunction(int a,int b)pop edi
;恢復edi、esi、ebx
pop esi
pop ebx
mov esp,ebp
;恢復原來的ebp和esp,讓上乙個呼叫的函式
;正常使用
pop ebp
ret
主程式中對這個函式的呼叫方式是:
mov eax,dword ptr[b];把b、a兩個引數壓入堆疊
push eax
mov ecx,dword ptr[a]
push ecx
call myfunction
;呼叫函式myfunction
add esp,8
;恢復堆疊
這樣一來,函式呼叫的過程就很清楚了。在下一章開始,進一步介紹各種各樣的c語言程式,變成了怎樣的彙編指令。
重點觀察那些涉及call、ret、push和pop,操作ebp和esp的指令,就能看到c語言函式的呼叫過程。
C 函式呼叫過程
int fun1 int a,int b int main 將彙編 執行到函式呼叫的地方,檢視函式呼叫引數帶入的指令。int a fun1 10,20 0131141e push 14h 01311420 push 0ah 01311422 call fun1 0131118bh 引數順序是10,2...
函式呼叫過程
每乙個未執行完的函式都對應著乙個棧幀,系統為單個函式分配的那部分棧空間就叫做棧幀,棧幀儲存了函式的資訊。以下面的 為例,通過彙編 的執行過程介紹棧幀建立和銷毀的過程 include int add int x,int y int main 從main函式建立自己的棧幀開始 其他內容先忽略 初始狀態 ...
函式呼叫過程
c語言種有三種迴圈 do.while while for 初始化 條件判斷 步進 主函式 main 庫函式自定義函式函式的發明,使得變成可以以函式為單位進行模組化,叫做面向過程。軟體工程中,有 高內聚,低耦合 的要求。函式就是為了實現以上要求發明的產物。函式是面向過程的 介面 其介面包含了 引數 返...