本文的主要目的是理解函數(shù)棧以及涉及的相關(guān)指令
在講函數(shù)的本質(zhì)之前,首先需要講下以下幾個(gè)概念棧、SP、FP
常識(shí)
棧
- 棧:是一種
具有特殊的訪問方式的存儲(chǔ)空間(即先進(jìn)后出 Last In First Out,LIFO)
入棧出棧圖示高地址往低地址存數(shù)據(jù)(
存:高-->低)??臻g開辟:往低地址開辟(
開辟:高-->低)
SP和FP寄存器
SP寄存器:在任意時(shí)刻會(huì)保存棧頂?shù)牡刂?/code>FP寄存器(也稱為x29寄存器):屬于通用寄存器,但是在某些時(shí)刻(例如函數(shù)嵌套調(diào)用時(shí))可以利用它保存棧底的地址
注意:
arm64開始,取消了32位的LDM、STM、PUSH、POP指令,取而代之的是
ldr/ldp、str/stp(r和p的區(qū)別在于處理的寄存器個(gè)數(shù),r表示處理1個(gè)寄存器,p表示處理兩個(gè)寄存器)arm64中,對(duì)棧的操作是
16字節(jié)對(duì)齊的?。?!
以下是arm64之前和arm64之后的一個(gè)對(duì)比

