原文鏈接 https://azeria-labs.com/functions-and-the-stack-part-7/
在這部分我們將研究一篇獨(dú)特的內(nèi)存區(qū)域叫做棧,講解棧的目的以及相關(guān)操作。除此之外族扰,我們還會(huì)研究ARM架構(gòu)中函數(shù)的調(diào)用約定耿戚。
棧
一般來(lái)說(shuō)瞳收,棧是一片在程序/進(jìn)程中的內(nèi)存區(qū)域涮阔。這部分內(nèi)存是在進(jìn)程創(chuàng)建的時(shí)候被創(chuàng)建的。我們利用棧來(lái)存儲(chǔ)一些臨時(shí)數(shù)據(jù)比如說(shuō)函數(shù)的局部變量秩贰,環(huán)境變量等霹俺。在之前的文章中,我們講了操作棧的相關(guān)指令PUSH和POP毒费。
在我們開(kāi)始之前丙唧,還是了解一下棧的相關(guān)知識(shí)以及其實(shí)現(xiàn)方式吧。首先談?wù)剹5脑鲩L(zhǎng)觅玻,即當(dāng)我們把32位的數(shù)據(jù)放到棧上時(shí)候它的變化想际。棧可以向上增長(zhǎng)(當(dāng)棧的實(shí)現(xiàn)是負(fù)向增長(zhǎng)時(shí))溪厘,或者向下增長(zhǎng)(當(dāng)棧的實(shí)現(xiàn)是正向增長(zhǎng)時(shí))胡本。具體的關(guān)于下一個(gè)32位的數(shù)據(jù)被放到哪里是由棧指針來(lái)決定的,更精確的說(shuō)是由SP寄存器決定畸悬。不過(guò)這里面所指向的位置打瘪,可能是當(dāng)前(也就是上一次)存儲(chǔ)的數(shù)據(jù),也可能是下一次存儲(chǔ)時(shí)的位置傻昙。如果SP當(dāng)前指向上一次存放的數(shù)據(jù)在棧中的位置(滿棧實(shí)現(xiàn)),SP將會(huì)遞減(降序棧)或者遞增(升序棧)彩扔,然后再對(duì)指向的內(nèi)容進(jìn)行操作妆档。而如果SP指向的是下一次要操作的數(shù)據(jù)的空閑位置(空棧實(shí)現(xiàn)),數(shù)據(jù)會(huì)先被存放虫碉,而后SP會(huì)被遞減(降序棧)或遞增(升序棧)贾惦。
不同的棧實(shí)現(xiàn),可以用不同情形下的多次存取指令來(lái)表示(這里很繞...):
棧類(lèi)型 | 壓棧(存儲(chǔ)) | 彈棧(加載) |
---|---|---|
滿棧降序(FD,Full descending) | STMFD(等價(jià)于STMDB,操作之前遞減) | LDMFD(等價(jià)于LDM,操作之后遞加) |
滿棧增序(FA,Full ascending) | STMFA(等價(jià)于STMIB,操作之前遞加) | LDMFA(等價(jià)于LDMDA,操作之后遞減) |
空棧降序(ED,Empty descending) | STMED(等價(jià)于STMDA,操作之后遞減) | LDMED(等價(jià)于LDMIB,操作之前遞加) |
空棧增序(EA,Empty ascending) | STMEA(等價(jià)于STM,操作之后遞加) | LDMEA(等價(jià)于LDMDB,操作之前遞減) |
我們的例子中敦捧,使用的是滿棧降序的棧實(shí)現(xiàn)须板。讓我們看一個(gè)棧相關(guān)的例子。
/* azeria@labs:~$ as stack.s -o stack.o && gcc stack.o -o stack && gdb stack */
.global main
main:
mov r0, #2 /* 設(shè)置R0 */
push {r0} /* 將R0存在棧上 */
mov r0, #3 /* 修改R0 */
pop {r0} /* 恢復(fù)R0為初始值 */
bx lr /* 程序結(jié)束 */
在一開(kāi)始兢卵,棧指針指向地址0xbefff6f8,代表著上一次入棧數(shù)據(jù)的位置习瑰。可以看到當(dāng)前位置存儲(chǔ)了一些值秽荤。
gef> x/1x $sp
0xbefff6f8: 0xb6fc7000
在執(zhí)行完第一條指令MOV后甜奄,棧沒(méi)有改變。在只執(zhí)行完下一條PUSH指令后窃款,首先SP的值會(huì)被減4字節(jié)课兄。之后存儲(chǔ)在R0中的值會(huì)被存放到SP指向的位置中。現(xiàn)在我們?cè)诳纯碨P指向的位置以及其中的值晨继。
gef> x/x $sp
0xbefff6f4: 0x00000002
之后的指令將R0的值修改為3烟阐。然后我們執(zhí)行POP指令將SP中的值存放到R0中,并且將SP的值加4,指向當(dāng)前棧頂存放數(shù)據(jù)的位置蜒茄。z最終R0的值是2唉擂。
gef> info registers r0
r0 0x2 2
(下面的動(dòng)圖展示了低地址在頂部的棧的變化情況)
棧被用來(lái)存儲(chǔ)局部變量,之前的寄存器狀態(tài)扩淀。為了統(tǒng)一管理楔敌,函數(shù)使用了棧幀這個(gè)概念,棧幀是在棧內(nèi)用于存儲(chǔ)函數(shù)相關(guān)數(shù)據(jù)的特定區(qū)域驻谆。棧幀在函數(shù)開(kāi)始時(shí)被創(chuàng)建卵凑。棧幀指針(FP)指向棧幀的底部元素,棧幀指針確定后胜臊,會(huì)在棧上申請(qǐng)棧幀所屬的緩沖區(qū)勺卢。棧幀(從它的底部算起)一般包含著返回地址(之前說(shuō)的LR),上一層函數(shù)的棧幀指針象对,以及任何需要被保存的寄存器黑忱,函數(shù)參數(shù)(當(dāng)函數(shù)需要4個(gè)以上參數(shù)時(shí)),局部變量等勒魔。雖然棧幀包含著很多數(shù)據(jù)甫煞,但是這其中不少類(lèi)型我們之前已經(jīng)了解過(guò)了。最后冠绢,棧幀在函數(shù)結(jié)束時(shí)被銷(xiāo)毀抚吠。
下圖是關(guān)于棧幀的在棧中的位置的抽象描述(默認(rèn)棧,滿棧降序):
來(lái)一個(gè)例子來(lái)更具體的了解下棧幀吧:
/* azeria@labs:~$ gcc func.c -o func && gdb func */
int main()
{
int res = 0;
int a = 1;
int b = 2;
res = max(a, b);
return res;
}
int max(int a,int b)
{
do_nothing();
if(a<b)
{
return b;
}
else
{
return a;
}
}
int do_nothing()
{
return 0;
}
在下面的截圖中我們可以看到GDB中棧幀的相關(guān)信息:
可以看到上面的圖片中我們即將離開(kāi)函數(shù)max(最下面的反匯編中可以看到)弟胀。在此時(shí)楷力,F(xiàn)P(R11)寄存器指向的0xbefff254就是當(dāng)前棧幀的底部。這個(gè)地址對(duì)應(yīng)的棧上(綠色地址區(qū)域)位置存儲(chǔ)著0x00010418這個(gè)返回地址(LR)孵户。再往上看4字節(jié)是0xbefff26c萧朝。可以看到這個(gè)值是上層函數(shù)的棧幀指針夏哭。在0xbefff24c和0xbefff248的0x1和0x2是函數(shù)max執(zhí)行時(shí)產(chǎn)生的局部變量检柬。所以棧幀包含著我們之前說(shuō)過(guò)的LR,F(xiàn)P以及兩個(gè)局部變量竖配。
函數(shù)
在開(kāi)始學(xué)習(xí)ARM下的函數(shù)前厕吉,我們需要先明白一個(gè)函數(shù)的結(jié)構(gòu):
- 序言準(zhǔn)備(Prologue)
- 函數(shù)體
- 結(jié)束收尾(Epilogue)
序言的目的是為了保存之前程序的執(zhí)行狀態(tài)(通過(guò)存儲(chǔ)LR以及R11到棧上)以及設(shè)定棧以及局部函數(shù)變量。這些的步驟的實(shí)現(xiàn)可能根據(jù)編譯器的不同有差異械念。通常來(lái)說(shuō)是用PUSH/ADD/SUB這些指令头朱。舉個(gè)例子:
push {r11, lr} /* 保存R11與LR */
add r11, sp, #4 /* 設(shè)置棧幀底部,PUSH兩個(gè)寄存器,SP加4后指向棧幀底部元素 */
sub sp, sp, #16 /* 在棧上申請(qǐng)相應(yīng)空間 */
函數(shù)體部分就是函數(shù)本身要完成的任務(wù)了。這部分包括了函數(shù)自身的指令龄减,或者跳轉(zhuǎn)到其它函數(shù)等项钮。下面這個(gè)是函數(shù)體的例子。
mov r0, #1 /* 設(shè)置局部變量(a=1),同時(shí)也是為函數(shù)max準(zhǔn)備參數(shù)a */
mov r1, #2 /* 設(shè)置局部變量(b=2),同時(shí)也是為函數(shù)max準(zhǔn)備參數(shù)b */
bl max /* 分支跳轉(zhuǎn)調(diào)用函數(shù)max */
上面的代碼也展示了調(diào)用函數(shù)前需要如何準(zhǔn)備局部變量,以為函數(shù)調(diào)用設(shè)定參數(shù)烁巫。一般情況下署隘,前四個(gè)參數(shù)通過(guò)R0-R3來(lái)傳遞,而多出來(lái)的參數(shù)則需要通過(guò)棧來(lái)傳遞了亚隙。函數(shù)調(diào)用結(jié)束后磁餐,返回值存放在R0寄存器中。所以不管max函數(shù)如何運(yùn)作阿弃,我們都可以通過(guò)R0來(lái)得知返回值诊霹。而且當(dāng)返回值位64位值時(shí),使用的是R0與R1寄存器一同存儲(chǔ)64位的值渣淳。
函數(shù)的最后一部分即結(jié)束收尾脾还,這一部分主要是用來(lái)恢復(fù)程序寄存器以及回到函數(shù)調(diào)用發(fā)生之前的狀態(tài)。我們需要先恢復(fù)SP棧指針入愧,這個(gè)可以通過(guò)之前保存的棧幀指針寄存器外加一些加減操作做到(保證回到FP,LR的出棧位置)鄙漏。而當(dāng)我們重新調(diào)整了棧指針后,我們就可以通過(guò)出棧操作恢復(fù)之前保存的寄存器的值棺蛛≌觯基于函數(shù)類(lèi)型的不同,POP指令有可能是結(jié)束收尾的最后一條指令旁赊。然而桦踊,在恢復(fù)后我們可能還需要通過(guò)BX指令離開(kāi)函數(shù)。一個(gè)收尾的樣例代碼是這樣的彤恶。
sub sp, r11, #4 /* 收尾操作開(kāi)始,調(diào)整棧指針鳄橘,有兩個(gè)寄存器要POP声离,所以從棧幀底部元素再減4 */
pop {r11, pc} /* 收尾操作結(jié)束√绷恢復(fù)之前函數(shù)的棧幀指針术徊,以及通過(guò)之前保存的LR來(lái)恢復(fù)PC。 */
總結(jié)一下:
- 序言設(shè)定函數(shù)環(huán)境
- 函數(shù)體實(shí)現(xiàn)函數(shù)邏輯功能鲸湃,將結(jié)果存到R0
- 收尾恢復(fù)程序狀態(tài)赠涮,回到調(diào)用發(fā)生的地方。
關(guān)于函數(shù)暗挑,有一個(gè)關(guān)鍵點(diǎn)我們要知道笋除,函數(shù)的類(lèi)型分為葉函數(shù)以及非葉函數(shù)。葉函數(shù)是指函數(shù)中沒(méi)有分支跳轉(zhuǎn)到其他函數(shù)指令的函數(shù)炸裆。非葉函數(shù)指包含有跳轉(zhuǎn)到其他函數(shù)的分支跳轉(zhuǎn)指令的函數(shù)垃它。這兩種函數(shù)的實(shí)現(xiàn)都很類(lèi)似,當(dāng)然也有一些小不同。這里我們舉個(gè)例子來(lái)分析一下:
/* azeria@labs:~$ as func.s -o func.o && gcc func.o -o func && gdb func */
.global main
main:
push {r11, lr} /* Start of the prologue. Saving Frame Pointer and LR onto the stack */
add r11, sp, #4 /* Setting up the bottom of the stack frame */
sub sp, sp, #16 /* End of the prologue. Allocating some buffer on the stack */
mov r0, #1 /* setting up local variables (a=1). This also serves as setting up the first parameter for the max function */
mov r1, #2 /* setting up local variables (b=2). This also serves as setting up the second parameter for the max function */
bl max /* Calling/branching to function max */
sub sp, r11, #4 /* Start of the epilogue. Readjusting the Stack Pointer */
pop {r11, pc} /* End of the epilogue. Restoring Frame pointer from the stack, jumping to previously saved LR via direct load into PC */
max:
push {r11} /* Start of the prologue. Saving Frame Pointer onto the stack */
add r11, sp, #0 /* 設(shè)置棧幀底部,PUSH一個(gè)寄存器,SP加0后指向棧幀底部元素 */
sub sp, sp, #12 /* End of the prologue. Allocating some buffer on the stack */
cmp r0, r1 /* Implementation of if(a<b) */
movlt r0, r1 /* if r0 was lower than r1, store r1 into r0 */
add sp, r11, #0 /* 收尾操作開(kāi)始国拇,調(diào)整棧指針洛史,有一個(gè)寄存器要POP,所以從棧幀底部元素再減0 */
pop {r11} /* restoring frame pointer */
bx lr /* End of the epilogue. Jumping back to main via LR register */
上面的函數(shù)main以及max函數(shù)酱吝,一個(gè)是非葉函數(shù)另一個(gè)是葉函數(shù)也殖。就像之前說(shuō)的非葉函數(shù)中有分支跳轉(zhuǎn)到其他函數(shù)的邏輯,函數(shù)max中沒(méi)有在函數(shù)體邏輯中包含有這類(lèi)代碼务热,所以是葉函數(shù)忆嗜。
除此之外還有一點(diǎn)不同是兩類(lèi)函數(shù)序言與收尾的實(shí)現(xiàn)是有差異的。來(lái)看看下面這段代碼陕习,是關(guān)于葉函數(shù)與非葉函數(shù)的序言部分的差異的:
/* A prologue of a non-leaf function */
push {r11, lr} /* Start of the prologue. Saving Frame Pointer and LR onto the stack */
add r11, sp, #4 /* Setting up the bottom of the stack frame */
sub sp, sp, #16 /* End of the prologue. Allocating some buffer on the stack */
/* A prologue of a leaf function */
push {r11} /* Start of the prologue. Saving Frame Pointer onto the stack */
add r11, sp, #0 /* Setting up the bottom of the stack frame */
sub sp, sp, #12 /* End of the prologue. Allocating some buffer on the stack */
一個(gè)主要的差異是霎褐,非葉函數(shù)需要在棧上保存更多的寄存器,這是由于非葉函數(shù)的本質(zhì)決定的该镣,因?yàn)樵趫?zhí)行時(shí)LR寄存器會(huì)被修改冻璃,所以需要保存LR寄存器以便之后恢復(fù)。當(dāng)然如果有必要也可以在序言期保存更多的寄存器损合。
下面這段代碼可以看到省艳,葉函數(shù)與非葉函數(shù)在收尾時(shí)的差異主要是在于,葉函數(shù)的結(jié)尾直接通過(guò)LR中的值跳轉(zhuǎn)回去就好嫁审,而非葉函數(shù)需要先通過(guò)POP恢復(fù)LR寄存器跋炕,再進(jìn)行分支跳轉(zhuǎn)。
/* An epilogue of a leaf function */
add sp, r11, #0 /* Start of the epilogue. Readjusting the Stack Pointer */
pop {r11} /* restoring frame pointer */
bx lr /* End of the epilogue. Jumping back to main via LR register */
/* An epilogue of a non-leaf function */
sub sp, r11, #4 /* Start of the epilogue. Readjusting the Stack Pointer */
pop {r11, pc} /* End of the epilogue. Restoring Frame pointer from the stack, jumping to previously saved LR via direct load into PC */
最后律适,我們要再次強(qiáng)調(diào)一下在函數(shù)中BL和BX指令的使用辐烂。在我們的示例中,通過(guò)使用BL指令跳轉(zhuǎn)到葉函數(shù)中捂贿。在匯編代碼中我們使用了標(biāo)簽纠修,在編譯過(guò)程中,標(biāo)簽被轉(zhuǎn)換為對(duì)應(yīng)的內(nèi)存地址厂僧。在跳轉(zhuǎn)到對(duì)應(yīng)位置之前扣草,BL會(huì)將下一條指令的地址存儲(chǔ)到LR寄存器中這樣我們就能在函數(shù)max完成的時(shí)候返回了。
BX指令在被用在我們離開(kāi)一個(gè)葉函數(shù)時(shí)颜屠,使用LR作為寄存器參數(shù)辰妙。剛剛說(shuō)了LR存放著函數(shù)調(diào)用返回后下一條指令的地址。由于葉函數(shù)不會(huì)在執(zhí)行時(shí)修改LR寄存器甫窟,所以就可以通過(guò)LR寄存器跳轉(zhuǎn)返回到main函數(shù)了密浑。同樣BX指令還會(huì)幫助我們切換ARM/Thumb模式。同樣這也通過(guò)LR寄存器的最低比特位來(lái)完成粗井,0代表ARM模式肴掷,1代表Thumb模式敬锐。
最后,這張動(dòng)圖闡述了非葉函數(shù)調(diào)用葉函數(shù)時(shí)候的內(nèi)部寄存器的工作狀態(tài)呆瞻。