Arm 架构是一种 risc 架构,著名的精简指令集。和 x86 架构的汇编思维基本相同,栈帧建立释放,sp 寄存器操作栈,寄存器/内存等等。在区别上主要体现在名称不同。例如对于条件跳转,x86 架构的 jmp 指令,Arm 架构的 b(eq) 指令。x86 的命名明显能视读出 jump,但是 Arm 的汇编则没那么容易看出。所以本文的侧重点可能还会有点奇怪——我更想来看看所有汇编指令的含义,这样记忆起来也会更加深刻。

其次是为什么学习 Arm 架构汇编而不是 x86 架构?其实学习 x86 架构汇编应该是所有学习汇编的开始,无论从历史角度还是技术,然而事实是笔者学习这个技术的目的是为了逆向工程中对汇编的 patch 更加得心应手。而目前笔者逆向工程的对象一般是

Android Software
IOS Software
Mac Software

神奇的是这三都是 Arm 64 架构,所以自然而然接触下来也对 Arm 架构有所了解。

本文仅供初学者阅读,笔者水平很次,如有错误请谅解。本文目标仅仅是在 ida 阅读伪代码之后切换到IDA view的时候能快速定位到汇编,能够知道 patch 什么,怎么 patch。


在介绍一些命令之前,先讲讲一些汇编的基础。首先汇编本身会有一些比较隐含的规则。可能接触过其他任何编程语言,但没接触过 asm 的初学者会对汇编的返回值感觉奇怪,所有其他语言基本上都是

...     // some code
return param;

或者对于 rust 可以不写 return 这个关键字,直接写返回值。

...     // some code
param   // equals `return param;`

然而在汇编中就是

...     ; some asm code
ret

事实上在汇编里面,返回值固定就是第一个寄存器,在 x86 中是 rax,arm64 架构是 x0。也就是这和其他语言有一些很大的区别。在别的语言中单句话的语义是全的。也就是说,我做什么任何事情都是对于这个语句能直接看出来的。就以这个返回值为例,我返回一个值在所有语言里面几乎都是 return something,这个单句实际上表述了返回这个值,而汇编默认返回第一个寄存器;又或者 let a = func(b, c); 一眼就是知道调用了 func ,传入了 b 和 c,返回的值给了 a,而汇编中

mov x0, x3      // 将 b 的值加载到 x0,假如 b 的值原本在 x3 寄存器
mov x1, x4      // 将 c 的值加载到 x1,假如 c 的值原本在 x4 寄存器

bl func         // 调用 func 函数
mov w8, w0      // 假设将返回值存储到 w8 寄存器中

调用函数的默认传入参数就是 x0, x1,...,如果寄存器不够放则调用者负责把超出数量的参数压入栈。从这两个案例中笔者实际上想要表示的意思是,学习汇编不能盯着一行看,因为每一行内容都很短,而其作用会和上下相关。一个值会到处使用,而需要小心这里的东西被那里用到….

另外,例如比较语句,实际上

if (condition) {
    do something;
} else {
    do something else;
}

这个 if 在汇编中应该也有好几行。我们先简单标注一下位置

if (condition) {
LABEL_1:
    do something;
} else {
LABEL_2:
    do something else;
}

因此汇编的大体框架是

condition       ; 条件判断,例如 cmp r0, r1
beq LABEL_1     ; 如果条件为真,跳转到 LABEL_1
bne LABEL_2     ; 如果条件为假,跳转到 LABEL_2

这里 beq 表示 “branch if equal” 的缩写,假如条件为真则跳转到 LABEL_1。但是问题是条件是什么?自然是上文紧跟的 cmp,比较的结果直接影响到这里的 beq。如果只盯着这一行看显然是看不懂 beq 到底在做什么了。

对于函数的入栈出栈在本文还不太需要使用到,下篇将会详述。本篇先讲述一些基础的 Arm 架构的 asm 指令使用。我一向认为,先熟悉这些语法,在熟悉语法之后可以简单 patch 汇编之后,我们才需要研究 sp 的寄存器这些栈帧的事情了,到时候无论是举例子还是其他的都比较简单。下篇或是下下篇,应该会对一个真实的案例逐行分析。


第一个是 subsubs,这俩一看就知道是减法。具体来说

  • sub 是 “subtract” 的缩写,表示减法。
  • s 表示 “set flags”,即设置条件标志位。

使用的参数格式是

sub/subs <目标寄存器>, <源寄存器1>, <源寄存器2/立即数>
  • <目标寄存器>:存储结果的寄存器。
  • <源寄存器1>:被减数。
  • <源寄存器2/立即数>:减数。

需要注意的是subs:与 sub 类似,但会更新条件标志位(如零标志位 Z、负数标志位 N 等)。

一个简单的案例就是

