彙編002-函數本質(上)

上節回顧

  • 彙編概述
    • 使用助記符代替機器指令的一種編程語言
    • 彙編和機器指令一一對應的關係,拿到二進制能夠反彙編
    • 因爲彙編和CPU指令集是對應的,因此彙編不具有移植性
  • 總線:是一堆導線的集合
    • 地址總線:越寬尋址能力越強
    • 數據總線:寬度決定了CPU數據的吞吐量
    • 控制總線
  • 進制
    • 任意進制都是由對應個數的符號組成的,符號能夠自定義
    • 2/8/16是相對完美的進制,他們之間的關係爲
      • 3個2進制使用一個8進制標識
      • 4個2進制使用一個16進制標識
      • 2個16進制位能夠標識一個字節
    • 數量單位
      • 1024=1k; 1024k=1M; 1024M=1G
      • B:byte(字節) 1B=8bit
      • bit(比特):1個二進制位
    • 數據的寬度
      • 計算機中的數據是有寬度的,超過了就會溢出
  • 寄存器:CPU爲了性能,在內部開闢了一小塊臨時存儲區域
    • 浮點向量寄存器
    • 異常狀態寄存器
    • 通用寄存器:除了存儲數據有的時候也有特殊用途
      • ARM64擁有32個64位的通用寄存器x0-x30以及XZR(零寄存器)
      • 爲了兼容32位,因此ARM64擁有w0-w28\WZR30個32位寄存器
      • 32位寄存器並非獨立存在的,好比w0是x0的低32位
    • PC寄存器:指令指針寄存器
      • PC寄存器裏面的值保存的就是CPU接下來須要執行的指令地址
      • 改變PC的值能夠改變程序的執行流程
      • CPU執行過的指令必定被PC寄存器指向過

棧是一種具備特殊訪問方式的存儲空間(後進先出,Last In First Out,LIFO)編程

15193998892055.jpg

問:上節課最後的例子進入死循環,那麼死循環必定會形成崩潰嗎??數組

狀況一,每循環一次就會拉伸棧空間,當堆和棧碰頭了之後會形成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
_
複製代碼

SP和FP寄存器

  • SP寄存器在任意時刻會保存咱們棧頂的地址
  • FP寄存器也稱爲x29寄存器,在函數嵌套時利用它來保存棧底的地址

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字節優化

str(store register)指令

將數據從寄存器中讀出來,存到內存中spa

ldr(load register)指令

將數據從內存中讀出來,存到寄存器中,此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
複製代碼

image.png

咱們單步執行之,此時咱們即將拉伸棧空間,此時sp = 0x000000016af3dc50

image.png

棧空間拉伸32字節以後sp = 0x000000016af3dc30

image.png

此時咱們查看內存狀況Debug -> Debug Workflow -> View Memory,那麼這32字節就是咱們拉伸的棧空間

image.png

繼續單步執行,咱們能夠看到寄存器x0和x1被賦值了

image.png

image.png

繼續單步執行指令stp x0, x1, [sp, #0x10]能夠看到寄存器x0和x1的值已經被存儲到棧空間上了

image.png

再次單步執行能夠看到寄存器x0和x1的值交換了

image.png

棧平衡

image.png

能夠看到此時棧上的值還在,棧平衡之後這就成了垃圾數據,下次拉伸棧的時候會將內存的值覆蓋掉

image.png

bl和ret指令

bl指令

  • 將下一條指令的地址放入lr(x30)寄存器
  • 轉到標號處指令

bl有兩層含義,一是修改lr(x30)的值,另外一個是跳轉

ret指令

  • 默認使用lr(x30)寄存器的值,經過底層指令提示CPU此處做爲下條指令地址

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
複製代碼

image.png

咱們看到在遇到bl指令之前lr寄存器和pc寄存器存儲的地址值是同樣的 image.png

當遇到bl指令之後lr寄存器的值就再也不改變,直到遇到下一條bl指令或者ret指令,pc寄存器的值仍然指向即將執行的指令地址 image.png

image.png

image.png

再次遇到bl指令的以後lr的值發生了改變,保存了返回_A函數的地址 image.png

當遇到ret指令之後觸發lr寄存器中存儲的指令 image.png

image.png

再次遇到ret指令仍然會觸發lr寄存器中存儲的指令,此時問題就來了,lr跳轉到_B函數之後保存了返回_A函數的地址,可是沒有記錄返回ViewDidLoad函數的地址,因而形成了死循環 image.png

一直循環 image.png

這就找到了上節中死循環的緣由

保存回家的路(lr寄存器)

函數跳轉關係爲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函數的指令地址 image.png

此時從_A函數的棧中恢復了lr寄存器的值 image.png

遇到ret指令之後 image.png

能夠正常返回ViewDidLoad函數,上節遺留的死循環問題解決🎉 image.png

這兩句指令能夠優化爲一行指令

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
複製代碼

lr和pc小結

經過以上練習咱們知道當沒有遇到bl指令時lr寄存器和pc寄存器保存的都是即將執行的指令地址,可是遇到bl指令之後lr寄存器的值就再也不改變,直到遇到ret指令或者另外一條bl指令纔會改變,lr寄存器能夠理解爲函數嵌套調用時返回上一級函數的路徑,pc寄存器只是簡單指向下一條即將執行的指令。 當函數只有一級嵌套時咱們不須要對lr寄存器作操做,可是當函數多級嵌套時咱們就須要手動保存lr寄存器的值,不然會形成死循環

帶參數的函數

不會寫不要緊,寫個高級函數看看系統怎麼生成的

image.png

首先將參數保存在寄存器w0,w1中 image.png

先將寄存器w0,w1的值保存到棧上,再從棧上讀取到寄存器w8,w9上,對w8,w9作加法結果保存到w0,函數執行結束,看起來很囉嗦,這或許跟編譯時沒有編譯優化又關係 image.png

那麼咱們就能夠這樣實現一個帶參數的函數

.text
.global _A


_A:
    add x0,x0,x1
    ret
複製代碼

執行結果沒問題撒花鼓掌🎉👏 image.png

ARM64下,函數的參數時存放在x0-x7(w0-w7)這8個寄存器裏面的,若是超過8個參數就會入棧,函數的返回值時存放在x0寄存器裏面的,若是8字節裝不下也會放在棧空間

爲了效率考慮,咱們在寫OC代碼時參數總數最好不要超過6個,由於函數自己有兩個隱形參數self和selector,若是必須超過6個最好使用數組或者結構體指針

相關文章
相關標籤/搜索