1. 程式人生 > IOS開發 >iOS逆向學習之十(arm64彙編入門)

iOS逆向學習之十(arm64彙編入門)

iOS彙編

iOS彙編語音有很多鍾。常見的有8086彙編、arm彙編、x86彙編等等。

arm彙編

iOS的架構從最初的armv6發展到後來的armv7和armv7s,最後發展到現在的arm64,不管是armv6還是後來的armv7,以及arm64都是arm處理器的指令集。armv7和armv7s是真機32位處理器使用的架構,而arm64是真機64位處理器使用的架構。

iPhone 5C是最後一款arm32位版本的iPhone,在iPhone5s之後,所有的iPhone裝置都採用arm64架構。arm64彙編在真機上使用,如下:

TestFont`-[ViewController test]:
    0x10286e574 <+0>:  sub    sp,sp,#0x20             ; =0x20 
0x10286e578 <+4>: mov w8,#0x14 0x10286e57c <+8>: mov w9,#0xa 0x10286e580 <+12>: str x0,[sp,#0x18] 0x10286e584 <+16>: str x1,#0x10] -> 0x10286e588 <+20>: str w9,#0xc] 0x10286e58c <+24>: str w8,#0x8] 0x10286e590 <+28>: add sp,#0x20 ; =0x20
0x10286e594 <+32>: ret 複製程式碼

x86彙編

x86彙編是模擬器使用的組合語言,它的指令和arm64彙編的語法不同,如下

TestFont`-[ViewController test]:
    0x10b089520 <+0>:  pushq  %rbp
    0x10b089521 <+1>:  movq   %rsp,%rbp
    0x10b089524 <+4>:  movq   %rdi,-0x8(%rbp)
    0x10b089528 <+8>:  movq   %rsi,-0x10(%rbp)
->  0x10b08952c <+12>: movl   $0xa
,-0x14(%rbp) 0x10b089533 <+19>: movl $0x14,-0x18(%rbp) 0x10b08953a <+26>: popq %rbp 0x10b08953b <+27>: retq 複製程式碼

為什麼要學習arm64彙編?

程式碼除錯

在平常開發中,在除錯程式的時候,如果程式crash,通常會定位到具體的崩潰程式碼。但是有時候也會遇到一些比較詭異的crash,比如說崩潰在了系統庫中,這個時候定位到具體的crash原因會非常困難。如果利用匯編除錯技巧來進行除錯,可能會讓我們事半功倍。

逆向除錯

在逆向別人App過程中,我們可以通過LLDB對記憶體地址進行斷點操作,但是當執行到斷點時,LLDB展現給我們的是彙編程式碼,而不是OC程式碼,所以想要逆向並且動態除錯別人的App,就需要學習彙編的知識。

arm64彙編入門

想要學習arm64彙編,需要從以下三個方面入手,暫存器、指令和堆疊。

暫存器

arm64中有34個暫存器,如下

通用暫存器

  • 64 bit的通用暫存器喲28個,分別是x0 ~ x28
  • 32 bit的也有28個,分別是w0 ~ w28(屬於x0 ~ x28的低32位)

  • 其中x0 ~ x7通常拿來存放函式的引數,如果引數更多,則採用堆疊來進行傳遞
  • x0中通常存放函式的返回值

也會有人將x0 ~ x30叫做通用暫存器,但是在實際使用中x29和x30並沒有對應的低32位的暫存器w29、w30,而且x29和x30暫存器有著特殊的用途,所以在此我只講x0 ~ x28記為通用暫存器

程式計數器

pc (Program Counter)暫存器,它記錄著當前CPU正在執行的指令的地址,通過register read pc檢視暫存器中儲存的值

