# 4.1.3 指令编码
图 4-2 还给出了指令的字节级编码。每条指令需要 1~10 个字节不等,这取决于需要哪些字段。每条指令的第一个字节表明指令的类型。这个字节分为两个部分,每部分 4 位:高 4 位是代码(code)部分,低 4 位是功能(function)部分。如图 4-2 所示,代码值为 0~0xB。功能值只有在一组相关指令共用一个代码时才有用。图 4-3 给出了整数操作、分支和条件传送指令的具体编码。可以观察到,rrmovq 与条件传送有同样的指令代码。可以把它看作是一个“无条件传送”,就好像 jmp 指令是无条件跳转一样,它们的功能代码都是 0。
| 类别 | 指令 | 代码 | 功能 |
|---|---|---|---|
| 整数操作指令 | addq | 6 | 0 |
| 整数操作指令 | subq | 6 | 1 |
| 整数操作指令 | andq | 6 | 2 |
| 整数操作指令 | xorq | 6 | 3 |
| 分支指令 | jmp | 7 | 0 |
| 分支指令 | jle | 7 | 1 |
| 分支指令 | jl | 7 | 2 |
| 分支指令 | je | 7 | 3 |
| 分支指令 | jne | 7 | 4 |
| 分支指令 | jge | 7 | 5 |
| 分支指令 | jg | 7 | 6 |
| 传送指令 | rrmovq | 2 | 0 |
| 传送指令 | cmovle | 2 | 1 |
| 传送指令 | cmovl | 2 | 2 |
| 传送指令 | cmove | 2 | 3 |
| 传送指令 | cmovne | 2 | 4 |
| 传送指令 | cmovge | 2 | 5 |
| 传送指令 | cmovg | 2 | 6 |
图 4-3 Y86-64 指令集的功能码。这些代码指明是某个整数操作、分支条件还是数据传送条件。这些指令是图 4-2 中所示的 OPq、jXX 和 cmovXX
如图 4-4 所示,15 个程序寄存器中每个都有一个相对应的范围在 0 到 0xE 之间的寄存器标识符(register ID)。Y86-64 中的寄存器编号跟 x86-64 中的相同。程序寄存器存在 CPU 中的一个寄存器文件中,这个寄存器文件就是一个小的、以寄存器 ID 作为地址的随机访问存储器。在指令编码中以及在我们的硬件设计中,当需要指明不应访问任何寄存器时,就用 ID 值 0xF 来表示。
| 数字 | 寄存器名字 | 数字 | 寄存器名字 |
|---|---|---|---|
0 | %rax | 8 | %r8 |
1 | %rcx | 9 | %r9 |
2 | %rdx | A | %r10 |
3 | %rbx | B | %r11 |
4 | %rsp | C | %r12 |
5 | %rbp | D | %r13 |
6 | %rsi | E | %r14 |
7 | %rdi | F | 无寄存器 |
图 4-4 Y86-64 程序寄存器标识符。15 个程序寄存器中每个都有一个相对应的标识符(ID),范围为 0~0xE。如果指令中某个寄存器字段的 ID 值为 0xF,就表明此处没有寄存器操作数
有的指令只有一个字节长,而有的需要操作数的指令编码就更长一些。首先,可能有附加的寄存器指示符字节(register specifier byte),指定一个或两个寄存器。在图 4-2 中,这些寄存器字段称为 rA 和 rB。从指令的汇编代码表示中可以看到,根据指令类型,指令可以指定用于数据源和目的的寄存器,或是用于地址计算的基址寄存器。没有寄存器操作数的指令,例如分支指令和 call 指令,就没有寄存器指示符字节。那些只需要一个寄存器操作数的指令(irmovq、pushq 和 popq)将另一个寄存器指示符设为 0xF。这种约定在我们的处理器实现中非常有用。
有些指令需要一个附加的 8 字节常数字(constant word)。这个字能作为 irmovq 的立即数数据,rmmovq 和 mrmovq 的地址指示符的偏移量,以及分支指令和调用指令的目的地址。注意,分支指令和调用指令的目的是一个绝对地址,而不像 IA32 中那样使用 PC(程序计数器)相对寻址方式。处理器使用 PC 相对寻址方式,分支指令的编码会更简洁,同时这样也能允许代码从内存的一部分复制到另一部分而不需要更新所有的分支目标地址。因为我们更关心描述的简单性,所以就使用了绝对寻址方式。同 IA32 一样,所有整数采用小端法编码。当指令按照反汇编格式书写时,这些字节就以相反的顺序出现。
例如,用十六进制来表示指令 rmmovq %rsp, 0x123456789abcd(%rdx) 的字节编码。从图 4-2 我们可以看到,rmmovq 的第一个字节为 40。源寄存器 %rsp 应该编码放在 rA 字段中,而基址寄存器 %rdx 应该编码放在 rB 字段中。根据图 4-4 中的寄存器编号,我们得到寄存器指示符字节 42。最后,偏移量编码放在 8 字节的常数字中。首先在 0x123456789abcd 的前面填充上 0 变成 8 个字节,变成字节序列 00 01 23 45 67 89 ab cd。写成按字节反序就是 cd ab 89 67 45 23 01 00。将它们都连接起来就得到指令的编码:
4042cdab896745230100
指令集的一个重要性质就是字节编码必须有唯一的解释。任意一个字节序列要么是一个唯一的指令序列的编码,要么就不是一个合法的字节序列。Y86-64 就具有这个性质,因为每条指令的第一个字节有唯一的代码和功能组合,给定这个字节,我们就可以决定所有其他附加字节的长度和含义。这个性质保证了处理器可以无二义性地执行目标代码程序。即使代码嵌入在程序的其他字节中,只要从序列的第一个字节开始处理,我们仍然可以很容易地确定指令序列。反过来说,如果不知道一段代码序列的起始位置,我们就不能准确地确定怎样将序列划分成单独的指令。对于试图直接从目标代码字节序列中抽取出机器级程序的反汇编程序和其他一些工具来说,这就带来了问题。
练习题 4.1 确定下面的 Y86-64 指令序列的字节编码。“.pos 0x100”那一行表明这段目标代码的起始地址应该是 0x100。
.pos 0x100 # Start code at address 0x100
irmovq $15,%rbx
rrmovq %rbx,%rcx
loop:
rmmovq %rcx,-3(%rbx)
addq %rbx,%rcx
jmp loop
练习题 4.2 确定下列每个字节序列所编码的 Y86-64 指令序列。如果序列中有不合法的字节,指出指令序列中不合法值出现的位置。每个序列都先给出了起始地址,冒号,然后是字节序列。
A. 0x100: 30f3fcffffffffffffff40630008000000000000
B. 0x200: a06f800c020000000000000030f30a0000000000000090
C. 0x300: 5054070000000000000010f0b01f
D. 0x400: 611373000400000000000000
E. 0x500: 6362a0f0
旁注 比较 x86-64 和 Y86-64 的指令编码
同 x86-64 中的指令编码相比,Y86-64 的编码简单得多,但是没那么紧凑。在所有的 Y86-64 指令中,寄存器字段的位置都是固定的,而在不同的 x86-64 指令中,它们的位置是不一样的。x86-64 可以将常数值编码成 1、2、4 或 8 个字节,而 Y86-64 总是将常数值编码成 8 个字节。
旁注 RISC 和 CISC 指令集
x86-64 有时称为“复杂指令集计算机”(CISC,读作“sisk”),与“精简指令集计算机”(RISC,读作“risk”)相对。从历史上看,先出现了 CISC 机器,它从最早的计算机演化而来。到 20 世纪 80 年代早期,随着机器设计者加入了很多新指令来支持高级任务(例如处理循环缓冲区,执行十进制数计算,以及求多项式的值),大型机和小型机的指令集已经变得非常庞大了。最早的微处理器出现在 20 世纪 70 年代早期,因为当时的集成电路技术极大地制约了一块芯片上能实现些什么,所以它们的指令集非常有限。微处理器发展得很快,到 20 世纪 80 年代早期,大型机和小型机的指令集复杂度一直都在增加。x86 家族沿着这条道路发展到 IA32,最近是 x86-64。即使是 x86 系列也仍然在不断地变化,基于新出现的应用的需要,增加新的指令类。
20 世纪 80 年代早期,RISC 的设计理念是作为上述发展趋势的一种替代而发展起来的。IBM 的一组硬件和编译器专家受到 IBM 研究员 John Cocke 的很大影响,认为他们可以为更简单的指令集形式产生高效的代码。实际上,许多加到指令集中的高级指令很难被编译器产生,所以也很少被用到。一个较为简单的指令集可以用很少的硬件实现,能以高效的流水线结构组织起来,类似于本章后面描述的情况。直到多年以后 IBM 才将这个理念商品化,开发出了 Power 和 PowerPC ISA。
加州大学伯克利分校的 David Patterson 和斯坦福大学的 John Hennessy 进一步发展了 RISC 的概念。Patterson 将这种新的机器类型命名为 RISC,而将以前的那种称为 CISC,因为以前没有必要给一种几乎是通用的指令集格式起名字。
比较 CISC 和最初的 RISC 指令集,我们发现下面这些一般特性。
CISC 早期的 RISC 指令数量很多。Intel 描述全套指令的文档[51]有 1200 多页。 指令数量少得多。通常少于 100 个。 有些指令的延迟很长。包括将一个整块从内存的一部分复制到另一部分的指令,以及其他一些将多个寄存器的值复制到内存或从内存复制到多个寄存器的指令。 没有较长延迟的指令。有些早期的 RISC 机器甚至没有整数乘法指令,要求编译器通过一系列加法来实现乘法。 编码是可变长度的。x86-64 的指令长度可以是 1~15 个字节。 编码是固定长度的。通常所有的指令都编码为 4 个字节。 指定操作数的方式很多样。在 x86-64 中,内存操作数指示符可以有许多不同的组合,这些组合由偏移量、基址和变址寄存器以及伸缩因子组成。 简单寻址方式。通常只有基址和偏移量寻址。 可以对内存和寄存器操作数进行算术和逻辑运算。 只能对寄存器操作数进行算术和逻辑运算。允许使用内存引用的只有 load和store指令,load是从内存读到寄存器,store是从寄存器写到内存。这种方法被称为 load/store 体系结构。对机器级程序来说实现细节是不可见的。ISA 提供了程序和如何执行程序之间的清晰的抽象。 对机器级程序来说实现细节是可见的。有些 RISC 机器禁止某些特殊的指令序列,而有些跳转要到下一条指令执行完了以后才会生效。编译器必须在这些约束条件下进行性能优化。 有条件码。作为指令执行的副产品,设置了一些特殊的标志位,可以用于条件分支检测。 没有条件码。相反,对条件检测来说,要用明确的测试指令,这些指令会将测试结果放在一个普通的寄存器中。 栈密集的过程链接。栈被用来存取过程参数和返回地址。 寄存器密集的过程链接。寄存器被用来存取过程参数和返回地址。因此有些过程能完全避免内存引用。通常处理器有更多的(最多的有 32 个)寄存器。 Y86-64 指令集既有 CISC 指令集的属性,也有 RISC 指令集的属性。和 CISC 一样,它有条件码、长度可变的指令,并用栈来保存返回地址。和 RISC 一样的是,它采用 load/store 体系结构和规则编码,通过寄存器来传递过程参数。Y86-64 指令集可以看成是采用 CISC 指令集(x86),但又根据某些 RISC 的原理进行了简化。
RISC 与 CISC 之争
20 世纪 80 年代,计算机体系结构领域里关于 RISC 指令集和 CISC 指令集优缺点的争论十分激烈。RISC 的支持者声称在给定硬件数量的情况下,通过结合简约式指令集设计、高级编译器技术和流水线化的处理器实现,他们能够得到更强的计算能力。而 CISC 的拥趸反驳说要完成一个给定的任务只需要用较少的 CISC 指令,所以他们的机器能够获得更高的总体性能。
大多数公司都推出了 RISC 处理器系列产品,包括 Sun Microsystems(SPARC)、IBM 和 Motorola(PowerPC),以及 Digital Equipment Corporation(Alpha)。一家英国公司 Acorn Computers Ltd. 提出了自己的体系结构——ARM(最开始是“Acorn RISC Machine”的首字母缩写),广泛应用在嵌入式系统中(比如手机)。
20 世纪 90 年代早期,争论逐渐平息,因为事实已经很清楚了。无论是单纯的 RISC 还是单纯的 CISC 都不如结合两者思想精华的设计。RISC 机器发展进化的过程中,引入了更多的指令,而许多这样的指令都需要执行多个周期。今天的 RISC 机器的指令表中有几百条指令,几乎与“精简指令集机器”的名称不相符了。那种将实现细节暴露给机器级程序的思想已经被证明是目光短浅的。随着使用更加高级硬件结构的新处理器模型的开发,许多实现细节已经变得很落后了,但它们仍然是指令集的一部分。不过,作为 RISC 设计的核心的指令集仍然是非常适合在流水线化的机器上执行的。
比较新的 CISC 机器也利用了高性能流水线结构。就像我们将在 5.7 节中讨论的那样,它们读取 CISC 指令,并动态地翻译成比较简单的、像 RISC 那样的操作的序列。例如,一条将寄存器和内存相加的指令被翻译成三个操作:一个是读原始的内存值,一个是执行加法运算,第三就是将和写回内存。由于动态翻译通常可以在实际指令执行前进行,处理器仍然可以保持很高的执行速率。
除了技术因素以外,市场因素也在决定不同指令集是否成功中起了很重要的作用。通过保持与现有处理器的兼容性,Intel 以及 x86 使得从一代处理器迁移到下一代变得很容易。由于集成电路技术的进步,Intel 和其他 x86 处理器制造商能够克服原来 8086 指令集设计造成的低效率,使用 RISC 技术产生出与最好的 RISC 机器相当的性能。正如我们在第 3.1 节中看到的那样,IA32 发展演变到 x86-64 提供了一个机会,使得能够将 RISC 的一些特性结合到 x86 中。在桌面、便携计算机和基于服务器的计算领域里,x86 已经占据了完全的统治地位。
RISC 处理器在嵌入式处理器市场上表现得非常出色,嵌入式处理器负责控制移动电话、汽车刹车以及因特网电器等系统。在这些应用中,降低成本和功耗比保持后向兼容性更重要。就出卖的处理器数量来说,这是一个非常广阔而迅速成长着的市场。