程序的棧空間有什么特點(diǎn)呢?首先會(huì)想到的就是师坎,棧空間是往低地址增長的臂拓,當(dāng)調(diào)用一個(gè)函數(shù)時(shí),先開辟棧空間,用來存放當(dāng)前函數(shù)的參數(shù)和局部變量敲茄;執(zhí)行函數(shù)之前還需要先保護(hù)現(xiàn)場,當(dāng)函數(shù)執(zhí)行完之后會(huì)恢復(fù)現(xiàn)場搁宾。那么函數(shù)調(diào)用的過程中內(nèi)存和CPU的寄存器到底發(fā)生了什么變化呢折汞?這是本篇要探討的問題。
先看一段C代碼盖腿,定義了三個(gè)函數(shù),調(diào)用關(guān)系為:funcA
調(diào)用funcB,
funcB
調(diào)用funcC
损同。
int funcA(int a, int b) {
int ret = funcB(a, b);
return ret;
}
int funcB(int a, int b) {
return funcC(a, b);
}
int funcC(int a, int b) {
int c = a + b;
return c;
}
從函數(shù)是否還調(diào)用了其他函數(shù)的角度看翩腐,函數(shù)可分為葉子函數(shù)、非葉子函數(shù):
葉子函數(shù):函數(shù)內(nèi)部沒有調(diào)用其他函數(shù)了膏燃,例如上面的funcC
非葉子函數(shù):函數(shù)內(nèi)部還調(diào)用了其他的函數(shù)茂卦,例如上面的funcA、funcB
為什么要這么劃分呢组哩?因?yàn)?strong>葉子函數(shù)與非葉子函數(shù)生成的匯編代碼有所不同等龙,這也是本篇要分析的一個(gè)點(diǎn)。
獲取匯編代碼
得到對應(yīng)的匯編代碼有兩種方式
方式一:
用clang編譯器把C代碼編譯為匯編代碼伶贰,編譯函數(shù)所在的.c
文件即可
xcrun -sdk iphoneos clang -S -arch arm64 Ctest.c
指定架構(gòu)為arm64蛛砰,執(zhí)行命令后會(huì)得到Ctest.s
文件,就是對應(yīng)的匯編代碼了黍衙。
方式二:
新建一個(gè)iOS項(xiàng)目泥畅,并且運(yùn)行在真機(jī)設(shè)備上,在真機(jī)上才是arm64架構(gòu)的匯編哦琅翻。在三個(gè)函數(shù)分別打斷點(diǎn)位仁,把Xcode
的debug
模式設(shè)置為顯示匯編模式柑贞,在Debug -> Debug Workflow -> Aways Show Disassembly
設(shè)置。當(dāng)函數(shù)斷住時(shí)聂抢,會(huì)顯示當(dāng)前函數(shù)的匯編代碼钧嘶。
這里采用方式二,得到的匯編代碼如下:
funcA
Assembly`funcA:
0x100086ab8 <+0>: sub sp, sp, #0x20 ; =0x20
0x100086abc <+4>: stp x29, x30, [sp, #0x10]
0x100086ac0 <+8>: add x29, sp, #0x10 ; =0x10
0x100086ac4 <+12>: stur w0, [x29, #-0x4]
0x100086ac8 <+16>: str w1, [sp, #0x8]
0x100086acc <+20>: ldur w0, [x29, #-0x4]
0x100086ad0 <+24>: ldr w1, [sp, #0x8]
0x100086ad4 <+28>: bl 0x100086aec ; funcB at Ctest.c:57
0x100086ad8 <+32>: str w0, [sp, #0x4]
0x100086adc <+36>: ldr w0, [sp, #0x4]
0x100086ae0 <+40>: ldp x29, x30, [sp, #0x10]
0x100086ae4 <+44>: add sp, sp, #0x20 ; =0x20
0x100086ae8 <+48>: ret
funcB
Assembly`funcB:
0x100086aec <+0>: sub sp, sp, #0x20 ; =0x20
0x100086af0 <+4>: stp x29, x30, [sp, #0x10]
0x100086af4 <+8>: add x29, sp, #0x10 ; =0x10
0x100086af8 <+12>: stur w0, [x29, #-0x4]
0x100086afc <+16>: str w1, [sp, #0x8]
0x100086b00 <+20>: ldur w0, [x29, #-0x4]
0x100086b04 <+24>: ldr w1, [sp, #0x8]
0x100086b08 <+28>: bl 0x100086b18 ; funcC at Ctest.c:75
0x100086b0c <+32>: ldp x29, x30, [sp, #0x10]
0x100086b10 <+36>: add sp, sp, #0x20 ; =0x20
0x100086b14 <+40>: ret
funcC
Assembly`funcC:
0x100086b18 <+0>: sub sp, sp, #0x10 ; =0x10
0x100086b1c <+4>: str w0, [sp, #0xc]
0x100086b20 <+8>: str w1, [sp, #0x8]
0x100086b24 <+12>: ldr w0, [sp, #0xc]
0x100086b28 <+16>: ldr w1, [sp, #0x8]
0x100086b2c <+20>: add w0, w0, w1
0x100086b30 <+24>: str w0, [sp, #0x4]
0x100086b34 <+28>: ldr w0, [sp, #0x4]
0x100086b38 <+32>: add sp, sp, #0x10 ; =0x10
0x100086b3c <+36>: ret
匯編分析
先回憶一下幾個(gè)關(guān)鍵寄存器和指令琳疏,
sp (Stack Point) :寄存器r31
康辑,指向函數(shù)調(diào)用棧的棧頂
fp (Frame Point):寄存器r29
,指向當(dāng)前正在執(zhí)行函數(shù)棧幀的棧底
lr (Link Register) :寄存器r30
轿亮,存儲(chǔ)的是函數(shù)返回地址疮薇,用于當(dāng)函數(shù)結(jié)束時(shí),返回函數(shù)調(diào)用方繼續(xù)往下執(zhí)行我注。
bl:跳轉(zhuǎn)指令按咒,它做了兩件事情,
1. 把下一條指令的地址存儲(chǔ)到lr寄存器中
2. 跳轉(zhuǎn)到標(biāo)記處執(zhí)行指令
ret:函數(shù)返回但骨,返回到lr保存的地址繼續(xù)執(zhí)行指令励七。
更多的寄存器和指令學(xué)習(xí)可參考iOS逆向-arm64匯編學(xué)習(xí)。
由于funcA
與funcB
都是非葉子函數(shù)奔缠,生成的主要匯編代碼大致相同掠抬,所以下面只分析funcB
和funcC
對應(yīng)的匯編代碼。
Assembly funcB:
Assembly`funcB:
// 第一部分:開辟椥0ィ空間两波,保護(hù)現(xiàn)場
0x100086aec <+0>: sub sp, sp, #0x20 ;// sp指針往下移動(dòng)0x20個(gè)字節(jié),開辟椕贫撸空間腰奋。
0x100086af0 <+4>: stp x29, x30, [sp, #0x10] ;// x29(fp)、x30(lr)寄存器中的內(nèi)容存放內(nèi)存中
0x100086af4 <+8>: add x29, sp, #0x10 ;// x29(fp) 棧底指針往下移動(dòng)0x10個(gè)字節(jié)抱怔。
// 第二部分:函數(shù)邏輯
0x100086af8 <+12>: stur w0, [x29, #-0x4] ;// 把第一個(gè)參數(shù)存入內(nèi)存中
0x100086afc <+16>: str w1, [sp, #0x8] ;// 把第二個(gè)參數(shù)存入內(nèi)存中
0x100086b00 <+20>: ldur w0, [x29, #-0x4] ;// 從內(nèi)存中讀取到w0中劣坊,也就是第一個(gè)參數(shù)
0x100086b04 <+24>: ldr w1, [sp, #0x8] ;// 從內(nèi)存中讀取到w1中,也就是第二個(gè)參數(shù)
0x100086b08 <+28>: bl 0x100086b18 ;// 調(diào)用 funC 函數(shù)
// 第三部分:恢復(fù)現(xiàn)場屈留,回收椌直空間
0x100086b0c <+32>: ldp x29, x30, [sp, #0x10] ;// 從內(nèi)存中讀取數(shù)據(jù)到x29、x30灌危,恢復(fù) x29康二、x30的值。
0x100086b10 <+36>: add sp, sp, #0x20 ;// 函數(shù)執(zhí)行完畢乍狐,回收椩。空間
0x100086b14 <+40>: ret ;// 返回,跳轉(zhuǎn)回上一個(gè)函數(shù)(funcA)執(zhí)行
Assembly funcC
Assembly`funcC:
// 第一部分:開辟棧空間
0x100086b18 <+0>: sub sp, sp, #0x10 ;// sp指針往下移動(dòng)0x20個(gè)字節(jié)藕帜,開辟椞陶郑空間。
// 第二部分:函數(shù)邏輯
0x100086b1c <+4>: str w0, [sp, #0xc]
0x100086b20 <+8>: str w1, [sp, #0x8]
0x100086b24 <+12>: ldr w0, [sp, #0xc]
0x100086b28 <+16>: ldr w1, [sp, #0x8]
0x100086b2c <+20>: add w0, w0, w1
0x100086b30 <+24>: str w0, [sp, #0x4]
0x100086b34 <+28>: ldr w0, [sp, #0x4]
// 第三部分:回收椙⒐剩空間
0x100086b38 <+32>: add sp, sp, #0x10 ;// 函數(shù)執(zhí)行完畢贝攒,回收棧空間
0x100086b3c <+36>: ret ;// 返回时甚,跳轉(zhuǎn)回上一個(gè)函數(shù)(funcB)執(zhí)行
經(jīng)過上面的分析隘弊,Assembly funcB:與Assembly funcC:最大的卻別是在Assembly funcB:中有對x29 (fp)、x30 (lr)
的內(nèi)容存儲(chǔ)到內(nèi)存荒适,進(jìn)行保護(hù)梨熙,并在結(jié)束時(shí)恢復(fù)其原來的值;而在Assembly funcC:中沒有這樣的操作刀诬。
原因分析:
- 在Assembly funcC:有
bl
指令咽扇,bl
指令會(huì)修改x30( lr )
寄存器的值,所以需要在真正執(zhí)行函數(shù)之前保存陕壹。 - 那為什么要保存
x29
的值呢质欲?從匯編代碼看,x29
是被修改了糠馆,但是不修改也可以的嘶伟,其他指令并沒有修改x29
的值;單獨(dú)通過sp
也可以完成所有的尋址(訪問具體的內(nèi)存空間)工作又碌。說說我個(gè)人的理解
2.1)通過x29 (fp)
和x30(sp)
可以確定當(dāng)前函數(shù)的棧幀的地址范圍九昧,尋址時(shí)不可以超出這個(gè)范圍。訪問超出這個(gè)范圍地址的數(shù)據(jù)可能是臟數(shù)據(jù)赠橙,至少對當(dāng)前函數(shù)來說是臟數(shù)據(jù)耽装。
2.2)有了x29
寄存器,尋址的時(shí)候可以用x29
的值做一個(gè)偏移定位到要訪問的地址期揪,也可以用sp
的值做一個(gè)偏移找到要訪問的地址」娓觯靠近棧底的用x29
做偏移凤薛,靠近棧頂?shù)挠?code>sp做偏移,這樣可以提高尋址的速度诞仓。那CPU怎么知道要訪問的地址是靠近x29
還是靠近sp
呢缤苫?CPU是不知道的,但編譯器知道墅拭,要訪問什么地址活玲,在程序編譯的時(shí)候就確定了。
2.3)那Assembly funcC:為什么沒有棧底指針x29
,有的話也可以提高尋址速度啊舒憾。那為什么沒有自己的棧底指針呢镀钓?目前還沒找到答案,也是自己的一個(gè)疑惑镀迂。
好了丁溅,上面啰嗦了那么多,下面通過圖來看函數(shù)調(diào)用時(shí)椞阶瘢空間和寄存器的變化情況窟赏,
對圖稍微說明一下,棧是高地址往低地址增長的箱季,圖中每一行表示4個(gè)字節(jié)涯穷。左邊是函數(shù)調(diào)用時(shí)
sp
和fp
的變化過程,右邊是函數(shù)調(diào)用結(jié)束后sp
和fp
的變化過程藏雏。通過觀察拷况,開辟棧空間時(shí)诉稍,不是需要多大的內(nèi)存就開閉多少的內(nèi)存空間蝠嘉,而是16的倍數(shù)的字節(jié)數(shù)。這要做有助于提高CPU訪問內(nèi)存的速度杯巨。
總結(jié)
通過上面的分析蚤告,理解了函數(shù)調(diào)用時(shí)棧空間和寄存器的變化情況服爷,以及為什么需要那樣變化杜恰。但還留下了一個(gè)疑問,葉子函數(shù)為什么沒有自己fp仍源,知道的大神麻煩告知心褐。