(lldb) register read pc
      pc = 0x000000010286e588  TestFont`-[ViewController test] + 20 at ViewController.m:28
(lldb) 
複製程式碼

堆疊指標

  • sp (Stack Pointer)
  • fp (Frame Pointer),也就是之前所說的x29

連結暫存器

lr (Link Register)暫存器,也就是之前所說的x30暫存器,它儲存著函式的返回地址

程式狀態暫存器

arm體系中包含一個當前程式狀態暫存器cpsr (Current Program Status Register)和五個備份的程式狀態暫存器spsr (Saved Program Status Registe),備份的程式狀態暫存器用來進行異常處理。

  • 程式狀態暫存器的每一位都有特定的用途,此處只介紹幾種常用的標誌位

  • 其中N、Z、C、V均為條件碼標誌位,他們的內容可被算數或者邏輯運算的結果所改變,並且可以決定某條指令是否被執行。條件碼標誌各位的具體含義如下

指令

ARM指令列表

ARM指令如下:

助記符 ARM指令及功能描述
ADC 帶進位加法指令
ADD 加法指令
AND 邏輯與指令
B 跳轉指令
BIC 位清除指令
BL 帶返回的跳轉指令
BLX 帶返回和狀態切換的跳轉指令
BX 帶狀態切換的跳轉指令
CDP 協處理器資料操作指令
CMN 比較反值指令
CMP 比較指令
EOR 異或指令
LDC 儲存器帶協處理器的資料傳輸指令
LDM 載入多個暫存器指令
LDR 儲存器到暫存器的資料傳輸指令
MCR 從ARM暫存器到協處理器暫存器的資料傳輸指令
MLA 乘加運算指令
MOV 資料傳送指令
MRC 從協處理器暫存器到ARM暫存器的資料傳輸指令
MRS 傳送CPSR或SPSR的內容到通用暫存器指令
MSR 傳送通用暫存器到CPSR或SPSR指令
MUL 32位乘法指令
MLA 32位乘加指令
MVN 資料反傳送指令
ORR 邏輯或指令
RSB 逆向減法指令
RSC 帶借位的逆向減法指令
SBC 帶借位減法指令
STC 協處理器暫存器寫入儲存器指令
STM 批量記憶體字寫入指令
STR 暫存器到暫存器的資料傳輸指令
SUB 減法指令
SWI 軟體中斷指令
SWP 交換指令
TEQ 相等測試指令
TST 位測試指令

常用指令介紹

mov指令

指令介紹

mov指令可以將另一個暫存器、被移位的暫存器或者將一個立即數載入到目的暫存器

mov指令在arm64彙編中的實際使用
  • 在xcode中新建test.s檔案,在test.s檔案中新增以下程式碼
; 此處.text表示此程式碼放在text段中
.text
; .global表示將後面跟隨的方法給暴露出去,不然外部無法呼叫,方法名以_開頭
.global _test

; 此處為_test方法
_test:
; mov指令,將立即數4載入到x0暫存器中
mov x0,#0x4
mov x1,x0
; 彙編指令中,ret表示函式的終止
ret
複製程式碼
  • 在xcode中新建test.h標頭檔案,將test.s中的_test方法暴露出來
#ifndef test_h
#define test_h

void test(void);

#endif /* test_h */
複製程式碼
  • 在viewDidLoad中呼叫test()函式,然後在LLDB中使用register read x0 讀取暫存器中存放的值
(lldb) register read x0
      x0 = 0x000000010320c980
(lldb) si
(lldb) register read x0
      x0 = 0x0000000000000004
(lldb) register read x1
      x1 = 0x00000001e60f3bc7  "viewDidLoad"
(lldb) si
(lldb) register read x1
      x1 = 0x0000000000000004
複製程式碼

通過對彙編指令增加斷點,一步一步除錯可以看出,在執行完mov指令後,x0和x1暫存器的值都被修改了

ret指令

ret指令表示函式的返回,而且它還有一個非常重要的作用,就是將lr(x30)暫存器的值賦值給pc暫存器

  • 在viewDidLoad中呼叫test()函式,在test()函式上打上斷點,執行程式如下

  • 使用register read 檢視lr和pc暫存器的值
(lldb) register read lr
      lr = 0x00000001021965a4  TestFont`-[ViewController viewDidLoad] + 68 at ViewController.m:23
