日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問(wèn) 生活随笔!

生活随笔

當(dāng)前位置: 首頁(yè) > 编程资源 > 编程问答 >内容正文

编程问答

C 语言 函数调用栈

發(fā)布時(shí)間:2024/7/23 编程问答 29 豆豆
生活随笔 收集整理的這篇文章主要介紹了 C 语言 函数调用栈 小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,幫大家做個(gè)參考.

?

From:https://www.cnblogs.com/clover-toeic/p/3755401.html? ??https://www.cnblogs.com/clover-toeic/p/3756668.html

?

? ? ? ?程序的執(zhí)行過(guò)程可看作連續(xù)的函數(shù)調(diào)用。當(dāng)一個(gè)函數(shù)執(zhí)行完畢時(shí),程序要回到調(diào)用指令的下一條指令(緊接call指令)處繼續(xù)執(zhí)行。函數(shù)調(diào)用過(guò)程通常使用堆棧實(shí)現(xiàn),每個(gè)用戶(hù)態(tài)進(jìn)程對(duì)應(yīng)一個(gè)調(diào)用棧結(jié)構(gòu)(call stack)。編譯器使用堆棧傳遞函數(shù)參數(shù)、保存返回地址、臨時(shí)保存寄存器原有值(即函數(shù)調(diào)用的上下文)以備恢復(fù)以及存儲(chǔ)本地局部變量。

? ? ? ?不同處理器和編譯器的堆棧布局、函數(shù)調(diào)用方法都可能不同,但堆棧的基本概念是一樣的。

?

?

1 寄存器分配

?

? ? ? ?寄存器是處理器加工數(shù)據(jù)或運(yùn)行程序的重要載體,用于存放程序執(zhí)行中用到的數(shù)據(jù)和指令。因此函數(shù)調(diào)用棧的實(shí)現(xiàn)與處理器寄存器組密切相關(guān)。

? ? ? ?Intel 32位體系結(jié)構(gòu)(簡(jiǎn)稱(chēng)IA32)處理器包含8個(gè)四字節(jié)寄存器,如下圖所示:

? ? ?最初的8086中寄存器是16位,每個(gè)都有特殊用途,寄存器名城反映其不同用途。由于IA32平臺(tái)采用平面尋址模式,對(duì)特殊寄存器的需求大大降低,但由于歷史原因,這些寄存器名稱(chēng)被保留下來(lái)。在大多數(shù)情況下,上圖所示的前6個(gè)寄存器均可作為通用寄存器使用。某些指令可能以固定的寄存器作為源寄存器或目的寄存器,如一些特殊的算術(shù)操作指令imull/mull/cltd/idivl/divl要求一個(gè)參數(shù)必須在%eax中,其運(yùn)算結(jié)果存放在%edx(higher 32-bit)和%eax (lower32-bit)中;又如函數(shù)返回值通常保存在%eax中,等等。為避免兼容性問(wèn)題,ABI規(guī)范對(duì)這組通用寄存器的具體作用加以定義(如圖中所示)。

? ? ?對(duì)于寄存器%eax、%ebx、%ecx和%edx,各自可作為兩個(gè)獨(dú)立的16位寄存器使用,而低16位寄存器還可繼續(xù)分為兩個(gè)獨(dú)立的8位寄存器使用。編譯器會(huì)根據(jù)操作數(shù)大小選擇合適的寄存器來(lái)生成匯編代碼。在匯編語(yǔ)言層面,這組通用寄存器以%e(AT&T語(yǔ)法)或直接以e(Intel語(yǔ)法)開(kāi)頭來(lái)引用,例如mov $5, %eax或mov eax, 5表示將立即數(shù)5賦值給寄存器%eax。

? ? ?在x86處理器中,EIP(Instruction Pointer)是指令寄存器,指向處理器下條等待執(zhí)行的指令地址(代碼段內(nèi)的偏移量),每次執(zhí)行完相應(yīng)匯編指令EIP值就會(huì)增加。ESP(Stack Pointer)是堆棧指針寄存器,存放執(zhí)行函數(shù)對(duì)應(yīng)棧幀的棧頂?shù)刂?也是系統(tǒng)棧的頂部),且始終指向棧頂;EBP(Base Pointer)是棧幀基址指針寄存器,存放執(zhí)行函數(shù)對(duì)應(yīng)棧幀的棧底地址,用于C運(yùn)行庫(kù)訪問(wèn)棧中的局部變量和參數(shù)。

? ? ?注意,EIP是個(gè)特殊寄存器,不能像訪問(wèn)通用寄存器那樣訪問(wèn)它,即找不到可用來(lái)尋址EIP并對(duì)其進(jìn)行讀寫(xiě)的操作碼(OpCode)。EIP可被jmp、call和ret等指令隱含地改變(事實(shí)上它一直都在改變)。

? ? ?不同架構(gòu)的CPU,寄存器名稱(chēng)被添加不同前綴以指示寄存器的大小。例如x86架構(gòu)用字母“e(extended)”作名稱(chēng)前綴,指示寄存器大小為32位;x86_64架構(gòu)用字母“r”作名稱(chēng)前綴,指示各寄存器大小為64位。

? ? ?編譯器在將C程序編譯成匯編程序時(shí),應(yīng)遵循ABI所規(guī)定的寄存器功能定義。同樣地,編寫(xiě)匯編程序時(shí)也應(yīng)遵循,否則所編寫(xiě)的匯編程序可能無(wú)法與C程序協(xié)同工作。

【擴(kuò)展閱讀】棧幀指針寄存器

為了訪問(wèn)函數(shù)局部變量,必須能定位每個(gè)變量。局部變量相對(duì)于堆棧指針ESP的位置在進(jìn)入函數(shù)時(shí)就已確定,理論上變量可用ESP加偏移量來(lái)引用,但ESP會(huì)在函數(shù)執(zhí)行期隨變量的壓棧和出棧而變動(dòng)。盡管某些情況下編譯器能跟蹤棧中的變量操作以修正偏移量,但要引入可觀的管理開(kāi)銷(xiāo)。而且在有些機(jī)器上(如Intel處理器),用ESP加偏移量來(lái)訪問(wèn)一個(gè)變量需要多條指令才能實(shí)現(xiàn)。

因此,許多編譯器使用幀指針寄存器FP(Frame Pointer)記錄棧幀基地址。局部變量和函數(shù)參數(shù)都可通過(guò)幀指針引用,因?yàn)樗鼈兊紽P的距離不會(huì)受到壓棧和出棧操作的影響。有些資料將幀指針?lè)Q作局部基指針(LB-local base pointer)。

在Intel CPU中,寄存器BP(EBP)用作幀指針。在Motorola CPU中,除A7(堆棧指針SP)外的任何地址寄存器都可用作FP。當(dāng)堆棧向下(低地址)增長(zhǎng)時(shí),以FP地址為基準(zhǔn),函數(shù)參數(shù)的偏移量是正值,而局部變量的偏移量是負(fù)值。

