【ARM 匯編基礎(chǔ)速成7】ARM匯編之棧與函數(shù)

原文鏈接 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í)候它的變化。??梢韵蛏显鲩L(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ì)被遞減(降序棧)或遞增(升序棧)。

image

不同的棧實(shí)現(xiàn),可以用不同情形下的多次存取指令來(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指向的位置中?,F(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)圖展示了低地址在頂部的棧的變化情況)

image

棧被用來(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ù),但是這其中不少類型我們之前已經(jīng)了解過(guò)了。最后,棧幀在函數(shù)結(jié)束時(shí)被銷毀。

下圖是關(guān)于棧幀的在棧中的位置的抽象描述(默認(rèn)棧,滿棧降序):

image

來(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)信息:

image

可以看到上面的圖片中我們即將離開(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):

  1. 序言準(zhǔn)備(Prologue)
  2. 函數(shù)體
  3. 結(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ù)類型的不同,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é)一下:

  1. 序言設(shè)定函數(shù)環(huán)境
  2. 函數(shù)體實(shí)現(xiàn)函數(shù)邏輯功能,將結(jié)果存到R0
  3. 收尾恢復(fù)程序狀態(tài),回到調(diào)用發(fā)生的地方。

關(guān)于函數(shù),有一個(gè)關(guān)鍵點(diǎn)我們要知道,函數(shù)的類型分為葉函數(shù)以及非葉函數(shù)。葉函數(shù)是指函數(shù)中沒(méi)有分支跳轉(zhuǎn)到其他函數(shù)指令的函數(shù)。非葉函數(shù)指包含有跳轉(zhuǎn)到其他函數(shù)的分支跳轉(zhuǎn)指令的函數(shù)。這兩種函數(shù)的實(shí)現(xiàn)都很類似,當(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ù)體邏輯中包含有這類代碼,所以是葉函數(shù)。

除此之外還有一點(diǎn)不同是兩類函數(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)。

image
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容