深入理解计算机系统-3-程序的机器级表示

该系列文章摘抄于深入理解计算机系统第三章程序的机器级表示部分。书中部分章节介绍过于详细或基础,故不总结于此。只摘抄之前认识不深刻或者不理解的知识盲区。

数据格式

由于是从16位体系结构扩展成32位,Intel用术语“字(word)”,表示16位数据类型。因此称32位位“双字(double words)”,称64位位“四字(quad words)”。C语言基本数据类型对应的x86-64表示如下表:

C声明 Intel数据类型 汇编代码后缀 大小(字节)
char 字节 b 1
short w 2
int 双字 l 4
long 四字 q 8
char* 四字 q 8
float 单精度 s 4
double 双精度 l 8

如图所示,大多数GCC生成的汇编代码指令都有一个字符的后缀,报名操作数的大小。例如,数据传输指令有四个变种:

  • movb 传送字节
  • movw 传送字
  • movl 传送双字
  • movq 传送四字

访问信息

一个x86-64的中央处理单元CPU包含一组16个存储64位值的通用目的寄存器。这些寄存器用来存储整数数据和指针。下图所示。他们的名字都以%r开头,不过后面还跟着一些不同的命名规则的名字,这是由于指令集历史演化造成的。最初的80886中有8个16位的寄存区,即图中的%ax到%sp。扩展到IA32架构时,这些寄存器也扩展成了32位,标号从%eax到%esp。扩展到x86-64后,原来的8个寄存器扩展成64位,标号从%rax到%rsp。除此之外还增加了8个新的寄存器,标号按照新的命名规则制定,从%r8到%r15。
"register"

操作数指示符

大多数指令有一个或多个操作数(operand),指示出执行一个操作中要使用的源数据值,以及放置结果的目的位置。x86-64支持多种操作数格式。
"dataformat"

各种操作数类型分为三种类型:

  • 立即数(immediate)。用来表示常数值, “$” + 标准C表示法表示的整数。如$-123, $0x1F。
  • 寄存区(register)。表示某个寄存器的内容。用ra表示任意寄存区a,用引用R[ra]表示它的值。
  • 内存引用。它会根据计算出来的地址(有效地址)访问某个内存位置。因为它将内存看成一个很大的字节数组。

数据传送指令

最频繁使用的指令是将数据从一个位置复制到另一个位置的指令。下表的指令都执行同样的操作,主要的区别在于操作的数据大小不同,分别是1,2,4,8字节。

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

下面的MOV指令示例给出了源和目的类型的五种可能的组合,第一个是源操作数,第二个是目的操作数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
1. movl $0x4050,%eas
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

下图记录的是两类数据移动指令,在将较小的源值复制到较大的目的时使用。MOVZ类中的指令把目的中剩余的字节填充为0,而MOVS类中的指令通过符号扩展来填充。

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

这些指令以寄存区或内存地址作为源,以寄存区作为目的。
注意,并没有将做了零扩展的双字传送到四字的指令movzlq。不过这样的数据传送可以用以寄存器为目的的movl实现。这一技术利用的属性是,生成4字节值并以寄存器作为目的指令会把高四字节置为零。

指令 效果 描述
MOVS S, R R < - -符合扩展(S) 以符号扩展进行传送
movsbw 将做了符号扩展的字节传送到字
movsbl 将做了符号扩展的字节传送到双字
movsbq 将做了符号扩展的字节传送到四字
movswl 将做了符号扩展的字传送到双字
movswq 将做了符号扩展的字传送到四字
movslq 将做了符号扩展的双字传送到四字
cltq %rax < – 符号扩展(%eax) 把%eax符号扩展到%rax

MOVS这些指令以寄存区或内存地址作为源,以寄存区作为目的。cltq指令只用于寄存器%eax和%rax。

理解数据传输如何改变目的寄存器
关于数据传送指令是否以及如何修改目的寄存器的高位字节有两种不同的方法。

1
2
3
4
5
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

接下来的讨论中,都用十六进制表示。这个例子中,第一行的指令把寄存区%rax初始化为位模式0011223344556677。剩下的指令的源操作数是立即数值-1。-1的十六进制表示位FF…F,这里F的数量是表述中字节数量的2倍。2,3,5行结果源出于此。例外是第四行movl,大多数情况下mov指令只会修改目的操作数指定的那些寄存区字节或内存位置。但是movl指令以寄存器作为目的时,它会把寄存器的高位4字节设置为0,造成这个例外的原因是x86-64采用的惯例,即任何为寄存器生成32位值的指令都会把该寄存器的高位部分置零。

字节传送指令比较
下面这个例子说明不同的是数据传送指令如何改变或者不改变目的的高位字节。仔细观察就可以发现movb, movsbq, movzbq之间的细微差别。

1
2
3
4
5
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寄存区的初始化,后3行分别使用movb/movsbq/movzbq完成从%dl到%rax的低字节copy。movb指令不改变其他字节。movsbq根据符号将其他7个字节设为全1或全0,A的十六进制表示为0x1010,因此符号扩展会将高位字节全部设置为FF。movzbq指令总是将其他7个字节全部设置为0。

数据传送示例

假设变量sp和dp被声明为类型

1
2
3
4
src_t *sp;
dest_t *dp;

*dp = (dest_t)*sp;

假设sp和dp的值分别存储在寄存器%rdi和%rsi中,对于下面表中的每个表项,给出实现指定数据传送的两条指令。其中第一条指令应该从内存中读数,做适当的装换,并设置寄存器%rax的适当部分。然后第二条指令要把%rax的适当部分写到内存,在这两种情况下,寄存器的部分可以是%rax,%eax,%ax和%al,两者互不相同。

src_t dest_t 指令 注释
long long movq(%rdi), %rax
movq %rax, (%rsi)
读8个字节
存8个字节
char int movsbl(%rdi), %eax
movl %eax, (%rsi)
将char转换成int
存4个字节
char unsigned movsbl(%rdi), %eax
movl %eax, (%rsi)
将char转换成int
存4个字节
unsigned char long movzbl(%rdi), %eax
movq %rax, (%rsi)
读一个字节并零扩展
存8个字节
int char movl(%rdi), %eax
movb %al, (%rsi)
读4个字节
存低位字节
unsigned unsigned char movl(%rdi), %eax
movb %al, (%rsi)
读4个字节
存低位字节
char short movsbw(%rdi), %ax
movw %ax, (%rsi)
读1个字节并符号
存2个字节