?

?

2 寄存器使用約定

?

? ? ?程序寄存器組是唯一能被所有函數(shù)共享的資源。雖然某一時(shí)刻只有一個(gè)函數(shù)在執(zhí)行,但需保證當(dāng)某個(gè)函數(shù)調(diào)用其他函數(shù)時(shí),被調(diào)函數(shù)不會(huì)修改或覆蓋主調(diào)函數(shù)稍后會(huì)使用到的寄存器值。因此,IA32采用一套統(tǒng)一的寄存器使用約定,所有函數(shù)(包括庫(kù)函數(shù))調(diào)用都必須遵守該約定。

? ? ?根據(jù)慣例,寄存器%eax、%edx和%ecx為主調(diào)函數(shù)保存寄存器(caller-saved registers),當(dāng)函數(shù)調(diào)用時(shí),若主調(diào)函數(shù)希望保持這些寄存器的值,則必須在調(diào)用前顯式地將其保存在棧中;被調(diào)函數(shù)可以覆蓋這些寄存器,而不會(huì)破壞主調(diào)函數(shù)所需的數(shù)據(jù)。寄存器%ebx、%esi和%edi為被調(diào)函數(shù)保存寄存器(callee-saved registers),即被調(diào)函數(shù)在覆蓋這些寄存器的值時(shí),必須先將寄存器原值壓入棧中保存起來(lái),并在函數(shù)返回前從棧中恢復(fù)其原值,因?yàn)橹髡{(diào)函數(shù)可能也在使用這些寄存器。此外,被調(diào)函數(shù)必須保持寄存器%ebp和%esp,并在函數(shù)返回后將其恢復(fù)到調(diào)用前的值,亦即必須恢復(fù)主調(diào)函數(shù)的棧幀。

? ? ?當(dāng)然,這些工作都由編譯器在幕后進(jìn)行。不過(guò)在編寫(xiě)匯編程序時(shí)應(yīng)注意遵守上述慣例。

?

?

3 棧幀結(jié)構(gòu)

?

? ? ?函數(shù)調(diào)用經(jīng)常是嵌套的,在同一時(shí)刻,堆棧中會(huì)有多個(gè)函數(shù)的信息。每個(gè)未完成運(yùn)行的函數(shù)占用一個(gè)獨(dú)立的連續(xù)區(qū)域,稱(chēng)作棧幀(Stack Frame)。棧幀是堆棧的邏輯片段,當(dāng)調(diào)用函數(shù)時(shí)邏輯棧幀被壓入堆棧, 當(dāng)函數(shù)返回時(shí)邏輯棧幀被從堆棧中彈出。棧幀存放著函數(shù)參數(shù),局部變量及恢復(fù)前一棧幀所需要的數(shù)據(jù)等。

? ? ?編譯器利用棧幀,使得函數(shù)參數(shù)和函數(shù)中局部變量的分配與釋放對(duì)程序員透明。編譯器將控制權(quán)移交函數(shù)本身之前,插入特定代碼將函數(shù)參數(shù)壓入棧幀中,并分配足夠的內(nèi)存空間用于存放函數(shù)中的局部變量。使用棧幀的一個(gè)好處是使得遞歸變?yōu)榭赡?#xff0c;因?yàn)閷?duì)函數(shù)的每次遞歸調(diào)用,都會(huì)分配給該函數(shù)一個(gè)新的棧幀,這樣就巧妙地隔離當(dāng)前調(diào)用與上次調(diào)用。

? ? ?棧幀的邊界由棧幀基地址指針EBP和堆棧指針ESP界定(指針存放在相應(yīng)寄存器中)。EBP指向當(dāng)前棧幀底部(高地址),在當(dāng)前棧幀內(nèi)位置固定;ESP指向當(dāng)前棧幀頂部(低地址),當(dāng)程序執(zhí)行時(shí)ESP會(huì)隨著數(shù)據(jù)的入棧和出棧而移動(dòng)。因此函數(shù)中對(duì)大部分?jǐn)?shù)據(jù)的訪問(wèn)都基于EBP進(jìn)行。

? ? ?為更具描述性,以下稱(chēng)EBP為幀基指針, ESP為棧頂指針,并在引用匯編代碼時(shí)分別記為%ebp和%esp。

? ?函數(shù)調(diào)用棧的典型內(nèi)存布局如下圖所示:

? ? ?圖中給出主調(diào)函數(shù)(caller)和被調(diào)函數(shù)(callee)的棧幀布局,"m(%ebp)"表示以EBP為基地址、偏移量為m字節(jié)的內(nèi)存空間(中的內(nèi)容)。該圖基于兩個(gè)假設(shè):第一,函數(shù)返回值不是結(jié)構(gòu)體或聯(lián)合體,否則第一個(gè)參數(shù)將位于"12(%ebp)" 處;第二,每個(gè)參數(shù)都是4字節(jié)大小(棧的粒度為4字節(jié))。在本文后續(xù)章節(jié)將就參數(shù)的傳遞和大小問(wèn)題做進(jìn)一步的探討。? 此外,函數(shù)可以沒(méi)有參數(shù)和局部變量,故圖中“Argument(參數(shù))”和“Local Variable(局部變量)”不是函數(shù)棧幀結(jié)構(gòu)的必需部分。

? ? ?從圖中可以看出,函數(shù)調(diào)用時(shí)入棧順序?yàn)?/p>

? ? ?其中,主調(diào)函數(shù)將參數(shù)按照調(diào)用約定依次入棧(圖中為從右到左),然后將指令指針EIP入棧以保存主調(diào)函數(shù)的返回地址(下一條待執(zhí)行指令的地址)。進(jìn)入被調(diào)函數(shù)時(shí),被調(diào)函數(shù)將主調(diào)函數(shù)的幀基指針EBP入棧,并將主調(diào)函數(shù)的棧頂指針ESP值賦給被調(diào)函數(shù)的EBP(作為被調(diào)函數(shù)的棧底),接著改變ESP值來(lái)為函數(shù)局部變量預(yù)留空間。此時(shí)被調(diào)函數(shù)幀基指針指向被調(diào)函數(shù)的棧底。以該地址為基準(zhǔn),向上(棧底方向)可獲取主調(diào)函數(shù)的返回地址、參數(shù)值,向下(棧頂方向)能獲取被調(diào)函數(shù)的局部變量值,而該地址處又存放著上一層主調(diào)函數(shù)的幀基指針值。本級(jí)調(diào)用結(jié)束后,將EBP指針值賦給ESP,使ESP再次指向被調(diào)函數(shù)棧底以釋放局部變量;再將已壓棧的主調(diào)函數(shù)幀基指針彈出到EBP,并彈出返回地址到EIP。ESP繼續(xù)上移越過(guò)參數(shù),最終回到函數(shù)調(diào)用前的狀態(tài),即恢復(fù)原來(lái)主調(diào)函數(shù)的棧幀。如此遞歸便形成函數(shù)調(diào)用棧。

