【问题标题】:How can I write an interpreter for 'eq' for Hack Assembly language?如何为 Hack 汇编语言的 'eq' 编写解释器?
【发布时间】:2015-05-10 17:51:06
【问题描述】:

我正在阅读和学习The Elements of Computing Systems,但我被困在某一点上。示例章节跳过接下来的 5 条指令可以找到here

无论如何,我正在尝试实现一个虚拟机(或一个字节码到汇编翻译器),但我被困在跳过接下来的 5 条指令。

您可以找到汇编符号here

我们的目标是实现一个翻译器,将特定的字节码翻译成这个汇编代码。

我成功完成的一个例子是字节码

push constant 5

翻译成:

@5
D=A
@256
M=D

正如我所说,Hack 的汇编语言可以在我提供的链接中找到,但基本上是:

@5  // Load constant 5 to Register A
D=A // Assign the value in Reg A to Reg D
@256// Load constant 256 to Register A
M=D // Store the value found in Register D to Memory Location[A]

嗯,这很简单。根据定义,存储器位置 256 是堆栈的顶部。所以

push constant 5
push constant 98

将被翻译成:

@5
D=A
@256
M=D
@98
D=A
@257
M=D

没关系..

我还想再举一个例子:

push constant 5
push constant 98
add

翻译成:

@5
D=A
@256
M=D
@98
D=A
@257
M=D
@257  // Here starts the translation for 'add' // Load top of stack to A
D=M   // D = M[A] 
@256  // Load top of stack to A 
A=M   // A = M[A]
D=D+A
@256
M=D

我觉得很清楚。

但是我不知道如何翻译字节码

eq

到大会。 eq的定义如下:

三个命令(eq、gt、lt)返回布尔值。虚拟机 将真假表示为????-1(减一,0xFFFF)和0(零, 0x0000),分别。

所以我需要分别向寄存器 A 和 D 弹出两个值,这很容易。但是我应该如何创建一个汇编代码来检查值并在结果为真时压入 1,如果结果为假则压入 0?

Hack Computer 支持的汇编代码如下:

我可以这样做:

push constant 5
push constant 6
sub

如果压入堆栈的 2 个值相等,它将保存值 0,如果不相等,则保存 !0 但这有什么帮助?我尝试使用 D&A 或 D&M,但这也无济于事..

我也可以引入条件跳转,但我应该如何知道要跳转到哪条指令?哈克汇编代码没有“跳过接下来的 5 条指令”之类的东西。

[由 Spektre 编辑] 我看到的目标平台摘要

  • 16 位冯诺依曼架构(地址为 15 位,16 位字访问)
  • 数据存储器 32KW(读/写)
  • 指令(程序)内存 32KW(只读)
  • 原生 16 位寄存器 A,D
  • 通用 16 位寄存器 R0-R15 映射到位于 0x0000 - 0x000F 的数据存储器
  • 这些也很可能用于:SP(R0),LCL(R1),ARG(R2),This(R3),That(R4)
  • 屏幕在 0x4000-0x5FFF(512x256 B/W 像素 8KW)处映射到数据内存
  • 键盘在 0x6000 处映射到数据存储器(如果最后一次按键,则为 ASCII 码?)

【问题讨论】:

  • 实际上你似乎有任意跳转,请查看 jmp 指令。您可以使用它来创建两个分支,其中一个将 1 加载到目标寄存器中,另一个将 0 加载到目标寄存器中
  • @NiklasB。谢谢您的答复。但是我怎么知道跳到哪里呢? JMP 指令将跳转到加载到寄存器 A 的步骤。我的意思是,如何创建会显示“跳转 4 条指令”或“跳转 8 条指令”的汇编代码?
  • 1.字节码到汇编? ...汇编源代码是程序的文本表示(助记符),它被编译成字节...通常是代码二进制文件。 2. 比较指令在我接触到的所有架构上通过减法和设置标志Z,C 和一些还包括条件分支(主要是MCU)所以cmp a,b 通常做a-b 但扔掉结果离开只是旗帜。现在进入条件(不管是跳转还是其他指令)equal=(Z), greater=(NC),lower(C)
  • ...我怎么知道要跳转到什么指令? 将条件分支编码为符号标签。手册中有示例,我在答案中编写了这样的示例。 (如果你正在生成二进制代码,你知道分支指令在哪里;“跳过 5 条指令”的意思是“分支到分支的二进制位置,+5”。)
  • nand2Tetris 网站有一个问答部分。似乎这个问题需要一个非常具体的背景,所以这可能是一个很好的地方。我相信这就是你要找的东西:nand2tetris-questions-and-answers-forum.32033.n3.nabble.com/…

