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

iOS彙編

iOS彙編語音有不少鍾。常見的有8086彙編、arm彙編、x86彙編等等。xcode

arm彙編

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

iPhone 5C是最後一款arm32位版本的iPhone,在iPhone5s以後,全部的iPhone設備都採用arm64架構。arm64彙編在真機上使用,以下:markdown

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, [sp, #0x10]
->  0x10286e588 <+20>: str    w9, [sp, #0xc]
    0x10286e58c <+24>: str    w8, [sp, #0x8]
    0x10286e590 <+28>: add    sp, 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,就須要學習彙編的知識。iphone

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位的寄存器w2九、w30,並且x29和x30寄存器有着特殊的用途,因此在此我只講x0 ~ x28記爲通用寄存器測試

程序計數器

pc (Program Counter)寄存器,它記錄着當前CPU正在執行的指令的地址,經過register read pc查看寄存器中存儲的值spa

(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, #0x4
mov x1, #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, #0x4
mov x1, #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, #0x4
mov x1, #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, [x1, #8] ;將存儲器地址爲x1+8的字數據讀入寄存器x0
LDR x0, [x1, x2]!   ;將存儲器地址爲x1+x2的字數據讀入寄存器x0,並將新地址x1+x2寫入x1
LDR x0, [x1, #8]! ;將存儲器地址爲x1+8的字數據讀入寄存器x0,並將新地址x1+8寫入x1
LDR x0, [x1], x2    ;將存儲器地址爲x1的字數據讀入寄存器x0,並將新地址x1+x2寫入x1
LDR x0, [x1, x2, LSL#2]! ;將存儲地址爲x1+x2*4的字數據寫入寄存器x0,並將新地址x1+x2*4寫入x1
LDR x0. [x1], x2, 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, [x1, #8]

LDUR x0, [x1, #-8]
複製代碼
LDP指令

LDP中的P是pair的簡稱,能夠看出LDP能夠同時操做兩個寄存器

; 如下命令表示,從sp寄存器的地址加上0x30後的地址開始,讀取前8個字節的數據存放到寄存器x29中,讀取後8個字節的數據放入x30寄存器中
ldp    x29, x30, [sp, #0x30]
複製代碼
內存寫入指令STR、STUR、STP
STR指令

STP指令的格式爲:

STR{條件} 源寄存器, <存儲器地址>
複製代碼

STR指令用於從源寄存器中將一個32位的字數據傳送到存儲器中。示例以下

STR x0, [x1], #8 ;將x0中的字數據寫入以x1爲地址的存儲器中,並將新地址x1+8寫入x1
STR x0, [x1, #8] ;將x0中的字數據寫入以x1+8爲地址的存儲器中
複製代碼
STUR指令

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

STR x0, [x1, #8]

STUR x0, [x1, #-8]
複製代碼
STP指令

STP指令能夠同時操做兩個寄存器

; 如下指令表示,將x29+x30的字數據寫入以sp+0x8爲地址的存儲器中,
stp    x29, x30, [sp, #0x8]
複製代碼

零寄存器

零寄存器中存放的值爲0,主要做用是進行寄存器的置0操做

  • wzr(32位零寄存器)
  • xzr(64位零寄存器)
  • 在OC代碼中若是給變臉賦值爲0,實際上是執行以下指令
#OC代碼
int a = 0;

; 彙編代碼
str    wzr, [sp, #0xc]
複製代碼

具體效果是將wzr寄存器中的字數據,也就是0,寫入sp+0xc爲地址的存儲器中

尋址方式

所謂尋址方式就是處理器根據指令中給出的地址信息來尋找物理地址的方法,目前ARM支持如下幾種常見的尋址方式

當即尋址

當即尋址也叫作當即數尋址,是一種特殊的尋址方式,操做數自己就在指令中給出來,只要取出指令也就取到了操做數,這個操做數被稱爲當即數,對應的尋址方式也叫作當即尋址,例如如下指令:

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

在以上兩條指令中,第二個操做數即爲當即數,要求以「#」號爲前綴,對於以16進製表示的當即數,還要求在「#」後加上「0x」或「&」。

寄存器尋址

寄存器尋址就是利用寄存器中的數值做爲操做數,這種尋址方式是各種微處理器常常採用的一種方式,也是一種執行效率較高的尋址方式,指令以下

ADD x0, x1, x2          ; x0 ← x1+x2
複製代碼

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

寄存器間接尋址

寄存器間接尋址就是以寄存器中的值做爲操做數的地址,而操做數自己存放在存儲器中,例如以下指令

ADD x0, x1, [x2]        ; x0 ← x1+[x2]
LDR x0, [x1]            ; x0 ← [x1]
STR x0, [x1]            ; [x1] ← x0
複製代碼
  • 第一條指令中,以寄存器x2的值做爲操做數的地址,在寄存器中取得一個操做數後與x1相加,結果存儲到寄存器x0中
  • 第二條指令是將以x1的值爲地址的存儲器中的數據傳送到x0中
  • 第三條指令是將x0的值傳送到以x1的值爲地址的存儲器中

基址變址尋址

基址變址尋址就是將寄存器(該寄存器通常稱做基址寄存器)的內容與指令中給出的地址偏移量相加,從而獲得一個操做數的有效地址。變址尋址方式經常使用於訪問某基地址附近的地址單元。採用變址尋址方式的指令有如下常見的幾種形式:

LDR x0, [x1, #4] ; x0 ← [x1+4]
LDR x0, [x1, #4]! ; x0 ← [x1+4]、x1 ← x1+4
LDR x0, [x1], #4 ; x0 ← [x1]、x1 ← x1+4
LDR x0, [x1, x2]        ; x0 ← [x1+x2]
複製代碼
  • 第一條指令中,將寄存器x1的內容加上4造成操做數的有效地址,從而取得操做數存入寄存器x0中
  • 第二條指令中,將寄存器x1的內容加上4造成操做數的有效地址,從而取得操做數存入寄存器x0中,讓x1寄存器的內容自增4個字節
  • 第三條指令中,以寄存器x1的內容做爲操做數的有效地址,從而取得操做數存入寄存器x0中,而後寄存器x1的內容自增4個字節
  • 第四條指令中,將寄存器x1的內容加上寄存器x2的內容造成操做數的有效地址,從而取得操做數存入寄存器x0中

多寄存器尋址

採用多寄存器尋址方式,一條指令能夠完成多個寄存器值的傳送,這種尋址方式能夠用一條指令完成傳送最多16個通用寄存器的值,指令格式以下:

LDMIA x0, [x1, x2, 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, sp, #16 ; sp = sp - 16
    
	orr	w8, wzr, #0x2
	orr	w9, wzr, #0x1
	str	w9, [sp, #12]
	str	w8, [sp, #8]
	
	add	sp, sp, #16 ; sp = sp + 16
	ret
複製代碼
  • 獲得彙編代碼以後,咱們就來一句一句分析彙編代碼
    • sub sp, 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, wzr, #0x2指令是將wzr寄存器的值與#0x2作邏輯或運算,獲得的結果存放在w8寄存器中。通俗一點就是將#0x2賦值給了寄存器w8。指令orr w9, wzr, #0x1就是將#0x1賦值給了寄存器w9。
  • str w9, [sp, #12]指令表示將寄存器w9的值寫入到以sp + 12的地址開始4個字節大小的內存中去,str w8, [sp, #8]指令同上。具體操做流程以下圖:

  • 執行完內存的存儲操做以後,當前棧空間的工做已經完成,爲了保持堆棧平衡,須要將堆棧指針sp的位置還原成函數調用以前的初始位置。add sp, 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, sp, #32 ; sp=sp-32
	stp	x29, x30, [sp, #16] ; 8-byte Folded Spill
	add	x29, sp, #16 ; x29=sp+16

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

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

    • sub sp, sp, #32指令是執行內存分配的操做,將sp指針向前偏移32位,獲得一片連續的內存空間

    • stp x29, x30, [sp, #16]指令是將x29(fp)和x30(lr)寄存器中存放的值寫入以sp+16的地址爲起始地址的一段內存空間中去,每一個寄存器佔8個字節的空間。

    • add x29, sp, #16指令是將sp + 16的地址存放在x29(fp)寄存器中,由此,能夠獲得從sp到x29(fp),這兩個地址之間的一段內存空間就是當前函數可使用的內存空間。

    如上圖所示,橙色的那段內存就是咱們可使用的內存空間。

    • orr w8, wzr, #0x4和orr w9, wzr, #0x3其實就是將4賦值給寄存器w8,將3賦值給寄存器w9

    • stur w9, [x29, #-4]指令是將w9中存儲的值,也就是3,寫入到以x29(fp)- 4的地址爲開始地址的4個字節的內存中去。str w8, [sp, #8]指令則是將w8中存儲的值4,寫入到以sp+8爲起始地址的4個字節的內存中去,以下

    • bl _leafFuncion指令則表示跳轉到_leadFunction函數的操做,前面提到過,執行bl指令以前,會將bl指令的下一條彙編指令ldp x29, x30, [sp, #16]的地址存放到lr寄存器中,以便執行完_leadFunction函數以後,能跳轉回ldp x29, x30, [sp, #16]指令繼續執行。這就能夠明白爲何以前須要先存儲lr寄存器中的值,由於一旦執行完bl _leafFuncion指令以後,若是不將lr指令重置爲初始值的話,一旦執行到後面的ret函數,會從新跳到ldp x29, x30, [sp, #16]指令的地址處從新執行,如此反覆。

    • 執行完_leadFunction函數以後,會回到lr中存儲的地址,也就是ldp x29, x30, [sp, #16]指令繼續執行。ldp x29, x30, [sp, #16]指令的做用是以sp+16的地址爲開始地址,依次讀取16個字節的數據,前8個字節的數據存放到x29(fp)寄存器中去,後8個字節的數據存放到x30(lr)寄存器中去。其實就是將x29(fp)和x30(lr)寄存器的值恢復到調用函數以前所存放的值。

    • 最後add sp, sp, #32指令是將sp+32的地址值賦值給sp,其實就是還原sp指針的值,至此整個函數就調用完畢,給當前函數分配的內存空間就成了垃圾內存空間,能夠給以後的函數重複使用。至此,咱們就能夠明白葉子函數和非葉子函數的區別,以及堆棧指針在當前函數調用過程當中起到的做用。

    在非葉子函數調用過程當中,sp指針一直指向被分配棧空間的棧頂,因此又叫作棧頂指針,而fp指針指向可用棧空間的棧底,因此又叫作棧底指針。兩個指針所指地址的中間一段內存就是函數可使用的內存空間。

    函數執行開始和結束的彙編指令就是用來分配內存以及維持堆棧平衡的操做。

相關文章
相關標籤/搜索