? ? ?EBP指針在當(dāng)前函數(shù)運(yùn)行過(guò)程中(未調(diào)用其他函數(shù)時(shí))保持不變。在函數(shù)調(diào)用前,ESP指針指向棧頂?shù)刂?#xff0c;也是棧底地址。在函數(shù)完成現(xiàn)場(chǎng)保護(hù)之類(lèi)的初始化工作后,ESP會(huì)始終指向當(dāng)前函數(shù)棧幀的棧頂,此時(shí),若當(dāng)前函數(shù)又調(diào)用另一個(gè)函數(shù),則會(huì)將此時(shí)的EBP視為舊EBP壓棧,而與新調(diào)用函數(shù)有關(guān)的內(nèi)容會(huì)從當(dāng)前ESP所指向位置開(kāi)始?jí)簵!?/p>

? ? ?若需在函數(shù)中保存被調(diào)函數(shù)保存寄存器(如ESI、EDI),則編譯器在保存EBP值時(shí)進(jìn)行保存,或延遲保存直到局部變量空間被分配。在棧幀中并未為被調(diào)函數(shù)保存寄存器的空間指定標(biāo)準(zhǔn)的存儲(chǔ)位置。包含寄存器和臨時(shí)變量的函數(shù)調(diào)用棧布局可能如下圖所示:

? ? ?在多線程(任務(wù))環(huán)境,棧頂指針指向的存儲(chǔ)器區(qū)域就是當(dāng)前使用的堆棧。切換線程的一個(gè)重要工作,就是將棧頂指針設(shè)為當(dāng)前線程的堆棧棧頂?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所對(duì)應(yīng)的棧上整型臨時(shí)變量地址!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; }StackFrame

編譯鏈接并執(zhí)行后,輸出打印如下:

?函數(shù)棧布局示例如下圖所示。為直觀起見(jiàn),低于起始高地址0xbfc75a58的其他地址采用點(diǎn)記法,如0x.54表示0xbfc75a54,以此類(lèi)推。

? ? ?內(nèi)存地址從棧底到棧頂遞減,壓棧就是把ESP指針逐漸往地低址移動(dòng)的過(guò)程。而結(jié)構(gòu)體tStrt中的成員變量memberX地址=tStrt首地址+(memberX偏移量),即越靠近tStrt首地址的成員變量其內(nèi)存地址越小。因此,結(jié)構(gòu)體成員變量的入棧順序與其在結(jié)構(gòu)體中聲明的順序相反。

? ? ?函數(shù)調(diào)用以值傳遞時(shí),傳入的實(shí)參(locMain1~3)與被調(diào)函數(shù)內(nèi)操作的形參(para1~3)兩者存儲(chǔ)地址不同,因此被調(diào)函數(shù)無(wú)法直接修改主調(diào)函數(shù)實(shí)參值(對(duì)形參的操作相當(dāng)于修改實(shí)參的副本)。為達(dá)到修改目的,需要向被調(diào)函數(shù)傳遞實(shí)參變量的指針(即變量的地址)。

? ? ?此外,"[locMain1,2,3] = [0, 0, 3]"是因?yàn)閷?duì)四字節(jié)參數(shù)locMain2調(diào)用memset函數(shù)時(shí),會(huì)從低地址向高地址連續(xù)清零8個(gè)字節(jié),從而誤將位于高地址locMain1清零。

? ? ?注意,局部變量的布局依賴(lài)于編譯器實(shí)現(xiàn)等因素。因此,當(dāng)StackFrameContent函數(shù)中刪除打印語(yǔ)句時(shí),變量locVar3、locVar2和locVar1可能按照從高到低的順序依次存儲(chǔ)!而且,局部變量并不總在棧中,有時(shí)出于性能(速度)考慮會(huì)存放在寄存器中。數(shù)組/結(jié)構(gòu)體型的局部變量通常分配在棧內(nèi)存中。

【擴(kuò)展閱讀】函數(shù)局部變量布局方式

與函數(shù)調(diào)用約定規(guī)定參數(shù)如何傳入不同,局部變量以何種方式布局并未規(guī)定。編譯器計(jì)算函數(shù)局部變量所需要的空間總數(shù),并確定這些變量存儲(chǔ)在寄存器上還是分配在程序棧上(甚至被優(yōu)化掉)——某些處理器并沒(méi)有堆棧。局部變量的空間分配與主調(diào)函數(shù)和被調(diào)函數(shù)無(wú)關(guān),僅僅從函數(shù)源代碼上無(wú)法確定該函數(shù)的局部變量分布情況。

基于不同的編譯器版本(gcc3.4中局部變量按照定義順序依次入棧,gcc4及以上版本則不定)、優(yōu)化級(jí)別、目標(biāo)處理器架構(gòu)、棧安全性等,相鄰定義的兩個(gè)變量在內(nèi)存位置上可能相鄰,也可能不相鄰,前后關(guān)系也不固定。若要確保兩個(gè)對(duì)象在內(nèi)存上相鄰且前后關(guān)系固定,可使用結(jié)構(gòu)體或數(shù)組定義。

?

?

?

4 堆棧操作

?

? ? ?函數(shù)調(diào)用時(shí)的具體步驟如下:

? ? ?1)?主調(diào)函數(shù)將被調(diào)函數(shù)所要求的參數(shù),根據(jù)相應(yīng)的函數(shù)調(diào)用約定,保存在運(yùn)行時(shí)棧中。該操作會(huì)改變程序的棧指針。

? ? ?注:x86平臺(tái)將參數(shù)壓入調(diào)用棧中。而x86_64平臺(tái)具有16個(gè)通用64位寄存器,故調(diào)用函數(shù)時(shí)前6個(gè)參數(shù)通常由寄存器傳遞,其余參數(shù)才通過(guò)棧傳遞。

? ? ?2) 主調(diào)函數(shù)將控制權(quán)移交給被調(diào)函數(shù)(使用call指令)。函數(shù)的返回地址(待執(zhí)行的下條指令地址)保存在程序棧中(壓棧操作隱含在call指令中)。

? ? ?3) 若有必要,被調(diào)函數(shù)會(huì)設(shè)置幀基指針,并保存被調(diào)函數(shù)希望保持不變的寄存器值。

? ? ?4) 被調(diào)函數(shù)通過(guò)修改棧頂指針的值,為自己的局部變量在運(yùn)行時(shí)棧中分配內(nèi)存空間,并從幀基指針的位置處向低地址方向存放被調(diào)函數(shù)的局部變量和臨時(shí)變量。