标签: algorithm assembly bytecode interpreter nand2tetris


【解决方案1】:

似乎还有另一章更明确地定义了 Hack CPU。它说:

Hack CPU 由第 2 章和第 3 章中指定的 ALU 组成 寄存器称为数据寄存器 (D)、地址寄存器 (A) 和程序 计数器(PC)。 D 和 A 是通用的 16 位寄存器,可以 由算术和逻辑指令操作,如 A=D-1 , D=D|A ,依此类推,遵循章节中指定的 Hack 机器语言 4. 虽然 D 寄存器仅用于存储数据值,但 A 寄存器的内容可以用三种不同的方式解释, 取决于指令的上下文:作为数据值,作为 RAM 地址,或作为 ROM 地址

显然“M”访问是对由 A 控制的 RAM 位置。这是我遗漏的间接寻址。现在一切都很顺利。

消除了这种困惑,现在我们可以处理 OP 的问题(更容易)。

让我们从使用堆栈实现子程序调用开始。

     ; subroutine calling sequence
     @returnaddress   ; sets the A register
     D=A
     @subroutine
     0 ; jmp
  returnaddress:

     ...

  subroutine: ; D contains return address
  ; all parameters must be passed in memory locations, e.g, R1-R15
  ; ***** subroutine entry code *****
     @STK
     AM=M+1         ; bump stack pointer; also set A to new SP value
     M=D            ; write the return address into the stack
  ; **** subroutine entry code end ***
     <do subroutine work using any or all registers>
  ; **** subroutine exit code ****
     @STK
     AM=M-1         ; move stack pointer back
     A=M            ; fetch entry from stack
     0; jmp         ; jmp to return address
  ; **** subroutine exit code end ****

“推送常量”指令可以很容易地转换为存储到堆栈中的动态位置:

     @<constant>  ; sets A register
     D=A         ; save the constant someplace safe
     @STK
     AM=M+1         ; bump stack pointer; also set A to new SP value
     M=D            ; write the constant into the stack

如果我们想创建一个子程序来推送常量:

   pushR2: ; value to push in R2
     @R15           ; save return address in R15
     M=D            ; we can't really use the stack,...
     @R2            ; because we are pushing on it
     D=M
     @STK
     AM=M+1         ; bump stack pointer; also set A to new SP value
     M=D            ; write the return address into the stack
     @R15
     A=M
     0 ; jmp

并调用“推送常量”例程:

     @<constant>
     D=A
     @R2
     M=D
     @returnaddress   ; sets the A register
     D=A
     @pushR2
     0 ; jmp
  returnaddress:

推送变量值 X:

     @X
     D=M
     @R2
     M=D
     @returnaddress   ; sets the A register
     D=A
     @pushR2
     0 ; jmp
  returnaddress:

从栈中弹出一个值到 D 寄存器的子程序:

   popD:
     @R15           ; save return address in R15
     M=D            ; we can't really use the stack,...
     @STK
     AM=M-1         ; decrement stack pointer; also set A to new SP value
     D=M            ; fetch the popped value
     @R15
     A=M
     0 ; jmp

现在,执行 OP 原始请求的“EQ”计算:

EQ: ; compare values on top of stack, return boolean in D
      @R15         ; save return address
      M=D
      @EQReturn1
      D=A
      @PopD
      0; jmp
@EQReturn1:
      @R2
      M=D        ; save first popped value
      @EQReturn2
      D=A
      @PopD
      0; jmp
@EQReturn2:
      ; here D has 2nd popped value, R2 has first
      @R2
      D=D-M
      @EQDone
      equal; jmp
      @AddressOfXFFFF
      D=M
