汇编基本语法

用于 JOS 这个操作系统实验的汇编,语法和 intel 的汇编语法有所不同,它的风格是 GAS style 的。x86汇编的内容非常多,想要用一篇指南去全部介绍 x86 汇编基本上是不可能的,所以这个指南将会介绍那些重要且较为简单的汇编知识。

寄存器

汇编中最重要的一个部分是寄存器,下面的表格列出了一些重要的寄存器和它们基本的用法。一个比较有趣的事情是下面中的 调用者保护被调用者保护 这两个属性。下面用一个例子简要介绍这两个属性,假设有 2 个函数 A 和 B,A 如果调用了 B,那么 A 被称为调用者,B 被称为被调用者。对于 A 来说,他需要时刻注意那些 调用者保护 的寄存器,如果 A 要使用这些寄存器,比如 %eax(函数返回值),他应该先将它们保存在内存(通常是栈)中,否则可能因为修改了寄存器的值而导致返回值改变。这个部分的内容相信大家在学习编译原理的活动记录后会理解得更好。(实验中使用的是 32 位汇编,不过这里列出的是 64 位的寄存器表,对应的寄存器可以用 32 位那一列进行转换)

寄存器 通常的功能 低16位 低8位
%rax 函数返回值,调用者保护 %eax %ax
%rdi 第一个函数参量,调用者保护 %edi %di
%rsi 第二个函数参量,调用者保护 %esi %si
%rdx 第三个函数参量,调用者保护 %edx %dx
%rcx 第四个函数参量,调用者保护 %ecx %cx
%r8 第五个函数参量,调用者保护 %r8d %r8w
%r9 第六个函数参量,调用者保护 %r9d %r9w
%rsp 栈指针,被调用者保护 %esp %sp
%rbx 临时变量,被调用者保护 %ebx %bx
%rbp 通常记录上一个栈指针,被调用者保护 %ebp %bp
%r12 临时变量,被调用者保护 %r12d %r12w
%r13 临时变量,被调用者保护 %r13d %r13w
%r14 临时变量,被调用者保护 %r14d %r14w
%r15 临时变量,被调用者保护 %r15d %r15w
%rip 程序指令指针
%eflags 记录条件和状态

另外,在不同的编译器中,调用者保护被调用者保护 这两个属性可能会有所不同, gcc 是符合上表所述的,但是在微软的官方文档中却并不是这样,其中一个差别是, 它把 %rdi 和 %rsi 作为 被调用者保护 寄存器。

寻址模式

寻址的主要目的是找到对应的内存,下面以 mov 指令为例,列举 x86 中常见的几种寻址模式

movl $1, 0x604892         # 直接寻址,0x604892的位置填入立即数1
movl $1, (%rax)           # 间接寻址,在 %rax 寄存器这个值的地址填入立即数1

movl $1, -24(%rbp)        # 间接寻址,在 %rbp - 24 这个地址填入立即数1

movl $1, 8(%rsp, %rdi, 4) # 间接寻址,在 %rsp + 8 + %rdi*4 这个地址填入立即数1

movl $1, (%rax, %rcx, 8)  # 间接寻址,在 %rax + %rcx*8 这个地址填入立即数1

movl $1, 0x8(, %rdx, 4)   # 间接寻址,在 0x8 + %rcx*4 这个地址填入立即数1

movl $1, 0x4(%rax, %rcx)  # 间接寻址,在 %rax + 0x4 + %rcx*1 这个地址填入立即数1

基本指令

指令后缀

在实验中很多汇编指令都是带一个后缀的,比如 b, w, l, q, 它们的意思分别是操作的数据单元大小是 1,2,4,8 bytes。

当然也有一些特殊的指令带有 2 个后缀,比如 movsmovz,具体它们的意思可以在 x86百科 中查到

通常指令的格式

汇编通常指令基本可以按照操作数的数量来划分。比如下面的无操作数指令格式:

Instr

有一个操作数的指令格式:

Instr arg

有两个操作数的指令格式如下,大部分的这些指令都要求2个操作数必须至少有一个是立即数或寄存器。

Instr src, dest

有三个操作数的指令格式如下

Instr aux, src, dest

Mov和Lea

下面直接用例子介绍这两个的指令用法和区别。

mov src, dest        # 意思就是 dest = src
mov $0, %eax         # %eax = 0
movl %eax, 0x233     # 将%eax的值填入0x233这个地址
movl 8(%rsp), %eax   # %eax = %rsp + 8 这个地址存放的数据的值

lea src, dest        # dest = src 的地址
lea 0x20(%rsp), %eax # %eax = %rsp + 0x20  (可以与上面的mov比较)

基本的算术运算

这里也直接给出一些简单的例子,更详细的说明可以在这里查到。

add src, dst       # dst += src
sub src, dst       # dst -= src
imul src, dst      # dst *= src
neg dst            # dst = -dst

and src, dst       # dst &= src
or src, dst        # dst |= src
xor src, dst       # dst ^= src
not dst            # dst = ~dst

基本的控制指令

控制指令主要就是各种跳转和设置标志位状态的一些指令,想要了解标志位具体的设置方式和介绍可以参考这里

cmpl op2, op1    # 根据 op1 - op2 的结果设置标志位状态
test op2, op1    # 根据 op1 & op2 的结果设置标志位状态

jmp target    # 无条件跳转
je  target    # 相等时跳转 (ZF=1)
jne target    # 不等时跳转 (ZF=0)
jl  target    # 小于时跳转 (SF!=OF)
jle target    # 小于等于时跳转 (ZF=1 or SF!=OF)
jg  target    # 大于时跳转 (ZF=0 and SF=OF)
jge target    # 大于等于时跳转 (SF=OF)
ja  target    # 无符号比较大于时跳转 (CF=0 and ZF=0)
jb  target    # 无符号比较小于时跳转 (CF=1)
js  target    # 有符号时跳转 (SF=1)
jns target    # 无符号时跳转 (SF=0)

基本的栈指令

在x86汇编中,%rsp 通常是栈指针(32位里是 %esp),栈对应的操作有 poppush 两种。 push 指令有一个操作数,它会把这个操作数压入到栈里,并且会使栈指针递减。pop 指令也有一个操作数,它把栈顶的元素弹出并放到这个操作数中,同样,它会使栈指针递增。另外,还有下列的一些特殊的栈指令。

pushf     # 将标志寄存器压入栈中
pusha     # 将AX, CX, DX, BX, SP, BP, SI, DI压入栈中,如果指令是pushal那么将压入8个32位寄存器。
popf      # 将栈顶的元素弹出并填充到标志寄存器中
popa      # 依次弹出8个元素,并依序填入到DI, SI, BP, SP, BX, DX, CX, AX这8个寄存器中。

基本的函数指令

基本的函数指令就是 callret 这两个了。 call 会把当前指令(%eip)的下一条指令的地址压入栈里,然后跳转到函数的入口。ret 会取出栈顶的元素并把它赋值给 %eip,之后继续执行代码。ret 也是可以有操作数的,不过它并不表示返回值,而是代表在给 %eip 赋值后继续弹出多少个数。

参考资料


作者: Saurus (jia1Saurus@gmail.com)

如果有错误,请务必指出,以便及时更正