原文地址:
C語言函數(shù)調(diào)用棧(一)
C語言函數(shù)調(diào)用棧(二)
0 引言
程序的執(zhí)行過程可看作連續(xù)的函數(shù)調(diào)用鸡捐。當一個函數(shù)執(zhí)行完畢時励烦,程序要回到調(diào)用指令的下一條指令(緊接call指令)處繼續(xù)執(zhí)行姜骡。函數(shù)調(diào)用過程通常使用堆棧實現(xiàn)庶艾,每個用戶態(tài)進程對應一個調(diào)用棧結(jié)構(gòu)(call stack)魔种。編譯器使用堆棧傳遞函數(shù)參數(shù)掸屡、保存返回地址封寞、臨時保存寄存器原有值(即函數(shù)調(diào)用的上下文)以備恢復以及存儲本地局部變量。
不同處理器和編譯器的堆棧布局仅财、函數(shù)調(diào)用方法都可能不同狈究,但堆棧的基本概念是一樣的。
1 寄存器分配
寄存器是處理器加工數(shù)據(jù)或運行程序的重要載體盏求,用于存放程序執(zhí)行中用到的數(shù)據(jù)和指令抖锥。因此函數(shù)調(diào)用棧的實現(xiàn)與處理器寄存器組密切相關(guān)。
Intel 32位體系結(jié)構(gòu)(簡稱IA32)處理器包含8個四字節(jié)寄存器碎罚,如下圖所示:
最初的8086中寄存器是16位磅废,每個都有特殊用途,寄存器名稱反映其不同用途荆烈。由于IA32平臺采用平面尋址模式拯勉,對特殊寄存器的需求大大降低,但由于歷史原因憔购,這些寄存器名稱被保留下來宫峦。在大多數(shù)情況下,上圖所示的前6個寄存器均可作為通用寄存器使用玫鸟。某些指令可能以固定的寄存器作為源寄存器或目的寄存器导绷,如一些特殊的算術(shù)操作指令imull/mull/cltd/idivl/divl要求一個參數(shù)必須在%eax中,其運算結(jié)果存放在%edx(higher 32-bit)和%eax (lower32-bit)中屎飘;又如函數(shù)返回值通常保存在%eax中诵次,等等账蓉。為避免兼容性問題,ABI規(guī)范對這組通用寄存器的具體作用加以定義(如圖中所示)逾一。
對于寄存器%eax铸本、%ebx、%ecx和%edx遵堵,各自可作為兩個獨立的16位寄存器使用箱玷,而低16位寄存器還可繼續(xù)分為兩個獨立的8位寄存器使用。編譯器會根據(jù)操作數(shù)大小選擇合適的寄存器來生成匯編代碼陌宿。在匯編語言層面锡足,這組通用寄存器以%e(AT&T語法)或直接以e(Intel語法)開頭來引用,例如mov $5, %eax或mov eax, 5表示將立即數(shù)5賦值給寄存器%eax壳坪。
在x86處理器中舶得,EIP(Instruction Pointer)是指令寄存器,指向處理器下條等待執(zhí)行的指令地址(代碼段內(nèi)的偏移量)爽蝴,每次執(zhí)行完相應匯編指令EIP值就會增加沐批;
ESP(Stack Pointer)是堆棧指針寄存器,存放執(zhí)行函數(shù)對應棧幀的棧頂?shù)刂?也是系統(tǒng)棧的頂部)蝎亚,且始終指向棧頂九孩;
EBP(Base Pointer)是棧幀基址指針寄存器,存放執(zhí)行函數(shù)對應棧幀的棧底地址发框,用于C運行庫訪問棧中的局部變量和參數(shù)躺彬。
注意,EIP是個特殊寄存器梅惯,不能像訪問通用寄存器那樣訪問它宪拥,即找不到可用來尋址EIP并對其進行讀寫的操作碼(OpCode)。EIP可被jmp铣减、call和ret等指令隱含地改變(事實上它一直都在改變)她君。
不同架構(gòu)的CPU,寄存器名稱被添加不同前綴以指示寄存器的大小徙歼。例如x86架構(gòu)用字母“e(extended)”作名稱前綴犁河,指示寄存器大小為32位鳖枕;x86_64架構(gòu)用字母“r”作名稱前綴魄梯,指示各寄存器大小為64位。
編譯器在將C程序編譯成匯編程序時宾符,應遵循ABI所規(guī)定的寄存器功能定義酿秸。同樣地,編寫匯編程序時也應遵循魏烫,否則所編寫的匯編程序可能無法與C程序協(xié)同工作辣苏。
【擴展閱讀】棧幀指針寄存器
為了訪問函數(shù)局部變量肝箱,必須能定位每個變量。局部變量相對于堆棧指針ESP的位置在進入函數(shù)時就已確定稀蟋,理論上變量可用ESP加偏移量來引用煌张,但ESP會在函數(shù)執(zhí)行期隨變量的壓棧和出棧而變動。盡管某些情況下編譯器能跟蹤棧中的變量操作以修正偏移量退客,但要引入可觀的管理開銷骏融。而且在有些機器上(如Intel處理器),用ESP加偏移量來訪問一個變量需要多條指令才能實現(xiàn)萌狂。
因此档玻,許多編譯器使用幀指針寄存器FP(Frame Pointer)記錄棧幀基地址。局部變量和函數(shù)參數(shù)都可通過幀指針引用茫藏,因為它們到FP的距離不會受到壓棧和出棧操作的影響误趴。有些資料將幀指針稱作局部基指針(LB-local base pointer)。
在Intel CPU中务傲,寄存器BP(EBP)用作幀指針凉当。在Motorola CPU中,除A7(堆棧指針SP)外的任何地址寄存器都可用作FP树灶。當堆棧向下(低地址)增長時纤怒,以FP地址為基準,函數(shù)參數(shù)的偏移量是正值天通,而局部變量的偏移量是負值泊窘。
2 寄存器使用約定
程序寄存器組是唯一能被所有函數(shù)共享的資源。雖然某一時刻只有一個函數(shù)在執(zhí)行像寒,但需保證當某個函數(shù)調(diào)用其它函數(shù)時烘豹,被調(diào)函數(shù)不會修改或覆蓋主調(diào)函數(shù)稍后會使用到的寄存器值。因此诺祸,IA32采用一套統(tǒng)一的寄存器使用約定携悯,所有函數(shù)(包括庫函數(shù))調(diào)用都必須遵守該約定。
- 根據(jù)慣例筷笨,寄存器%eax憔鬼、%edx和%ecx為主調(diào)函數(shù)保存寄存器(caller-saved registers),當函數(shù)調(diào)用時胃夏,若主調(diào)函數(shù)希望保持這些寄存器的值轴或,則必須在調(diào)用前顯式地將其保存在棧中;被調(diào)函數(shù)可以覆蓋這些寄存器仰禀,而不會破壞主調(diào)函數(shù)所需的數(shù)據(jù)照雁。
- 寄存器%ebx、%esi和%edi為被調(diào)函數(shù)保存寄存器(callee-saved registers)答恶,即被調(diào)函數(shù)在覆蓋這些寄存器的值時饺蚊,必須先將寄存器原值壓入棧中保存起來萍诱,并在函數(shù)返回前從棧中恢復其原值,因為主調(diào)函數(shù)可能也在使用這些寄存器污呼。
- 被調(diào)函數(shù)必須保持寄存器%ebp和%esp裕坊,并在函數(shù)返回后將其恢復到調(diào)用前的值,亦即必須恢復主調(diào)函數(shù)的棧幀燕酷。
當然碍庵,這些工作都由編譯器在幕后進行。不過在編寫匯編程序時應注意遵守上述慣例悟狱。
3 棧幀結(jié)構(gòu)
函數(shù)調(diào)用經(jīng)常是嵌套的静浴,在同一時刻,堆棧中會有多個函數(shù)的信息挤渐。每個未完成運行的函數(shù)占用一個獨立的連續(xù)區(qū)域苹享,稱作棧幀(Stack Frame)。棧幀是堆棧的邏輯片段浴麻,當調(diào)用函數(shù)時邏輯棧幀被壓入堆棧, 當函數(shù)返回時邏輯棧幀被從堆棧中彈出得问。棧幀存放著函數(shù)參數(shù),局部變量及恢復前一棧幀所需要的數(shù)據(jù)等软免。
編譯器利用棧幀宫纬,使得函數(shù)參數(shù)和函數(shù)中局部變量的分配與釋放對程序員透明。編譯器將控制權(quán)移交函數(shù)本身之前膏萧,插入特定代碼將函數(shù)參數(shù)壓入棧幀中漓骚,并分配足夠的內(nèi)存空間用于存放函數(shù)中的局部變量。使用棧幀的一個好處是使得遞歸變?yōu)榭赡荛环海驗閷瘮?shù)的每次遞歸調(diào)用蝌蹂,都會分配給該函數(shù)一個新的棧幀,這樣就巧妙地隔離當前調(diào)用與上次調(diào)用曹锨。
棧幀的邊界由棧幀基地址指針EBP和堆棧指針ESP界定(指針存放在相應寄存器中)孤个。EBP指向當前棧幀底部(高地址),在當前棧幀內(nèi)位置固定沛简;ESP指向當前棧幀頂部(低地址)齐鲤,當程序執(zhí)行時ESP會隨著數(shù)據(jù)的入棧和出棧而移動。因此函數(shù)中對大部分數(shù)據(jù)的訪問都基于EBP進行椒楣。
為更具描述性给郊,以下稱EBP為幀基指針,ESP為棧頂指針撒顿,并在引用匯編代碼時分別記為%ebp和%esp丑罪。
函數(shù)調(diào)用棧的典型內(nèi)存布局如下圖所示:
圖中給出主調(diào)函數(shù)(caller)和被調(diào)函數(shù)(callee)的棧幀布局荚板,"m(%ebp)"表示以EBP為基地址凤壁、偏移量為m字節(jié)的內(nèi)存空間(中的內(nèi)容)吩屹。該圖基于兩個假設(shè):
- 函數(shù)返回值不是結(jié)構(gòu)體或聯(lián)合體,否則第一個參數(shù)將位于"12(%ebp)" 處拧抖;
- 每個參數(shù)都是4字節(jié)大小(棧的粒度為4字節(jié))煤搜。在本文后續(xù)章節(jié)將就參數(shù)的傳遞和大小問題做進一步的探討。
此外唧席,函數(shù)可以沒有參數(shù)和局部變量擦盾,故圖中“Argument(參數(shù))”和“Local Variable(局部變量)”不是函數(shù)棧幀結(jié)構(gòu)的必需部分。
從圖中可以看出稍计,函數(shù)調(diào)用時入棧順序為
實參N~1→主調(diào)函數(shù)返回地址→主調(diào)函數(shù)幀基指針EBP→被調(diào)函數(shù)局部變量1~N
- 主調(diào)函數(shù)將參數(shù)按照調(diào)用約定依次入棧(圖中為從右到左)畅卓;
- 將指令指針EIP入棧以保存主調(diào)函數(shù)的返回地址(下一條待執(zhí)行指令的地址)扬虚;
- 進入被調(diào)函數(shù)時,被調(diào)函數(shù)將主調(diào)函數(shù)的幀基指針EBP入棧腐碱,并將主調(diào)函數(shù)的棧頂指針ESP值賦給被調(diào)函數(shù)的EBP(作為被調(diào)函數(shù)的棧底);
- 改變ESP的值來為函數(shù)局部變量預留空間掉弛,此時被調(diào)函數(shù)幀基指針指向被調(diào)函數(shù)的棧底症见。以該地址為基準,向上(棧底方向)可獲取主調(diào)函數(shù)的返回地址殃饿、參數(shù)值谋作,向下(棧頂方向)能獲取被調(diào)函數(shù)的局部變量值,而該地址處又存放著上一層主調(diào)函數(shù)的幀基指針值乎芳。
- 本級調(diào)用結(jié)束后遵蚜,將EBP指針值賦給ESP,使ESP再次指向被調(diào)函數(shù)棧底以釋放局部變量奈惑;再將已壓棧的主調(diào)函數(shù)幀基指針彈出到EBP谬晕,并彈出返回地址到EIP。ESP繼續(xù)上移越過參數(shù)携取,最終回到函數(shù)調(diào)用前的狀態(tài)攒钳,即恢復原來主調(diào)函數(shù)的棧幀。如此遞歸便形成函數(shù)調(diào)用棧雷滋。
EBP指針在當前函數(shù)運行過程中(未調(diào)用其他函數(shù)時)保持不變不撑。在函數(shù)調(diào)用前,ESP指針指向棧頂?shù)刂肺钫叮彩菞5椎刂坊烂省T诤瘮?shù)完成現(xiàn)場保護之類的初始化工作后,ESP會始終指向當前函數(shù)棧幀的棧頂澳泵,此時实愚,若當前函數(shù)又調(diào)用另一個函數(shù),則會將此時的EBP視為舊EBP壓棧,而與新調(diào)用函數(shù)有關(guān)的內(nèi)容會從當前ESP所指向位置開始壓棧腊敲。
若需在函數(shù)中保存被調(diào)函數(shù)保存寄存器(如ESI击喂、EDI),則編譯器在保存EBP值時進行保存碰辅,或延遲保存直到局部變量空間被分配懂昂。在棧幀中并未為被調(diào)函數(shù)保存寄存器的空間指定標準的存儲位置。包含寄存器和臨時變量的函數(shù)調(diào)用棧布局可能如下圖所示:
在多線程(任務)環(huán)境没宾,棧頂指針指向的存儲器區(qū)域就是當前使用的堆棧凌彬。切換線程的一個重要工作,就是將棧頂指針設(shè)為當前線程的堆棧棧頂?shù)刂贰?/p>
以下代碼用于函數(shù)棧布局示例:
//StackFrame.c
#include <stdio.h>
#include <string.h>
struct Strt{
int member1;
int member2;
int member3;
};
#define PRINT_ADDR(x) printf("&"#x" = %p\n", &x)
int StackFrameContent(int para1, int para2, int para3){
int locVar1 = 1;
int locVar2 = 2;
int locVar3 = 3;
int arr[] = {0x11,0x22,0x33};
struct Strt tStrt = {0};
PRINT_ADDR(para1); //若para1為char或short型循衰,則打印para1所對應的棧上整型臨時變量地址铲敛!
PRINT_ADDR(para2);
PRINT_ADDR(para3);
PRINT_ADDR(locVar1);
PRINT_ADDR(locVar2);
PRINT_ADDR(locVar3);
PRINT_ADDR(arr);
PRINT_ADDR(arr[0]);
PRINT_ADDR(arr[1]);
PRINT_ADDR(arr[2]);
PRINT_ADDR(tStrt);
PRINT_ADDR(tStrt.member1);
PRINT_ADDR(tStrt.member2);
PRINT_ADDR(tStrt.member3);
return 0;
}
int main(void){
int locMain1 = 1, locMain2 = 2, locMain3 = 3;
PRINT_ADDR(locMain1);
PRINT_ADDR(locMain2);
PRINT_ADDR(locMain3);
StackFrameContent(locMain1, locMain2, locMain3);
printf("[locMain1,2,3] = [%d, %d, %d]\n", locMain1, locMain2, locMain3);
memset(&locMain2, 0, 2*sizeof(int));
printf("[locMain1,2,3] = [%d, %d, %d]\n", locMain1, locMain2, locMain3);
return 0;
}
編譯鏈接并執(zhí)行后,輸出打印如下:
函數(shù)棧布局示例如下圖所示会钝。為直觀起見原探,低于起始高地址0xbfc75a58的其他地址采用點記法,如0x.54表示0xbfc75a54顽素,以此類推咽弦。
內(nèi)存地址從棧底到棧頂遞減,壓棧就是把ESP指針逐漸往地低址移動的過程胁出。而結(jié)構(gòu)體tStrt中的成員變量memberX地址=tStrt首地址+(memberX偏移量)型型,即越靠近tStrt首地址的成員變量其內(nèi)存地址越小。因此全蝶,結(jié)構(gòu)體成員變量的入棧順序與其在結(jié)構(gòu)體中聲明的順序相反闹蒜。
函數(shù)調(diào)用以值傳遞時,傳入的實參(locMain13)與被調(diào)函數(shù)內(nèi)操作的形參(para13)兩者存儲地址不同抑淫,因此被調(diào)函數(shù)無法直接修改主調(diào)函數(shù)實參值(對形參的操作相當于修改實參的副本)绷落。為達到修改目的,需要向被調(diào)函數(shù)傳遞實參變量的指針(即變量的地址)始苇。
此外砌烁,"[locMain1,2,3] = [0, 0, 3]"是因為對四字節(jié)參數(shù)locMain2調(diào)用memset函數(shù)時,會從低地址向高地址連續(xù)清零8個字節(jié)催式,從而誤將位于高地址locMain1清零函喉。
注意,局部變量的布局依賴于編譯器實現(xiàn)等因素荣月。因此管呵,當StackFrameContent函數(shù)中刪除打印語句時,變量locVar3哺窄、locVar2和locVar1可能按照從高到低的順序依次存儲捐下!而且账锹,局部變量并不總在棧中,有時出于性能(速度)考慮會存放在寄存器中坷襟。數(shù)組/結(jié)構(gòu)體型的局部變量通常分配在棧內(nèi)存中奸柬。
【擴展閱讀】函數(shù)局部變量布局方式
與函數(shù)調(diào)用約定規(guī)定參數(shù)如何傳入不同,局部變量以何種方式布局并未規(guī)定啤握。編譯器計算函數(shù)局部變量所需要的空間總數(shù),并確定這些變量存儲在寄存器上還是分配在程序棧上(甚至被優(yōu)化掉)——某些處理器并沒有堆棧晶框。局部變量的空間分配與主調(diào)函數(shù)和被調(diào)函數(shù)無關(guān)排抬,僅僅從函數(shù)源代碼上無法確定該函數(shù)的局部變量分布情況。
基于不同的編譯器版本(gcc3.4中局部變量按照定義順序依次入棧授段,gcc4及以上版本則不定)蹲蒲、優(yōu)化級別、目標處理器架構(gòu)侵贵、棧安全性等届搁,相鄰定義的兩個變量在內(nèi)存位置上可能相鄰,也可能不相鄰窍育,前后關(guān)系也不固定卡睦。若要確保兩個對象在內(nèi)存上相鄰且前后關(guān)系固定,可使用結(jié)構(gòu)體或數(shù)組定義漱抓。
4 堆棧操作
函數(shù)調(diào)用時的具體步驟如下:
- 主調(diào)函數(shù)將被調(diào)函數(shù)所要求的參數(shù)表锻,根據(jù)相應的函數(shù)調(diào)用約定,保存在運行時棧中乞娄。該操作會改變程序的棧指針瞬逊。
注:x86平臺將參數(shù)壓入調(diào)用棧中。而x86_64平臺具有16個通用64位寄存器仪或,故調(diào)用函數(shù)時前6個參數(shù)通常由寄存器傳遞确镊,其余參數(shù)才通過棧傳遞。
- 主調(diào)函數(shù)將控制權(quán)移交給被調(diào)函數(shù)(使用call指令)范删。函數(shù)的返回地址(待執(zhí)行的下條指令地址)保存在程序棧中(壓棧操作隱含在call指令中)蕾域。
- 若有必要,被調(diào)函數(shù)會設(shè)置幀基指針到旦,并保存被調(diào)函數(shù)希望保持不變的寄存器值束铭。
- 被調(diào)函數(shù)通過修改棧頂指針的值,為自己的局部變量在運行時棧中分配內(nèi)存空間厢绝,并從幀基指針的位置處向低地址方向存放被調(diào)函數(shù)的局部變量和臨時變量契沫。
- 被調(diào)函數(shù)執(zhí)行自己任務,此時可能需要訪問由主調(diào)函數(shù)傳入的參數(shù)昔汉。若被調(diào)函數(shù)返回一個值懈万,該值通常保存在一個指定寄存器中(如EAX)拴清。
- 一旦被調(diào)函數(shù)完成操作,為該函數(shù)局部變量分配的椈嵬ǎ空間將被釋放口予。這通常是步驟4的逆向執(zhí)行。
- 恢復步驟3中保存的寄存器值涕侈,包含主調(diào)函數(shù)的幀基指針寄存器沪停。
- 被調(diào)函數(shù)將控制權(quán)交還主調(diào)函數(shù)(使用ret指令)。根據(jù)使用的函數(shù)調(diào)用約定裳涛,該操作也可能從程序棧上清除先前傳入的參數(shù)木张。
- 主調(diào)函數(shù)再次獲得控制權(quán)后,可能需要將先前的參數(shù)從棧上清除端三。在這種情況下舷礼,對棧的修改需要將幀基指針值恢復到步驟1之前的值。
- 步驟3與步驟4在函數(shù)調(diào)用之初常一同出現(xiàn)郊闯,統(tǒng)稱為函數(shù)序(prologue)妻献;
- 步驟6到步驟8在函數(shù)調(diào)用的最后常一同出現(xiàn),統(tǒng)稱為函數(shù)跋(epilogue)团赁。
- 函數(shù)序和函數(shù)跋是編譯器自動添加的開始和結(jié)束匯編代碼育拨,其實現(xiàn)與CPU架構(gòu)和編譯器相關(guān)。除步驟5代表函數(shù)實體外欢摄,其它所有操作組成函數(shù)調(diào)用至朗。
下面介紹函數(shù)調(diào)用過程中的主要指令。
- 壓棧(push):棧頂指針ESP減小4個字節(jié)剧浸;以字節(jié)為單位將寄存器數(shù)據(jù)(四字節(jié)锹引,不足補零)壓入堆棧,從高到低按字節(jié)依次將數(shù)據(jù)存入ESP-1唆香、ESP-2嫌变、ESP-3、ESP-4指向的地址單元躬它。
- 出棧(pop):棧頂指針ESP指向的棧中數(shù)據(jù)被取回到寄存器腾啥;棧頂指針ESP增加4個字節(jié)。
可見冯吓,壓棧操作將寄存器內(nèi)容存入棧內(nèi)存中(寄存器原內(nèi)容不變)倘待,棧頂?shù)刂窚p小组贺;出棧操作從棧內(nèi)存中取回寄存器內(nèi)容(棧內(nèi)已存數(shù)據(jù)不會自動清零)凸舵,棧頂?shù)刂吩龃蟆m斨羔楨SP總是指向棧中下一個可用數(shù)據(jù)失尖。
- 調(diào)用(call):將當前的指令指針EIP(該指針指向緊接在call指令后的下條指令)壓入堆棧啊奄,以備返回時能恢復執(zhí)行下條指令渐苏;然后設(shè)置EIP指向被調(diào)函數(shù)代碼開始處,以跳轉(zhuǎn)到被調(diào)函數(shù)的入口地址執(zhí)行菇夸。
- 離開(leave):恢復主調(diào)函數(shù)的棧幀以準備返回琼富。等價于指令序列movl %ebp, %esp(恢復原ESP值,指向被調(diào)函數(shù)棧幀開始處)和popl %ebp(恢復原ebp的值庄新,即主調(diào)函數(shù)幀基指針)鞠眉。
- 返回(ret):與call指令配合,用于從函數(shù)或過程返回择诈。從棧頂彈出返回地址(之前call指令保存的下條指令地址)到EIP寄存器中械蹋,程序轉(zhuǎn)到該地址處繼續(xù)執(zhí)行(此時ESP指向進入函數(shù)時的第一個參數(shù))。若帶立即數(shù)吭从,ESP再加立即數(shù)(丟棄一些在執(zhí)行call前入棧的參數(shù))朝蜘。使用該指令前恶迈,應使當前棧頂指針所指向位置的內(nèi)容正好是先前call指令保存的返回地址涩金。
基于以上指令,使用C調(diào)用約定的被調(diào)函數(shù)典型的函數(shù)序和函數(shù)跋實現(xiàn)如下:
若主調(diào)函數(shù)和被調(diào)函數(shù)均未使用局部變量寄存器EDI暇仲、ESI和EBX步做,則編譯器無須在函數(shù)序中對其壓棧,以便提高程序的執(zhí)行效率奈附。
參數(shù)壓棧指令因編譯器而異全度,如下兩種壓棧方式基本等效:
兩種壓棧方式均遵循C調(diào)用約定,但方式二中主調(diào)函數(shù)在調(diào)用返回后并未顯式清理堆棾饴耍空間将鸵。因為在被調(diào)函數(shù)序階段,編譯器在棧頂為函數(shù)參數(shù)預先分配內(nèi)存空間(sub指令)佑颇。函數(shù)參數(shù)被復制到棧中(而非壓入棧中)顶掉,并未修改棧頂指針,故調(diào)用返回時主調(diào)函數(shù)也無需修改棧頂指針挑胸。gcc3.4(或更高版本)編譯器采用該技術(shù)將函數(shù)參數(shù)傳遞至棧上痒筒,相比棧頂指針隨每次參數(shù)壓棧而多次下移,一次性設(shè)置好棧頂指針更為高效茬贵。設(shè)想連續(xù)調(diào)用多個函數(shù)時簿透,方式二僅需預先分配一次參數(shù)內(nèi)存(大小足夠容納參數(shù)尺寸和最大的函數(shù)即可),后續(xù)調(diào)用無需每次都恢復棧頂指針解藻。注意老充,函數(shù)被調(diào)用時,兩種方式均使棧頂指針指向函數(shù)最左邊的參數(shù)螟左。本文不再區(qū)分兩種壓棧方式蚂维,"壓棧"或"入棧"所提之處均按相應匯編代碼理解戳粒,若無匯編則指方式二。
某些情況下虫啥,編譯器生成的函數(shù)調(diào)用進入/退出指令序列并不按照以上方式進行蔚约。例如,若C函數(shù)聲明為static(只在本編譯單元內(nèi)可見)且函數(shù)在編譯單元內(nèi)被直接調(diào)用涂籽,未被顯示或隱式取地址(即沒有任何函數(shù)指針指向該函數(shù))苹祟,此時編譯器確信該函數(shù)不會被其它編譯單元調(diào)用,因此可隨意修改其進/出指令序列以達到優(yōu)化目的评雌。
盡管使用的寄存器名字和指令在不同處理器架構(gòu)上有所不同树枫,但創(chuàng)建棧幀的基本過程一致。
注意景东,棧幀是運行時概念砂轻,若程序不運行,就不存在棧和棧幀斤吐。但通過分析目標文件中建立函數(shù)棧幀的匯編代碼(尤其是函數(shù)序和函數(shù)跋過程)搔涝,即使函數(shù)沒有運行,也能了解函數(shù)的棧幀結(jié)構(gòu)和措。通過分析可確定分配在函數(shù)棧幀上的局部變量空間準確值庄呈,函數(shù)中是否使用幀基指針,以及識別函數(shù)棧幀中對變量的所有內(nèi)存引用派阱。
5 函數(shù)調(diào)用約定
創(chuàng)建一個棧幀的最重要步驟是主調(diào)函數(shù)如何向棧中傳遞函數(shù)參數(shù)诬留。主調(diào)函數(shù)必須精確存儲這些參數(shù),以便被調(diào)函數(shù)能夠訪問到它們贫母。函數(shù)通過選擇特定的調(diào)用約定文兑,來表明其希望以特定方式接收參數(shù)。此外腺劣,當被調(diào)函數(shù)完成任務后绿贞,調(diào)用約定規(guī)定先前入棧的參數(shù)由主調(diào)函數(shù)還是被調(diào)函數(shù)負責清除,以保證程序的棧頂指針完整性誓酒。
函數(shù)調(diào)用約定通常規(guī)定如下幾方面內(nèi)容:
函數(shù)參數(shù)的傳遞順序和方式
最常見的參數(shù)傳遞方式是通過堆棧傳遞樟蠕。主調(diào)函數(shù)將參數(shù)壓入棧中,被調(diào)函數(shù)以相對于幀基指針的正偏移量來訪問棧中的參數(shù)靠柑。對于有多個參數(shù)的函數(shù)寨辩,調(diào)用約定需規(guī)定主調(diào)函數(shù)將參數(shù)壓棧的順序(從左至右還是從右至左)。某些調(diào)用約定允許使用寄存器傳參以提高性能歼冰。棧的維護方式
主調(diào)函數(shù)將參數(shù)壓棧后調(diào)用被調(diào)函數(shù)體靡狞,返回時需將被壓棧的參數(shù)全部彈出,以便將椄舻眨恢復到調(diào)用前的狀態(tài)甸怕。該清棧過程可由主調(diào)函數(shù)負責完成甘穿,也可由被調(diào)函數(shù)負責完成。名字修飾(Name-mangling)策略
又稱函數(shù)名修飾(Decorated Name)規(guī)則梢杭。編譯器在鏈接時為區(qū)分不同函數(shù)温兼,對函數(shù)名作不同修飾。
若函數(shù)之間的調(diào)用約定不匹配武契,可能會產(chǎn)生堆棧異衬寂校或鏈接錯誤等問題。因此咒唆,為了保證程序能正確執(zhí)行届垫,所有的函數(shù)調(diào)用均應遵守一致的調(diào)用約定。
5.1 常見調(diào)用約定
下面分別介紹常見的幾種函數(shù)調(diào)用約定全释。
-
cdecl調(diào)用約定
又稱C調(diào)用約定装处,是C/C++編譯器默認的函數(shù)調(diào)用約定。所有非C++成員函數(shù)和未使用stdcall或fastcall聲明的函數(shù)都默認是cdecl方式浸船。函數(shù)參數(shù)按照從右到左的順序入棧妄迁,函數(shù)調(diào)用者負責清除棧中的參數(shù),返回值在EAX中糟袁。由于每次函數(shù)調(diào)用都要產(chǎn)生清除(還原)堆棧的代碼判族,故使用cdecl方式編譯的程序比使用stdcall方式編譯的程序大(后者僅需在被調(diào)函數(shù)內(nèi)產(chǎn)生一份清棧代碼)躺盛。但cdecl調(diào)用方式支持可變參數(shù)函數(shù)(即函數(shù)帶有可變數(shù)目的參數(shù)项戴,如printf),且調(diào)用時即使實參和形參數(shù)目不符也不會導致堆棧錯誤槽惫。對于C函數(shù)周叮,cdecl方式的名字修飾約定是在函數(shù)名前添加一個下劃線;對于C++函數(shù)界斜,除非特別使用extern "C"仿耽,C++函數(shù)使用不同的名字修飾方式。
【擴展閱讀】可變參數(shù)函數(shù)支持條件
若要支持可變參數(shù)的函數(shù)各薇,則參數(shù)應自右向左進棧项贺,并且由主調(diào)函數(shù)負責清除棧中的參數(shù)(參數(shù)出棧)。
首先峭判,參數(shù)按照從右向左的順序壓棧开缎,則參數(shù)列表最左邊(第一個)的參數(shù)最接近棧頂位置。所有參數(shù)距離幀基指針的偏移量都是常數(shù)林螃,而不必關(guān)心已入棧的參數(shù)數(shù)目奕删。只要不定的參數(shù)的數(shù)目能根據(jù)第一個已明確的參數(shù)確定,就可使用不定參數(shù)疗认。例如printf函數(shù)完残,第一個參數(shù)即格式化字符串可作為后繼參數(shù)指示符伏钠。通過它們就可得到后續(xù)參數(shù)的類型和個數(shù),進而知道所有參數(shù)的尺寸谨设。當傳遞的參數(shù)過多時熟掂,以幀基指針為基準,獲取適當數(shù)目的參數(shù)扎拣,其他忽略即可打掘。若函數(shù)參數(shù)自左向右進棧,則第一個參數(shù)距離棧幀指針的偏移量與已入棧的參數(shù)數(shù)目有關(guān)鹏秋,需要計算所有參數(shù)占用的空間后才能精確定位尊蚁。當實際傳入的參數(shù)數(shù)目與函數(shù)期望接受的參數(shù)數(shù)目不同時,偏移量計算會出錯侣夷!
其次横朋,調(diào)用函數(shù)將參數(shù)壓棧,只有它才知道棧中的參數(shù)數(shù)目和尺寸百拓,因此調(diào)用函數(shù)可安全地清棧琴锭。而被調(diào)函數(shù)永遠也不能事先知道將要傳入函數(shù)的參數(shù)信息,難以對棧頂指針進行調(diào)整衙传。
C++為兼容C决帖,仍然支持函數(shù)帶有可變的參數(shù)。但在C++中更好的選擇常常是函數(shù)多態(tài)蓖捶。
stdcall調(diào)用約定(微軟命名)
Pascal程序缺省調(diào)用方式地回,WinAPI也多采用該調(diào)用約定。stdcall調(diào)用約定主調(diào)函數(shù)參數(shù)從右向左入棧俊鱼,除指針或引用類型參數(shù)外所有參數(shù)采用傳值方式傳遞刻像,由被調(diào)函數(shù)負責清除棧中的參數(shù),返回值在EAX中并闲。stdcall調(diào)用約定僅適用于參數(shù)個數(shù)固定的函數(shù)细睡,因為被調(diào)函數(shù)清棧時無法精確獲知棧上有多少函數(shù)參數(shù);而且如果調(diào)用時實參和形參數(shù)目不符會導致堆棧錯誤帝火。對于C函數(shù)溜徙,stdcall名稱修飾方式是在函數(shù)名字前添加下劃線,在函數(shù)名字后添加@和函數(shù)參數(shù)的大小犀填,如_functionname@number蠢壹。fastcall調(diào)用約定
stdcall調(diào)用約定的變形,通常使用ECX和EDX寄存器傳遞前兩個DWORD(四字節(jié)雙字)類型或更少字節(jié)的函數(shù)參數(shù)宏浩,其余參數(shù)按照從右向左的順序入棧知残,被調(diào)函數(shù)在返回前負責清除棧中的參數(shù),返回值在EAX中。因為并不是所有的參數(shù)都有壓棧操作求妹,所以比stdcall和cdecl快些乏盐。編譯器使用兩個@修飾函數(shù)名字,后跟十進制數(shù)表示的函數(shù)參數(shù)列表大小(字節(jié)數(shù))制恍,如@function_name@number父能。需注意fastcall函數(shù)調(diào)用約定在不同編譯器上可能有不同的實現(xiàn),比如16位編譯器和32位編譯器净神。另外何吝,在使用內(nèi)嵌匯編代碼時,還應注意不能和編譯器使用的寄存器有沖突鹃唯。thiscall調(diào)用約定
C++類中的非靜態(tài)函數(shù)必須接收一個指向主調(diào)對象的類指針(this指針)爱榕,并可能較頻繁的使用該指針。主調(diào)函數(shù)的對象地址必須由調(diào)用者提供坡慌,并在調(diào)用對象非靜態(tài)成員函數(shù)時將對象指針以參數(shù)形式傳遞給被調(diào)函數(shù)黔酥。編譯器默認使用thiscall調(diào)用約定以高效傳遞和存儲C++類的非靜態(tài)成員函數(shù)的this指針參數(shù)。
thiscall調(diào)用約定函數(shù)參數(shù)按照從右向左的順序入棧洪橘。若參數(shù)數(shù)目固定跪者,則類實例的this指針通過ECX寄存器傳遞給被調(diào)函數(shù),被調(diào)函數(shù)自身清理堆棧熄求;若參數(shù)數(shù)目不定渣玲,則this指針在所有參數(shù)入棧后再入棧,主調(diào)函數(shù)清理堆棧弟晚。thiscall不是C++關(guān)鍵字忘衍,故不能使用thiscall聲明函數(shù),它只能由編譯器使用指巡。
注意淑履,該調(diào)用約定特點隨編譯器不同而不同隶垮,g++中thiscall與cdecl基本相同藻雪,只是隱式地將this指針當作非靜態(tài)成員函數(shù)的第1個參數(shù),主調(diào)函數(shù)在調(diào)用返回后負責清理棧上參數(shù)狸吞;而在VC中勉耀,this指針存放在%ecx寄存器中,參數(shù)從右至左壓棧蹋偏,非靜態(tài)成員函數(shù)負責清理棧上參數(shù)便斥。
-
naked call調(diào)用約定
對于使用naked call方式聲明的函數(shù),編譯器不產(chǎn)生保存(prologue)和恢復(epilogue)寄存器的代碼威始,且不能用return返回返回值(只能用內(nèi)嵌匯編返回結(jié)果)枢纠,故稱naked call。該調(diào)用約定用于一些特殊場合黎棠,如聲明處于非C/C++上下文中的函數(shù)晋渺,并由程序員自行編寫初始化和清棧的內(nèi)嵌匯編指令镰绎。注意,naked call并非類型修飾符木西,故該調(diào)用約定必須與__declspec同時使用畴栖,如VC下定義求和函數(shù),代碼示例如下(Windows采用Intel匯編語法八千,注釋符為;):
__declspec(naked) int __stdcall function(int a, int b)
{
;mov DestRegister, SrcImmediate(Intel) vs. movl $SrcImmediate, %DestRegister(AT&T)
__asm mov eax, a
__asm add eax, b
__asm ret 8
}
注意吗讶,__declspec是微軟關(guān)鍵字,其它系統(tǒng)上可能沒有恋捆。
-
pascal調(diào)用約定
Pascal語言調(diào)用約定照皆,參數(shù)按照從左至右的順序入棧。Pascal語言只支持固定參數(shù)的函數(shù)沸停,參數(shù)的類型和數(shù)量完全可知纵寝,故由被調(diào)函數(shù)自身清理堆棧。pascal調(diào)用約定輸出的函數(shù)名稱無任何修飾且全部大寫星立。
Win3.X(16位)時支持真正的pascal調(diào)用約定爽茴;而Win9.X(32位)以后pascal約定由stdcall約定代替(以C約定壓棧以Pascal約定清棧)。
上述調(diào)用約定的主要特點如下表所示:
Windows下可直接在函數(shù)聲明前添加關(guān)鍵字__stdcall绰垂、__cdecl或__fastcall等標識確定函數(shù)的調(diào)用方式室奏,如int __stdcall func()。
Linux下可借用函數(shù)attribute 機制劲装,如int attribute((stdcall)) func()胧沫。
代碼示例如下:
int __attribute__((__cdecl__)) CalleeFunc(int i, int j, int k){
// int __attribute__((__stdcall__)) CalleeFunc(int i, int j, int k){
//int __attribute__((__fastcall__)) CalleeFunc(int i, int j, int k){
return i+j+k;
}
void CallerFunc(void){
CalleeFunc(0x11, 0x22, 0x33);
}
int main(void){
CallerFunc();
return 0;
}
被調(diào)函數(shù)CalleeFunc分別聲明為cdecl、stdcall和fastcall約定時占业,其匯編代碼比較如下表所示:
5.2 調(diào)用約定影響
當函數(shù)導出被其他程序員所使用(如庫函數(shù))時绒怨,該函數(shù)應遵循主要的調(diào)用約定,以便于程序員使用谦疾。若函數(shù)僅供內(nèi)部使用南蹂,則其調(diào)用約定可只被使用該函數(shù)的程序所了解。
在多語言混合編程(包括A語言中使用B語言開發(fā)的第三方庫)時念恍,若函數(shù)的原型聲明和函數(shù)體定義不一致或調(diào)用函數(shù)時聲明了不同的函數(shù)約定六剥,將可能導致嚴重問題(如堆棧被破壞)。
以Delphi調(diào)用C函數(shù)為例峰伙。Delphi函數(shù)缺省采用stdcall調(diào)用約定疗疟,而C函數(shù)缺省采用cdecl調(diào)用約定。一般將C函數(shù)聲明為stdcall約定瞳氓,如:int __stdcall add(int a, int b);
在Delphi中調(diào)用該函數(shù)時也應聲明為stdcall約定:
function add(a: Integer; b: Integer): Integer; stdcall; //參數(shù)類型應與DLL中的函數(shù)或過程參數(shù)類型一致策彤,且引用時使用stdcall參數(shù)
external 'a.dll'; //指定被調(diào)DLL文件的路徑和名稱
不同編譯器產(chǎn)生棧幀的方式不盡相同,主調(diào)函數(shù)不一定能正常完成清棧工作;而被調(diào)函數(shù)必然能自己完成正常清棧店诗,因此叽赊,在跨(開發(fā))平臺調(diào)用中,通常使用stdcall調(diào)用約定(不少WinApi均采用該約定)必搞。
此外必指,主調(diào)函數(shù)和被調(diào)函數(shù)所在模塊采用相同的調(diào)用約定,但分別使用C++和C語法編譯時恕洲,會出現(xiàn)鏈接錯誤(報告被調(diào)函數(shù)未定義)塔橡。這是因為兩種語言的函數(shù)名字修飾規(guī)則不同,解決方式是使用extern "C"告知主調(diào)函數(shù)所在模塊:被調(diào)函數(shù)是C語言編譯的霜第。采用C語言編譯的庫應考慮到使用該庫的程序可能是C++程序(使用C++編譯器)葛家,通常應這樣聲明頭文件:
#ifdef _cplusplus
extern "C" {
#endif
type Func(type para);
#ifdef _cplusplus
}
#endif
這樣C++編譯器就會按照C語言修飾策略鏈接Func函數(shù)名,而不會出現(xiàn)找不到函數(shù)的鏈接錯誤泌类。
5.3 x86函數(shù)參數(shù)傳遞方法
x86處理器ABI規(guī)范中規(guī)定癞谒,所有傳遞給被調(diào)函數(shù)的參數(shù)都通過堆棧來完成,其壓棧順序是以函數(shù)參數(shù)從右到左的順序刃榨。當向被調(diào)函數(shù)傳遞參數(shù)時弹砚,所有參數(shù)最后形成一個數(shù)組。由于采用從右到左的壓棧順序枢希,數(shù)組中參數(shù)的順序(下標0N-1)與函數(shù)參數(shù)聲明順序(Para1N)一致桌吃。因此,在函數(shù)中若知道第一個參數(shù)地址和各參數(shù)占用字節(jié)數(shù)苞轿,就可通過訪問數(shù)組的方式去訪問每個參數(shù)茅诱。
5.3.1 整型和指針參數(shù)的傳遞
整型參數(shù)與指針參數(shù)的傳遞方式相同悉患,因為在32位x86處理器上整型與指針大小相同(均為四字節(jié))栅组。下表給出這兩種類型的參數(shù)在棧幀中的位置關(guān)系。注意贞瞒,該表基于tail函數(shù)的棧幀契邀。
5.3.2 浮點參數(shù)的傳遞
浮點參數(shù)的傳遞與整型類似摆寄,區(qū)別在于參數(shù)大小。x86處理器中浮點類型占8個字節(jié)蹂安,因此在棧中也需要占用8個字節(jié)椭迎。下表給出浮點參數(shù)在棧幀中的位置關(guān)系。圖中田盈,調(diào)用tail函數(shù)的第一個和第三個參數(shù)均為浮點類型,因此需各占用8個字節(jié)缴阎,三個參數(shù)共占用20個字節(jié)允瞧。表中word類型的大小是4字節(jié)。
5.3.3 結(jié)構(gòu)體和聯(lián)合體參數(shù)的傳遞
結(jié)構(gòu)體和聯(lián)合體參數(shù)的傳遞與整型、浮點參數(shù)類似述暂,只是其占用字節(jié)大小視數(shù)據(jù)結(jié)構(gòu)的定義不同而異痹升。x86處理器上棧寬是4字節(jié),故結(jié)構(gòu)體在棧上所占用的字節(jié)數(shù)為4的倍數(shù)畦韭。編譯器會對結(jié)構(gòu)體進行適當?shù)奶畛湟允沟媒Y(jié)構(gòu)體大小滿足4字節(jié)對齊的要求疼蛾。
對于一些RISC處理器(如PowerPC),其參數(shù)傳遞并不是全部通過棧來實現(xiàn)艺配。PowerPC處理器寄存器中察郁,R3~R10共8個寄存器用于傳遞整型或指針參數(shù),F(xiàn)1~F8共8個寄存器用于傳遞浮點參數(shù)转唉。當所需傳遞的參數(shù)少于8個時皮钠,不需要用到棧。結(jié)構(gòu)體和long double參數(shù)的傳遞通過指針來完成赠法,這與x86處理器完全不同麦轰。PowerPC的ABI規(guī)范中規(guī)定,結(jié)構(gòu)體的傳遞采用指針方式砖织,而不是像x86處理器那樣將結(jié)構(gòu)從一個函數(shù)棧幀中拷貝到另一個函數(shù)棧幀中款侵,顯然x86處理器的方式更低效〔啻浚可見喳坠,PowerPC程序中,函數(shù)參數(shù)采用指向結(jié)構(gòu)體的指針(而非結(jié)構(gòu)體)并不能提高效率茂蚓,不過通常這是良好的編程習慣壕鹉。
5.4 x86函數(shù)返回值傳遞方法
函數(shù)返回值可通過寄存器傳遞。當被調(diào)用函數(shù)需要返回結(jié)果給調(diào)用函數(shù)時:
- 返回值不超過4字節(jié)(如int聋涨、short晾浴、char、指針等類型)牍白,通常將其保存在EAX寄存器中脊凰,調(diào)用方通過讀取EAX獲取返回值。
- 若返回值大于4字節(jié)而小于8字節(jié)(如long long或_int64類型)茂腥,則通過EAX+EDX寄存器聯(lián)合返回狸涌,其中EDX保存返回值高4字節(jié),EAX保存返回值低4字節(jié)最岗。
- 若返回值為浮點類型(如float和double)帕胆,則通過專用的協(xié)處理器浮點數(shù)寄存器棧的棧頂返回。
- 若返回值為結(jié)構(gòu)體或聯(lián)合體般渡,則主調(diào)函數(shù)向被調(diào)函數(shù)傳遞一個額外參數(shù)懒豹,該參數(shù)指向?qū)⒁4娣祷刂档牡刂奋脚獭<春瘮?shù)調(diào)用foo(p1, p2)被轉(zhuǎn)化為foo(&p0, p1, p2),以引用型參數(shù)形式傳回返回值脸秽。具體步驟可能為:
a. 主調(diào)函數(shù)將顯式的實參逆序入棧儒老;
b. 將接收返回值的結(jié)構(gòu)體變量地址作為隱藏參數(shù)入棧(若未定義該接收變量,則在棧上額外開辟空間作為接收返回值的臨時變量)记餐;
c. 被調(diào)函數(shù)將待返回數(shù)據(jù)拷貝到隱藏參數(shù)所指向的內(nèi)存地址驮樊,并將該地址存入%eax寄存器。因此片酝,在被調(diào)函數(shù)中完成返回值的賦值工作囚衔。
注意,函數(shù)如何傳遞結(jié)構(gòu)體或聯(lián)合體返回值依賴于具體實現(xiàn)钠怯。不同編譯器佳魔、平臺、調(diào)用約定甚至編譯參數(shù)下可能采用不同的實現(xiàn)方法晦炊。如VC6編譯器對于不超過8字節(jié)的小結(jié)構(gòu)體鞠鲜,會通過EAX+EDX寄存器返回。而對于超過8字節(jié)的大結(jié)構(gòu)體断国,主調(diào)函數(shù)在棧上分配用于接收返回值的臨時結(jié)構(gòu)體贤姆,并將地址通過棧傳遞給被調(diào)函數(shù);被調(diào)函數(shù)根據(jù)返回值地址設(shè)置返回值(拷貝操作)稳衬;調(diào)用返回后主調(diào)函數(shù)根據(jù)需要霞捡,再將返回值賦值給需要的臨時變量(二次拷貝)。實際使用中為提高效率薄疚,通常將結(jié)構(gòu)體指針作為實參傳遞給被調(diào)函數(shù)以接收返回值碧信。
- 不要返回指向棧內(nèi)存的指針,如返回被調(diào)函數(shù)內(nèi)局部變量地址(包括局部數(shù)組名)街夭。因為函數(shù)返回后砰碴,其棧幀空間被“釋放”,原棧幀內(nèi)分配的局部變量空間的內(nèi)容是不穩(wěn)定和不被保證的板丽。
函數(shù)返回值通過寄存器傳遞呈枉,無需空間分配等操作,故返回值的代價很低埃碱〔瑁基于此原因,C89規(guī)范中約定砚殿,不寫明返回值類型的函數(shù)啃憎,返回值類型默認為int。但這會帶來類型安全隱患瓮具,如函數(shù)定義時返回值為浮點數(shù)荧飞,而函數(shù)未聲明或聲明時未指明返回值類型凡人,則調(diào)用時默認從寄存器EAX(而不是浮點數(shù)寄存器)中獲取返回值名党,導致錯誤叹阔!因此在C++中,不寫明返回值類型的函數(shù)返回值類型為void传睹,表示不返回值耳幢。
【擴展閱讀】GCC返回結(jié)構(gòu)體和聯(lián)合體
通常GCC被配置為使用與目標系統(tǒng)一致的函數(shù)調(diào)用約定。這通過機器描述宏來實現(xiàn)欧啤。但是睛藻,在一些目標機上采用不同方式返回結(jié)構(gòu)體和聯(lián)合體的值。因此邢隧,使用PCC編譯的返回這些類型的函數(shù)不能被使用GCC編譯的代碼調(diào)用店印,反之亦然。但這并未造成麻煩倒慧,因為很少有Unix庫函數(shù)返回結(jié)構(gòu)體或聯(lián)合體按摘。
GCC代碼使用存放int或double類型返回值的寄存器來返回1、2纫谅、4或8個字節(jié)的結(jié)構(gòu)體和聯(lián)合體(GCC通常還將此類變量分配在寄存器中)炫贤。其它大小的結(jié)構(gòu)體和聯(lián)合體在返回時,將其存放在一個由調(diào)用者傳遞的地址中(通常在寄存器中)付秕。
相比之下兰珍,PCC在大多目標機上返回任何大小的結(jié)構(gòu)體和聯(lián)合體時,都將數(shù)據(jù)復制到一個靜態(tài)存儲區(qū)域询吴,再將該地址當作指針值返回掠河。調(diào)用者必須將數(shù)據(jù)從那個內(nèi)存區(qū)域復制到需要的地方。這比GCC使用的方法要慢猛计,而且不可重入唠摹。
在一些目標機上(如RISC機器和80386),標準的系統(tǒng)約定是將返回值的地址傳給子程序有滑。在這些機器上跃闹,當使用這種約定方法時,GCC被配置為與標準編譯器兼容毛好。這可能會對于1望艺,2,4或8字節(jié)的結(jié)構(gòu)體不兼容肌访。
GCC使用系統(tǒng)的標準約定來傳遞參數(shù)找默。在一些機器上,前幾個參數(shù)通過寄存器傳遞吼驶;在另一些機器上惩激,所有的參數(shù)都通過棧傳遞店煞。原本可在所有機器上都使用寄存器來傳遞參數(shù),而且此法還可能顯著提高性能风钻。但這樣就與使用標準約定的代碼完全不兼容顷蟀。所以這種改變只在將GCC作為系統(tǒng)唯一的C編譯器時才實用。當擁有一套完整的GNU 系統(tǒng)骡技,能夠用GCC來編譯庫時鸣个,可在特定機器上實現(xiàn)寄存器參數(shù)傳遞。
在一些機器上(特別是SPARC)布朦,一些類型的參數(shù)通過“隱匿引用”(invisible reference)來傳遞囤萤。這意味著值存儲在內(nèi)存中,將值的內(nèi)存地址傳給子程序是趴。