? ? ?5) 被調(diào)函數(shù)執(zhí)行自己任務(wù),此時(shí)可能需要訪問(wèn)由主調(diào)函數(shù)傳入的參數(shù)。若被調(diào)函數(shù)返回一個(gè)值,該值通常保存在一個(gè)指定寄存器中(如EAX)。

? ? ?6) 一旦被調(diào)函數(shù)完成操作,為該函數(shù)局部變量分配的棧空間將被釋放。這通常是步驟4的逆向執(zhí)行。

? ? ?7) 恢復(fù)步驟3中保存的寄存器值,包含主調(diào)函數(shù)的幀基指針寄存器。

? ? ?8) 被調(diào)函數(shù)將控制權(quán)交還主調(diào)函數(shù)(使用ret指令)。根據(jù)使用的函數(shù)調(diào)用約定,該操作也可能從程序棧上清除先前傳入的參數(shù)。

? ? ?9) 主調(diào)函數(shù)再次獲得控制權(quán)后,可能需要將先前的參數(shù)從棧上清除。在這種情況下,對(duì)棧的修改需要將幀基指針值恢復(fù)到步驟1之前的值。

? ? ?步驟3與步驟4在函數(shù)調(diào)用之初常一同出現(xiàn),統(tǒng)稱(chēng)為函數(shù)序(prologue);步驟6到步驟8在函數(shù)調(diào)用的最后常一同出現(xiàn),統(tǒng)稱(chēng)為函數(shù)跋(epilogue)。函數(shù)序和函數(shù)跋是編譯器自動(dòng)添加的開(kāi)始和結(jié)束匯編代碼,其實(shí)現(xiàn)與CPU架構(gòu)和編譯器相關(guān)。除步驟5代表函數(shù)實(shí)體外,其它所有操作組成函數(shù)調(diào)用。

? ? ?以下介紹函數(shù)調(diào)用過(guò)程中的主要指令。

? ? ?壓棧(push):棧頂指針ESP減小4個(gè)字節(jié);以字節(jié)為單位將寄存器數(shù)據(jù)(四字節(jié),不足補(bǔ)零)壓入堆棧,從高到低按字節(jié)依次將數(shù)據(jù)存入ESP-1、ESP-2、ESP-3、ESP-4指向的地址單元。

? ? ?出棧(pop):棧頂指針ESP指向的棧中數(shù)據(jù)被取回到寄存器;棧頂指針ESP增加4個(gè)字節(jié)。

? ? ?可見(jiàn),壓棧操作將寄存器內(nèi)容存入棧內(nèi)存中(寄存器原內(nèi)容不變),棧頂?shù)刂窚p小;出棧操作從棧內(nèi)存中取回寄存器內(nèi)容(棧內(nèi)已存數(shù)據(jù)不會(huì)自動(dòng)清零),棧頂?shù)刂吩龃蟆m斨羔楨SP總是指向棧中下一個(gè)可用數(shù)據(jù)。

? ? ?調(diào)用(call):將當(dāng)前的指令指針EIP(該指針指向緊接在call指令后的下條指令)壓入堆棧,以備返回時(shí)能恢復(fù)執(zhí)行下條指令;然后設(shè)置EIP指向被調(diào)函數(shù)代碼開(kāi)始處,以跳轉(zhuǎn)到被調(diào)函數(shù)的入口地址執(zhí)行。

? ? ?離開(kāi)(leave): 恢復(fù)主調(diào)函數(shù)的棧幀以準(zhǔn)備返回。等價(jià)于指令序列movl %ebp, %esp(恢復(fù)原ESP值,指向被調(diào)函數(shù)棧幀開(kāi)始處)和popl %ebp(恢復(fù)原ebp的值,即主調(diào)函數(shù)幀基指針)。

? ? ?返回(ret):與call指令配合,用于從函數(shù)或過(guò)程返回。從棧頂彈出返回地址(之前call指令保存的下條指令地址)到EIP寄存器中,程序轉(zhuǎn)到該地址處繼續(xù)執(zhí)行(此時(shí)ESP指向進(jìn)入函數(shù)時(shí)的第一個(gè)參數(shù))。若帶立即數(shù),ESP再加立即數(shù)(丟棄一些在執(zhí)行call前入棧的參數(shù))。使用該指令前,應(yīng)使當(dāng)前棧頂指針?biāo)赶蛭恢玫膬?nèi)容正好是先前call指令保存的返回地址。

? ? ?基于以上指令,使用C調(diào)用約定的被調(diào)函數(shù)典型的函數(shù)序和函數(shù)跋實(shí)現(xiàn)如下:

? ? ?若主調(diào)函數(shù)和調(diào)函數(shù)均未使用局部變量寄存器EDI、ESI和EBX,則編譯器無(wú)須在函數(shù)序中對(duì)其壓棧,以便提高程序的執(zhí)行效率。

? ? ?參數(shù)壓棧指令因編譯器而異,如下兩種壓棧方式基本等效:

? ? ?兩種壓棧方式均遵循C調(diào)用約定,但方式二中主調(diào)函數(shù)在調(diào)用返回后并未顯式清理堆棧空間。因?yàn)樵诒徽{(diào)函數(shù)序階段,編譯器在棧頂為函數(shù)參數(shù)預(yù)先分配內(nèi)存空間(sub指令)。函數(shù)參數(shù)被復(fù)制到棧中(而非壓入棧中),并未修改棧頂指針,故調(diào)用返回時(shí)主調(diào)函數(shù)也無(wú)需修改棧頂指針。gcc3.4(或更高版本)編譯器采用該技術(shù)將函數(shù)參數(shù)傳遞至棧上,相比棧頂指針隨每次參數(shù)壓棧而多次下移,一次性設(shè)置好棧頂指針更為高效。設(shè)想連續(xù)調(diào)用多個(gè)函數(shù)時(shí),方式二僅需預(yù)先分配一次參數(shù)內(nèi)存(大小足夠容納參數(shù)尺寸和最大的函數(shù)即可),后續(xù)調(diào)用無(wú)需每次都恢復(fù)棧頂指針。注意,函數(shù)被調(diào)用時(shí),兩種方式均使棧頂指針指向函數(shù)最左邊的參數(shù)。本文不再區(qū)分兩種壓棧方式,"壓棧"或"入棧"所提之處均按相應(yīng)匯編代碼理解,若無(wú)匯編則指方式二。

? ? ?某些情況下,編譯器生成的函數(shù)調(diào)用進(jìn)入/退出指令序列并不按照以上方式進(jìn)行。例如,若C函數(shù)聲明為static(只在本編譯單元內(nèi)可見(jiàn))且函數(shù)在編譯單元內(nèi)被直接調(diào)用,未被顯示或隱式取地址(即沒(méi)有任何函數(shù)指針指向該函數(shù)),此時(shí)編譯器確信該函數(shù)不會(huì)被其它編譯單元調(diào)用,因此可隨意修改其進(jìn)/出指令序列以達(dá)到優(yōu)化目的。

? ? ?盡管使用的寄存器名字和指令在不同處理器架構(gòu)上有所不同,但創(chuàng)建棧幀的基本過(guò)程一致。

? ? ?注意,棧幀是運(yùn)行時(shí)概念,若程序不運(yùn)行,就不存在棧和棧幀。但通過(guò)分析目標(biāo)文件中建立函數(shù)棧幀的匯編代碼(尤其是函數(shù)序和函數(shù)跋過(guò)程),即使函數(shù)沒(méi)有運(yùn)行,也能了解函數(shù)的棧幀結(jié)構(gòu)。通過(guò)分析可確定分配在函數(shù)棧幀上的局部變量空間準(zhǔn)確值,函數(shù)中是否使用幀基指針,以及識(shí)別函數(shù)棧幀中對(duì)變量的所有內(nèi)存引用。

?

?

5 函數(shù)調(diào)用約定

?

? ? ?創(chuàng)建一個(gè)棧幀的最重要步驟是主調(diào)函數(shù)如何向棧中傳遞函數(shù)參數(shù)。主調(diào)函數(shù)必須精確存儲(chǔ)這些參數(shù),以便被調(diào)函數(shù)能夠訪問(wèn)到它們。函數(shù)通過(guò)選擇特定的調(diào)用約定,來(lái)表明其希望以特定方式接收參數(shù)。此外,當(dāng)被調(diào)函數(shù)完成任務(wù)后,調(diào)用約定規(guī)定先前入棧的參數(shù)由主調(diào)函數(shù)還是被調(diào)函數(shù)負(fù)責(zé)清除,以保證程序的棧頂指針完整性。

? ? ?函數(shù)調(diào)用約定通常規(guī)定如下幾方面內(nèi)容:

? ? ?1) 函數(shù)參數(shù)的傳遞順序和方式

