iOS ARM64 汇编入门

这里主要介绍 iOS 平台上 ARM64 架构的寄存器和指令以及内存堆栈。

堆栈

在 iOS 中,栈空间是向下生长的,也就是从高地址向低地址生长,栈顶在低地址,栈底在高地址,比如:
栈顶地址:0x00
栈底地址:0x1C

寄存器

ARM64 主要有34个寄存器,其中包括 31 个通用寄存器(x0-x30)以及 SP,PC,CPSR,可以通过在调试时输入 register read 命令查看

寄存器是64位的,可以使用W0-W30来访问前32位空间

x0-x7

主要用来存放函数传递的参数,以及临时变量,超过8个参数的存放在栈上,其中 x0 也用来存放函数的返回值

FP(x29)

通常存放栈帧地址(栈底指针),指向当前函数栈的底部

LR(x30)

通常称 x30 为程序链接寄存器,因为这个寄存器会记录着当前方法的调用方地址,也就是当前方法返回后程序继续执行的下一条指令地址

SP

栈顶指针,指向当前函数栈的顶部

PC

程序计数器,指向即将要执行的下一条指令地址,软件无法修改

CPSR

状态寄存器,有些指令会修改 CPSR 的值,比如 CPM,有些指令会根据 CPSR 的值来做跳转操作。
CPSR 的每一位值都有特殊的意义,其中最常见的是 NZCV 标志位

NZCV是状态寄存器的条件标志位,分别代表运算过程中产生的状态,其中:
N, negative condition flag,一般代表运算结果是负数
Z, zero condition flag, 指令结果为0时Z=1,否则Z=0;
C, carry condition flag, 无符号运算有溢出时,C=1。
V, oVerflow condition flag 有符号运算有溢出时,V=1。

常用指令

常用指令可大致分为运算指令,寻址指令,跳转指令等

运算指令

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,左边统一补0
asr x0,#1          ;算术右移:x0 = x0 >> 1,左边补符号位

寻址指令

分为两种,存和取
L 打头的基本都是取值指令,如 LDR(Load Register)、LDP(Load Pair)
S 打头的基本都是存值指令,如 STR(Store Register)、STP(Store Pair)

ldr    x0,[x1]               ;从 x1 指向的地址里面取出一个64位大小的数存入x0
ldp    x1,x2,[x10, #0x10]    ;从 x10+0x10 指向的地址里面取出2个64位的数,分别存入x1、x2
str    x5,[sp, #24]          ;往内存中写数据(偏移值为正), 把 x5 的值(64位的数值)存到 sp+24 指向的地址内存上
stur   w0,[x29, #0x8]        ;往内存中写数据(偏移值为负),将 w0 的值存储到 x29 - 0x8 这个地址里
stp    x29,x30,[sp, #-16]!   ;把 x29、x30 的值存到 sp-16 的地址上,并且把sp-=16  Note:后面有个感叹号的,然后没有stup这个指令哈
ldp    x29,x30,[sp],#16      ;从 sp 地址取出16 byte数据,分别存入x29、x30,然后 sp+=16

跳转指令

bl/b bl 是有返回的跳转;b 是无返回的跳转,BL的L也可以理解为Lr
跳转指令可以配合条件状态(根据 CPSR 相应位置的值)

B    ;跳转指令,可带条件跳转与cmp配合使用
BL   ;带返回的跳转指令, 返回地址保存到LR(X30)
BLR  ; 带返回的跳转指令,跳转到指令后边跟随寄存器中保存的地址(例:blr    x8 ;跳转到x8保存的地址中去执行)
RET  ;子程序返回指令,返回地址默认保存在LR(X30)

比较指令

cmp x1,x0  ;将x1和x0(可以是立即数)的值相减,并根据结果设置 CPSR 标志位
CBZ  ;比较(Compare),如果结果为零(Zero)就转移(只能跳到后面的指令)
CBNZ ;比较,如果结果非零(Non Zero)就转移(只能跳到后面的指令)

入门Demo

以一个简单的 C 程序为例,如下代码:

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个函数,main 函数和 sum 函数,通过 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 函数的代码(简单起见,这里去除一些不(看)重(不)要(懂)的代码)

_sum:                    ; sum 函数入口
    sub sp, sp, #16      ; sp = sp-16,栈顶指针向下移16字节,相当于分配了16个字节的栈空间(iOS中,栈是向下(低地址)生长的)
    str w0, [sp, #12]    ; 将 w0(x0的低32位) 存放到 s p12的位置(参数a)
    str w1, [sp, #8]     ; 将 w1(x1的低32位) 存放到 sp+8 的位置(参数b)
    ldr w8, [sp, #12]    ; 将 sp+12 位置的值读到 w8 上,也就是 w8=a
    ldr w9, [sp, #8]     ; 将 sp+8 位置的值读到 w9 上,也就是 w9=b
    add w8, w8, w9       ; 两数相加 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也用来存放函数返回值)
    add sp, sp, #16      ; 将栈顶指针向上移16字节(恢复栈指针,相当于释放堆栈)
    ret                  ; 函数返回

总体上逻辑比较简单,基本上没什么难懂的地方

这里有个不明白的地方就是

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

为什么不直接

mov w0, w8

有知道的大佬还请指教一下,为了寄存器内容可以恢复么?

再来看 main 函数的,同样去除一些不重要的代码

_main:                        ; main函数入口
    sub sp, sp, #32           ; sp = sp - 32,将栈顶指针向下移动 32 字节
    stp x29, x30, [sp, #16]   ; 将 x29,x30 存储到 sp+16 的位置(x29,x30 参考前面介绍,这里相当于做个备份,因为后面要修改这2个寄存器)
    add x29, sp, #16          ; x29 = sp + 16(相当于移动栈底指针,堆栈大小 16 字节)
    stur    wzr, [x29, #-4]   ; 将 wzr(也叫零寄存器,这是一个特殊的寄存器,值为0)存储到 x29 - 4 的位置,相当于把这快内存清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是存放参数的)
    bl  _sum                  ; 跳转到 sum 函数 
    str w0, [sp]              ; 存储函数返回值(w0)到 sp
    ldr w8, [sp]              ; 读取 sp 的值到 w8
    cmp w8, #30               ; 将 w8 的内容和30比较,同时设置 CPSR 寄存器
    b.le    LBB1_2            ; 根据 CPSR 结果来决定是跳转 LBB1_2(其中条件le的意思是小于等于的意思,具体可以参考我下面的链接)
    stur    wzr, [x29, #-4]   ; 将 x29-4 位置内存清0 
    b   LBB1_3                ; 跳转到 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 作为函数返回值
    ldp x29, x30, [sp, #16]   ; 将 sp+16 位置的值读取到 x29,x30(恢复x29,x30)
    add sp, sp, #32           ; sp = sp + 32,恢复栈顶指针,释放堆栈
    ret                       ; 函数返回 

main 函数和 sum 函数的分析基本就到这里,基本逻辑还是比较简单的,这里有个最大的区别就是 main 函数有 x29,x30 的备份&恢复操作,原因是 sum 函数是叶子函数,而 main 函数是非叶子函数,非叶子函数有对其他函数的调用,需要开辟新的堆栈空间,以及其他函数返回后需要继续执行下一条指令,这需要修改 x29,x30的值,所以要先备份起来,而叶子函数就不需要。

参考

ARM64 汇编基础
iOS开发同学的arm64汇编入门
iOS 常用汇编指令集

你可能感兴趣的:(iOS ARM64 汇编入门)