iOS ARM64 匯編入門

這里主要介紹 iOS 平臺上 ARM64 架構(gòu)的寄存器和指令以及內(nèi)存堆棧。

堆棧

在 iOS 中,棧空間是向下生長的,也就是從高地址向低地址生長,棧頂在低地址,棧底在高地址,比如:
棧頂?shù)刂罚?x00
棧底地址:0x1C

寄存器

ARM64 主要有34個(gè)寄存器,其中包括 31 個(gè)通用寄存器(x0-x30)以及 SP,PC,CPSR,可以通過在調(diào)試時(shí)輸入 register read 命令查看

寄存器是64位的,可以使用W0-W30來訪問前32位空間

x0-x7

主要用來存放函數(shù)傳遞的參數(shù),以及臨時(shí)變量,超過8個(gè)參數(shù)的存放在棧上,其中 x0 也用來存放函數(shù)的返回值

FP(x29)

通常存放棧幀地址(棧底指針),指向當(dāng)前函數(shù)棧的底部

LR(x30)

通常稱 x30 為程序鏈接寄存器,因?yàn)檫@個(gè)寄存器會記錄著當(dāng)前方法的調(diào)用方地址,也就是當(dāng)前方法返回后程序繼續(xù)執(zhí)行的下一條指令地址

SP

棧頂指針,指向當(dāng)前函數(shù)棧的頂部

PC

程序計(jì)數(shù)器,指向即將要執(zhí)行的下一條指令地址,軟件無法修改

CPSR

狀態(tài)寄存器,有些指令會修改 CPSR 的值,比如 CPM,有些指令會根據(jù) CPSR 的值來做跳轉(zhuǎn)操作。
CPSR 的每一位值都有特殊的意義,其中最常見的是 NZCV 標(biāo)志位

NZCV是狀態(tài)寄存器的條件標(biāo)志位,分別代表運(yùn)算過程中產(chǎn)生的狀態(tài),其中:
N, negative condition flag,一般代表運(yùn)算結(jié)果是負(fù)數(shù)
Z, zero condition flag, 指令結(jié)果為0時(shí)Z=1,否則Z=0;
C, carry condition flag, 無符號運(yùn)算有溢出時(shí),C=1。
V, oVerflow condition flag 有符號運(yùn)算有溢出時(shí),V=1。

常用指令

常用指令可大致分為運(yùn)算指令,尋址指令,跳轉(zhuǎn)指令等

運(yùn)算指令

mov x1,x0          ;傳送:x1 = x0
add x0,x1,x2       ;加法:x0 = x1 + x2
sub x0,x1,x2       ;減法:x0 = x1 - x2
mul x0,x1,x2.      ;乘法:x0 = x1 * x2
and x0,x0,#0xF     ;位與:x0 = x0 & 0xF
orr x0,x0,#9       ;位或:x0 = x0 | 9
eor x0,x0,#0xF     ;異或:x0 = x0 ^ 0xF;
lsl x0,#1          ;邏輯左移:x0 = x0 << 1
lsr x0,#1          ;邏輯右移:x0 = x0 >> 1,左邊統(tǒng)一補(bǔ)0
asr x0,#1          ;算術(shù)右移:x0 = x0 >> 1,左邊補(bǔ)符號位

尋址指令

分為兩種,存和取
L 打頭的基本都是取值指令,如 LDR(Load Register)、LDP(Load Pair)
S 打頭的基本都是存值指令,如 STR(Store Register)、STP(Store Pair)

ldr    x0,[x1]               ;從 x1 指向的地址里面取出一個(gè)64位大小的數(shù)存入x0
ldp    x1,x2,[x10, #0x10]    ;從 x10+0x10 指向的地址里面取出2個(gè)64位的數(shù),分別存入x1、x2
str    x5,[sp, #24]          ;往內(nèi)存中寫數(shù)據(jù)(偏移值為正), 把 x5 的值(64位的數(shù)值)存到 sp+24 指向的地址內(nèi)存上
stur   w0,[x29, #0x8]        ;往內(nèi)存中寫數(shù)據(jù)(偏移值為負(fù)),將 w0 的值存儲到 x29 - 0x8 這個(gè)地址里
stp    x29,x30,[sp, #-16]!   ;把 x29、x30 的值存到 sp-16 的地址上,并且把sp-=16  Note:后面有個(gè)感嘆號的,然后沒有stup這個(gè)指令哈
ldp    x29,x30,[sp],#16      ;從 sp 地址取出16 byte數(shù)據(jù),分別存入x29、x30,然后 sp+=16

跳轉(zhuǎn)指令

bl/b bl 是有返回的跳轉(zhuǎn);b 是無返回的跳轉(zhuǎn),BL的L也可以理解為Lr
跳轉(zhuǎn)指令可以配合條件狀態(tài)(根據(jù) CPSR 相應(yīng)位置的值)

B    ;跳轉(zhuǎn)指令,可帶條件跳轉(zhuǎn)與cmp配合使用
BL   ;帶返回的跳轉(zhuǎn)指令, 返回地址保存到LR(X30)
BLR  ; 帶返回的跳轉(zhuǎn)指令,跳轉(zhuǎn)到指令后邊跟隨寄存器中保存的地址(例:blr    x8 ;跳轉(zhuǎn)到x8保存的地址中去執(zhí)行)
RET  ;子程序返回指令,返回地址默認(rèn)保存在LR(X30)

比較指令

cmp x1,x0  ;將x1和x0(可以是立即數(shù))的值相減,并根據(jù)結(jié)果設(shè)置 CPSR 標(biāo)志位
CBZ  ;比較(Compare),如果結(jié)果為零(Zero)就轉(zhuǎn)移(只能跳到后面的指令)
CBNZ ;比較,如果結(jié)果非零(Non Zero)就轉(zhuǎn)移(只能跳到后面的指令)

入門Demo

以一個(gè)簡單的 C 程序?yàn)槔?,如下代碼:

int sum(int a, int b)
{
    int c = a + b + 1;
    return c;
}
int main() 
{
    int a = 10;
    int b = 20;
    int rs = sum(a,b);
    if (rs > 30) {
        return 0;
    }
    else {
        return 1;
    }    
}

有2個(gè)函數(shù),main 函數(shù)和 sum 函數(shù),通過 clang 命令成匯編代碼

clang -arch arm64 -S testAsm.c

最后生成的匯編代碼如下:

    .section    __TEXT,__text,regular,pure_instructions
    .build_version macos, 10, 15    sdk_version 10, 15, 4
    .globl  _sum                    ; -- Begin function sum
    .p2align    2
_sum:                                   ; @sum
    .cfi_startproc
; %bb.0:
    sub sp, sp, #16             ; =16
    .cfi_def_cfa_offset 16
    str w0, [sp, #12]
    str w1, [sp, #8]
    ldr w8, [sp, #12]
    ldr w9, [sp, #8]
    add w8, w8, w9
    add w8, w8, #1              ; =1
    str w8, [sp, #4]
    ldr w0, [sp, #4]
    add sp, sp, #16             ; =16
    ret
    .cfi_endproc
                                        ; -- End function
    .globl  _main                   ; -- Begin function main
    .p2align    2
_main:                                  ; @main
    .cfi_startproc
; %bb.0:
    sub sp, sp, #32             ; =32
    stp x29, x30, [sp, #16]     ; 16-byte Folded Spill
    add x29, sp, #16            ; =16
    .cfi_def_cfa w29, 16
    .cfi_offset w30, -8
    .cfi_offset w29, -16
    stur    wzr, [x29, #-4]
    mov w8, #10
    str w8, [sp, #8]
    mov w8, #20
    str w8, [sp, #4]
    ldr w0, [sp, #8]
    ldr w1, [sp, #4]
    bl  _sum
    str w0, [sp]
    ldr w8, [sp]
    cmp w8, #30                 ; =30
    b.le    LBB1_2
; %bb.1:
    stur    wzr, [x29, #-4]
    b   LBB1_3
LBB1_2:
    mov w8, #1
    stur    w8, [x29, #-4]
LBB1_3:
    ldur    w0, [x29, #-4]
    ldp x29, x30, [sp, #16]     ; 16-byte Folded Reload
    add sp, sp, #32             ; =32
    ret
    .cfi_endproc
                                        ; -- End function
.subsections_via_symbols

先看 sum 函數(shù)的代碼(簡單起見,這里去除一些不(看)重(不)要(懂)的代碼)

_sum:                    ; sum 函數(shù)入口
    sub sp, sp, #16      ; sp = sp-16,棧頂指針向下移16字節(jié),相當(dāng)于分配了16個(gè)字節(jié)的??臻g(iOS中,棧是向下(低地址)生長的)
    str w0, [sp, #12]    ; 將 w0(x0的低32位) 存放到 s p12的位置(參數(shù)a)
    str w1, [sp, #8]     ; 將 w1(x1的低32位) 存放到 sp+8 的位置(參數(shù)b)
    ldr w8, [sp, #12]    ; 將 sp+12 位置的值讀到 w8 上,也就是 w8=a
    ldr w9, [sp, #8]     ; 將 sp+8 位置的值讀到 w9 上,也就是 w9=b
    add w8, w8, w9       ; 兩數(shù)相加 w8 = w8 + w9
    add w8, w8, #1       ; w8 = w8 + 1
    str w8, [sp, #4]     ; 將 w8 保存到 sp+4 的位置
    ldr w0, [sp, #4]     ; 將 sp+4 的位置讀到 w0 上(前面說過,x0也用來存放函數(shù)返回值)
    add sp, sp, #16      ; 將棧頂指針向上移16字節(jié)(恢復(fù)棧指針,相當(dāng)于釋放堆棧)
    ret                  ; 函數(shù)返回

總體上邏輯比較簡單,基本上沒什么難懂的地方

這里有個(gè)不明白的地方就是

str w8, [sp, #4]
ldr w0, [sp, #4]

為什么不直接

mov w0, w8

有知道的大佬還請指教一下,為了寄存器內(nèi)容可以恢復(fù)么?

再來看 main 函數(shù)的,同樣去除一些不重要的代碼

_main:                        ; main函數(shù)入口
    sub sp, sp, #32           ; sp = sp - 32,將棧頂指針向下移動 32 字節(jié)
    stp x29, x30, [sp, #16]   ; 將 x29,x30 存儲到 sp+16 的位置(x29,x30 參考前面介紹,這里相當(dāng)于做個(gè)備份,因?yàn)楹竺嬉薷倪@2個(gè)寄存器)
    add x29, sp, #16          ; x29 = sp + 16(相當(dāng)于移動棧底指針,堆棧大小 16 字節(jié))
    stur    wzr, [x29, #-4]   ; 將 wzr(也叫零寄存器,這是一個(gè)特殊的寄存器,值為0)存儲到 x29 - 4 的位置,相當(dāng)于把這快內(nèi)存清0
    mov w8, #10               ; 將 w8 置為 10
    str w8, [sp, #8]          ; 將 w8 存儲到 sp+8 的位置
    mov w8, #20               ; 將 w8 置為 20
    str w8, [sp, #4]          ; 將 w8 存儲到 sp+4 的位置  
    ldr w0, [sp, #8]          ; 將 sp+8 讀到 w0(w0 = 10)
    ldr w1, [sp, #4]          ; 將 sp+4 讀到 w1 (w1 = 20,前面說過x0-x7是存放參數(shù)的)
    bl  _sum                  ; 跳轉(zhuǎn)到 sum 函數(shù) 
    str w0, [sp]              ; 存儲函數(shù)返回值(w0)到 sp
    ldr w8, [sp]              ; 讀取 sp 的值到 w8
    cmp w8, #30               ; 將 w8 的內(nèi)容和30比較,同時(shí)設(shè)置 CPSR 寄存器
    b.le    LBB1_2            ; 根據(jù) CPSR 結(jié)果來決定是跳轉(zhuǎn) LBB1_2(其中條件le的意思是小于等于的意思,具體可以參考我下面的鏈接)
    stur    wzr, [x29, #-4]   ; 將 x29-4 位置內(nèi)存清0 
    b   LBB1_3                ; 跳轉(zhuǎn)到 LBB1_3
LBB1_2:
    mov w8, #1                ; 將 w8 置 1  
    stur    w8, [x29, #-4]    ; 將 w8 保存到 x29-4 的位置
LBB1_3:
    ldur    w0, [x29, #-4]    ; 讀取 x29-4 位置的值到 w0 作為函數(shù)返回值
    ldp x29, x30, [sp, #16]   ; 將 sp+16 位置的值讀取到 x29,x30(恢復(fù)x29,x30)
    add sp, sp, #32           ; sp = sp + 32,恢復(fù)棧頂指針,釋放堆棧
    ret                       ; 函數(shù)返回 

main 函數(shù)和 sum 函數(shù)的分析基本就到這里,基本邏輯還是比較簡單的,這里有個(gè)最大的區(qū)別就是 main 函數(shù)有 x29,x30 的備份&恢復(fù)操作,原因是 sum 函數(shù)是葉子函數(shù),而 main 函數(shù)是非葉子函數(shù),非葉子函數(shù)有對其他函數(shù)的調(diào)用,需要開辟新的堆??臻g,以及其他函數(shù)返回后需要繼續(xù)執(zhí)行下一條指令,這需要修改 x29,x30的值,所以要先備份起來,而葉子函數(shù)就不需要。

參考

ARM64 匯編基礎(chǔ)
iOS開發(fā)同學(xué)的arm64匯編入門
iOS 常用匯編指令集

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容