subs w8, w8, #0
  • 将寄存器 w8 的值减去 0,并将结果存回 w8,同时更新条件标志位。

第二个是cset,可以根据条件标志位的值,将目标寄存器设置为 01。其中cset 是 “conditional set” 的缩写,表示根据条件设置寄存器。

cset <目标寄存器>, <条件>
  • <目标寄存器>:存储结果的寄存器。
  • <条件>:如 ne(不等于)、eq(等于)等。

简单的案例:

cset w8, ne
  • 如果条件标志位表示“不等于”(ne),则将 w8 设置为 1,否则设置为 0

第三个 tbnz 是 “test bit and branch if nonzero” 的缩写。用于测试寄存器中的某一位是否为 1,如果是,则跳转到指定标签。

tbnz <寄存器>, <位索引>, <标签>
  • <寄存器>:要测试的寄存器。
  • <位索引>:要测试的位(从 0 开始计数)。
  • <标签>:如果测试结果为真,则跳转到的目标地址。
    简单案例
    tbnz w8, #0, LBB0_2
    
  • 测试 w8 的第 0 位是否为 1,如果是,则跳转到 LBB0_2

刚才说过 b 是跳转,其实应该说是无条件跳转。对应 x86 中的 jmp label.

b LBB0_1

表示无条件跳转到 LBB0_1。而b 是 “branch” 的缩写,表示分支跳转。


接下来就是神秘命名了。

  • ldr:从内存加载数据到寄存器。
  • str:将寄存器中的数据存储到内存。
    其中ldr 是 “load register” 的缩写。str 是 “store register” 的缩写。有点离谱,但是仔细想想又很合理的感觉。
ldr/str <寄存器>, [<基址寄存器>, #偏移量]
  • <寄存器>:加载或存储的目标寄存器。
  • <基址寄存器>:内存地址的基址。
  • #偏移量:相对于基址的偏移。
ldr w8, [sp, #8]
str w8, [sp, #4]
  • 从栈指针 sp 偏移 8 的位置加载数据到 w8
  • w8 的值存储到栈指针 sp 偏移 4 的位置。

与这俩类似的是 sturldur,加上 u 实际表示的是 “unscaled”,也就是支持负偏移。用法和上述两者一致,简单案例如下

stur w8, [x29, #-4]
ldur w0, [x29, #-4]
  • w8 存储到 x29 偏移 -4 的位置。
  • x29 偏移 -4 的位置加载数据到 w0

7. mov

  • 功能
    • 将一个寄存器的值复制到另一个寄存器,或将立即数加载到寄存器。
  • 命名原因
    • mov 是 “move” 的缩写,表示移动数据。
  • 参数格式
    mov <目标寄存器>, <源寄存器/立即数>
    
    • <目标寄存器>:存储结果的寄存器。
    • <源寄存器/立即数>:要复制的值。
  • 例子
    mov w8, #5
    mov x10, x8
    
    • 将立即数 5 加载到 w8
    • 将寄存器 x8 的值复制到 x10

8. mul

  • 功能
    • 执行乘法操作,结果存储在目标寄存器中。
  • 命名原因
    • mul 是 “multiply” 的缩写,表示乘法。
  • 参数格式
    mul <目标寄存器>, <源寄存器1>, <源寄存器2>
    
    • <目标寄存器>:存储结果的寄存器。
    • <源寄存器1><源寄存器2>:两个乘数。
  • 例子
    mul w8, w8, w0
    
    • w8w0 相乘,结果存回 w8

9. bl

  • 功能
    • 调用函数(跳转到指定地址并保存返回地址到 x30)。
  • 命名原因
    • bl 是 “branch with link” 的缩写,表示带链接的分支跳转。
  • 参数格式
    bl <函数地址>
    
    • <函数地址>:调用的目标函数。
  • 例子
    bl _factorial
    
    • 调用 _factorial 函数。

10. ret

  • 功能
    • 返回到调用者(从 x30 中恢复返回地址)。
  • 命名原因
    • ret 是 “return” 的缩写,表示函数返回。
  • 参数格式
    ret
    
    • 无需参数。

总结

以上是代码中所有汇编指令的详细解释。每个指令都有其特定的功能和命名逻辑,理解这些指令有助于深入学习汇编语言。如果有其他疑问,欢迎继续提问!

文章作者: YinMo19

文章链接: https://blog.yinmo19.top/2025/03/07/Arm-%E6%B1%87%E7%BC%96-%E5%AD%A6%E4%B9%A0%E5%B0%8F%E8%AE%B0-1/

版权声明:除另有声明外,本博客文章均采用 CC BY-NC-SA 4.0 许可协议。转载请注明原作者与文章出处。

Arm 汇编

本研教学服务-反root/模拟器校验 «
Prev «
None
» Next