當調用了一個子程序,調用的代碼和子程序(被調用的代碼)必須協(xié)商好在它們之間如何傳遞數(shù)據(jù)。高級語言有標準傳遞數(shù)據(jù)的方法稱為調用約定。要讓高級語言接口于匯編語言,匯編語言代碼就一定要使用與高級語言一樣的約定。不同的編譯器有不同的調用約定或者說不同的約定可能取決于代碼如何被編譯。(例如:是否進行了優(yōu)化)。一個廣泛的約定是:使用一條CALL指令來調用代碼再通過RET指令返回。
所有PC的C編譯器支持的調用約定將在本章的后續(xù)部分階段進行描述。這些約定允許你創(chuàng)建可重入的子程序。一個可重入的子程序可以在程序中的任意一點被安全調用(甚至在子程序內部)。
在堆棧上傳遞參數(shù)
給子程序的參數(shù)需要在堆棧中傳遞。它們在CALL指令之前就已經(jīng)被壓入棧中了。和在C中是一樣的,如果參數(shù)被子程序改變了,那么需要傳遞的是數(shù)據(jù)的地址,而不是值。如果參數(shù)的大小小于雙字,它就需要在壓入棧之
前轉換成雙字。
在堆棧里的參數(shù)并沒有由子程序彈出,取而代之的是:它們自己從堆棧中訪問本身。為什么?
1、因為它們在CALL指令之前被壓入棧中,所以返回時首先彈出的是返回地址(然后修改堆棧指針使其指向參數(shù)入棧以前的值)。
2、參數(shù)往往將會使用在子程序中幾個的地方。通常在整個程序中,它們不可以保存在一個寄存器中,而應該儲存在內存中。把它們留在堆棧里就相當于把數(shù)據(jù)復制到了內存中,這樣就可以在子程序的任意一點訪問數(shù)據(jù)。
考慮通過堆棧傳遞了一個參數(shù)的子程序。當子程序被調用了,堆棧狀態(tài)如圖4.1。這個參數(shù)可以通過間接尋址訪問到。([ESP+4])。
如果在子程序內部使用了堆棧儲存數(shù)據(jù),那么與ESP相加的數(shù)將要改變。例如:圖4.2展示了如果一個雙字壓入棧中后堆棧的狀態(tài)?,F(xiàn)在參數(shù)在ESP + 8中,而不在ESP + 4中。因此,引用參數(shù)時若使用ESP就很容易犯錯了。為了解決這個問題,80386提供使用另外一個寄存器:EBP。這個寄存器的唯一目的就是引用堆棧中的數(shù)據(jù)。C調用約定要求子程序首先把EBP的值保存到堆棧中,然后再使EBP的值等于ESP。當數(shù)據(jù)壓入或彈出堆棧時,這就允許ESP值被改變的同時EBP不會被改變。在子程序的結束處,EBP的原始值必須恢復出來(這就是為什么它在子程序的開始處被保存的緣故。圖4.3展示了遵循這些約定的子程序的一般格式。
圖4.3中的第2行和第3行組成了一個子程序的大體上的開始部分。第5行和第6行組成了結束部分。圖4.4展示了剛執(zhí)行完開始部分之后堆棧的狀態(tài)。現(xiàn)在參數(shù)可以在子程序中的任何地方通過[EBP + 8]來訪問,而不用擔心在子程序中有什么數(shù)據(jù)壓入到堆棧中了。
執(zhí)行完子程序之后,壓入棧中的參數(shù)必須移除掉。C調用約定規(guī)定調用的代碼必須做這件事。其它約定可能不同。例如:Pascal 調用約定規(guī)定子程序必須移除參數(shù)。(RET指令的另一種格式可以很容易做這件事。)一些C編譯器同樣支持這種約定。關鍵字pascal用在函數(shù)的原型和定義中來告訴編譯器使用這種約定。事實上,MS Windows API的C函數(shù)使用的stdcall調用約定同樣以這種方式運作。這種方式有什么優(yōu)點?它比C調用約定更有效一點。那為什么所有的C函數(shù)都使用C調用約定呢?一般說來,C允許一個函數(shù)的參數(shù)為變化的個數(shù)(例如:printf和scanf函數(shù))。對于這種類型的函數(shù),將參數(shù)移除出棧的操作在這次函數(shù)調用中和下次函數(shù)調用中是不同的。C調用約定能使指令簡單地執(zhí)行這種不同的操作。Pascal和stdcall調用約定執(zhí)行這種操作是非常困難的。因此,Pascal調用約定(和Pascal語言一樣)不允許有這種類型的函數(shù)。MS Windows只有當它的API函數(shù)不可以攜帶變化個數(shù)的參數(shù)時才可以使用這種約定。
圖4.5展示了一個將被調用的子程序如何使用C調用約定。第3行通過直接操作堆棧指針將參數(shù)移除出棧。同樣可以使用POP指令來做這件事,但是常常使用在要求將無用的結果儲存到一個寄存器的情況下。實際上,對于這種情況,許多編譯器常常使用一條POP ECX來移除參數(shù)。編譯器會使用POP指令來代替ADD指令,因為ADD指令需要更多的字節(jié)。但是,POP會改變ECX的值。下面是一個有兩個子程序的例子,它們使用了上面討論的C調用約定。54行(和其它行)展示了多個數(shù)據(jù)和文本段可以在同一個源文件中聲明。進行連接處理時,它們將會組合成單一的數(shù)據(jù)段和文本段。把數(shù)據(jù)和文本段分成單獨的幾段就允許數(shù)據(jù)定義在子程序代碼附近,這也是子程序
經(jīng)常做的。
堆棧上的局部變量
堆??梢苑奖愕赜脕韮Υ婢植孔兞?。這實際上也是C儲存普通變量(或C lingo中的自動變量)的地方。如果你希望子程序是可重入的,那么使用堆棧存儲變量是非常重要的。一個可重入的子程序不管在任何地方被調用都能正常運行,包括子程序本身。換句話說,可重入子程序可以嵌套調用。儲存變量的堆棧同樣在內存中。不儲存在堆棧里的數(shù)據(jù)從程序開始到程序結束都使用內存(C稱這種類型的變量為全局變量或靜態(tài)變量)。儲存在堆棧里的數(shù)據(jù)只有當定義它們的子程序是活動的時候才使用內存。
在堆棧中,局部變量恰好儲存在保存的EBP值之后。它們通過在子程序的開始部分用ESP減去一定的字節(jié)數(shù)來分配存儲空間。圖4.6展示了子程序新的骨架。EBP用來訪問局部變量??紤]圖4.7中的C函數(shù)。圖4.8 展示如何用匯編語言編寫等價的子程序。
圖4.9展示了執(zhí)行完圖4.8中程序的開始部分后的堆棧狀態(tài)。這一節(jié)的堆棧包含了參數(shù),返回信息和局部變量,這樣堆棧稱為一個堆棧幀。C函數(shù)的每一次調用都會在堆棧上創(chuàng)建一個新的堆棧幀。
可以使用兩條專門的指令來簡化一個子程序的開始部分和結束部分,它們是為這個目的而專門設計的。ENTER指令執(zhí)行開始部分的代碼,而LEAVE指令執(zhí)行結束部分。ENTER指令攜帶兩個立即數(shù)。對于C調用約定, 第二個操作數(shù)總是為0。第一個操作數(shù)是局部變量所需要的字節(jié)數(shù)。LEAVE指令沒有操作數(shù)。圖4.10展示了如何使用這些指令。注意程序skeleton同樣使用了ENTER和LEAVE指令。
更多建議: