【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í)候它的變化想际。棧可以向上增長(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)表示(這里很繞...):

棧類(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)圖展示了低地址在頂部的棧的變化情況)

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ù)甫煞,但是這其中不少類(lèi)型我們之前已經(jīng)了解過(guò)了。最后冠绢,棧幀在函數(shù)結(jié)束時(shí)被銷(xiāo)毀抚吠。

下圖是關(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ù)類(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é)一下:

  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ù)的類(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)呆瞻。

image
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末台夺,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子痴脾,更是在濱河造成了極大的恐慌颤介,老刑警劉巖,帶你破解...
    沈念sama閱讀 219,039評(píng)論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件赞赖,死亡現(xiàn)場(chǎng)離奇詭異滚朵,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)前域,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,426評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門(mén)辕近,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人匿垄,你說(shuō)我怎么就攤上這事移宅。” “怎么了椿疗?”我有些...
    開(kāi)封第一講書(shū)人閱讀 165,417評(píng)論 0 356
  • 文/不壞的土叔 我叫張陵漏峰,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我届榄,道長(zhǎng)浅乔,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,868評(píng)論 1 295
  • 正文 為了忘掉前任铝条,我火速辦了婚禮靖苇,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘班缰。我一直安慰自己贤壁,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,892評(píng)論 6 392
  • 文/花漫 我一把揭開(kāi)白布鲁捏。 她就那樣靜靜地躺著芯砸,像睡著了一般萧芙。 火紅的嫁衣襯著肌膚如雪给梅。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 51,692評(píng)論 1 305
  • 那天双揪,我揣著相機(jī)與錄音动羽,去河邊找鬼。 笑死渔期,一個(gè)胖子當(dāng)著我的面吹牛运吓,可吹牛的內(nèi)容都是我干的渴邦。 我是一名探鬼主播,決...
    沈念sama閱讀 40,416評(píng)論 3 419
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼拘哨,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼谋梭!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起倦青,我...
    開(kāi)封第一講書(shū)人閱讀 39,326評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤瓮床,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后产镐,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體隘庄,經(jīng)...
    沈念sama閱讀 45,782評(píng)論 1 316
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,957評(píng)論 3 337
  • 正文 我和宋清朗相戀三年癣亚,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了丑掺。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,102評(píng)論 1 350
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡述雾,死狀恐怖街州,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情绰咽,我是刑警寧澤菇肃,帶...
    沈念sama閱讀 35,790評(píng)論 5 346
  • 正文 年R本政府宣布,位于F島的核電站取募,受9級(jí)特大地震影響琐谤,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜玩敏,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,442評(píng)論 3 331
  • 文/蒙蒙 一斗忌、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧旺聚,春花似錦织阳、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,996評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至碱璃,卻和暖如春弄痹,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背嵌器。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,113評(píng)論 1 272
  • 我被黑心中介騙來(lái)泰國(guó)打工肛真, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人爽航。 一個(gè)月前我還...
    沈念sama閱讀 48,332評(píng)論 3 373
  • 正文 我出身青樓蚓让,卻偏偏與公主長(zhǎng)得像乾忱,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,044評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容