3.4.2 数据传送指令
3.4.2 数据传送指令
最频繁使用的指令是将数据从一个位置复制到另一个位置的指令。操作数表示的通用性使得一条简单的数据传送指令能够完成在许多机器中要好几条不同指令才能完成的功能。我们会介绍多种不同的数据传送指令,它们或者源和目的类型不同,或者执行的转换不同,或者具有的一些副作用不同。在我们的讲述中,把许多不同的指令划分成指令类,每一类中的指令执行相同的操作,只不过操作数大小不同。
图 3-4 列出的是最简单形式的数据传送指令——MOV 类。这些指令把数据从源位置复制到目的位置,不做任何变化。MOV 类由四条指令组成:movb、movw、movl 和 movq。这些指令都执行同样的操作;主要区别在于它们操作的数据大小不同:分别是 1、2、4 和 8 字节。
| 指令 | 效果 | 描述 |
|---|---|---|
| MOV S, D | D ← S | 传送 |
movb |
传送字节 | |
movw |
传送字 | |
movl |
传送双字 | |
movq |
传送四字 | |
movabsq I, R |
R ← I | 传送绝对的四字 |
图 3-4 简单的数据传送指令
源操作数指定的值是一个立即数,存储在寄存器中或者内存中。目的操作数指定一个位置,要么是一个寄存器,要么是一个内存地址。x86-64 加了一条限制,传送指令的两个操作数不能都指向内存位置。将一个值从一个内存位置复制到另一个内存位置需要两条指令——第一条指令将源值加载到寄存器中,第二条将该寄存器值写入目的位置。
参考图 3-2,这些指令的寄存器操作数可以是 16 个寄存器有标号部分中的任意一个,寄存器部分的大小必须与指令最后一个字符(b、w、l 或 q)指定的大小匹配。大多数情况中,MOV 指令只会更新目的操作数指定的那些寄存器字节或内存位置。唯一的例外是 movl 指令以寄存器作为目的时,它会把该寄存器的高位 4 字节设置为 0。造成这个例外的原因是 x86-64 采用的惯例,即任何为寄存器生成 32 位值的指令都会把该寄存器的高位部分置成 0。
下面的 MOV 指令示例给出了源和目的类型的五种可能的组合。记住,第一个是源操作数,第二个是目的操作数:
1 movl $0x4050,%eax Immediate--Register, 4 bytes
2 movw %bp,%sp Register--Register, 2 bytes
3 movb (%rdi,%rcx),%al Memory--Register, 1 byte
4 movb $-17,(%rsp) Immediate--Memory, 1 byte
5 movq %rax,-12(%rbp) Register--Memory, 8 bytes
图 3-4 中记录的最后一条指令是处理 64 位立即数数据的。常规的 movq 指令只能以表示为 32 位补码数字的立即数作为源操作数,然后把这个值符号扩展得到 64 位的值,放到目的位置。movabsq 指令能够以任意 64 位立即数值作为源操作数,并且只能以寄存器作为目的。
图 3-5 和图 3-6 记录的是两类数据移动指令,在将较小的源值复制到较大的目的时使用。所有这些指令都把数据从源(在寄存器或内存中)复制到目的寄存器。MOVZ 类中的指令把目的中剩余的字节填充为 0,而 MOVS 类中的指令通过符号扩展来填充,把源操作数的最高位进行复制。可以观察到,每条指令名字的最后两个字符都是大小指示符:第一个字符指定源的大小,而第二个指明目的的大小。正如看到的那样,这两个类中每个都有三条指令,包括了所有的源大小为 1 个和 2 个字节、目的大小为 2 个和 4 个字节的情况,当然只考虑目的大于源的情况。
| 指令 | 效果 | 描述 |
|---|---|---|
| MOVZ S, R | R ← 零扩展(S) | 以零扩展进行传送 |
movzbw |
将做了零扩展的字节传送到字 | |
movzbl |
将做了零扩展的字节传送到双字 | |
movzwl |
将做了零扩展的字传送到双字 | |
movzbq |
将做了零扩展的字节传送到四字 | |
movzwq |
将做了零扩展的字传送到四字 |
图 3-5 零扩展数据传送指令。这些指令以寄存器或内存地址作为源,以寄存器作为目的
| 指令 | 效果 | 描述 |
|---|---|---|
| MOVS S, R | R ← 符号扩展(S) | 传送符号扩展的字节 |
movsbw |
将做了符号扩展的字节传送到字 | |
movsbl |
将做了符号扩展的字节传送到双字 | |
movswl |
将做了符号扩展的字传送到双字 | |
movsbq |
将做了符号扩展的字节传送到四字 | |
movswq |
将做了符号扩展的字传送到四字 | |
movslq |
将做了符号扩展的双字传送到四字 | |
cltq |
%rax ← 符号扩展(%eax) |
把 %eax 符号扩展到 %rax |
图 3-6 符号扩展数据传送指令。MOVS 指令以寄存器或内存地址作为源,以寄存器作为目的。cltq 指令只作用于寄存器 %eax 和 %rax
旁注:理解数据传送如何改变目的寄存器
正如我们描述的那样,关于数据传送指令是否以及如何修改目的寄存器的高位字节有两种不同的方法。下面这段代码序列会说明其差别:
1 movabsq $0x0011223344556677, %rax %rax = 0011223344556677 2 movb $-1, %al %rax = 00112233445566FF 3 movw $-1, %ax %rax = 001122334455FFFF 4 movl $-1, %eax %rax = 00000000FFFFFFFF 5 movq $-1, %rax %rax = FFFFFFFFFFFFFFFF在接下来的讨论中,我们使用十六进制表示。在这个例子中,第 1 行的指令把寄存器
%rax初始化为位模式0011223344556677。剩下的指令的源操作数值是立即数值 -1。回想一下,-1 的十六进制表示形如FF...F,这里 F 的数量是表述中字节数量的两倍。因此movb指令(第 2 行)把%rax的低位字节设置为FF,而movw指令(第 3 行)把低 2 位字节设置为FFFF,剩下的字节保持不变。movl指令(第 4 行)将低 4 个字节设置为FFFFFFFF,同时把高位 4 字节设置为00000000。最后movq指令(第 5 行)把整个寄存器设置为FFFFFFFFFFFFFFFF。
注意图 3-5 中并没有一条明确的指令把 4 字节源值零扩展到 8 字节目的。这样的指令逻辑上应该被命名为 movzlq,但是并没有这样的指令。不过,这样的数据传送可以用以寄存器为目的的 movl 指令来实现。这一技术利用的属性是,生成 4 字节值并以寄存器作为目的的指令会把高 4 字节置为 0。对于 64 位的目标,所有三种源类型都有对应的符号扩展传送,而只有两种较小的源类型有零扩展传送。
图 3-6 还给出 cltq 指令。这条指令没有操作数:它总是以寄存器 %eax 作为源,%rax 作为符号扩展结果的目的。它的效果与指令 movslq %eax,%rax 完全一致,不过编码更紧凑。
练习题 3.2 对于下面汇编代码的每一行,根据操作数,确定适当的指令后缀。(例如,mov 可以被重写成 movb、movw、movl 或者 movq。)
mov_ %eax, (%rsp)
mov_ (%rax), %dx
mov_ $0xFF, %bl
mov_ (%rsp,%rdx,4), %dl
mov_ (%rdx), %rax
mov_ %dx, (%rax)旁注:字节传送指令比较
下面这个示例说明了不同的数据传送指令如何改变或者不改变目的的高位字节。仔细观察可以发现,三个字节传送指令
movb、movsbq和movzbq之间有细微的差别。示例如下:1 movabsq $0x0011223344556677, %rax %rax = 0011223344556677 2 movb $0xAA, %dl %dl = AA 3 movb %dl, %al %rax = 00112233445566AA 4 movsbq %dl, %rax %rax = FFFFFFFFFFFFFFAA 5 movzbq %dl, %rax %rax = 00000000000000AA在下面的讨论中,所有的值都使用十六进制表示。代码的头 2 行将寄存器
%rax和%dl分别初始化为0011223344556677和AA。剩下的指令都是将%rdx的低位字节复制到%rax的低位字节。movb指令(第 3 行)不改变其他字节。根据源字节的最高位,movsbq指令(第 4 行)将其他 7 个字节设为全 1 或全 0。由于十六进制 A 表示二进制值 1010,符号扩展会把高位字节都设置为FF。movzbq指令(第 5 行)总是将其他 7 个字节全都设置为 0。
练习题 3.3 当我们调用汇编器的时候,下面代码的每一行都会产生一个错误消息。解释每一行都是哪里出了错。
movb $0xF, (%ebx)
movl %rax, (%rsp)
movw (%rax), 4(%rsp)
movb %al, %sl
movq %rax, $0x123
movl %eax, %rdx
movb %si, 8(%rbp)