棧是一種具備特殊訪問方式的存儲空間(後進先出,Last In First Out,LIFO)編程
問:上節課最後的例子進入死循環,那麼死循環必定會形成崩潰嗎??
數組
狀況一,每循環一次就會拉伸棧空間,當堆和棧碰頭了之後會形成OOM(Out Of Memory)崩潰markdown
注意此時稱爲堆棧溢出
,沒有單獨的堆溢出和棧溢出,堆從低地址向高地址延伸,棧空間從高地址向低地址延伸,系統給每一個進程分配必定的虛擬空間
,當系統內存緊張或者進程本身的虛擬空間快用完時會以必定的策略決定先殺死那些進程編程語言
.text
.global _A
_A:
sub sp,sp,#0x20
stp x0,x1,[sp,#0x10]
ldp x1,x0,[sp,#0x10]
bl _A
add sp,sp,#0x20 ;棧平衡
ret
_
複製代碼
狀況二,每次循環都能棧平衡,那麼就會一直執行,不會崩潰函數
.text
.global _A
_A:
sub sp,sp,#0x20
stp x0,x1,[sp,#0x10]
ldp x1,x0,[sp,#0x10]
add sp,sp,#0x20 ;棧平衡
bl _A
ret
_
複製代碼
ARM64開始,取消32位的LDM、STM、PUSH、POP指令,取而代之的是LDR、LDP、STR、STP,ARM64裏面,
對棧的操做是16字節對齊的
post
棧地址是從高地址向底地址開闢的,因此開闢地址是對SP指針減sub
,回收棧空間是對SP指針作加add
,性能
sub sp,sp,#0x40 ;開闢了0x40(64字節)空間
stp x29,x30,[sp,#0x30] ;x29/x30寄存器入棧保護
add x29,x29,#0x30 ;x20(fp)寄存器指向棧底的位置
...
ldp x29,x30,[sp,#0x30] ;恢復x20、x30寄存器的值
add sp,sp,#0x40 ;棧平衡
ret
複製代碼
注意,讀寫數據都是往高地址讀寫
,例如開闢的32字節空間可是存儲16字節的數據,那麼先存儲高地址的16字節優化
將數據從寄存器中讀出來,存到內存中spa
將數據從內存中讀出來,存到寄存器中,此ldr和str的變種ldp和stp能夠同時操做兩個寄存器,例如我想將x0
寄存器的值存儲到棧空間能夠這樣寫3d
sub sp,sp,#0x10
str x0,[sp] ;將x0寄存器的值存儲到sp指向的棧空間
ldr x0,[sp] ;將sp指向的棧上的值恢復到x0寄存器
add sp,sp,#0x10
複製代碼
若是我想交換x0、x1
兩個寄存器的值,能夠這樣寫
sub sp,sp,#0x20
stp x0,x1,[sp,#0x10] ;將x0、x1寄存器的值存儲到sp指向的棧空間
ldp x1,x0,[sp,#0x10] ;將sp指向的棧空間的值存儲到x一、x0寄存器上
add sp,sp,#0x10
複製代碼
str\stp、ldr\ldp是專門用來操做寄存器和內存的指令
咱們拿到sp指針之後先拉伸棧空間,再操做棧空間
咱們新建工程並新建文件命名爲asm.s
.text
.global _A
_A:
sub sp,sp,#0x20
mov x0,#0xaaaa
mov x1,#0xbbbb
stp x0,x1,[sp,#0x10]
ldp x1,x0,[sp,#0x10]
add sp,sp,#0x20
ret
複製代碼
咱們單步執行之,此時咱們即將拉伸棧空間,此時sp = 0x000000016af3dc50
棧空間拉伸32字節以後sp = 0x000000016af3dc30
此時咱們查看內存狀況Debug -> Debug Workflow -> View Memory
,那麼這32字節就是咱們拉伸的棧空間
繼續單步執行,咱們能夠看到寄存器x0和x1
被賦值了
繼續單步執行指令stp x0, x1, [sp, #0x10]
能夠看到寄存器x0和x1
的值已經被存儲到棧空間上了
再次單步執行能夠看到寄存器x0和x1
的值交換了
棧平衡
能夠看到此時棧上的值還在,棧平衡之後這就成了垃圾數據,下次拉伸棧的時候會將內存的值覆蓋掉
bl有兩層含義,一是修改lr(x30)的值,另外一個是跳轉
ARM64平臺的特點指令,它面向硬件作了優化處理
bl指令和ret指令是成對出現的,當遇到bl指令的時候lr存儲下一條指令的地址,直到遇到ret指令會觸發lr寄存器中的指令執行
.text
.global _A,_B
_A:
sub sp,sp,#0x20
mov x0,#0xa
mov x1,#0xb
bl _B
add sp,sp,#0x20
ret
_B:
mov x0,#0xb
mov x1,#0xa
ret
複製代碼
咱們看到在遇到bl指令之前lr寄存器和pc寄存器存儲的地址值是同樣的
當遇到bl指令之後lr寄存器的值就再也不改變,直到遇到下一條bl指令或者ret指令,pc寄存器的值仍然指向即將執行的指令地址
再次遇到bl指令的以後lr的值發生了改變,保存了返回_A
函數的地址
當遇到ret指令之後觸發lr寄存器中存儲的指令
再次遇到ret指令仍然會觸發lr寄存器中存儲的指令,此時問題就來了,lr跳轉到_B
函數之後保存了返回_A
函數的地址,可是沒有記錄返回ViewDidLoad
函數的地址,因而形成了死循環
一直循環
這就找到了上節中死循環的緣由
函數跳轉關係爲ViewDidLoad
-> _A
-> _B
ViewDidLoad
-> _A
時lr寄存器保存了返回ViewDidLoad
函數的地址_A
-> _B
時lr寄存器保存了返回_A
函數的地址_A
<- _B
時能夠正常經過lr寄存器保存的指令返回到_A
函數ViewDidLoad
<- _A
此時lr寄存器仍然存儲的是回到_A
函數的地址,因而形成死循環解決方案就是當函數嵌套調用的時候保存一下回家的路(lr寄存器的值),那麼咱們是否是能夠保存在另外的寄存器中呢???這是不行的,誰也不肯定寄存器在以後的調用中會不會被使用到,因此咱們應該將lr寄存器保存在當前函數的棧空間中,做爲局部變量保存起來,咱們能夠這樣修改
.text
.global _A,_B
_A:
sub sp,sp,#0x20
stp x29,x30,[sp,#0x10] ;保存x29,x30的值
mov x0,#0xa
mov x1,#0xb
bl _B
ldp x29,x30,[sp,#0x10] ;恢復x29,x30的值
add sp,sp,#0x20
ret
_B:
mov x0,#0xb
mov x1,#0xa
ret
複製代碼
此時lr保存的是返回_A
函數的指令地址
此時從_A
函數的棧中恢復了lr寄存器的值
遇到ret指令之後
能夠正常返回ViewDidLoad
函數,上節遺留的死循環問題解決🎉
這兩句指令能夠優化爲一行指令
sub sp,sp,#0x10 ;拉伸棧空間
stp x29,x30,[sp] ;保存x29,x30的值
複製代碼
stp x29,x30,[sp,#-0x10]! ;拉伸棧空間並賦值
複製代碼
一樣如下這兩句指令能夠優化爲一行指令
ldp x29,x30,[sp,#0x10] ;恢復x29,x30的值
add sp,sp,#0x10 ;棧平衡
複製代碼
ldp x29,x30,[sp],#0x10 ;恢復x29,x30的值並恢復棧平衡
複製代碼
再次強調一下對棧的操做是以16字節對齊的,切記切記
sub sp,sp,0x8
str x0,[sp]
ldr x0,[sp] ;這是會出問題的
add sp,sp,0x8
複製代碼
經過以上練習咱們知道當沒有遇到bl指令時lr寄存器和pc寄存器保存的都是即將執行的指令地址,可是遇到bl指令之後lr寄存器的值就再也不改變,直到遇到ret指令或者另外一條bl指令纔會改變,lr寄存器能夠理解爲函數嵌套調用時返回上一級函數的路徑,pc寄存器只是簡單指向下一條即將執行的指令。 當函數只有一級嵌套時咱們不須要對lr寄存器作操做,可是當函數多級嵌套時咱們就須要手動保存lr寄存器的值,不然會形成死循環
不會寫不要緊,寫個高級函數看看系統怎麼生成的
首先將參數保存在寄存器w0,w1中
先將寄存器w0,w1的值保存到棧上,再從棧上讀取到寄存器w8,w9上,對w8,w9作加法結果保存到w0,函數執行結束,看起來很囉嗦,這或許跟編譯時沒有編譯優化又關係
那麼咱們就能夠這樣實現一個帶參數的函數
.text
.global _A
_A:
add x0,x0,x1
ret
複製代碼
執行結果沒問題撒花鼓掌🎉👏
ARM64下,函數的參數時存放在x0-x7(w0-w7)這8個寄存器裏面的,若是超過8個參數就會入棧,函數的返回值時存放在x0寄存器裏面的,若是8字節裝不下也會放在棧空間
爲了效率考慮,咱們在寫OC代碼時參數總數最好不要超過6個,由於函數自己有兩個隱形參數self和selector,若是必須超過6個最好使用數組或者結構體指針