Fork me on GitHub

深入理解《深入理解计算机系统》(三)

汇编基础,数据传送指令

书接上文,既然已经了解了操作数指示符,就应当了解基础的数据传送指令了。

访问信息

数据传送指令

最频繁使用的指令是将数据从一个位置复制到另一个位置的指令。操作数表示的通用性使得一条简单的数据传送指令能够完成在许多机器中要好几条不同指令才能完成的功能。我们会使用不同的数据传送指令,它们或者源和目的类型不同,或者执行的转换不同,或者具有的一些副作用不同。在我们的讲述中,把许多不同的指令划分为指令类,每一类中的指令执行的操作相同,只是操作数大小存在差异。
下表列出了最简单形式的数据传送指令——MOV 类。这些指令将数据复制到目的位置,不做任何变化。MOV 类由四条指令组成:movb、movw、movl、movq。这些指令都执行同样的操作,主要区别在于它们操作的数据大小不同:分别为1、2、4 和 8 字节。

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

表-1 简单的数据传送指令

源操作数指定的值是一个立即数,存储在寄存器中或者内存中。目的操作数指定一个位置,要么是一个寄存器或者,要么是一个内存地址。在 x86-64 中有一条限制,两个操作数不能都指向内存位置。将一个值从一个内存位置复制到另一个内存位置需要两条指令——借助寄存器。这些指令可以是上一篇中的 16 个寄存器有标号部分的任意一个,寄存器部分的大小必须与指令最后一个字符(‘b’‘w’‘l’或‘q’)指定的大小匹配。大多数情况下,MOV 指令值汇更新目的操作数指定的那些寄存器字节或内存位置。唯一得例外是 movl 指令以寄存器作为目的时,它会把该寄存器得高位 4 字节设置为 0 。造成这个例外的原因是 x86-64 采用的惯例,即任何为寄存器生成 32 位值得指令都会把该寄存器的高位部分置成 0 。
下面的 MOV 指令示例给出了源和目的类型的五种可能的组合。记住,第一个是源操作数,第二个是目的操作数:
1
2
3
4
5
movl $0x4050, %eax
movw %bp, %sp
movb (%rdi, %rcx), %al
movb $-17, (%rsp)
movq %rax, -12(%rbp)

表-1 记录的最后一条指令是处理 64 位立即数数据的。常规的 movq 指令以只能表示为 32 位补码数字的立即数作为源操作数,然后把这个值符号扩展得到 64 位的值,放到目的位置。movabsq 指令能够以任意 64 位立即数值作为源操作数,并且只能以寄存器作为目的。
下量表记录了两类数据移动指令,在将较小的源值复制到较大的目的时使用。所有这些指令都把数据从源(在寄存器或内存中)复制到目的寄存器。MOVZ 类中的指令把目的中的剩余字节填充为 0,而 MOVS 类中的指令通过符号扩展来填充,吧源操作数的最高位进行复制。可以观察到,指令名字的最后两个字符都是大小指示符:第一个字符指定源的大小,第二个字符指定目的的大小。

指令效果描述
MOVZ S, RR←零扩展(S)以零扩展进行传送
movzbw将做了零扩展的字节传送到字
movzbl将做了零扩展的字节传送到双字
movzwl将做了零扩展的字传送到双字
movzbq将做了零扩展的字节传送到四字
movzwq将做了零扩展的字传送到四字

表-2 零扩展数据传送指令。这些指令以寄存器或内存地址作为源,以寄存器作为目的

指令效果描述
MOVS S, RR←符号扩展(S)传送符号扩展的字节
movsbw将做了符号扩展的字节传送到字
movsbl将做了符号扩展的字节传送到双字
movswl将做了符号扩展的字传送到双字
movsbq将做了符号扩展的字节传送到四字
movswq将做了符号扩展的字传送到四字
movslq将做了符号扩展的双字传送到四字
cltq%rax ←符号扩展(%eax)把 %eax 符号扩展到 %rax

表-3 符号扩展数据传送指令。MOVS 指令以寄存器或内存地址作为源,以寄存器作为目的。
cltq 指令只作用于寄存器 %eax 和 %rax

数据传送如何改变目的寄存器

下列代码能够很好地说明问题:

1
2
3
4
5
movabsq $0x0011223344556677, %rax	; %rax = 0011223344556677
movb $-1, %al ; %rax = 00112233445566FF
movw $-1, %ax ; %rax = 001122334455FFFF
movl $-1, %eax ; %rax = 00000000FFFFFFFF
movq $-1, %rax ; %rax = FFFFFFFFFFFFFFFF

字节传送指令比较

下列代码能够很好地说明问题:

1
2
3
4
5
movabsq $0x0011223344556677, %rax	; %rax = 0011223344556677
movb $0xAA, %dl ; %dl = AA
movb %dl, %al ; %rax = 00112233445566AA
movsbq %dl, %rax ; %rax = FFFFFFFFFFFFFFAA
movzbq %dl, %rax ; %rax = 00000000000000AA

数据传送示例

考虑下列 C 语言代码与 GCC 产生的汇编代码:

1
2
3
4
5
long exchange(long *xp, long y){
long x = *xp;
*xp = y;
return x;
}

1
2
3
4
5
6
; long exchange(long *xp, long y)
; xp in %rdi, y in %rsi
exchange:
movq (%rdi), %rax ; Get x at xp. Set as return value
movq %rsi, (%rdi) ; Store y at xp
ret ; Return

如上述代码所示,函数 exchange 由三条指令实现:两个数据传送(movq),加上一个返回函数被调用点的指令(ret)。我们会在后续讨论它,现在只需要了解参数通过寄存器传递给函数。我们加了注释来予以说明。函数通过把值存在寄存器 %rax 或该寄存器地某个低位部分中返回。
当过程开始执行时,过程参数 xp 和 y 分别存储在寄存器 %rdi 和 %rsi 中。然后,指令从内存中读出 x,把它存放到寄存器 %rax 中,直接实现了 C 程序中的操作 x = *xp。稍后,用寄存器 %rax 从这个函数返回一个值,因而返回值就是 x。下一条指令又将 y 写入到寄存器 %rdi 中的 xp 指向的内存位置,直接实现了操作 *xp = y。这个例子说明了如何用 MOV 指令从内存中读值到寄存器,如何从寄存器写到内存。
在这里,我们注意到,C 语言中的“指针”其实就是地址。间接引用指针就是将该指针放在一个寄存器中,然后在内存引用中使用这个寄存器。其次,像 x 这样的局部变量通常是保存在寄存器中,而不是内存中。访问寄存器要比访问内存快得多。
在下一节中,我们会使用来处理数据。

0%