? ? ?最常見(jiàn)的參數(shù)傳遞方式是通過(guò)堆棧傳遞。主調(diào)函數(shù)將參數(shù)壓入棧中,被調(diào)函數(shù)以相對(duì)于幀基指針的正偏移量來(lái)訪問(wèn)棧中的參數(shù)。對(duì)于有多個(gè)參數(shù)的函數(shù),調(diào)用約定需規(guī)定主調(diào)函數(shù)將參數(shù)壓棧的順序(從左至右還是從右至左)。某些調(diào)用約定允許使用寄存器傳參以提高性能。

? ? ?2) 棧的維護(hù)方式

? ? ?主調(diào)函數(shù)將參數(shù)壓棧后調(diào)用被調(diào)函數(shù)體,返回時(shí)需將被壓棧的參數(shù)全部彈出,以便將?;謴?fù)到調(diào)用前的狀態(tài)。該清棧過(guò)程可由主調(diào)函數(shù)負(fù)責(zé)完成,也可由被調(diào)函數(shù)負(fù)責(zé)完成。

? ? ?3) 名字修飾(Name-mangling)策略

? ? ?又稱(chēng)函數(shù)名修飾(Decorated Name)規(guī)則。編譯器在鏈接時(shí)為區(qū)分不同函數(shù),對(duì)函數(shù)名作不同修飾。

? ? ?若函數(shù)之間的調(diào)用約定不匹配,可能會(huì)產(chǎn)生堆棧異?;蜴溄渝e(cuò)誤等問(wèn)題。因此,為了保證程序能正確執(zhí)行,所有的函數(shù)調(diào)用均應(yīng)遵守一致的調(diào)用約定。

?

5.1 常見(jiàn)調(diào)用約定

? ? ?下面分別介紹常見(jiàn)的幾種函數(shù)調(diào)用約定。

? ? ?1. cdecl調(diào)用約定

? ? ?又稱(chēng)C調(diào)用約定,是C/C++編譯器默認(rèn)的函數(shù)調(diào)用約定。所有非C++成員函數(shù)和未使用stdcall或fastcall聲明的函數(shù)都默認(rèn)是cdecl方式。函數(shù)參數(shù)按照從右到左的順序入棧,函數(shù)調(diào)用者負(fù)責(zé)清除棧中的參數(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í)即使實(shí)參和形參數(shù)目不符也不會(huì)導(dǎo)致堆棧錯(cuò)誤。對(duì)于C函數(shù),cdecl方式的名字修飾約定是在函數(shù)名前添加一個(gè)下劃線;對(duì)于C++函數(shù),除非特別使用extern "C",C++函數(shù)使用不同的名字修飾方式。

【擴(kuò)展閱讀】可變參數(shù)函數(shù)支持條件

若要支持可變參數(shù)的函數(shù),則參數(shù)應(yīng)自右向左進(jìn)棧,并且由主調(diào)函數(shù)負(fù)責(zé)清除棧中的參數(shù)(參數(shù)出棧)。

首先,參數(shù)按照從右向左的順序壓棧,則參數(shù)列表最左邊(第一個(gè))的參數(shù)最接近棧頂位置。所有參數(shù)距離幀基指針的偏移量都是常數(shù),而不必關(guān)心已入棧的參數(shù)數(shù)目。只要不定的參數(shù)的數(shù)目能根據(jù)第一個(gè)已明確的參數(shù)確定,就可使用不定參數(shù)。例如printf函數(shù),第一個(gè)參數(shù)即格式化字符串可作為后繼參數(shù)指示符。通過(guò)它們就可得到后續(xù)參數(shù)的類(lèi)型和個(gè)數(shù),進(jìn)而知道所有參數(shù)的尺寸。當(dāng)傳遞的參數(shù)過(guò)多時(shí),以幀基指針為基準(zhǔn),獲取適當(dāng)數(shù)目的參數(shù),其他忽略即可。若函數(shù)參數(shù)自左向右進(jìn)棧,則第一個(gè)參數(shù)距離棧幀指針的偏移量與已入棧的參數(shù)數(shù)目有關(guān),需要計(jì)算所有參數(shù)占用的空間后才能精確定位。當(dāng)實(shí)際傳入的參數(shù)數(shù)目與函數(shù)期望接受的參數(shù)數(shù)目不同時(shí),偏移量計(jì)算會(huì)出錯(cuò)!

其次,調(diào)用函數(shù)將參數(shù)壓棧,只有它才知道棧中的參數(shù)數(shù)目和尺寸,因此調(diào)用函數(shù)可安全地清棧。而被調(diào)函數(shù)永遠(yuǎn)也不能事先知道將要傳入函數(shù)的參數(shù)信息,難以對(duì)棧頂指針進(jìn)行調(diào)整。

C++為兼容C,仍然支持函數(shù)帶有可變的參數(shù)。但在C++中更好的選擇常常是函數(shù)多態(tài)。

? ? ?2. stdcall調(diào)用約定(微軟命名)

