ARM Assembly on MacOS
在 MacOS M1 Pro (ARM) 上编写一个简单的 Hello World 汇编程序。
x86 的样例可参考 x86 Assembly on MacOS
CPU 指令集架构
通过 uname -a
来查看 CPU 指令集架构,常见的 有:
$ uname -a
Darwin Pipe-Macbook 21.6.0 Darwin Kernel Version 21.6.0: Mon Aug 22 20:19:52 PDT 2022; root:xnu-8020.140.49~2/RELEASE_ARM64_T6000 arm64
$ uname -p
arm
编写最简单的 ARM 汇编
参考 apple-m1-assembly-language-hello-world 以及 基本的 ARM 指令含义
新建 main.s
文件,并编写以下内容
.global _main
_main:
mov x0, #1
mov x1, #2
add x0, x0, x1
ret
关于 r0、x0、w0 寄存器的区别:
The aarch64 registers are named:
r0 through r30 - to refer generally to the registers
x0 through x30 - for 64-bit-wide access (same registers)
w0 through w30 - for 32-bit-wide access (same registers - upper 32 bits are either cleared on load or sign-extended (set to the value of the most significant bit of the loaded value)).
带注释版本
.global _main
// 主入口
_main:
// 对寄存器0 赋值 1
mov x1, #1
// 对寄存器1 赋值 2
mov x2, #2
// 对寄存器 1 和 寄存器 2 进行相加
// 并赋值给寄存器 0
add x0, x1, x2
// 返回值
ret
将以上汇编代码编译之后,可以得到以下内容:
// 编译代码
$ gcc main.s
// 执行代码
$ ./a.out
// 查看程序输出,应该会输出 3
$ echo $?
3
调试程序
LLDB 调试技巧
参考 x86 Assembly on MacOS,不再重复。
调试程序
通过以下命令开始调试程序:
# 开始调试程序
$ lldb a.out
(lldb) target create "a.out"
Current executable set to '/xxx/a.out' (arm64).
# 设置 main 为断点
(lldb) b main
Breakpoint 1: where = a.out`main, address = 0x0000000100003fa8
# 运行程序
(lldb) r
Process 19910 launched: '/xxx/a.out' (arm64)
Process 19910 stopped
* thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 1.1
frame #0: 0x0000000100003fa8 a.out`main
a.out`main:
-> 0x100003fa8 <+0>: mov x1, #0x1
0x100003fac <+4>: mov x2, #0x2
0x100003fb0 <+8>: add x0, x1, x2
0x100003fb4 <+12>: ret
Target 0: (a.out) stopped.
可以看到最终的执行代码中,以上汇编代码被编译成了
0x100003fa8 <+0>: mov x1, #0x1
0x100003fac <+4>: mov x2, #0x2
0x100003fb0 <+8>: add x0, x1, x2
0x100003fb4 <+12>: ret
# 下一步
(lldb) n
Process 19910 stopped
* thread #1, queue = 'com.apple.main-thread', stop reason = instruction step over
frame #0: 0x0000000100003fb0 a.out`main + 8
a.out`main:
-> 0x100003fb0 <+8>: add x0, x1, x2
0x100003fb4 <+12>: ret
0x100003fb8: udf #0x1
0x100003fbc: udf #0x1c
Target 0: (a.out) stopped.
# 执行完赋值之后,查看一下寄存器的状态
# 可以看到 x1 为 1,x2 为 2,符合预期
(lldb) re r
General Purpose Registers:
x0 = 0x0000000000000001
x1 = 0x0000000000000001
x2 = 0x0000000000000002
# 下一步,执行加法逻辑
(lldb) n
Process 19910 stopped
* thread #1, queue = 'com.apple.main-thread', stop reason = instruction step over
frame #0: 0x0000000100003fb4 a.out`main + 12
a.out`main:
-> 0x100003fb4 <+12>: ret
0x100003fb8: udf #0x1
0x100003fbc: udf #0x1c
0x100003fc0: udf #0x0
Target 0: (a.out) stopped.
# 执行完加法逻辑后,x0 为 3
(lldb) re r
General Purpose Registers:
x0 = 0x0000000000000003
x1 = 0x0000000000000001
x2 = 0x0000000000000002
# 执行完成所有程序
# 可以看到最终的返回状态为 3,符合预期
(lldb) thread continue
Resuming thread 0x1817033 in process 19910
Process 19910 resuming
Process 19910 exited with status = 3 (0x00000003)
操作指令
确认 main 函数入口地址:
$ nm a.out
0000000100000000 T __mh_execute_header
0000000100003fa8 T _main
通过查看 a.out
的二进制代码:
$ xxd -s 0x3fa8 -l 20 -c 4 a.out
00003fa8: 2100 80d2 !...
00003fac: 4200 80d2 B...
00003fb0: 2000 028b ...
00003fb4: c003 5fd6 .._.
00003fb8: 0100 0000 ....
...
可以看到,最终的二进制码如下:
|----- 运行时代码 ------------------| |- 二进制-| |------ 前 32 位的二进制指令 --------|
0x100003fa8 <+0>: mov x1, #0x1 2100 80d2 00100001 00000000 10000000 11010010
0x100003fac <+4>: mov x2, #0x2 4200 80d2 01000010 00000000 10000000 11010010
0x100003fb0 <+8>: add x0, x1, x2 2000 028b 00100000 00000000 00000010 10001011
0x100003fb4 <+12>: ret c003 5fd6 11000000 00000011 01011111 11010110
然后 ARM 的二进制源码是从右往左读的(这块我看了很久才意识到的),也就是按每个字节(8 bit),需要做个反转,即得到如下代码:
|----- 运行时代码 ------------------| |- 二进制-| |------ 前 32 位的二进制指令 --------|
0x100003fa8 <+0>: mov x1, #0x1 d280 0021 11010010 10000000 00000000 00100001
0x100003fac <+4>: mov x2, #0x2 d280 0042 11010010 10000000 00000000 01000010
0x100003fb0 <+8>: add x0, x1, x2 8b02 0020 10001011 00000010 00000000 00100000
0x100003fb4 <+12>: ret d65f 03c0 11010110 01011111 00000011 11000000
MOV
从操作指令上看,可以确定具体的操作为 MOV (wide immediate) 因为是 64 位(wide)的数值(immediate)
31 | 30 | 29 | 28 | 27 | 26 | 25 | 24 | 23 | 22 | 21 | 20 | 19 | 18 | 17 | 16 | 15 | 14 | 13 | 12 | 11 | 10 | 9 | 8 | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
sf | 1 | 0 | 1 | 0 | 0 | 1 | 0 | 1 | hw | imm16 | Rd | ||||||||||||||||||||
1(64位系统) | 1 | 0 | 1 | 0 | 0 | 1 | 0 | 1 | 00 | 00000 00000000 001(16 bit 的数值) | 00001 (寄存器1) | ||||||||||||||||||||
opc |
所以,最终的 32 位二进制为 1/1010010 1/00/00000 00000000 001/00001
,十六进制为 d280 0021
,即汇编的 mov x1, #0x1
,将数值 1
存到 寄存器1 中。
1/1010010 1/00/00000 00000000 010/00010
同理,十六进制为 d280 0042
,即汇编的 mov x2, #0x2
,将数值 2
存到 寄存器2 中。
ADD
读懂了 MOV 之后,ADD 也就比较容易看到。
ADD 指令的含义 是:
add r0,r1,r2 // load r0 with r1+r2
31 | 30 | 29 | 28 | 27 | 26 | 25 | 24 | 23 | 22 | 21 | 20 | 19 | 18 | 17 | 16 | 15 | 14 | 13 | 12 | 11 | 10 | 9 | 8 | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
sf | 0 | 0 | 0 | 1 | 0 | 1 | 1 | shift | 0 | Rm | imm6 | Rn | Rd | ||||||||||||||||||
1(64位系统) | 0 | 0 | 0 | 1 | 0 | 1 | 1 | 00 | 0 | 00010(寄存器2) | 000000(数值0) | 00001(寄存器1) | 00000(寄存器0) | ||||||||||||||||||
op | S |
二进制 1/0001011 00/0/00010 000000/00 001/00000
的十六进制为 8b02 0020
,即汇编的 add x0, x1, x2
,将寄存器 1 和寄存器 2 相加,并赋值给寄存器 0。
RET
RET 就当做个巩固练习吧。
31 | 30 | 29 | 28 | 27 | 26 | 25 | 24 | 23 | 22 | 21 | 20 | 19 | 18 | 17 | 16 | 15 | 14 | 13 | 12 | 11 | 10 | 9 | 8 | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
1 | 1 | 0 | 1 | 0 | 1 | 1 | 0 | 0 | 1 | 0 | 1 | 1 | 1 | 1 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | Rn | 0 | 0 | 0 | 0 | 0 | ||||
1 | 1 | 0 | 1 | 0 | 1 | 1 | 0(1Byte) | 0 | 1 | 0 | 1 | 1 | 1 | 1 | 1(1Byte) | 0 | 0 | 0 | 0 | 0 | 0 | 011 110 | 0 | 0 | 0 | 0 | 0 | ||||
Z | op | A | M | Rm |
二进制 11010110 01011111 00000/011 110/00000
的十六进制为 d65f 03c0
(其中 rn 的含义可以参考其他资料查看,我没继续查了。。。),即汇编 ret
命令。