前几天写了一个小玩具,asmbf,用纯汇编
做了一个 brainfuck 解释器,这里总结一下过程里遇到的一些坑。
在一个技术群里差点和一位吵起来,当年 Intel 力推 IA-64 的时候,AMD 推出了
兼容原 x86 架构的 amd64 架构,依靠兼容性干倒了 IA-64,于是从此
兼容 x86 架构的 amd64 (Intel 称之为 x86-64)成为了现行主流架构
在 x86_64 Call Convention 中,Linux 系统调用传参方式和用户空间函数
传参方式使用的寄存器是不同的,后者与本文无关不加赘述。
进行系统调用时,我们使用 syscall 指令陷入内核,此时按照此顺序传递参数
%rdi, %rsi, %rdx, %r10, %r8, %r9
%rax 中存放系统调用号,详见本文末附表
(描述编译汇编程序的过程用汇编 (assemble) 更合适)
代码是纯 AT&T 汇编(也叫 GAS 汇编)开始我用 as 直接编译代码,
然而 as 生成的代码仍需要链接,同时无法使用宏(简单的常量定义可以使用
.equ 伪指令),最后使用了 gcc 进行编译
作为纯汇编程序,我不使用任何标准库,又因为我用的程序结构自行定义了
_start 作为入口,-nostdlib 被用来禁用标准库。
这种汇编代码没办法编译为运行时地址无关的目标文件,所以要用 -no-pie 让
gcc 闭嘴
文件命名使用了 .S 作为后缀,用来指示 gcc 对其调用 C 预处理器
最后使用的指令类似
gcc asmbf.S -o asmbf -nostdlib -no-pie
.global _start // 导出 _start 符号
.code // 唯一代码段
_start: // 代码入口点
/* ... */
movq $60, %rax
movq $0, %rdi
syscall // 60 号系统调用,结束程序
.data // 唯一数据段
msg: .asciz "Hello"
gdb 是个好东西,甚至可以拿来调汇编代码。
在编译时打开 -g -O0 之后,我们可以直接用 gdb 单步调试代码
一个常用的指令是 info register,缩写为 i r,可以查看寄存器信息
要记住一条指令必须换行必须换行
asmbf 末尾有一串定义跳转表的伪指令,开始我想用类似
#define fill(x) \
.rept x \
.quad xxxxx \
.endr
的代码省事少写几行,结果发现这么写之后会报无效指令
原因是 \ 是续行符号,在宏替换时这些内容都会被合成一行,而在一行类似
.rept 1 .quad xxxx .endr 的内容汇编器是不认识的
我不想通过开大 .data 段的方式去分配内存(用来储存 brainfuck 机器的
纸带内容),所以就用了 mmap() 来分配内存。
fd = -1,addr = NULL 同时 type 置位 MAP_ANONYMOUS 时,mmap() 会
分配 length 长度的内存
O_RDONLY 00
SEEK_SET 0
SEEK_CUR 1
SEEK_END 2
PROT_READ 0x1
PROT_WRITE 0x2
MAP_SHARED 0x01
MAP_ANONYMOUS 0x20