? ? ?Pascal程序缺省調(diào)用方式,WinAPI也多采用該調(diào)用約定。stdcall調(diào)用約定主調(diào)函數(shù)參數(shù)從右向左入棧,除指針或引用類(lèi)型參數(shù)外所有參數(shù)采用傳值方式傳遞,由被調(diào)函數(shù)負(fù)責(zé)清除棧中的參數(shù),返回值在EAX中。stdcall調(diào)用約定僅適用于參數(shù)個(gè)數(shù)固定的函數(shù),因?yàn)楸徽{(diào)函數(shù)清棧時(shí)無(wú)法精確獲知棧上有多少函數(shù)參數(shù);而且如果調(diào)用時(shí)實(shí)參和形參數(shù)目不符會(huì)導(dǎo)致堆棧錯(cuò)誤。對(duì)于C函數(shù),stdcall名稱(chēng)修飾方式是在函數(shù)名字前添加下劃線,在函數(shù)名字后添加@和函數(shù)參數(shù)的大小,如_functionname@number。

? ? ?3. fastcall調(diào)用約定

? ? ?stdcall調(diào)用約定的變形,通常使用ECX和EDX寄存器傳遞前兩個(gè)DWORD(四字節(jié)雙字)類(lèi)型或更少字節(jié)的函數(shù)參數(shù),其余參數(shù)按照從右向左的順序入棧,被調(diào)函數(shù)在返回前負(fù)責(zé)清除棧中的參數(shù),返回值在 EAX 中。因?yàn)椴⒉皇撬械膮?shù)都有壓棧操作,所以比stdcall和cdecl快些。編譯器使用兩個(gè)@修飾函數(shù)名字,后跟十進(jìn)制數(shù)表示的函數(shù)參數(shù)列表大小(字節(jié)數(shù)),如@function_name@number。需注意fastcall函數(shù)調(diào)用約定在不同編譯器上可能有不同的實(shí)現(xiàn),比如16位編譯器和32位編譯器。另外,在使用內(nèi)嵌匯編代碼時(shí),還應(yīng)注意不能和編譯器使用的寄存器有沖突。

? ? ?4. thiscall調(diào)用約定

? ? ?C++類(lèi)中的非靜態(tài)函數(shù)必須接收一個(gè)指向主調(diào)對(duì)象的類(lèi)指針(this指針),并可能較頻繁的使用該指針。主調(diào)函數(shù)的對(duì)象地址必須由調(diào)用者提供,并在調(diào)用對(duì)象非靜態(tài)成員函數(shù)時(shí)將對(duì)象指針以參數(shù)形式傳遞給被調(diào)函數(shù)。編譯器默認(rèn)使用thiscall調(diào)用約定以高效傳遞和存儲(chǔ)C++類(lèi)的非靜態(tài)成員函數(shù)的this指針參數(shù)。

? ? ?thiscall調(diào)用約定函數(shù)參數(shù)按照從右向左的順序入棧。若參數(shù)數(shù)目固定,則類(lèi)實(shí)例的this指針通過(guò)ECX寄存器傳遞給被調(diào)函數(shù),被調(diào)函數(shù)自身清理堆棧;若參數(shù)數(shù)目不定,則this指針在所有參數(shù)入棧后再入棧,主調(diào)函數(shù)清理堆棧。thiscall不是C++關(guān)鍵字,故不能使用thiscall聲明函數(shù),它只能由編譯器使用。

? ? ?注意,該調(diào)用約定特點(diǎn)隨編譯器不同而不同,g++中thiscall與cdecl基本相同,只是隱式地將this指針當(dāng)作非靜態(tài)成員函數(shù)的第1個(gè)參數(shù),主調(diào)函數(shù)在調(diào)用返回后負(fù)責(zé)清理?xiàng)I蠀?shù);而在VC中,this指針存放在%ecx寄存器中,參數(shù)從右至左壓棧,非靜態(tài)成員函數(shù)負(fù)責(zé)清理?xiàng)I蠀?shù)。

? ? ?5. naked call調(diào)用約定

? ? ?對(duì)于使用naked call方式聲明的函數(shù),編譯器不產(chǎn)生保存(prologue)和恢復(fù)(epilogue)寄存器的代碼,且不能用return返回返回值(只能用內(nèi)嵌匯編返回結(jié)果),故稱(chēng)naked call。該調(diào)用約定用于一些特殊場(chǎng)合,如聲明處于非C/C++上下文中的函數(shù),并由程序員自行編寫(xiě)初始化和清棧的內(nèi)嵌匯編指令。注意,naked?call并非類(lèi)型修飾符,故該調(diào)用約定必須與__declspec同時(shí)使用,如VC下定義求和函數(shù):

? ? ?代碼示例如下(Windows采用Intel匯編語(yǔ)法,注釋符為;):

__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)上可能沒(méi)有。?

? ? ?6. pascal調(diào)用約定

? ? ?Pascal語(yǔ)言調(diào)用約定,參數(shù)按照從左至右的順序入棧。Pascal語(yǔ)言只支持固定參數(shù)的函數(shù),參數(shù)的類(lèi)型和數(shù)量完全可知,故由被調(diào)函數(shù)自身清理堆棧。pascal調(diào)用約定輸出的函數(shù)名稱(chēng)無(wú)任何修飾且全部大寫(xiě)。

? ? ?Win3.X(16位)時(shí)支持真正的pascal調(diào)用約定;而Win9.X(32位)以后pascal約定由stdcall約定代替(以C約定壓棧以Pascal約定清棧)。

? ? ?上述調(diào)用約定的主要特點(diǎn)如下表所示:

?? ? Windows下可直接在函數(shù)聲明前添加關(guān)鍵字__stdcall、__cdecl或__fastcall等標(biāo)識(shí)確定函數(shù)的調(diào)用方式,如int __stdcall func()。Linux下可借用函數(shù)attribute 機(jī)制,如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約定時(shí),其匯編代碼比較如下表所示:

?

5.2 調(diào)用約定影響

? ? ?當(dāng)函數(shù)導(dǎo)出被其他程序員所使用(如庫(kù)函數(shù))時(shí),該函數(shù)應(yīng)遵循主要的調(diào)用約定,以便于程序員使用。若函數(shù)僅供內(nèi)部使用,則其調(diào)用約定可只被使用該函數(shù)的程序所了解。

? ? ?在多語(yǔ)言混合編程(包括A語(yǔ)言中使用B語(yǔ)言開(kāi)發(fā)的第三方庫(kù))時(shí),若函數(shù)的原型聲明和函數(shù)體定義不一致或調(diào)用函數(shù)時(shí)聲明了不同的函數(shù)約定,將可能導(dǎo)致嚴(yán)重問(wèn)題(如堆棧被破壞)。

? ? ?以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ù)時(shí)也應(yīng)聲明為stdcall約定:

//參數(shù)類(lèi)型應(yīng)與DLL中的函數(shù)或過(guò)程參數(shù)類(lèi)型一致,且引用時(shí)使用stdcall參數(shù) function add(a: Integer; b: Integer): Integer; stdcall; external 'a.dll'; //指定被調(diào)DLL文件的路徑和名稱(chēng)

