今日头条
如果您将自己限制在只有 16 条指令,您应该选择哪些指令,如果没有那些被搁置的指令,您将如何管理?
在我之前关于从头开始构建 4 位 HRRG(Heath Robinson,Rube Goldberg)计算机项目的专栏中,我们介绍了 CPU 寄存器和指令集。您可能还记得,由于我们只有 4 位数据总线(以及 12 位地址总线),我们选择只有 2^4 = 16 条指令以及 2^4 = 16 个 CPU 寄存器。
为了确保我们都跟着同一个鼓点跳舞,让我们提醒自己,六个通用寄存器 R0 到 R5 用于存储数据值并“累积”来自任何算术或逻辑运算的结果。状态寄存器 S0 和 S1 主要用于存储任何算术或逻辑运算的状态结果,例如减法的结果是否为零。
另请参阅此索引,其中列出了构成我们的 4 位 HRRG 计算机项目的所有文章,以及一些有趣的相关专栏。
程序计数器 (PC) 用于跟踪 CPU 在程序中的当前位置。堆栈指针 (SP) 用于跟踪堆栈的顶部。索引寄存器 (IX) 主要用于保存计数值或访问内存的偏移量。中断向量 (IV) 用于保存称为中断服务路由 (ISR) 的特殊类型子程序的内存地址。
介绍堆栈指针
我们将在以后的专栏中考虑所有这些小寄存器流氓如何在令人难以忍受的细节中发挥它们的魔力,但如果这对您来说是新的,那么简要描述 SP 的操作可能是个好主意。
我们大多数人都去过自助餐厅,那里有一堆盘子堆叠在一个弹簧机构的顶部。假设您是负责将板装入机械装置的人员。让我们还假设板的编号为(1、2、3……),并且——作为一个强迫症工程师——这是将前三个板加载到机制中的顺序,如下图所示:
现在假设一个顾客进来拿盘子。当然,它们会检索您添加到堆栈顶部的最后一个盘子(在我们的示例中为 3 号)。在计算方面,这种形式的存储和检索将被归类为后进先出 (LIFO)过程。
好吧,我们的 SP 以类似的方式工作。在我们的程序开始时,我们将使用我们不用于其他任何内容的内存区域中某个位置的地址加载 SP。随后,我们每次执行 PUSH 操作时,CPU 都会将指定的数据写入 SP 当前指向的内存位置(“栈顶”),然后将 SP 加一以指向下一个空闲位置。相比之下,每次执行 POP 操作时,CPU 都会先递减 SP 以指向堆栈顶部的数据,然后从堆栈中读取该数据并将其存储到我们告诉它的任何位置。
介绍 6502
出于以下讨论的目的,我们将使用MOS 技术 6502来提供比较的基础。1975 年推出的 6502 有一个 8 位数据总线和一个 16 位地址总线,其寄存器包括一个 8 位累加器寄存器(A)、两个 8 位索引寄存器(X 和 Y)、一个 7-位处理器状态标志寄存器 (P)、一个 8 位堆栈指针 (S) 和一个 16 位程序计数器 (PC)。
与 HRRG 不同,我们可以将 12 位 SP 加载为我们想要的任何值,6502 的 8 位 SP 在上电时自动加载 $00(请记住,我们使用“$”字符表示十六进制值),并且堆栈的起始地址被硬连线到 $0100。这意味着 6502 的堆栈地址空间被限制在 256 个地址 $0100 到 $01FF 之间。
尽管与今天的微处理器产品相比,6502 看起来很简单,但它在其诞生之初就被认为是相当了不起的,尤其是因为它的可承受的价格标签(1975 年为 25 美元)。许多人继续基于这款处理器创造出令人惊叹的项目,例如这款基于 6502 的虚拟现实 (VR) 系统。6502 的新版本不断出现,例如MOnSter 6502 CPU。
此外,与 HRRG 不同的是,我们可以将 12 位中断向量 (IV) 加载为我们想要的任何值,6502 被硬连线以查看内存地址 $FFFE 和 $FFFF 以检索其 16 位中断向量,其中这个 2 字节的值将由用户加载到内存中(当我们说“由用户”时,我们真正的意思是“由用户的程序”)。
在可用的 2^8 = 256 个可能的操作码(指令)中,最初的 6502 使用 151 个组织成 56 条指令(取决于指令),一种或多种寻址模式。根据指令和寻址模式,6502 操作码可能需要 0、1 或 2 个额外字节用于操作数;因此 6502 条机器指令的长度从 1 个字节到 3 个字节不等。
MOV(加载和存储)
6502 允许用户将值从内存加载到累加器(A)和索引寄存器(X 和 Y)。同样,它允许用户将这些寄存器中的值存储到内存中。所有这些都需要六个指令,如下所示:
LDA(加载累加器)
LDX(加载 X 寄存器)
LDY(加载 Y 寄存器)
STA(存储累加器)
STX(存储 X 寄存器)
STY(存储 Y 寄存器)
相比之下,HRRG 有一条 MOV 指令,可用于根据其操作数将数据从寄存器到寄存器、寄存器到内存、内存到寄存器和内存到内存移动(复制)。此外,这些指令适用于所有 HRRG 的寄存器(即使这样做没有意义 - 更多内容见下文)。
INC(递增)和 DEC(递减)
6502 允许用户递增(加 1)和递减(减 1)指定内存位置或其索引寄存器(X 和 Y)中的值。为了做到这一点,它需要以下六个指令:
INC(增加内存位置的内容)
INX(增加 X 寄存器的内容)
INY(增加 Y 寄存器的内容)
DEC(减少内存位置的内容)
DEX(减少 X 寄存器的内容)
DEY(减少 Y 寄存器的内容)
“增加或减少累加器的内容呢?” 我听到你哭了。好吧,为了使用 6502 实现这一点,您必须执行本专栏后面讨论的常规加法或减法运算。
相比之下,HRRG 的 INC 和 DEC 指令可用于递增内存位置以及任何 CPU 的 4 位和 12 位寄存器的内容。
“什么?任何寄存器——甚至是程序计数器?” 我听到你紧张地尖叫。是的,您可以在任何寄存器上使用这些指令,即使这样做似乎没有意义。例如,增加程序计数器 (PC) 通常被认为是一件坏事,但 HRRG 允许在机器代码和底层硬件中这样做。
我们可能会在汇编程序中标记某些“愚蠢”(我们将在以后的专栏中讨论),但如果用户决定忽略并绕过汇编程序发出的任何警告和/或错误消息,那么就这样吧,因为 (a)在没有无数例外和特殊情况的情况下设计硬件更容易工作,(b)用户可能会想出我们没有想到的狡猾的使用模型,以及(c)我们不是“明智的警察”(除了别的,我没有合适的裤子)。
ADDC 和 SUBB(加法和减法)
当我们在简单计算机的情况下考虑加法时,我们通常会想到将两个数字相加,例如 3 + 2 = 5。问题是我们可以表示的数字的大小是受限于我们的数据总线和数据字段的宽度。例如,在 HRRG 的情况下,单个 4 位 nybble 可用于表示 0 到 15 范围内的无符号数或 -8 到 +7 范围内的有符号数。
这显然有点限制。幸运的是,我们可以使用多个 nybbles 来表示我们的值。例如,在 HRRG 的情况下,一对 4 位 nybbles 可用于表示 0 到 255 范围内的无符号数或 -128 到 +127 范围内的有符号数。
假设我们想将两个 2-nybble 值相加。在这种情况下,我们将从添加两个最不重要的 nybbles (LSN) 开始。根据它们的值,这将导致 0 或 1 值存储在进位 (C) 状态标志中。当我们添加下一对 nybbles 时,我们还需要包含(添加)进位标志的内容。
一些早期的 8 位处理器提供了两个加法指令,例如 ADD(“无进位相加”)和 ADDC(“有进位相加”)。其他的,比如6502,只提供了“带进位相加”的版本,由用户自己实现“不带进位相加”,先将进位标志加载0,再进行相加。
同样的事情也适用于减法。在这种情况下,一些早期的 8 位处理器提供了两个减法指令,例如 SUB(“无借位减法”)和 SUBB(“有借位减法”)。其他的,比如6502,只提供了“借位减法”的版本,由用户来实现“不借位减法”,首先将进位标志加载1,然后执行减法。
“等一下,我们没有借用状态标志,”我听到你在呜咽。这是真的,但在减法的情况下,进位 (C) 标志承担借位 (B) 标志的角色。基于唯一的物理标志是进位标志,一些设计师更喜欢说“有/没有进位减法”,并使用类似 SUBC 助记符的东西,但是,在我看来,这最终会导致更多的混乱而不是它的价值.
底线是 6502 提供了如下两条指令:
ADC(进位加法)
SBC(进位减法)
此外,这些指令只允许您将指定内存位置的内容添加/减去累加器的内容,结果存储在累加器中。
同样,HRRH 提供如下两条指令:
ADDC(进位加法)
SUBB(借位减法)
然而,这些指令允许执行寄存器到寄存器、寄存器到存储器、存储器到寄存器以及存储器到存储器的加法和减法。(在我的下一篇专栏中,我们将考虑使用 ADDC 和 SUBB 指令来实现其对应的 ADD 和 SUB 的各种方式。)
ROLC 和 RORC(旋转和移位)
可能有八种基本的旋转和移位操作,我们可以为其分配助记符,如下所示:
ROL(左移)
ROR(右移)
ROLC(通过进位标志左移)
RORC(通过进位标志右移)
LSHL(逻辑左移)
ASHL(算术左移)
LSHR(逻辑右移)
ASHR(算术移位正确的)
请记住,不同 CPUS 的设计者对这类事情使用各种不同的助记符;我在上面展示的那些对我来说最有意义。现在,如果我们决定(我们没有)在我们的 4 位 HRRG 中实现所有这八个指令,它们的操作的图形表示将如下所示:
在 ROL(左移)的情况下,所有位都左移一位;此外,概念上“落下”的最高有效位 (MSB) 被复制到最低有效位 (LSB) 和进位标志中。相比之下,在 ROR(左移)的情况下,所有位都右移一位;此外,概念上“落下”的 LSB 被复制到 MSB 和进位标志中。
ROLC(通过进位向左旋转)与ROL非常相似,只是进位标志的原始内容被复制到LSB中。类似地,RORC(通过进位向右旋转)与ROR非常相似,只是进位标志的原始内容被复制到MSB中。
LSHL(逻辑左移)操作与ROL(向左旋转)和ROLC(通过catty向左旋转)操作非常相似,只是0被复制到LSB中。类似地,LSHR(逻辑右移)操作与ROR(向右旋转)和RORC(通过进位向右旋转)操作非常相似,只是0被复制到MSB中。
ASHL(算术左移)操作在功能上与LSHL(逻辑左移)相同-两者都会导致将0复制到LSB中-因此没有设计人员会费心将其作为CPU中的单独指令来实现。另一方面,在编写程序时,我们可能更喜欢使用这两种不同的助记符作为注释,以提醒自己(和其他读者)当我们捕获代码时的想法。
最后,ASHR(算术右移)与LSHR(逻辑右移)类似,只是MSB(符号位)被复制回自身(另请参阅“C/C++>移位运算符的工作原理”)。
在HRRG的情况下,被限制为16条指令,我们决定只实现八个基本旋转和移位中的两个:
ROLC(通过进位标志向左旋转)
RORC(通过进位标志向右旋转)
我们选择这两条指令的原因是,很容易将它们作为实现其他指令功能的基础。(我们将在下一篇专栏文章中考虑使用ROLC和RORC指令实现其ROL、ROR、LSHL、LSHR、ASHL和ASHR对应项的各种方式。)
AND、OR、XOR 和 CMP(逻辑运算)
这些指令的工作方式与地球上几乎任何其他处理器上的对应指令类似,因此我们不会在此花太多时间讨论它们。可以这么说,6502 的 AND(逻辑与)、EOR(异或)和 ORA(包括或)仅允许您使用内存中保存的另一个值对累加器的内容执行操作,结果存储在蓄能器。相比之下,HRRG 的 AND、OR 和 XOR 等效项支持寄存器到寄存器、寄存器到内存和内存到寄存器操作。
在 HRRG 的 CMP(比较指令)的情况下,它还支持寄存器到寄存器、寄存器到内存、内存到寄存器和内存到内存操作,被比较的两个值被视为是无符号二进制值。
CLR 和 SET(位操作操作)
一些处理器提供一套指令,可用于清除或设置状态寄存器中的各个位。例如,6502 支持七种这样的指令:
CLC(清除进位标志)
CLD(清除十进制模式标志)
CLI(清除中断禁止标志)
CLV(清除溢出标志)
SEC(设置进位标志)
SED(设置十进制模式标志)
SET(设置中断禁止标志)
HRRG 没有提供任何这些说明,但如果提供了,他们的基因组学将如下(我们将看到我的疯狂是有原因的):
CLRN(清除负标志)
CLRZ(清除零标志)
CLRC(清除进位标志)
CLRO(清除溢出标志)
CLRI(清除中断屏蔽标志)
SETN(设置负标志)
SETZ(设置零标志)
SETC(设置进位标志)
SETO(设置溢出标志)
SETI(设置中断屏蔽标志)
SETH(设置停止标志)
观察没有 CLRH(清除停止标志)。这是因为一旦暂停标志设置为 1,重置它的唯一方法是触发中断(假设中断屏蔽标志设置为 1)或重置机器。
关键是我们可以使用 AND 和 OR 逻辑运算来实现所有这些指令。假设我们想将进位标志(状态寄存器 S0 中的第 2 位)清除为 0,我们可以通过将 S1 的内容与 %1011 进行“与”运算来做到这一点(请记住,我们使用 '%' 字符来表示二进制值)。类似地,如果我们想将进位标志设置为 1,我们可以通过将状态寄存器 S0 的内容与 %0100 进行或运算来实现。
说了这么多,如果我们在编写汇编代码时可以使用位操作指令,那就太好了,所以我们将在下一篇专栏中讨论如何使用我们的汇编程序将它们添加到我们的指令库中。
推入和弹出(或拉出)
这些指令用于将值压入堆栈并再次弹出(或拉出)它们。对于 6502,有 6 条与堆栈相关的指令(请记住,正如我们之前讨论的),6502 的 8 位堆栈指针本身在上电时会自动加载 00 美元。
TSX(将堆栈指针的值传送到变址寄存器 X)
TXS(将变址寄存器 X 的内容传送到堆栈指针)
PHA(将累加器的内容压入堆栈)
PHP(将处理器状态寄存器的内容压入)
PLA(将栈顶的值拉入累加器)
PLP(将栈顶的值拉入处理器状态寄存器)
对于 HRRG,我们只有两条指令:
PUSH(将选定的寄存器或内存位置的内容压入堆栈)
POP(将堆栈顶部的值弹出到选定的寄存器或内存位置)
HRRG 的指令适用于任何 CPU 的寄存器或内存位置。此外,HRRG 的 MOV 指令提供(并超过)6502 的 TSX 和 TXS 指令的功能。
JMP、JSR 和相关指令
JMP(无条件跳转)指令允许 CPU 跳转到程序的另一部分。JSR 指令告诉 CPU 跳转到子程序。JSR 通常的工作方式是用户将任何相关信息推送到堆栈上,然后调用 JSR。反过来,CPU 将程序计数器 (PC) 中的返回地址压入堆栈,然后跳转到子程序。
仍然在谈论这通常的工作方式,在子程序结束时,使用 RTS(从子程序返回)指令将返回地址从堆栈顶部弹出到程序计数器(PC)中,并将我们返回到主程序程序。
还值得注意的是,中断服务程序 (ISR) 的行为有点像子程序,因为中断会导致 CPU 在服务中断之前将返回地址压入堆栈顶部。在 ISR 结束时,使用 RTI(从中断返回)指令将返回地址从堆栈顶部弹出并返回到主程序。
6502 拥有所有这四个指令:
JMP(无条件跳转)
JSR(跳转到子程序)
RTS(从子程序返回)
RTI(从中断返回)
处理器还将支持一组指令,这些指令将根据状态标志的状态触发跳转(或分支)。例如,6502 提供了 8 个这样的指令,如下所示:
BCC(进位标志清除分支)
BCS(进位标志设置分支)
BEQ(零标志设置分支)
BMI(负标志设置分支)
BNE(零标志清除分支)
BPL(负标志清除分支)
BVC(溢出标志清除时分支)
BVS(溢出标志设置时分支)
与 6502 的 JMP 和 JSR 指令允许 CPU 跳转到其 16 位地址空间内的任何位置不同,这些分支指令使用带符号的 8 位相对地址将控制转移到位于向前(之后)和 128 字节内的目标向后(之前)分支指令的字节。程序往往会进行大量跳转——例如循环循环——因此在时钟有限的日子里,使用 1 字节的分支地址而不是 2 字节的跳转地址可能会显着节省空间和时间速度、处理器周期和内存位置。
在 HRRG 的情况下,我们只有两条与跳转相关的指令:
JMP(无条件跳转)
JSR(跳转到子程序)
我们没有 RTS 或 RTI 指令——我们通过简单地从堆栈顶部检索返回地址并使用 POP 指令将其加载到程序计数器 (PC) 中来实现相同的效果。
问题是我们实现 JMP 指令的方式意味着我们可以使用它来实现与使用以下指令套件相同的效果:
JMP(无条件跳转)
JMPN(无条件跳转,或“从不跳转”)*
JPN(如果为负则跳转;如果 N 标志为 1)
JPNN(如果非负跳转;如果 N 标志为 0)
JPZ(如果为零则跳转;如果Z 标志为 1)
JPNZ(如果不为零则跳转;如果 Z 标志为 0)
JPC(如果进位则跳转;如果 C 标志为 1)
JPNC(如果不进位则跳转;如果 C 标志为 0)
JPO(如果溢出则跳转;如果O 标志为 1)
JPNO(如果没有溢出则跳转;如果 O 标志为 0)
JPI(如果中断屏蔽跳转;如果 I 标志为 1)**
JPNI(如果没有中断屏蔽跳转;如果 I 标志为 0)**
JPH(如果停止则跳转;如果 H 标志为 1)***
JPNH(如果不停止则跳转;如果 H 标志为 0)**
注意 * JMPN(“从不跳转”)可用于调试目的。
注意 ** 基于 I 标志为 0 或 1 或 H 标志为 0 的状态的跳转并不是特别有用,因为程序员已经知道这些标志包含什么(不像 N、Z、C 和 O标志,其值由算术和逻辑运算的结果确定)。然而,它们是通过实现 HRRG 的 JMP 指令的方式实现的。
注意 *** JPH(如果 H 标志为 1 则跳转)完全没有意义,这是因为一旦程序将此标志设置为 1,CPU 就会停止操作,并且只能通过触发中断来重置标志(假设中断屏蔽标志设置为 1) 或通过重置机器,所以这里包含这条指令只是为了完整起见。
在大多数处理器的情况下,JSR(跳转到子程序)指令的行为方式类似于 JMP(无条件跳转)指令。也就是说,没有任何与 JPN、JPNN 等等效的 JSR。但是,由于 HRRG 的指令架构,我们可以使用 JSR 来实现与使用以下指令相同的效果:
JSR(无条件 JSR)
JSRN(无条件 JSR)*
JSN(如果为负,则为 JSR;如果 N 标志为 1)
JSNN(如果非负,则为 JSR;如果 N 标志为 0)
JSZ(如果为零,则为 JSR;如果 Z 标志为 1)
JSNZ(如果不为零,则 JSR;如果 Z 标志为 0)
JSC(如果进位,则 JSR;如果 C 标志为 1)
JSNC(如果不进位,则 JSR;如果 C 标志为 0)
JSO(如果溢出,则 JSR;如果 O 标志为 1)
JSNO(如果没有溢出,则 JSR;如果 O 标志为 0)
JSI(如果中断掩码,则 JSR;如果 I 标志为 1)**
JSNI(如果不是中断掩码,则 JSR;如果 I 标志为 0)**
JSH(如果暂停,则 JSR;如果H 标志为 1)***
JSNH(如果不停止,则 JSR;如果 H 标志为 0)**
注意 *、** 和 ***;与上面讨论的各种跳转指令相同的警告适用。
即将推出……
呸!这比我预期的要花费更多的工作,但我希望它能提供一些见解,让我们了解我们选择指令形成我们的极简集的方式、我们的指令与其他机器的比较,以及小骗子的使用方式。
在我的下一个 HRRG 专栏中,我们将开始研究 HRRG 的汇编语言,包括我们如何使用汇编器来实现不在我们核心集中的指令。此外,我们将讨论与所有这一切的历史方面有关的一些方面,例如第一个汇编程序是如何产生的。在此之前,我一如既往地期待您的评论、问题和建议。
审核编辑 黄昊宇
全部0条评论
快来发表一下你的评论吧 !