EQDone: ; D contains 0 or FFFF here
      @R15
      A=M         ; fetch return address
      0; jmp

把它们放在一起:

     @5           ; push constant 5
     D=A
     @R2
     M=D
     @returnaddress1
     D=A
     @pushR2
     0 ; jmp
  returnaddress1:

     @X                ; now push X
     D=M
     @R2
     M=D
     @returnaddress2 
     D=A
     @pushR2
     0 ; jmp
  returnaddress2:

     @returnaddress3   ; pop and compare the values
     D=A
     @EQ
     0 ; jmp
  returnaddress3:

此时,OP可以生成代码将D压入堆栈:

     @R2                ; push D onto stack
     M=D
     @returnaddress4 
     D=A
     @pushR2
     0 ; jmp
  returnaddress4:

或者他可以生成代码以根据 D 的值进行分支:

     @jmptarget
     EQ ; jmp

【讨论】:

  • 这也是我的第一个意见,但我想我错过了什么。感谢您的出色回答。但还有一个问题:我在哪里声明没有间接寻址?
  • @KorayTugay:你没有。您提供了文件的副本;我查看了原件,这就是我最初拥有的所有数据。你唯一的“罪过”是指向一个似乎有关于指令集的only信息的文档。重新阅读有关您的问题的 cmets,以了解其思想是如何演变的。我给本书作者发了一条消息,说拆分指令集文档不是一件好事。 PS:我实际上是查看 Jack 生成的代码来说服自己我理解它在做什么。
  • 谢谢。尽管我最终使用了更原始、无子程序的 eq 实现,但我发现您的子程序实现非常有启发性。出于某种原因在运行本课程时,我从未想过在汇编级别实现子例程。
  • 这会导致执行速度非常慢。调用 push 子程序的代码比写原始 push 的代码要长,5 行的操作变成了 18 行的乱七八糟。此外,按照惯例,堆栈指针应始终指向下一个可用插槽,因此请注意,这种推送方法需要在所有其他操作中进行范式转换。
  • @MatheusLeão:人们总是通过构建解释器来驯服丑陋的指令集,故意放弃性能以缩短实施时间。
【解决方案2】:

正如我在上一条评论中所写,有一个无分支方式,因此您需要直接从操作数计算返回值

现在让我们像eq这样简单的操作

  • 如果我做对了eq a,d 就像a=(a==d)
  • true 为0xFFFF,false 为0x0000
  • 所以 this if a==d then a-d==0 this 可以直接使用

    1. 计算a=a-d
    2. 计算OR 的所有位的级联a

      • 如果结果为 0,则返回 0
      • 如果结果为 1,则返回 0xFFFF
      • 这可以通过表格或0-OR_Cascade(a)来实现
    3. OR 级联

      • 我在您的描述中没有看到任何位移操作
      • 所以你需要使用a+a 而不是a&lt;&lt;1
      • 如果需要右移,则需要实现除以 2

所以当我总结这个eq a,d 时可能是这样的:

  • a=a-d;
  • a=(a|(a&gt;&gt;1)|(a&gt;&gt;2)|...|(a&gt;&gt;15))&amp;1
  • a=0-a;
  • 您只需将其编码到您的程序集中
  • 由于您没有直接支持除法或移位,这可能会更好
  • a=a-d;
  • a=(a|(a&lt;&lt;1)|(a&lt;&lt;2)|...|(a&lt;&lt;15))&amp;0x8000
  • a=0-(a&gt;&gt;15);

较低和较大的比较要复杂得多

  • 你需要计算减法的进位标志
  • 或使用结果的符号(结果的 MSB)
  • 如果将操作数限制为 15 位,那么它只是第 15 位
  • 对于完整的 16 位操作数,您需要计算结果的第 16 位
  • 为此,您需要了解相当多的逻辑电路和 ALU 求和原理
  • 或将值分成 8 位对并执行 2x8 位减法级联
  • 所以a=a-d 将变为:
  • sub al,dl
  • sbc ah,dh
  • 进位/符号位于结果的第 8 位,可访问

【讨论】:

  • 我想你会发现实现右移或除以两个非常困难。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2016-06-18
  • 1970-01-01
  • 2018-10-21
  • 2018-01-10
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多