这里主要介绍 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的值,所以要先备份起来,而叶子函数就不需要。