汇编入门(二)

x86-64 架构中的整型寄存器如下图所示(暂时不考虑浮点数的部分)

img

仔细看看寄存器的分布,我们可以发现有不同的颜色以及不同的寄存器名称,黄色部分是 16 位寄存器,也就是 16 位处理器 8086 的设计,然后绿色部分是 32 位寄存器(这里我是按照比例画的),给 32 位处理器使用,而蓝色部分是为 64 位处理器设计的。这样的设计保证了令人震惊的向下兼容性,几十年前的 x86 代码现在仍然可以运行!

前六个寄存器(%rax, %rbx, %rcx, %rdx, %rsi, %rdi)称为通用寄存器,有其『特定』的用途:

  • %rax(%eax) 用于做累加
  • %rcx(%ecx) 用于计数
  • %rdx(%edx) 用于保存数据
  • %rbx(%ebx) 用于做内存查找的基础地址
  • %rsi(%esi) 用于保存源索引值
  • %rdi(%edi) 用于保存目标索引值

而 %rsp(%esp) 和 %rbp(%ebp) 则是作为栈指针和基指针来使用的。下面我们通过 movq 这个指令来了解操作数的三种基本类型:立即数(Imm)、寄存器值(Reg)和内存值(Mem)。

对于 movq 指令来说,需要源操作数和目标操作数,源操作数可以是立即数、寄存器值或内存值的任意一种,但目标操作数只能是寄存器值或内存值。指令的具体格式可以这样写 movq [Imm|Reg|Mem], [Reg|Mem],第一个是源操作数,第二个是目标操作数,例如:

  • movq Imm, Reg -> mov $0x5, %rax -> temp = 0x5;
  • movq Imm, Mem -> mov $0x5, (%rax) -> *p = 0x5;
  • movq Reg, Reg -> mov %rax, %rdx -> temp2 = temp1;
  • movq Reg, Mem -> mov %rax, (%rdx) -> *p = temp;
  • movq Mem, Reg -> mov (%rax), %rdx -> temp = *p;

这里有一种情况是不存在的,没有 movq Mem, Mem 这个方式,也就是说,我们没有办法用一条指令完成内存间的数据交换。

上面的例子中有些操作数是带括号的,括号的意思就是寻址,这也分两种情况:

  • 普通模式,(R),相当于 Mem[Reg[R]],也就是说寄存器 R 指定内存地址,类似于 C 语言中的指针,语法为:movq (%rcx), %rax 也就是说以 %rcx 寄存器中存储的地址去内存里找对应的数据,存到寄存器 %rax 中
  • 移位模式,D(R),相当于 Mem[Reg[R]+D],寄存器 R 给出起始的内存地址,然后 D 是偏移量,语法为:movq 8(%rbp),%rdx 也就是说以 %rbp 寄存器中存储的地址再加上 8 个偏移量去内存里找对应的数据,存到寄存器 %rdx 中

因为寻址这个内容比较重要,所以多说两句,不然之后接触指针会比较吃力。对于寻址来说,比较通用的格式是 D(Rb, Ri, S) -> Mem[Reg[Rb]+S*Reg[Ri]+D],其中:

  • D - 常数偏移量
  • Rb - 基寄存器
  • Ri - 索引寄存器,不能是 %rsp
  • S - 系数

除此之外,还有如下三种特殊情况

  • (Rb, Ri) -> Mem[Reg[Rb]+Reg[Ri]]
  • D(Rb, Ri) -> Mem[Reg[Rb]+Reg[Ri]+D]
  • (Rb, Ri, S) -> Mem[Reg[Rb]+S*Reg[Ri]]

我们通过具体的例子来巩固一下,这里假设 %rdx 中的存着 0xf000,%rcx 中存着 0x0100,那么

  • 0x8(%rdx) = 0xf000 + 0x8 = 0xf008
  • (%rdx, %rcx) = 0xf000 + 0x100 = 0xf100
  • (%rdx, %rcx, 4) = 0xf000 + 4*0x100 = 0xf400
  • 0x80(, %rdx, 2) = 2*0xf000 + 0x80 = 0x1e080

了解了寻址之后,我们来看看运算指令,这里以 leaq 指令为例子。具体格式为 leaq Src, Dst,其中 Src 是地址的表达式,然后把计算的值存入 Dst 指定的寄存器,也就是说,无须内存引用就可以计算,类似于 p = &x[i];。我们来看一个具体的例子,假设一个 C 函数是:

1
2
3
4
long m12(long x)
{
return x * 12;
}

复制

对应的汇编代码为:

1
2
leaq (%rdi, %rdi, 2), %rax # t <- x+x*2
salq $2, %rax # return t << 2

复制

可以看到是直接对 %rdi 寄存器中存的数据(地址)进行运算,然后赋值给 %rax。最后给出一些常见的算术运算指令,注意参数的顺序,而且对于有符号和无符号数都是一样的,更多的信息可以参考 Intel 官方文档[3]。

需要两个操作数的指令

  • addq Src, Dest -> Dest = Dest + Src
  • subq Src, Dest -> Dest = Dest - Src
  • imulq Src, Dest -> Dest = Dest * Src
  • salq Src, Dest -> Dest = Dest << Src
  • sarq Src, Dest -> Dest = Dest >> Src
  • shrq Src, Dest -> Dest = Dest >> Src
  • xorq Src, Dest -> Dest = Dest ^ Src
  • andq Src, Dest -> Dest = Dest & Src
  • orq Src, Dest -> Dest = Dest | Src

需要一个操作数的指令

  • incq Dest -> Dest = Dest + 1
  • decq Dest -> Dest = Dest - 1
  • negq Dest -> Dest = -Dest
  • notq Dest -> Dest = ~Dest