
架构
ARM64 是 Load/Store 架构
这句话非常重要,他决定了ARM64 不能直接对内存做复杂运算,大多数运算必须先把数据从内存加载到寄存器,再在寄存器里计算,最后写回内存。
算术逻辑运算通常只在寄存器之间进行,内存访问主要靠 LDR / STR。
指令长度
ARM64常规指令:固定32位(4字节),取值与解码更简单,按4字节对齐。所以看arm64的汇编会发现地址以4递增。
0x1000: stp x29, x30, [sp, #-0x10]!0x1004: mov x29, sp0x1008: bl 0x20000x100c: ldp x29, x30, [sp], #0x100x1010: retRISC风格
ARM64 属于 RISC 风格,特点是:
指令长度固定编码规则整齐大多数指令功能比较单一寄存器数量多访存和计算分离不像 x86 那样,一条指令可以又访问内存又做复杂计算。
寄存器
通用寄存器
ARM64有31个通用寄存器:
x0 ~ x30 64位w0 ~ w30 x寄存器低32位注意:
mov w0, #1这会把 x0 的高 32 位清零,结果是:
x0 = 0x0000000000000001XZR / WZR 零寄存器
ARM64 有一个特殊寄存器:
xzrwzr它永远是 0。
读它,得到 0:
mov x0, xzr等价于:
x0 = 0写它,结果会被丢弃:
add xzr, x0, x1这个计算结果不会保存。
sp栈指针
常见操作:
sub sp, sp, #0x20表示开辟 0x20 字节栈空间。
add sp, sp, #0x20表示释放 0x20 字节栈空间。
ARM64 要求函数调用时栈通常保持 16 字节对齐。
FR和LR
ARM64 里:
x29 = fp ; frame pointer,帧指针x30 = lr ; link register,返回地址典型函数开头:
stp x29, x30, [sp, #-0x10]!mov x29, sp含义:
把 x29 和 x30 入栈更新 sp把当前 sp 作为新的栈帧基址典型函数结尾:
ldp x29, x30, [sp], #0x10ret含义:
恢复 x29 和 x30sp 回收栈空间跳回 x30 保存的返回地址条件标志 NZCV
ARM64 的算术/逻辑指令会修改 CPU 的条件标志寄存器(PSTATE 寄存器的一部分),简称 NZCV:
ARM64函数调用约定
参数传递
arm64函数参数优先放在x0到x7
返回值
返回值一般放在x0/w0(x0的低32位)
函数调用
ARM64 调用函数常见指令:
bl funcbl 的意思是:
branch with link跳转到目标函数,并把返回地址保存到 x30例如:
bl strcmp调用 strcmp。
函数返回:
retret 默认跳转到:
x30也就是返回地址。
ARM64栈结构
ARM64 栈一般是:
高地址| || 调用者栈帧 ||----------------|| 返回地址 x30 || 旧的 x29 ||----------------| <- x29 / fp| 局部变量 || 临时保存寄存器 ||----------------| <- sp低地址栈是向低地址增长的。
开辟栈空间:
sub sp, sp, #0x40释放栈空间:
add sp, sp, #0x40保存寄存器:
str x19, [sp, #0x10]恢复寄存器:
ldr x19, [sp, #0x10]成对保存:
stp x19, x20, [sp, #0x20]成对恢复:
ldp x19, x20, [sp, #0x20]ARM64 常见指令速查表
常见条件跳转
JNI 调用大概形态
JNI 函数通常第一个参数是:
JNIEnv *env第二个参数是:
jobject / jclass所以 ARM64 里:
x0 = JNIEnv*x1 = jobject / jclassx2 = 第一个 Java 层传入参数x3 = 第二个 Java 层传入参数比如:
Java_com_demo_MainActivity_check(JNIEnv *env, jobject thiz, jstring input)进入 native 后大概:
x0 = envx1 = thizx2 = input这点在分析 so 里的 JNI 函数时非常重要。