MacOS Arm64 汇编 part 2 - MacOS Arm64 Assembly part 2
- flow control
- branch
- flags
- a basic printer
Unconditional branch
无条件分支(跳转)
1 | B label |
直接跳转到 label 处,理论上计算时是利用 PC
再加上一个 26 bits 的 offset 实现,但是 clang
这边会自动处理超长,因此只需考虑标签名称,无需考虑指令距离超过 26 bits
offset 表示长度
Condition Flags
- Negative: N 是
如果有符号数是负数,否则为 - Zero: Z 是
如果结果是 ,通常用于表示相等比较的结果 - Carry: 对于加法类型的操作,如果有溢出,则此标志为
,否则为 ,对于减法则相反;同时这一标志通常存储上一被移位出去的 bit - oVerflow:
对于加法或减法,如果有符号计算溢出发生,则置
一些指令通过 oVerflow 标志来表示错误情况
通常能需要添加一个S在指令后面以使它设置标志,否则将不会修改标志,处了比较指令
Branch on Condition
有条件分支
1 | B.{condition} label |
其中 {condtion} 来源于下表
| {condition} | Flags | Meaning |
|---|---|---|
| EQ | Z set | Equal |
| NE | Z clear | Not equal |
| CS or HS | C set | Higher or same (unsigned >=) |
| CC or LO | C clear | Lower (unsigned <) |
| MI | N set | Negative |
| PL | N clear | Positive or zero |
| VS | V set | Overflow |
| VC | V clear | No overflow |
| HI | C set and Z clear | Higher (unsigned >) |
| LS | C clear and Z set | Lower or same (unsigned <=) |
| GE | N and V the same | Signed >= |
| LT | N and V differ | Signed < |
| GT | Z clear, N and V the same | Signed > |
| LE | Z set, N and V differ | Signed <= |
| AL | Any | Always (same as no suffix) |
set 表示为
,clear 表示为
CMP
用于进行比较的指令
1 | CMP Xn, Operand2 |
事实上,这里也是别名,等价于
1 SUBS XZR, Xn, Operand2
CMN / TST
同样是比较指令,但与 CMP
相比有些差别,具体在比较方法的选取上,CMN 采用加法替代
CMP 的加法,TST 则采用按位与。
在手写汇编时,若
Operand2不合法时,汇编器会自动在三条指令中尝试切换
Loop
For Loop
对于这样的 C 代码
1 | int main(){ |
可以采用
1 | // Assume X5 denotes i |
这是 clang 实现的简化版,真实的版本会采用栈上存储的变量,且
cmp 在编译时替换为 subs 我们只保留了结构
1 | t[0x100000328] <+0>: sub sp, sp, #0x10 |
While Loop
与 for loop 类似,区别是无需维护循环变量
1 | int main(){ |
可以采用
1 | // Assume X5 denotes i |
而 clang 的实现是
1 | t[0x100000328] <+0>: sub sp, sp, #0x10 |
IF-ELSE IF-ELSE
一样是通过 CMP 加标签跳转,和上面是一样的
Logical Operators (bitwise)
逻辑运算
AND EOR
ORR
1 | AND{S} Xd, Xs, Operand2 // Xd = Xs & Operand2 |
BIC
特殊的一条指令(bit clear),执行的实际上是
Xd = Xs & ~Operand2,也即当对应 bit 在
Operand2 中为 0 时,才输出 Xs
对应位到 Xd,否则为 0
1 | BIC{S} Xd, Xs, Operand2 |
Converting Integers to ASCII
转换一个整型到 ASCII 字符串并输出
1 | // |
其中 X4 存储我们需要打印的整型,这里采用了
.data
段,该段可进行读写,我们的程序修改这里到我们需要的结果,并最终进行系统调用打印这一字符串
我们的程序采用了一个特殊的指令
adrp,这一指令加载了符号对应页的首地址到
X1,随后我们通过 add
指令在这一地址上添加这一符号对应页内偏移量
分两条是因为指令长度有限,存不下 64 bit 的内存地址
我们还额外添加了 #17 的 offset 是为了移到字符串的
\n 前一位,因为我们从后往前输出
程序当中,我们通过 W5 维护循环变量,执行 and 操作,通过 0xf 也就是 0b1111
作为掩码,取出最后 4 bit,也就是一位 16 进制数,存入 W6
随后我们先判断其所是大于 W6,在代码中 #'0' 或 #('A'-10)
这样的写法是取 ASCII 的值
然后我们通过 STR 指令保存 W6 到
[X1],中括号意为间接,也就是要存储到 [X1]
所存储的地址的内存,随后我们将 X1 减去 X4
做了右移,获取高一位的十六进制数
循环结束后,我们在 X1
存储字符串开头地址,进行系统调用。
下面是一个通过递减右移实现顺序打印的版本,无需变长buffer,但是多次 syscall 应该是会导致性能降低
1 | .global _start |
Switch case
1 | // AArch64 Assembly Code |
这个程序除了 switch...case...
之外还给出了读取命令行参数的方法,下面是一个命令行参数获取的demo
1 | .global _start |
x0对应int argc是参数的数量
x1对应char *argv参数字符串指针数组的首地址
argv + 1对应[x1, #8],这个理解为先将 x1 存储的首地址偏移 8 字节再解引用(读取偏移后地址存储内容)ldr是读取寄存器中的地址(并计算偏移)将地址结果存储到另一个寄存器adr是基于pc计算标签的地址(小范围)并存储到寄存器adrp是基于pc计算全局页变量(大范围)并存储到寄存器,与@PAGE@PAGEOFF连用,可用add累加@PAGEOFF