在arm64之前,棧頂指針是壓棧時(shí)一個(gè)數(shù)據(jù)移動(dòng)一個(gè)單元
在arm64開始,首先是
從高地址往低地址開辟一段??臻g(由編譯器決定),然后再放入數(shù)據(jù),所以不存在push、pop操作。這種情況可以通過內(nèi)存讀寫指令(ldr/ldp、str/stp)對(duì)其進(jìn)行操作
函數(shù)調(diào)用棧
以下是常見的函數(shù)調(diào)用開辟 (sub)以及恢復(fù)??臻g (add)的匯編代碼
//開辟??臻g
sub sp, sp, #0x40 ; 拉伸0x40(64字節(jié))空間
stp x29, x30, [sp, #0x30] ;x29\x30 寄存器入棧保護(hù)
add x29, sp, #0x30 ; x29指向棧幀的底部
...
ldp x29, x30, [sp, #0x30] ;恢復(fù)x29/x30 寄存器的值
//恢復(fù)棧空間
add sp, sp, #0x40 ; 棧平衡
ret
內(nèi)存讀寫指令
str(store register)指令(能和內(nèi)存和寄存器交互的專門的指令):將數(shù)據(jù)從寄存器中讀出來,存到內(nèi)存中 (即一個(gè)寄存器是8字節(jié)-64位)ldr(load register)指令:將數(shù)據(jù)從內(nèi)存中讀出來,存到寄存器中此時(shí)ldr和str的變種
ldp和stp還可以操作2個(gè)寄存器(即128位-16字節(jié))
注意:
讀/寫數(shù)據(jù)都是往
高地址讀/寫寫數(shù)據(jù):先拉伸??臻g,再拿sp進(jìn)行寫數(shù)據(jù),即
先申請(qǐng)空間再寫數(shù)據(jù)
練習(xí)
使用32個(gè)字節(jié)空間作為這段程序的??臻g,然后利用棧將x0和x1的值進(jìn)行交換
sub sp, sp, #0x20 ;拉伸棧空間32個(gè)字節(jié)
stp x0, x1, [sp, #0x10] ;sp往上加16個(gè)字節(jié),存放x0和x1
ldp x1, x0, [sp, #0x10] ;將sp偏移16個(gè)字節(jié)的值取出來,放入x1和x0,內(nèi)存是temp(寄存器里面的值進(jìn)行交換了)
add sp, sp, #0x20 ;棧平衡
ret ;返回
棧的操作如下圖所示

調(diào)試查看棧
-
重寫x0、x1的值
調(diào)試查看棧-01 -
register read sp【查看棧的存儲(chǔ)情況:debug - debug workflow - view Memory】
調(diào)試查看棧-02 -
然后單步往下執(zhí)行,發(fā)現(xiàn)x0、x1已經(jīng)變成我們寫入的值
調(diào)試查看棧-03)
查看內(nèi)存變化,發(fā)現(xiàn)sp拉伸了32字節(jié)
調(diào)試查看棧-04 -
stp x0, x1, [sp, #0x10]:將x0、x1寫入fp偏移0x10的位置,繼續(xù)往下執(zhí)行一步
調(diào)試查看棧-05
調(diào)試查看棧-06
此時(shí)sp的值并沒有變化,還是指向40
調(diào)試查看棧-07 -
ldp x1, x0, [sp, #0x10]:讀取x0,x1的數(shù)據(jù)并交換,繼續(xù)往下執(zhí)行一步,此時(shí)內(nèi)存并沒有變化
調(diào)試查看棧-08
疑問:再來看sp是否有變化?
從結(jié)果來看,也沒有變化。所以這里只是讀出來進(jìn)行的交換,并不會(huì)導(dǎo)致內(nèi)存變化
調(diào)試查看棧-09 -
add sp, sp, #0x20:繼續(xù)執(zhí)行一步,走到棧平衡,即sp恢復(fù)了,此時(shí)的a和b仍然在內(nèi)存中,等待著下一輪棧拉伸后數(shù)據(jù)的寫入覆蓋。如果此時(shí)讀取,讀取到的是垃圾數(shù)據(jù)
調(diào)試查看棧-10
疑問:??臻g不斷開辟,死循環(huán),會(huì)不會(huì)崩潰?
在這里我們將會(huì)處理上篇iOS逆向 01:初識(shí)匯編文章中文末遺留的問題
下面我們通過一個(gè)匯編代碼來演示
<!--asm.s-->
.text
.global _B
_B:
sub sp,sp,#0x20
stp x0,x1,[sp,#0x10]
ldp x1,x0,[sp,#0x10];寄存器里面的值進(jìn)行交換
bl _B
add sp,sp,#0x20
ret
<!--調(diào)用-->
int B();
int main(int argc, char * argv[]) {
B();
}
運(yùn)行結(jié)果發(fā)現(xiàn):死循環(huán)會(huì)崩潰,會(huì)導(dǎo)致堆棧溢出

bl 、ret指令
b 標(biāo)號(hào) :跳轉(zhuǎn)
-
bl標(biāo)號(hào)
- 將下一條指令的地址放入
lr(x30)寄存器(lr保存的是回家的路)(即l) -
轉(zhuǎn)到標(biāo)號(hào)處執(zhí)行指令(即b)
bl圖示
等到B函數(shù)ret時(shí),通過lr獲取回家的路(注:lr就是保存回家的路)
- 將下一條指令的地址放入
-
ret默認(rèn)使用
lr(x30)寄存器的值,通過底層指令提示CPU此處作為下條指令地址arm64平臺(tái)的特色指令,它面向硬件做了優(yōu)化處理的
練習(xí)
下面通過匯編代碼來演示bl、ret指令
.text
.global _A, _B
_A:
mov x0. #0xaaaa
bl _B
mov x0, #0xaaaa
ret
_B:
mov x0, #0xbbbb
ret
-
斷點(diǎn)運(yùn)行
演示bl、ret指令-01
疑問:發(fā)現(xiàn)A和print之間你還有幾個(gè)匯編操作,這個(gè)是什么意思呢?
演示bl、ret指令-02 -
執(zhí)行
mov x0. #0xaaaa:x0變成aaaa,此時(shí)此刻lr寄存器保存的是5f34
演示bl、ret指令-03 -
驗(yàn)證
lr是否保存的是5f34,通過查看寄存器發(fā)現(xiàn)結(jié)果與預(yù)期是一致的
演示bl、ret指令-04 -
繼續(xù)執(zhí)行
bl _B,跳轉(zhuǎn)到B,此時(shí)的lr會(huì)變成A中bl的下一條指令的地址5eb8
演示bl、ret指令-05 -
執(zhí)行完B中的
mov x0, #0xbbbb,x0變成bbbb
演示bl、ret指令-06 -
執(zhí)行B中的
ret,會(huì)回到A中5eb8
演示bl、ret指令-07 -
繼續(xù)執(zhí)行A中的ret,會(huì)再次回到5eb8
演示bl、ret指令-08
走到這里,發(fā)現(xiàn)死循環(huán)了,主要是因?yàn)?code>lr一直是5eb8,ret只會(huì)看lr。其中pc是指接下來要執(zhí)行的內(nèi)存地址,ret是指讓CPU將lr作為接下來執(zhí)行的地址(相當(dāng)于將lr賦值給pc)
演示bl、ret指令-09
疑問1:此時(shí)B回到A沒問題,那么A回到viewDidload怎么回呢?
- 需要在A的bl之前
保護(hù)lr寄存器-
疑問2:是否可以保存到其他寄存器上?
答案是不可以,原因是不安全,因?yàn)槟悴淮_定這個(gè)寄存器會(huì)在什么時(shí)候被別人使用 - 正確做法:保存到棧區(qū)域
-
疑問2:是否可以保存到其他寄存器上?
系統(tǒng)中函數(shù)嵌套是如何返回?
下面我們來看下系統(tǒng)是如何操作的,例如:d -> c -> viewDidLoad
void d(){
}
void c(){
d();
return;
}
- (void)viewDidLoad{
[super viewDidLoad];
printf("A");
c();
printf("B");
}
-
查看匯編,斷點(diǎn)斷在c函數(shù)
函數(shù)嵌套調(diào)試-01 -
進(jìn)入c函數(shù)的匯編
函數(shù)嵌套調(diào)試-02stp x29,x30,[sp,#-0x10]!:邊開辟棧,邊寫入,其中x29就是fp,x30是lr。!表示將這里算出來的結(jié)果,賦值給splsp x29,x30,[sp],#0x10:讀取sp指向地址的數(shù)據(jù),放入x29、x30,然后,,#0x10表示將sp+0x10,賦值給sp
-
結(jié)論:當(dāng)有函數(shù)嵌套調(diào)用時(shí),將
上一個(gè)函數(shù)的地址通過x30(即lr)放在棧中保存,保證可以找到回家的路,如下圖所示
函數(shù)嵌套調(diào)試-03
自定義匯編代碼完善:_A中保存回家的路
所以根據(jù)系統(tǒng)的函數(shù)嵌套操作,最終在_A中增加了如下匯編代碼,用于保存回家的路
<!--導(dǎo)致死循環(huán)的匯編代碼-->
_A:
mov x0. #0xaaaa
bl _B
mov x0, #0xaaaa
ret
<!--增加lr保存:可以找到回家的路-->
_A:
sub sp, sp, #0x10 //拉伸
str x30, [sp] //存
mov x0, #0xaaaa
//保護(hù)lr寄存器,存儲(chǔ)到棧區(qū)域
bl _B
mov x0, #0xaaa
ldr x30, [sp] //修改lr,用于A找到回家的路
add sp, sp, #0x10 //棧平衡
ret
修改_A、_B:改成簡(jiǎn)寫形式
- 其中
lr是x30的一個(gè)別名
_A:
sub sp, sp, #0x10 //拉伸
str x30, [sp] //存
mov x0, #0xaaaa
//保護(hù)lr寄存器,存儲(chǔ)到棧區(qū)域
bl _B
mov x0, #0xaaa
ldr x30, [sp] //修改lr,用于A找到回家的路
add sp, sp, #0x10 //棧平衡
ret
_B:
mov x0, #0xbbbb
ret
<!--改成簡(jiǎn)寫形式-->
_A:
//sub sp, sp, #0x10 //拉伸
//str x30, [sp] //存
str x30, [sp, #-0x10]
mov x0, #0xaaaa
//保護(hù)lr寄存器,存儲(chǔ)到棧區(qū)域
bl _B
mov x0, #0xaaa
//ldr x30, [sp] //修改lr,用于A找到回家的路
//add sp, sp, #0x10 //棧平衡
ldr x30, [sp], #0x10 //將sp的值讀取出來,給到x30,然后sp += 0x10
ret
_B:
mov x0, #0xbbbb
ret
斷點(diǎn)調(diào)試
-
查看此時(shí)sp寄存器的地址
函數(shù)嵌套調(diào)試-04 -
執(zhí)行
str x30, [sp, #-0x10],繼續(xù)查看sp,發(fā)現(xiàn)sp變化了,但是此時(shí)lr沒變
函數(shù)嵌套調(diào)試-05
查看0x16f5a1c50的memory,此時(shí)放入的是lr的值861f2c,即ViewDidLoad中的bl下一條指令的地址,目前只放了8個(gè)字節(jié)(1個(gè)寄存器)
函數(shù)嵌套調(diào)試-06 -
執(zhí)行A中的
mov x0, #0xaaaa:x0變成aaaa
函數(shù)嵌套調(diào)試-07 -
執(zhí)行A中的
bl _B,跳轉(zhuǎn)到B,此時(shí)lr變成 1e94,x0變成bbbb
函數(shù)嵌套調(diào)試-08 -
執(zhí)行B的
ret:從B回到A,此時(shí)lr還是 1e94
函數(shù)嵌套調(diào)試-09 -
執(zhí)行A中的
ldr x30, [sp], #0x10
函數(shù)嵌套調(diào)試-10
發(fā)現(xiàn)此時(shí)sp也變了,從0x16f5a1c50->0x16f5a1c60。從這里可以看出,A找到了回家的路
函數(shù)嵌套調(diào)試-11
疑問:為什么是拉伸16字節(jié),而不是8字節(jié)呢?
通過手動(dòng)嘗試,有以下說明:
寫入沒問題
-
讀取時(shí)會(huì)崩潰:因?yàn)閟p中,對(duì)棧的操作
必須是16字節(jié)對(duì)齊的,所以會(huì)在做棧的操作時(shí)就會(huì)崩潰
函數(shù)嵌套調(diào)試-12
x30寄存器
x30寄存器存放的是函數(shù)的返回地址,當(dāng)ret指令執(zhí)行時(shí)刻,會(huì)尋找x30寄存器保存的地址值注意:
在函數(shù)嵌套調(diào)用時(shí),需要將x30入棧lr是x30的別名
sp棧里面的操作必須是16字節(jié)對(duì)齊,崩潰是在棧的操作時(shí)掛的
總結(jié)
-
棧:是一種具有特殊的訪問方式的存儲(chǔ)空間(后進(jìn)先出,Last in First out,
LIFO)- ARM64里面對(duì)
棧的操作是16字節(jié)對(duì)齊的
- ARM64里面對(duì)
-
SP和FP寄存器-
SP寄存器在任意時(shí)刻會(huì)保存棧頂?shù)牡刂?/code> -
FP寄存器也稱為x29寄存器,屬于通用寄存器,但是在某些時(shí)刻利用它保存棧底的地址
-
-
棧的讀寫指令
讀:
ldr(load register)指令 LDR、LDP寫:
str(store register)指令 STR、STP
-
匯編練習(xí)
- 指令
sub sp,sp,$0x10 ;拉伸??臻g18字節(jié)
stp x0,x1,[sp] ;sp所在位置存放x0、x1
- 簡(jiǎn)寫
- str x0,x1,[sp,$-0x10]!(!就是將[]里面的結(jié)果賦值給sp)
- 指令
-
bl指令跳轉(zhuǎn)指令:
bl 標(biāo)號(hào),表示程序執(zhí)行到標(biāo)號(hào)處,將下一條指令的地址保存到lr寄存器B代表著跳轉(zhuǎn)L表示lr(x30)寄存ios_reverse_02器
-
ret指令- 類似函數(shù)的
return - 讓CPU執(zhí)行l(wèi)r寄存器所指向的指令
- 類似函數(shù)的
避免嵌套函數(shù)無法回去:需要保護(hù)bl(即
lr寄存器,存放回家的路),保存在當(dāng)前函數(shù)自己的棧空間
































