首先定义一个简单的指令集,作为我们处理器实现的运行示例。因为受x86-64指令集的启发,它被俗称为“x86”,所以我们称我们的指令集为“Y86-64”指令集。与x86-64相比,Y86-64指令集的数据类型、指令和寻址方式都要少一些。它的字节级编码也比较简单,机器代码没有相应的x86-64代码紧凑,不过设计它的CPU译码逻辑也要简单一些。虽然Y86-64指令集很简单,它仍然足够完整,能让我们写一些处理整数的程序设计一个实现Y86-64的处理器要求我们解决许多处理器设计者同样会面对的问题。
4.1 Y86-64 指令集体系结构
Y86-64指令集基本上是x86-64指令集的一个子集。它只包括8字节整数操作,寻址方式较少,操作也较少。因为我们只有8字节数据,所以称之为“字(word)”不会有任何歧义。在图4-2中,左边是指令的汇编码表示,右边是字节编码。图4-3给出了其中一些指令更详细的内容。汇编代码格式类似于x86-64的ATT格式。
下面是Y86-64指令的一些细节。
x86-64的movq指令分成了4个不同的指令:irmovq、rrmovq、mrmovq和rmmovq:分别显式地指明源和目的的格式。源可以是立即数(i)、寄存器(r)或内存(m)。指令名字的第一个字母就表明了源的类型。目的可以是寄存器(r)或内存(m)。指令名字的第二个字母指明了目的的类型。
两个内存传送指令中的内存引用方式是简单的基址和偏移量形式。在地址计算中,我们不支持第二变址寄存器(second index register)和任何寄存器值的伸缩(scaling)。
同x86-64一样,我们不允许从一个内存地址直接传送到另一个内存地址。另外,也不允许将立即数传送到内存。
(1)有4个整数操作指令,如图4-2中的OPq。它们是 addq、subq、andq和 xorq。它们只对寄存器数据进行操作,而x86-64还允许对内存数据进行这些操作。这些指令会设置3个条件码 ZE、SF和 OF(零、符号和溢出)。
(2)7个跳转指令(图4-2中的jxx)是jmp、jle、jl、je、jne、jge和j g。根据分支指令的类型和条件代码的设置来选择分支。分支条件和x86-64的一样。
(3)有6个条件传送指令(图4-2中的cmovXX):cmovle、cmovl、cmove、cmovne、cmovge和cmovg。这些指令的格式与寄存器-寄存器传送指令rrmovq一样,但是只有当条件码满足所需要的约束时,才会更新目的寄存器的值。
(4)call指令将返回地址入栈,然后跳到目的地址。ret指令从这样的调用中返回
(5)pushq和popq指令实现了入栈和出栈,就像在x86-64中一样。
(6)halt指令停止指令的执行。x86-64中有一个与之相当的指令hlt。x86-64的应用程序不允许使用这条指令,因为它会导致整个系统暂停运行。对于Y86-64来说,执行 halt指令会导致处理器停止,并将状态码设置为HLT。
4.1.1 指令代码
图4-2还给出了指令的字节级编码。每条指令需要1~10个字节不等,这取决于需要哪些字段。每条指令的第一个字节表明指令的类型。
这个字节分为两个部分,每部分4位:高4位是代码(code)部分,低4位是功能(function)部分。如图4-2所示,代码值为0~0xB。功能值只有在一组相关指令共用一个代码时才有用。图4-3给出了整数操作、分支和条件传送指令的具体编码。可以观察到,rrmovq与条件传送有同样的指令代码。可以把它看作是一个“无条件传送”,就好像jmp指令是无条件跳转一样,它们的功能代码都是0。
如图4-4所示,15个程序寄存器中每个都有一个相对应的范围在0到0xE之间的寄存器标识符(register ID)。Y86-64中的寄存器编号跟x86-64中的相同。程序寄存器存在CPU中的一个寄存器文件中,这个寄存器文件就是一个小的、以寄存器ID作为地址的随机访问存储器。在指令编码中以及在我们的硬件设计中,当需要指明不应访问任何寄存器时,就用ID值0xF来表示。
有的指令只有一个字节长,而有的需要操作数的指令编码就更长一些。首先,可能有附加的寄存器指示符字节(register specifier byte),指定一个或两个寄存器。在图 4-2中,这些寄存器字段称为rA 和rB。从指令的汇编代码表示中可以看到,根据指令类型,指令可以指定用于数据源和目的的寄存器,或是用于地址计算的基址寄存器。没有寄存器操作数的指令,例如分支指令和call指令,就没有寄存器指示符字节。那些只需要一个寄存器操作数的指令(irmovq、pushq和popq)将另一个寄存器指示符设为0xF。这种约定在我们的处理器实现中非常有用。
有些指令需要一个附加的4字节常数字(constant word)。这个字能作为 irmovg的立即数数据,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个字节,变成字节序列000123456789abcd写成按字节反序就是cdab896745230100。将它们都连接起来就得到指令的编码4042cdab896745230100(10字节指令)。
指令集的一个重要性质就是字节编码必须有唯一的解释。任意一个字节序列要么是-个唯一的指令序列的编码,要么就不是一个合法的字节序列。Y86-64就具有这个性质因为每条指令的第一个字节有唯一的代码和功能组合,给定这个字节,我们就可以决定所有其他附加字节的长度和含义。这个性质保证了处理器可以无二义性地执行目标代码程序。即使代码嵌入在程序的其他字节中,只要从序列的第一个字节开始处理,我们仍然可以很容易地确定指令序列。反过来说,如果不知道一段代码序列的起始位置,我们就不能准确地确定怎样将序列划分成单独的指令。
4.1.2 Y86-64 异常
对 Y86-64 来说,程序员可见的状态包括状态码 stat,它描述程序执行的总体状态。这个代码可能的值如图 4-5所示。代码值1,命名为 OK,表示程序执行正常,而其他一些代码则表示发生了某种类型的异常。代码2,命名为HLT,表示处理器执行了一条halt指令。代码3,命名为ADR,表示处理器试图从一个非法内存地址读或者向一个非法内存地址写,可能是当取指令的时候,也可能是当读或者写数据的时候。我们会限制最大的地址(确切的限定值因实现而异),任何访问超出这个限定值的地址都会引发ADR异常。代码4,命名为INS,表示遇到了非法的指令代码。
4.1.3 一些 Y86-64 指令的详情
大多数Y86-64指令是以一种直接明了的方式修改程序状态的,所以定义每条指令想要达到的结果并不困难。不过,两个特别的指令的组合需要特别注意一下。
pushq指令会把栈指针减8,并且将一个寄存器值写人内存中。因此,当执行pushq %rsp指令时,处理器的行为是不确定的,因为要入栈的寄存器会被同一条指令修改。通常有两种不同的约定:1)压入%rsp的原始值,2)压入减去8的%rsp的值。