(lldb) register read pc
      pc = 0x00000001021965a4  TestFont`-[ViewController viewDidLoad] + 68 at ViewController.m:23
(lldb) 
複製程式碼

此時,lr暫存器和pc暫存器的值都是test()函式起始地址

  • 使用si指令跳轉到test()函式中

  • 再次檢視lr和pc暫存器的值,發現lr的值變成了test()函式的下一條指令的地址,也就是test()函式執行完成之後,主程式需要執行的下一條指令。pc暫存器儲存了當前即將執行的指令的地址,如下
(lldb) register read lr
      lr = 0x00000001021965a8  TestFont`-[ViewController viewDidLoad] + 72 at ViewController.m:24
(lldb) register read pc
      pc = 0x0000000102196abc  TestFont`test
複製程式碼
  • 執行完test()函式,發現程式跳轉到了lr暫存器所儲存的指令地址,也就是0x00000001021965a8,此時再次檢視lr和pc暫存器的值,發現pc暫存器存放的地址已經變成了lr暫存器存放的地址
(lldb) register read lr
      lr = 0x00000001021965a8  TestFont`-[ViewController viewDidLoad] + 72 at ViewController.m:24
(lldb) register read pc
      pc = 0x00000001021965a8  TestFont`-[ViewController viewDidLoad] + 72 at ViewController.m:24
(lldb) 
複製程式碼

add指令

add指令是將兩個運算元相加,並將結果存放到目標暫存器中。具體說明如下

在arm64彙編中,相應的就是操作x0~x28,執行如下彙編程式碼

.text
.global _test

_test:

mov x0,#0x3

add x0,x1,x0

ret
複製程式碼

執行完test()函式,通過register read查詢x0的值,最後可以看到x0存放的值為7,如下

(lldb) register read x0
      x0 = 0x0000000000000004
(lldb) si
(lldb) register read x1
      x1 = 0x0000000000000003
(lldb) si
(lldb) register read x0
      x0 = 0x0000000000000007
複製程式碼

sub指令

sub指令是將運算元1減去運算元2,再減去cpsr中的C條件標誌位的反碼,並將結果存放到目標暫存器中

cmp指令

cmp指令是把一個暫存器的內容和另一個暫存器的內容或者立即數做比較,同時會更新CPSR暫存器中條件標誌位的值

  • 執行如下彙編程式碼
.text
.global _test

_test:

mov x0,#0x3

cmp x0,x1

ret
複製程式碼
  • 在執行cmp程式碼之前和之後各列印一次CPSR暫存器的值如下
(lldb) register read cpsr
    cpsr = 0x60000000
(lldb) si
(lldb) si
(lldb) si
(lldb) register read cpsr
    cpsr = 0x20000000
(lldb) 
複製程式碼

可以發現,在執行cmp操作之後,cpsr暫存器的值變成了0x20000000,轉換成16進位制後,得到32位標誌位如下

可以發現第31位,也就是N位的值為0,同時第30位,也就是Z位的值也為0,這就表示,x0和x1暫存器相比較之後的值為非零非負,而使用x0 - x1得到的結果是1,符合非零非負的條件。

  • 修改彙編程式碼,調換x0和x1暫存器的位置,如下
_test:

mov x0,#0x3

cmp x1,x0

ret
複製程式碼
  • 再次在cmp程式碼執行前後讀取CPSR暫存器的值
(lldb) register read cpsr
    cpsr = 0x60000000
(lldb) s
(lldb) register read cpsr
    cpsr = 0x80000000
(lldb) 
複製程式碼

這個時候,cpsr暫存器的值變成了0x80000000,轉換成16進位制後,如下

可以看出,第31位N位的值變成了1,第30位Z位的值為0,這表示,x0和x1暫存器相比較之後的值為非零負數,使用x1-x0得到的結果是-1,符合非零負數的條件

跳轉指令

B指令

B指令是最簡單的跳轉指令,一旦遇到B指令,程式會無條件跳轉到B之後所指定的目標地址處執行。

BL指令

BL指令是另一個跳轉指令,但是在跳轉之前,它會先將當前標記位的下一條指令儲存在暫存器lr(x30)中,然後跳轉到標記處開始執行程式碼,當遇到ret時,會將lr(x30)中儲存的地址重新載入到PC暫存器中,使得程式能返回標記位的下一條指令繼續執行。

  • 首先執行以下彙編程式碼
.text
.global _test

label:
mov x0,#0x1
mov x1,#0x8
ret

_test:
mov x0,#0x4
bl label
mov x1,#0x3
cmp x1,x0
ret
複製程式碼
  • 斷點到bl label指令時,讀取lr暫存器和PC暫存器的值

  • 執行bl label指令,跳轉到label標記處,再次讀取lr(x30)暫存器和PC暫存器的值,這個時候會發現lr(x30)暫存器存放的地址已經變成mov x1,#0x3這條指令的記憶體地址

  • 執行完label標記中的所有程式碼,發現程式再次回到lr暫存器所儲存的地址,也就是mov x1,#0x3這句指令繼續執行,並且此時pc暫存器所儲存的地址也變成了mov x1,#0x3這句指令的地址。

條件域指令

當處理器工作在arm狀態時,幾乎所有的指令均根據CPSR暫存器中條件碼的狀態和指令的條件域有條件的執行,當指令的執行條件滿足時,指令被執行,否則指令被忽略。
每一條ARM指令包含4位的條件碼,位於指令的最高四位[31:28]。條件碼共有16種,每種條件碼可用兩個字元表示,這兩個字元可用新增在指令助記符的後面和指令同時使用。例如:跳轉指令B後可用加上字尾EQ變為BEQ,表示相等則跳轉,即當CPSR暫存器中的Z標誌置位時發生跳轉。

OC程式碼演示條件域指令的作用
  • 在ViewController中增加以下程式碼
- (void)test{
    int a = 1;
    int b = 2;
    if (a == b) {
        NSLog(@"a==b");
    }else{
        printf("a!=b");
    }
}
複製程式碼
  • 斷點到test方法中,得到關鍵彙編程式碼如下

  • 其中w8,w9分別存放這0x2和0x1,cmp指令則對比w8和w9暫存器的值,並且修改CPSR暫存器對應的標誌位
  • 執行cmp w8,w9指令後,檢視CPSR暫存器的值如下
(lldb) register read cpsr
    cpsr = 0x80000000
複製程式碼

得到對應16進位制的值為

  • b.ne 0x102522584這條指令表示如果CPSR中的Z標誌位(即第30位)為0,則執行跳轉操作,跳轉到0x102522584地址處指令執行,如上圖所示。通常也可以理解為w8和w9兩個暫存器存放的立即數不相等時,則執行跳轉操作。此處因為1!=2,所以跳轉到0x102522584處執行。
條件標誌碼

在16種條件標誌碼中,只有15種可以使用,如下圖,第16種(1111)為系統保留,暫時不能使用

記憶體操作指令

記憶體操作指令分為記憶體讀取和記憶體寫入指令

記憶體讀取指令LDR、LDUR、LDP
LDR指令格式為
LDR(條件) 目的暫存器,<儲存器地址>
複製程式碼

LDR指令用於從儲存器中將一個32位的字資料傳送到目的暫存器中。該指令通常用於從儲存器中讀取32位字資料到通用暫存器中,然後對資料進行處理。當程式計數器PC作為目的暫存器時,指令從儲存器中讀取的字資料被當做目的地址,從而實現程式流程的跳轉。該指令在程式設計中比較常用,切定址方式靈活多樣。示例如下:

LDR x0,[x1]        ;將儲存器地址為x1的字資料讀入暫存器x0
LDR x0,[x1,x2]    ;將儲存器地址為x1+x2的字資料讀入暫存器x0
LDR x0,#8]    ;將儲存器地址為x1+8的字資料讀入暫存器x0
LDR x0,x2]!   ;將儲存器地址為x1+x2的字資料讀入暫存器x0,並將新地址x1+x2寫入x1
LDR x0,#8]!   ;將儲存器地址為x1+8的字資料讀入暫存器x0,並將新地址x1+8寫入x1
LDR x0,[x1],x2    ;將儲存器地址為x1的字資料讀入暫存器x0,x2,LSL#2]!    ;將儲存地址為x1+x2*4的字資料寫入暫存器x0,並將新地址x1+x2*4寫入x1
LDR x0. [x1],LSL#2     ;將儲存地址為x1的字資料寫入暫存器x0,並將新地址x1+x2*4寫入x1
複製程式碼

通過一個簡單的例子來了解LDR的作用:

  • 首先建立test.s檔案,在檔案中新增如下程式碼
.text
.global _test

_test:
; ldr指令,找到x1暫存器中儲存的地址,從該地址開始讀取8個位元組的資料,存放到x0暫存器中
ldr x0,[x1]

ret
複製程式碼

為什麼此處是讀取8個位元組的資料呢?因為目標暫存器x0可以存放8個位元組的資料,如果將x0換成w0,則讀取4個位元組的資料存放到w0中

  • 在viewDidLoad中呼叫test()函式,同時在test()函式之前宣告一個區域性變數,如下
- (void)viewDidLoad{
    [super viewDidLoad];

    int a = 5;
    test();

}
複製程式碼
  • 斷點到test()函式處,執行程式,首先讀取變數a的記憶體地址,將其記憶體地址存放到x1暫存器中,操作如下

可以發現,此時的x1暫存器存放著a變數的地址。

  • 輸入si執行語句ldr x0,[x1],檢視x0暫存器的值,此時發現x0暫存器的值變為0x31e09a5000000005,而不是5,這是因為變數a是int型別,而int型別為4個位元組,但是LDR指令會將x1暫存器存放地址開始的8個位元組的資料讀取出來存放到x0暫存器中,所以x0暫存器中存放的值不是5,通過x 0x000000016f2c52ec也可以看出
(lldb) x 0x000000016f2c52ec
0x16f2c52ec: 05 00 00 00 50 9a e0 31 01 00 00 00 58 0f b4 00  ....P..1....X...
0x16f2c52fc: 01 00 00 00 c7 3b 0f e6 01 00 00 00 50 9a e0 31  .....;......P..1
複製程式碼

前4個位元組存放的是5,也就是變數a的值

  • 將x0暫存器換成w0,重新執行上面的步驟,最後會發現w0中存放的是變數a的值,也就是5
LDUR指令

LDUR指令用法和LDR指令相同,區別在於LDUR後的立即數為負數,如下

LDR x0,#8]

LDUR x0,#-8]
複製程式碼
LDP指令

LDP中的P是pair的簡稱,可以看出LDP可以同時操作兩個暫存器

; 以下命令表示,從sp暫存器的地址加上0x30後的地址開始,讀取前8個位元組的資料存放到暫存器x29中,讀取後8個位元組的資料放入x30暫存器中
ldp    x29,x30,#0x30]
複製程式碼
記憶體寫入指令STR、STUR、STP
STR指令

STP指令的格式為:

STR{條件} 源暫存器,<儲存器地址>
複製程式碼

STR指令用於從源暫存器中將一個32位的字資料傳送到儲存器中。示例如下

STR x0,#8        ;將x0中的字資料寫入以x1為地址的儲存器中,並將新地址x1+8寫入x1
STR x0,#8]        ;將x0中的字資料寫入以x1+8為地址的儲存器中
複製程式碼
STUR指令

STUR指令和STR指令用法相同,區別在於STUR後的立即數為負數

STR x0,#8]

STUR x0,#-8]
複製程式碼
STP指令

STP指令可以同時操作兩個暫存器

; 以下指令表示,將x29+x30的字資料寫入以sp+0x8為地址的儲存器中,
stp    x29,#0x8]
複製程式碼

零暫存器

零暫存器中存放的值為0,主要作用是進行暫存器的置0操作

  • wzr(32位零暫存器)
  • xzr(64位零暫存器)
  • 在OC程式碼中如果給變臉賦值為0,其實是執行如下指令
#OC程式碼
int a = 0;

; 彙編程式碼
str    wzr,#0xc]
複製程式碼

具體效果是將wzr暫存器中的字資料,也就是0,寫入sp+0xc為地址的儲存器中

定址方式

所謂定址方式就是處理器根據指令中給出的地址資訊來尋找實體地址的方法,目前ARM支援以下幾種常見的定址方式

立即定址

立即定址也叫做立即數定址,是一種特殊的定址方式,運算元本身就在指令中給出來,只要取出指令也就取到了運算元,這個運算元被稱為立即數,對應的定址方式也叫做立即定址,例如以下指令:

ADD x0,#1          ; x0 ← x1+1
ADD x0,#0x3f       ; x0 ← x1+0x3f
複製程式碼

在以上兩條指令中,第二個運算元即為立即數,要求以“#”號為字首,對於以16進製表示的立即數,還要求在“#”後加上“0x”或“&”。

暫存器定址

暫存器定址就是利用暫存器中的數值作為運算元,這種定址方式是各類微處理器經常採用的一種方式,也是一種執行效率較高的定址方式,指令如下

ADD x0,x2          ; x0 ← x1+x2
複製程式碼

該指令的執行效果是將暫存器x1和x2的內容相加,其結果存放在暫存器x0中

暫存器間接定址

暫存器間接定址就是以暫存器中的值作為運算元的地址,而運算元本身存放在儲存器中,例如如下指令

ADD x0,[x2]        ; x0 ← x1+[x2]
LDR x0,[x1]            ; x0 ← [x1]
STR x0,[x1]            ; [x1] ← x0
複製程式碼
  • 第一條指令中,以暫存器x2的值作為運算元的地址,在暫存器中取得一個運算元後與x1相加,結果儲存到暫存器x0中
  • 第二條指令是將以x1的值為地址的儲存器中的資料傳送到x0中
  • 第三條指令是將x0的值傳送到以x1的值為地址的儲存器中

基址變址定址

基址變址定址就是將暫存器(該暫存器一般稱作基址暫存器)的內容與指令中給出的地址偏移量相加,從而得到一個運算元的有效地址。變址定址方式常用於訪問某基地址附近的地址單元。採用變址定址方式的指令有以下常見的幾種形式:

LDR x0,#4]        ; x0 ← [x1+4]
LDR x0,#4]!       ; x0 ← [x1+4]、x1 ← x1+4
LDR x0,#4        ; x0 ← [x1]、x1 ← x1+4
LDR x0,x2]        ; x0 ← [x1+x2]
複製程式碼
  • 第一條指令中,將暫存器x1的內容加上4形成運算元的有效地址,從而取得運算元存入暫存器x0中
  • 第二條指令中,將暫存器x1的內容加上4形成運算元的有效地址,從而取得運算元存入暫存器x0中,讓x1暫存器的內容自增4個位元組
  • 第三條指令中,以暫存器x1的內容作為運算元的有效地址,從而取得運算元存入暫存器x0中,然後暫存器x1的內容自增4個位元組
  • 第四條指令中,將暫存器x1的內容加上暫存器x2的內容形成運算元的有效地址,從而取得運算元存入暫存器x0中

多暫存器定址

採用多暫存器定址方式,一條指令可以完成多個暫存器值的傳送,這種定址方式可以用一條指令完成傳送最多16個通用暫存器的值,指令格式如下:

LDMIA x0,x3,x4]      ; x1 ← [x0]
                                ; x2 ← [x0+4]
                                ; x3 ← [x0+8]
                                ; x4 ← [x0+12]
複製程式碼

該指令的字尾IA表示在每次執行完載入/儲存操作後,x0按字長度增加,因此,指令可以將連續儲存單元的值傳送到x1~x4

相對定址

與基址變址定址方式相類似,相對定址以程式計數器PC的當前值為基地址,指令中的地址標號為偏移量,將兩者相加之和得到運算元的有效地址。以下程式段完成子程式的呼叫和返回,跳轉指令BL就是採用了相對定址方式:

    BL  NEXT        ; 跳轉到子程式NEXT處執行
    ......
NEXT
    ......
    MOV PC,LR      ; 從子程式返回
複製程式碼

堆疊定址

堆疊是喲中資料結構,按先進後出(FILO)的方式工作,使用一個稱作堆疊指標的專用暫存器指示當前的操作位置,堆疊指標總是指向棧頂位置。
當棧頂指標指向最後壓入堆疊的資料時,稱為滿堆疊(Full Stack),而當堆疊指標指向下一個將要放入資料的空位置時,稱為空堆疊(Empty Stack)
同時、根據堆疊的生成方式,又可以分為遞增堆疊(Ascending Stack)和遞減堆疊(Decending Stack),當堆疊由低地址向高地址生成時,稱為遞增堆疊,當堆疊由高地址向低地址生成時,稱為遞減堆疊。這樣就有四種類型的堆疊工作方式,ARM微處理器支援這四種類型的堆疊工作方式。即:

  • 滿遞增堆疊:堆疊指標指向最後壓入的資料,且由低地址向高地址生成。
  • 滿遞減堆疊:堆疊指標指向最後壓入的資料,且由高地址向低地址生成
  • 空遞增堆疊:堆疊指標指向下一個將要放入資料的空位置,且由低地址向高地址生成
  • 空遞減堆疊:堆疊指標指向下一個將要放入資料的空位置,且由高地址向低地址生成

堆疊操作

函式的型別

在瞭解堆疊操作之前,首先得了解函式的型別,函式型別主要分為兩種:葉子函式、非葉子函式

  • 葉子函式是指在此函式中,沒有呼叫其它任何函式
  • 非葉子函式是值在此函式中,有呼叫其它函式

瞭解了什麼是葉子函式和非葉子函式,那麼我們就要從彙編程式碼的層面來深入理解葉子函式和非葉子函式的區別,以及堆疊指標在其中起到的作用。

葉子函式

上文介紹過葉子函式的具體定義,下面通過具體的彙編程式碼來深入瞭解葉子函式

  • 首先在Xcode中建立MyTest.c檔案,在檔案中新增如下程式碼
void leafFuncion(){
    int a = 1;
    int b = 2;
}
複製程式碼
  • 進入MyTest.c檔案所在目錄,使用以下指令生成MyTest.s檔案
xcrun --sdk iphoneos clang -S -arch arm64 MyTest.c -o MyTest.s
複製程式碼
  • 得到MyTest.s中的關鍵彙編程式碼如下
    sub	sp,#16             ; sp = sp - 16
    
	orr	w8,wzr,#0x2
	orr	w9,#0x1
	str	w9,#12]
	str	w8,#8]
	
	add	sp,#16             ; sp = sp + 16
	ret
複製程式碼
  • 得到彙編程式碼之後,我們就來一句一句分析彙編程式碼
    • sub sp,#16指令表示將堆疊指標sp向前偏移#16

堆疊指標<font color=red>sp</font>開始指向<font color=red>0x10010</font>,偏移之後指向<font color=red>0x10000</font>,相當於開闢了從<font color=red>0x10000</font>到<font color=red>0x10010</font>這一段記憶體供函式使用。
複製程式碼
  • orr指令用於在兩個運算元上進行邏輯或運算,並把結果放置到目的暫存器中。上文中orr w8,#0x2指令是將wzr暫存器的值與#0x2做邏輯或運算,得到的結果存放在w8暫存器中。通俗一點就是將#0x2賦值給了暫存器w8。指令orr w9,#0x1就是將#0x1賦值給了暫存器w9
  • str w9,#12]指令表示將暫存器w9的值寫入到以sp + 12的地址開始4個位元組大小的記憶體中去,str w8,#8]指令同上。具體操作流程如下圖:

  • 執行完記憶體的儲存操作之後,當前棧空間的工作已經完成,為了保持堆疊平衡,需要將堆疊指標sp的位置還原成函式呼叫之前的初始位置。add sp,#16的作用就是將sp指標的位置向後偏移16個位元組,重新指向0x10010的位置。

為什麼要維持堆疊平衡?因為在函式呼叫之前,堆疊指標sp會偏移一段記憶體地址,為當前需要呼叫的函式分配一段記憶體空間,在函式呼叫完成之後將sp指標重置到開始位置,這樣,剛剛分配的那段記憶體空間就是垃圾記憶體,下一次再有函式呼叫的時候,這段記憶體空間可重複利用。這就做到了堆疊平衡。如果函式呼叫完成之後不重置sp指標,那麼,如果有足夠多的函式一直呼叫,最後肯定會出現棧溢位的問題。

非葉子函式

非葉子函式和葉子函式的區別在於是否有呼叫其它函式,下面同樣通過具體的彙編程式碼來深入瞭解非葉子函式

  • 首先在Xcode中建立MyTest.c檔案,在檔案中新增以下程式碼
void leafFuncion(){
    int a = 1;
    int b = 2;
}

void nonLeafFunction(){
    int a = 3;
    int b = 4;
    leafFuncion();
}
複製程式碼
  • 進入MyTest.c檔案所在目錄,使用以下指令生成MyTest.s檔案
xcrun --sdk iphoneos clang -S -arch arm64 MyTest.c -o MyTest.s
複製程式碼
  • 得到MyTest.s中的關鍵彙編程式碼如下
	sub	sp,#32             ; sp=sp-32
	stp	x29,#16]     ; 8-byte Folded Spill
	add	x29,#16            ; x29=sp+16

	orr	w8,#0x4
	orr	w9,#0x3
	stur	w9,[x29,#-4]
	str	w8,#8]
	bl	_leafFuncion

	ldp	x29,#16]     ; 8-byte Folded Reload
	add	sp,#32             ; sp=sp+32
	ret
複製程式碼
  • 開始分析彙編程式碼

    • sub sp,#32指令是執行記憶體分配的操作,將sp指標向前偏移32位,得到一片連續的記憶體空間

    • stp x29,#16]指令是將x29(fp)x30(lr)暫存器中存放的值寫入以sp+16的地址為起始地址的一段記憶體空間中去,每個暫存器佔8個位元組的空間。

    • add x29,#16指令是將sp + 16的地址存放在x29(fp)暫存器中,由此,可以得到從spx29(fp),這兩個地址之間的一段記憶體空間就是當前函式可以使用的記憶體空間。

    如上圖所示,橙色的那段記憶體就是我們可以使用的記憶體空間。

    • orr w8,#0x4orr w9,#0x3其實就是將4賦值給暫存器w8,將3賦值給暫存器w9

    • stur w9,#-4]指令是將w9中儲存的值,也就是3,寫入到以x29(fp)- 4的地址為開始地址的4個位元組的記憶體中去。str w8,#8]指令則是將w8中儲存的值4,寫入到以sp+8為起始地址的4個位元組的記憶體中去,如下

    • bl _leafFuncion指令則表示跳轉到_leadFunction函式的操作,前面提到過,執行bl指令之前,會將bl指令的下一條彙編指令ldp x29,#16]的地址存放到lr暫存器中,以便執行完_leadFunction函式之後,能跳轉回ldp x29,#16]指令繼續執行。這就可以明白為什麼之前需要先儲存lr暫存器中的值,因為一旦執行完bl _leafFuncion指令之後,如果不將lr指令重置為初始值的話,一旦執行到後面的ret函式,會重新跳到ldp x29,#16]指令的地址處重新執行,如此反覆。

    • 執行完_leadFunction函式之後,會回到lr中儲存的地址,也就是ldp x29,#16]指令繼續執行。ldp x29,#16]指令的作用是以sp+16的地址為開始地址,依次讀取16個位元組的資料,前8個位元組的資料存放到x29(fp)暫存器中去,後8個位元組的資料存放到x30(lr)暫存器中去。其實就是將x29(fp)x30(lr)暫存器的值恢復到呼叫函式之前所存放的值。

    • 最後add sp,#32指令是將sp+32的地址值賦值給sp,其實就是還原sp指標的值,至此整個函式就呼叫完畢,給當前函式分配的記憶體空間就成了垃圾記憶體空間,可以給之後的函式重複使用。至此,我們就可以明白葉子函式和非葉子函式的區別,以及堆疊指標在當前函式呼叫過程中起到的作用。

    在非葉子函式呼叫過程中,sp指標一直指向被分配棧空間的棧頂,所以又叫做棧頂指標,而fp指標指向可用棧空間的棧底,所以又叫做棧底指標。兩個指標所指地址的中間一段記憶體就是函式可以使用的記憶體空間。

    函式執行開始和結束的彙編指令就是用來分配記憶體以及維持堆疊平衡的操作。