? ? ?不同編譯器產(chǎn)生棧幀的方式不盡相同,主調(diào)函數(shù)不一定能正常完成清棧工作;而被調(diào)函數(shù)必然能自己完成正常清棧,因此,在跨(開(kāi)發(fā))平臺(tái)調(diào)用中,通常使用stdcall調(diào)用約定(不少WinApi均采用該約定)。

? ? ?此外,主調(diào)函數(shù)和被調(diào)函數(shù)所在模塊采用相同的調(diào)用約定,但分別使用C++和C語(yǔ)法編譯時(shí),會(huì)出現(xiàn)鏈接錯(cuò)誤(報(bào)告被調(diào)函數(shù)未定義)。這是因?yàn)閮煞N語(yǔ)言的函數(shù)名字修飾規(guī)則不同,解決方式是使用extern "C"告知主調(diào)函數(shù)所在模塊:被調(diào)函數(shù)是C語(yǔ)言編譯的。采用C語(yǔ)言編譯的庫(kù)應(yīng)考慮到使用該庫(kù)的程序可能是C++程序(使用C++編譯器),通常應(yīng)這樣聲明頭文件:

#ifdef _cplusplusextern "C" { #endiftype Func(type para); #ifdef _cplusplus} #endif

?? ?這樣C++編譯器就會(huì)按照C語(yǔ)言修飾策略鏈接Func函數(shù)名,而不會(huì)出現(xiàn)找不到函數(shù)的鏈接錯(cuò)誤。

?

5.3 x86函數(shù)參數(shù)傳遞方法

? ? ?x86處理器ABI規(guī)范中規(guī)定,所有傳遞給被調(diào)函數(shù)的參數(shù)都通過(guò)堆棧來(lái)完成,其壓棧順序是以函數(shù)參數(shù)從右到左的順序。當(dāng)向被調(diào)函數(shù)傳遞參數(shù)時(shí),所有參數(shù)最后形成一個(gè)數(shù)組。由于采用從右到左的壓棧順序,數(shù)組中參數(shù)的順序(下標(biāo)0~N-1)與函數(shù)參數(shù)聲明順序(Para1~N)一致。因此,在函數(shù)中若知道第一個(gè)參數(shù)地址和各參數(shù)占用字節(jié)數(shù),就可通過(guò)訪問(wèn)數(shù)組的方式去訪問(wèn)每個(gè)參數(shù)。

5.3.1 整型和指針參數(shù)的傳遞

? ? ?整型參數(shù)與指針參數(shù)的傳遞方式相同,因?yàn)樵?2位x86處理器上整型與指針大小相同(均為四字節(jié))。下表給出這兩種類(lèi)型的參數(shù)在棧幀中的位置關(guān)系。注意,該表基于tail函數(shù)的棧幀。

?

5.3.2 浮點(diǎn)參數(shù)的傳遞

? ? ?浮點(diǎn)參數(shù)的傳遞與整型類(lèi)似,區(qū)別在于參數(shù)大小。x86處理器中浮點(diǎn)類(lèi)型占8個(gè)字節(jié),因此在棧中也需要占用8個(gè)字節(jié)。下表給出浮點(diǎn)參數(shù)在棧幀中的位置關(guān)系。圖中,調(diào)用tail函數(shù)的第一個(gè)和第三個(gè)參數(shù)均為浮點(diǎn)類(lèi)型,因此需各占用8個(gè)字節(jié),三個(gè)參數(shù)共占用20個(gè)字節(jié)。表中word類(lèi)型的大小是4字節(jié)。

?

5.3.3 結(jié)構(gòu)體和聯(lián)合體參數(shù)的傳遞

? ? ?結(jié)構(gòu)體和聯(lián)合體參數(shù)的傳遞與整型、浮點(diǎn)參數(shù)類(lèi)似,只是其占用字節(jié)大小視數(shù)據(jù)結(jié)構(gòu)的定義不同而異。x86處理器上棧寬是4字節(jié),故結(jié)構(gòu)體在棧上所占用的字節(jié)數(shù)為4的倍數(shù)。編譯器會(huì)對(duì)結(jié)構(gòu)體進(jìn)行適當(dāng)?shù)奶畛湟允沟媒Y(jié)構(gòu)體大小滿足4字節(jié)對(duì)齊的要求。

? ? ?對(duì)于一些RISC處理器(如PowerPC),其參數(shù)傳遞并不是全部通過(guò)棧來(lái)實(shí)現(xiàn)。PowerPC處理器寄存器中,R3~R10共8個(gè)寄存器用于傳遞整型或指針參數(shù),F1~F8共8個(gè)寄存器用于傳遞浮點(diǎn)參數(shù)。當(dāng)所需傳遞的參數(shù)少于8個(gè)時(shí),不需要用到棧。結(jié)構(gòu)體和long double參數(shù)的傳遞通過(guò)指針來(lái)完成,這與x86處理器完全不同。PowerPC的ABI規(guī)范中規(guī)定,結(jié)構(gòu)體的傳遞采用指針?lè)绞?#xff0c;而不是像x86處理器那樣將結(jié)構(gòu)從一個(gè)函數(shù)棧幀中拷貝到另一個(gè)函數(shù)棧幀中,顯然x86處理器的方式更低效??梢?jiàn),PowerPC程序中,函數(shù)參數(shù)采用指向結(jié)構(gòu)體的指針(而非結(jié)構(gòu)體)并不能提高效率,不過(guò)通常這是良好的編程習(xí)慣。

?

5.4 x86函數(shù)返回值傳遞方法

? ? ?函數(shù)返回值可通過(guò)寄存器傳遞。當(dāng)被調(diào)用函數(shù)需要返回結(jié)果給調(diào)用函數(shù)時(shí)

? ? ?1) 若返回值不超過(guò)4字節(jié)(如int、short、char、指針等類(lèi)型),通常將其保存在EAX寄存器中,調(diào)用方通過(guò)讀取EAX獲取返回值。

? ? ?2) 若返回值大于4字節(jié)而小于8字節(jié)(如long long或_int64類(lèi)型),則通過(guò)EAX+EDX寄存器聯(lián)合返回,其中EDX保存返回值高4字節(jié),EAX保存返回值低4字節(jié)。

? ? ?3) 若返回值為浮點(diǎn)類(lèi)型(如float和double),則通過(guò)專(zhuān)用的協(xié)處理器浮點(diǎn)數(shù)寄存器棧的棧頂返回。

