从零设计四位栈处理器(2)——结构与指令集
一句话概括: 在Toxic处理器中,万物皆栈。
熟悉汇编语言的同学会了解,一般的汇编语言,会包含以下几个部分:
- 寄存器
- 地址
- 立即数
- 操作码
在这期文章中,我们将通过类比的方法来解释Toxic处理器的运行方法以及设计上作出的妥协。
上图显示的是RISC-V的最基础的四类指令,其中R类负责寄存器与寄存器之间的运算操作,I类负责寄存器和小立即数之间的操作,S类负责写入内存(读取指令被归结为I类内),U类负责处理大立即数以及PC相关的寻址。
一、寄存器
寄存器的本质是用来暂存数据,避免读写内存带来的巨大性能开销。
我们先举一个R类RISC-V汇编的例子:
add x1, x1, x1
这条命令的意思是:将x1寄存器的值与x1寄存器相加,并且将结果写入x1寄存器。
事实上这是一个非常典型的寄存器寻址的汇编指令,在X86构架、ARM构架和MIPS构架中几乎都能找到相似的指令。
但是我们在Toxic处理器中并没有这样的指令出现。那么我们为什么不用这种类型的指令呢?
我们上面展现的汇编代码来自RV32I指令集,本质上是一个32位整数指令集,在该指令集中总共定义了32个通用寄存器。我们为了寻址32个寄存器,在指令中每包含一个寄存器,需要花费log_2(32)=5比特的宽度。
而Toxic处理器是一个4位等长指令处理器,而4位二进制码只有16种不同的组合,这也就导致了在Toxic处理器中应用这种“寄存器寻址”的体系是非常不现实的:一条指令总共只有4比特,算上操作码占的空间,我们恐怕最多只能留2位给寄存器寻址,但这显然是不够的。所以既想维持4位的简洁又想拥有更多的”暂存空间“要怎么办呢?
于是我们引出了:Stack Machine(栈处理器)这个概念。
所谓栈处理器是相对于寄存器处理器而言的,一个栈处理器是不存在通用寄存器的,取而代之的是一个“栈”。栈这个概念相信各位已经非常清楚了,Toxic处理器里面的栈和我们通常说的栈道理上是完全相同的。不同之处在于:1. Toxic处理器的栈是由硬件逻辑门实现。2. Toxic处理器的栈,除了实现了push,pop之外,还实现了tos和ntos两个功能。tos就是所谓的栈顶,ntos就是次栈顶,在访问tos和ntos的时候不会改变栈内部的内容。
在这里,我们将这个作用是“替代通用寄存器”的栈命名为:数据栈。
二、地址
以RV32I为例,如果我们有一个32位宽度的总线,那么我们总共可以寻址的空间是2^32 Byte(假设每个地址寻1 Byte)。这个寻址空间说大不大,但是对于普通应用来说已经足够了。
但是Toxic处理器是一个四位处理器,2^4=16 Byte的寻址空间对于几乎任何程序来说都是不够的。很自然的我们就可以想到去运用多个4位总线连接起来一同来寻址,这样的话可以扩大我们的寻址空间。于是乎就出现了一个问题:到底要多大的寻址空间才好呢?在这里,Toxic处理器的设计给每个去尝试实现它的人都留下了空间,我们规定:总线宽度只要为4位的倍数,都可以让Toxic指令集良好的工作。
在开头我们就说过:Toxic处理器中,万物皆栈。与数据栈的思路相同,我们同样用栈来表示一个总线地址,在此我们称之为:地址栈。
8位的地址栈的实现与数据栈基本相同,为了避免混淆,我们暂时不引入更高位的地址栈的设计。
到此为止,我们已经讲述了Toxic处理器中最重要的两个部件。如果你感觉有似懂非懂的地方,没有关系,在我们一步步解释整个Toxic指令集之后你会有更清晰的认识。
三、立即数
立即数在处理器中的作用是将指令中的数直接作为处理器内的数据进行计算。
用RV32I指令集举个例子:
addi x1, x1, 1
该指令的意思是:将x1寄存器+1的结果存进x1寄存器。在该指令中,数字1就是一个立即数。
对于我们的Toxic处理器来说,处理立即数的本质就是将我们指定的任何一个数字,push进数据栈内。
下图是Toxic指令集的所有指令以及编码:
在了解生成立即数的具体操作之前我们先看3个指令:P1, ADD, LS。
- P1: 将数据“1”push进数据栈。
- ADD:将数据栈tos和ntos的加和push进数据栈。
- LS:将数据栈中的tos左移一位。
明白了这三个指令的操作,我们来思考一下:如何把“5”这个数字push到数据栈内呢?
首先,5的二进制是:0101。我们有以下代码:
P1 // tos为0001
LS // tos为0010
LS // tos为0100
P1 // tos为0001, 这时候之前的tos已经成为了ntos,为0100
ADD // 0001 + 0100 = 0101;这时候tos为0101,我们完成了将“5”push到栈顶
为了更高效的生成立即数比如说:15(也就是二进制中的1111),我们引入了P11这个指令,P11所做的事情与P1类似,就是将0011给push到栈顶。
四、操作码
操作码是指在机械码中存在的用来指定处理器指令的几位。
在RV32I指令集中,我们可以看到0-6位都是“opcode”,也就是我们说的操作码。
再次地,Toxic指令集是一个4位等长指令指令集。
然而不难发现,在Toxic指令集中,我们虽然只有4位的空间来描述一个指令,但是我们对于寄存器的寻址并不占用指令空间,对总线地址的访问也不占用指令空间,甚至连对寄存器的操作都不占用指令空间。所以,到最后,所有4位空间,我们都将分配给操作码。换句话说,在Toxic指令集中,所有机器码都是操作码。
(其实关于操作码的界定问题,学术界也没有一个非常准确的标准,比如说很多时候,RV32I的funct3都会被说成操作码的一部分。但是这都不重要,只是个人的区分和习惯问题)
我们这期文章就暂时先讲这么多,下期文章我们会更加仔细、更加完整的解读这16条指令,并且聊一聊如何用Toxic指令集编写一个真实有意义的程序。在讲完指令集后会讲一下整个Toxic处理器电路层面的实现。
当然,在那之前还有很多工作需要完善,大家可以先看看我的GitHub上Toxic_v2的英文文档还有最近很快就要完成了的Toxic_v2模拟器,如果大家能给我点个星星,我会非常感谢:
文档:https://github.com/Entropy-xcy/Toxic_v2
模拟器:https://github.com/Entropy-xcy/Toxic_v2_simulator
引用:
【1】 RISC-V ISA Specifications: https://riscv.org/specifications/