基本计算指令

这里的汇编指令均基于x86-64架构

0. 先验知识

0.1 寄存器设置

一个x86-64的中央处理单元包含一组16个64位通用目的寄存器。这些寄存器用来存储整数数据和指针。指令可以对这16个寄存器的低位字节中存放的不同大小的数据进行操作。字节级操作可以访问最低的字节,16位操作可以访问最低的2个字节,32位操作可以访问最低的4个字节,而64位操作可以访问整个寄存器。

3. 寻址方式

1. 数据传送指令

最简单形式的数据传送指令–mov类。这些指令把数据从源位置复制到目的位置,不做任何变化。mov类指令由四条指令组成:movb, movw, movl,movq.这些指令都执行相同的操作,区别在于它们操作的数据大小不同:分别是1,2,4,8字节。

由于历史原因,Intel处理器将16位作为一个字(w),8位为一个字节(b),32位为双字(l),64位为4字(q)

指令效果描述
MOV S DD <- S传送
movb传送字节
movw传送字
movl传送双字
movq传送四字
movabsq I, R传送绝对的四字

传送指令的两个操作数不能都指向内存位置。将一个值从一个内存位置复制到另一个内存位置需要两个步骤,第一个指令将源值加载到寄存器,第二条指令将该寄存器写入目的位置。

example

C code

long exchange(lone *xp, long y){
    long x = *xp;
    *xp = y;
    return x;
}

汇编代码

exchange:
    movq (%rdi), %rax     get x at xp, Set as return value
    movq %rsi, (%rdi)     Store y at xp.
    ret

2. 压栈和出栈

在x86-64中,程序栈存放在内存中某个区域,栈向下增长,这样,栈顶元素的地址是所有栈中元素地址最低的。栈指针%rsp保存着栈顶元素的地址。

pushq指令的功能是把数据压入到栈上,而popq指令是弹出数据。这些指令都只有一个操作数–压入的数据源和弹出的数据目的。
将一个四字值压入栈中,首先要将栈指针减8,然后再将值写入到新的栈顶地址。因此,pushq %rbp的行为等价于下面两条命令:

subq $8, %rsp         Decrement stack pointer
movq %rbp, (%rsp)     Store %rbp on stack

弹出一个四字的操作包括从栈顶位置读出数据,然后将栈指针加8,因此,指令popq %rax等价于下面两条指令:

movq (%rsp) , %rax        Read %rax from stack
addq $8, %rsp             Increment stack pointer

example

3. 算术和逻辑操作

每个指令类都有b, w, l, q这四种不同大小的指令

算术和逻辑操作指令

算术和逻辑操作指令

3.1 加载有效地址

加载有效地址(load effective address)指令leaq实际上是movq指令的变形。它的指令形式是从内存读数据到寄存器,但实际上它根本就没有引用内存。它的第一个操作数看上去是一个内存引用,但该指令并不是从指定的位置读入数据,而是将有效地址写入到目的操作数。
lea还可以简洁的描述普通的算术操作。例如,如果寄存器%rdx的值为x, 那么指令leaq 7(%rdx, %rdx, 4),%rax将设置寄存器%rax的值为5x+7

example

c code

long scale(long x, long y, long z){
    long t = x + 4 * y + 12 * z;
    return t;
}

汇编指令

lone scale(long x, long y, long z)
x in %rdi, y in %rsi, z in %rdx
scale:
    leaq (%rdi, %rsi, 4), %rax      x + 4 * y
    leaq (%rdx, %rdx, 2), %rax      z + 2 * z = 3 * z
    leaq (%rax, %rdx, 4), %rax      (x+4×y) + 4*(3*z) = x + 4*y + 12*z
    ret

3.2 一元和二元操作

上表中的第二组中的操作是一元操作,只有一个操作数,既是源又是目的。这个操作数可以是寄存器,也可以是内存位置。比如说,指令incq (%rsp)会使栈顶的8字节元素+1.

第三组是二元操作,其中第二个操作数既是源又是目的。不过,要注意,源操作数是第一个,目的操作数是第二个,对于不可交换来说,这看上去很奇特。例如,指令subq %rax, %rdx使寄存器%rdx的值减去%rax中的值。

3.3 移位操作

最后一组是移位操作,先给出移位量,然后第二项给出的是要移位的数。可以进行算术和逻辑右移。移位量可以是一个立即数,或者放在单字节寄存器%cl中。

左移指令有两个名字:salshl。二者的效果是一样的,都是将右边填上0.右移指令不同,sar执行算术移位(填上符号位),而shr执行逻辑移位(填上0).

当寄存器%cl的十六进制值为0xFF时,指令salb会移7位,salw会移15为,sall会移31位,salq会移63位`

3.4 特殊的算术操作

两个64位有符号或无符号整数相乘得到的乘积需要128位来表示。intel把16字节的数成为八字(oct word).

特殊的算术操作

example: 如何从两个无符号64位数字x和y生成128位的乘积

c code

#include<inttypes.h>

typedef unsigned __int128 uint128_t

void store_uprod(uint128_t *dest, uint64_t x, uint64_t y){
    *dest = x * (uint128_t) y;
}

汇编代码

dest in %rdi, x in %rsi, y in %rdx
store_uprod:
    movq %rsi, %rax          copy x to multiplicand
    mulq %rdx                multiply by y
    movq %rax, (%rdi)        store lower 8 bytes at dest
    movq %rdx, 8(%rdi)       store upper 8 bytes at dest+8
    ret

example: 如何实现除法

c code

void remdiv(long x, long y, long *qp, long *rp){
    long q = x/y;
    long r = x%y;
    *qp = q;
    *rp = r;
}

汇编代码

x in %rdi, y in %rsi, qp in %rdx, rp in %rcx
remdiv:
    movq %rdx, %r8       copy qp
    movq %rdi, %rax      mov x to lower 8 bytes of divident
    cqto
    idivq %rsi           divide by y
    movq %rax, (%r8)     store quotient at qp
    movq %rdx, (%rcx)    store remainder at rp

cqto指令不需要操作数,它隐含读出%rax的符号位,并将它复制到%rdx的所有位