? ? ?4) 若返回值為結(jié)構(gòu)體或聯(lián)合體,則主調(diào)函數(shù)向被調(diào)函數(shù)傳遞一個(gè)額外參數(shù),該參數(shù)指向?qū)⒁4娣祷刂档牡刂?。即函?shù)調(diào)用foo(p1, p2)被轉(zhuǎn)化為foo(&p0, p1, p2),以引用型參數(shù)形式傳回返回值。具體步驟可能為:a.主調(diào)函數(shù)將顯式的實(shí)參逆序入棧;b.將接收返回值的結(jié)構(gòu)體變量地址作為隱藏參數(shù)入棧(若未定義該接收變量,則在棧上額外開(kāi)辟空間作為接收返回值的臨時(shí)變量);c. 被調(diào)函數(shù)將待返回?cái)?shù)據(jù)拷貝到隱藏參數(shù)所指向的內(nèi)存地址,并將該地址存入%eax寄存器。因此,在被調(diào)函數(shù)中完成返回值的賦值工作。

? ? ?注意,函數(shù)如何傳遞結(jié)構(gòu)體或聯(lián)合體返回值依賴(lài)于具體實(shí)現(xiàn)。不同編譯器、平臺(tái)、調(diào)用約定甚至編譯參數(shù)下可能采用不同的實(shí)現(xiàn)方法。如VC6編譯器對(duì)于不超過(guò)8字節(jié)的小結(jié)構(gòu)體,會(huì)通過(guò)EAX+EDX寄存器返回。而對(duì)于超過(guò)8字節(jié)的大結(jié)構(gòu)體,主調(diào)函數(shù)在棧上分配用于接收返回值的臨時(shí)結(jié)構(gòu)體,并將地址通過(guò)棧傳遞給被調(diào)函數(shù);被調(diào)函數(shù)根據(jù)返回值地址設(shè)置返回值(拷貝操作);調(diào)用返回后主調(diào)函數(shù)根據(jù)需要,再將返回值賦值給需要的臨時(shí)變量(二次拷貝)。實(shí)際使用中為提高效率,通常將結(jié)構(gòu)體指針作為實(shí)參傳遞給被調(diào)函數(shù)以接收返回值。

? ? ?5) 不要返回指向棧內(nèi)存的指針,如返回被調(diào)函數(shù)內(nèi)局部變量地址(包括局部數(shù)組名)。因?yàn)楹瘮?shù)返回后,其棧幀空間被“釋放”,原棧幀內(nèi)分配的局部變量空間的內(nèi)容是不穩(wěn)定和不被保證的。

? ? ?函數(shù)返回值通過(guò)寄存器傳遞,無(wú)需空間分配等操作,故返回值的代價(jià)很低。基于此原因,C89規(guī)范中約定,不寫(xiě)明返回值類(lèi)型的函數(shù),返回值類(lèi)型默認(rèn)為int。但這會(huì)帶來(lái)類(lèi)型安全隱患,如函數(shù)定義時(shí)返回值為浮點(diǎn)數(shù),而函數(shù)未聲明或聲明時(shí)未指明返回值類(lèi)型,則調(diào)用時(shí)默認(rèn)從寄存器EAX(而不是浮點(diǎn)數(shù)寄存器)中獲取返回值,導(dǎo)致錯(cuò)誤!因此在C++中,不寫(xiě)明返回值類(lèi)型的函數(shù)返回值類(lèi)型為void,表示不返回值。

【擴(kuò)展閱讀】GCC返回結(jié)構(gòu)體和聯(lián)合體

通常GCC被配置為使用與目標(biāo)系統(tǒng)一致的函數(shù)調(diào)用約定。這通過(guò)機(jī)器描述宏來(lái)實(shí)現(xiàn)。但是,在一些目標(biāo)機(jī)上采用不同方式返回結(jié)構(gòu)體和聯(lián)合體的值。因此,使用PCC編譯的返回這些類(lèi)型的函數(shù)不能被使用GCC編譯的代碼調(diào)用,反之亦然。但這并未造成麻煩,因?yàn)楹苌儆蠻nix庫(kù)函數(shù)返回結(jié)構(gòu)體或聯(lián)合體。

GCC代碼使用存放int或double類(lèi)型返回值的寄存器來(lái)返回1、2、4或8個(gè)字節(jié)的結(jié)構(gòu)體和聯(lián)合體(GCC通常還將此類(lèi)變量分配在寄存器中)。其它大小的結(jié)構(gòu)體和聯(lián)合體在返回時(shí),將其存放在一個(gè)由調(diào)用者傳遞的地址中(通常在寄存器中)。

相比之下,PCC在大多目標(biāo)機(jī)上返回任何大小的結(jié)構(gòu)體和聯(lián)合體時(shí),都將數(shù)據(jù)復(fù)制到一個(gè)靜態(tài)存儲(chǔ)區(qū)域,再將該地址當(dāng)作指針值返回。調(diào)用者必須將數(shù)據(jù)從那個(gè)內(nèi)存區(qū)域復(fù)制到需要的地方。這比GCC使用的方法要慢,而且不可重入。

在一些目標(biāo)機(jī)上(如RISC機(jī)器和80386),標(biāo)準(zhǔn)的系統(tǒng)約定是將返回值的地址傳給子程序。在這些機(jī)器上,當(dāng)使用這種約定方法時(shí),GCC被配置為與標(biāo)準(zhǔn)編譯器兼容。這可能會(huì)對(duì)于1,2,4或8字節(jié)的結(jié)構(gòu)體不兼容。

GCC使用系統(tǒng)的標(biāo)準(zhǔn)約定來(lái)傳遞參數(shù)。在一些機(jī)器上,前幾個(gè)參數(shù)通過(guò)寄存器傳遞;在另一些機(jī)器上,所有的參數(shù)都通過(guò)棧傳遞。原本可在所有機(jī)器上都使用寄存器來(lái)傳遞參數(shù),而且此法還可能顯著提高性能。但這樣就與使用標(biāo)準(zhǔn)約定的代碼完全不兼容。所以這種改變只在將GCC作為系統(tǒng)唯一的C編譯器時(shí)才實(shí)用。當(dāng)擁有一套完整的GNU 系統(tǒng),能夠用GCC來(lái)編譯庫(kù)時(shí),可在特定機(jī)器上實(shí)現(xiàn)寄存器參數(shù)傳遞。

在一些機(jī)器上(特別是SPARC),一些類(lèi)型的參數(shù)通過(guò)“隱匿引用”(invisible reference)來(lái)傳遞。這意味著值存儲(chǔ)在內(nèi)存中,將值的內(nèi)存地址傳給子程序。

?

?

?

創(chuàng)作挑戰(zhàn)賽新人創(chuàng)作獎(jiǎng)勵(lì)來(lái)咯,堅(jiān)持創(chuàng)作打卡瓜分現(xiàn)金大獎(jiǎng)

總結(jié)

以上是生活随笔為你收集整理的C 语言 函数调用栈的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。

如果覺(jué)得生活随笔網(wǎng)站內(nèi)容還不錯(cuò),歡迎將生活随笔推薦給好友。