在 MacOS x86 上编写一个简单的 Hello World 汇编程序。

CPU 指令集架构

通过 uname -a 来查看 CPU 指令集架构,常见的 有:

$ uname -a
Darwin xxx-mac13 22.1.0 Darwin Kernel Version 22.1.0: Sun Oct  9 20:14:54 PDT 2022; root:xnu-8792.41.9~2/RELEASE_X86_64 x86_64

编写最简单的 x86 汇编

参考 Guide to x86

新建 main.s 文件,并编写以下内容

.globl	_main
_main:
	mov	$0x1, %rax
	mov	$0x2, %rbx
	add	%rbx, %rax
	ret

带注释版本

.globl	_main

# 主入口
_main:

	# 设置寄存器 a 的值为 1
	mov	$0x1, %rax

	# 设置寄存器 b 的值为 2
	mov	$0x2, %rbx

	# 将寄存器 a 与 b 的值相加
	# 并将最终值赋值给 a
	add	%rbx, %rax

	# 默认返回寄存器 a 的值
	ret

将以上汇编代码编译之后,可以得到以下内容:

// 编译代码
$ gcc main.s

// 执行代码
$ ./a.out

// 查看程序输出,应该会输出 3
$ echo $?
3

调试程序

LLDB 调试技巧

参考 lldb, lldb cheat sheet

# 编译代码
$ gcc main.c

# 调试程序
$ lldb a.out

# 设置断点 setbreakpoint
(lldb) b main
# 运行
(lldb) r
# 下一步 next
(lldb) n
# 或使用 step-over 命令
# (lldb) thread step-over


# 读取内存信息
(lldb) x $rbp
# 读取寄存器信息
(lldb) re r
# 或制定读取
# (lldb) register read $rbp
# (lldb) re r -a

# 退出 lldb
(lldb) exit

调试程序

通过以下命令开始调试程序:

# 开始调试程序
$ lldb a.out

(lldb) target create "a.out"
Current executable set to '/xxx/a.out' (x86_64).

# 设置 main 为断点
(lldb) b main
Breakpoint 1: where = a.out`main, address = 0x0000000100003fa6

# 运行
(lldb) r
Process 6600 launched: '/xxx/a.out' (x86_64)
Process 6600 stopped
* thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 1.1
    frame #0: 0x0000000100003fa6 a.out`main
a.out`main:
->  0x100003fa6 <+0>:  movq   $0x1, %rax
    0x100003fad <+7>:  movq   $0x2, %rbx
    0x100003fb4 <+14>: addq   %rbx, %rax
    0x100003fb7 <+17>: retq
Target 0: (a.out) stopped.

可以看到最终的执行代码中,以上汇编代码被编译成了

0x100003fa6 <+0>:  movq   $0x1, %rax
0x100003fad <+7>:  movq   $0x2, %rbx
0x100003fb4 <+14>: addq   %rbx, %rax
0x100003fb7 <+17>: retq

参考 movq

MOVD/MOVQ — Move Doubleword/Move Quadword

其实 movmovq 的区别就在于 32 位与 64 位,在此程序中可看成同等。

继续执行程序。

# 下一步
(lldb) n
Process 7248 stopped
* thread #1, queue = 'com.apple.main-thread', stop reason = instruction step over
    frame #0: 0x0000000100003fb4 a.out`main + 14
a.out`main:
->  0x100003fb4 <+14>: addq   %rbx, %rax
    0x100003fb7 <+17>: retq
    0x100003fb8:       addl   %eax, (%rax)
    0x100003fba:       addb   %al, (%rax)
Target 0: (a.out) stopped.

# 执行完赋值之后,查看一下寄存器的状态
# 可以看到 rax 为 1,rbx 为 2,符合预期
(lldb) re r
General Purpose Registers:
       rax = 0x0000000000000001
       rbx = 0x0000000000000002

# 下一步,执行加法逻辑
(lldb) n
Process 7248 stopped
* thread #1, queue = 'com.apple.main-thread', stop reason = instruction step over
    frame #0: 0x0000000100003fb7 a.out`main + 17
a.out`main:
->  0x100003fb7 <+17>: retq
    0x100003fb8:       addl   %eax, (%rax)
    0x100003fba:       addb   %al, (%rax)
    0x100003fbc:       sbbb   $0x0, %al
Target 0: (a.out) stopped.

# 执行完加法逻辑后,rax 为 3
(lldb) re r
General Purpose Registers:
       rax = 0x0000000000000003
       rbx = 0x0000000000000002

# 执行完成所有程序
# 可以看到最终的返回状态为 3,符合预期
(lldb) thread continue
Resuming thread 0x4c614d in process 7248
Process 7248 resuming
Process 7248 exited with status = 3 (0x00000003)

操作指令

通过查看 a.out 的二进制代码:

$ xxd -s 0x3fa6 -c 7 a.out
00003fa6: 48c7 c001 0000 00  H......
00003fad: 48c7 c302 0000 00  H......
00003fb4: 4801 d8c3 0100 00  H......
00003fbb: 001c 0000 0000 00  .......
00003fc2: 0000 1c00 0000 00  .......
...

可以看到,最终的二进制码如下:

|-----  运行时代码 ------------------|  |-- 二进制源码 --|   |------ 前 32 位的二进制指令 --------|
0x100003fa6 <+0>:  movq   $0x1, %rax  48c7 c001 0000 00   01001000 11000111 11000000 00000001
0x100003fad <+7>:  movq   $0x2, %rbx  48c7 c302 0000 00   01001000 11000111 11000011 00000010
0x100003fb4 <+14>: addq   %rbx, %rax  4801 d8             01001000 00000001 11011000
0x100003fb7 <+17>: retq               c3                  11000011
0x100003fb8:       ...                ...

对于 0x48c7 c00, 参考 x86-64-assembly-opcode

  • 0x48long mode 也就是 64 位 rax 寄存器。
  • 0xc7mov 指令
    • 0xc3 顺便可以看到 RETN 指令
    • 0x01ADD 指令
  • 0xc0rax 寄存器
    • 同时可以看到 0xc1, 0xc2, 0xc3 分别是 rcx, rdx, rbx 寄存器。

参考