背景
我喜欢旧的8位6502芯片。在6502机器代码中解决PPCG方面的一些挑战甚至很有趣。但是,一些应该很简单的事情(例如,读入数据或输出到stdout)在机器代码中不必要地麻烦。因此,我想到一个粗略的想法:发明我自己的8位虚拟机,该虚拟机受6502的启发,但修改后的设计使其更能应对挑战。开始实施某些东西时,我意识到如果将VM的设计减少到最低限度,这本身就是一个不错的挑战:)
任务
实现符合以下规范的8位虚拟机。这是code-golf,因此以最少的字节数实现。
输入值
您的实现应采用以下输入:
一个无符号字节
pc,它是初始程序计数器(0基于VM的内存开始执行的内存中的地址)具有最大
256条目长度的字节列表,这是虚拟机的RAM(及其初始内容)
您可以采用任何合理的格式输入此信息。
输出量
字节列表,这是VM终止后RAM的最终内容(请参阅下文)。您可以假设您只得到导致最终终止的输入。允许使用任何明智的格式。
虚拟CPU
虚拟CPU具有
- 一个8位程序计数器
- 一个8位累加器寄存器称为
A和 - 一个称为的8位索引寄存器
X。
有三个状态标志:
Z-在执行某些操作后置零标志0N-在某些操作导致负数之后设置负标志(现在将结果的第7位设置为1)C-进位标志由加法设置,并移位结果的“缺失”位
在开始的标志被全部清零,程序计数器设置为给定值和内容A,并X是不确定的。
8位值代表
- 范围内的无符号整数
[0..255] - 有符号整数(2的补码),范围为
[-128..127]
取决于上下文。如果某个操作上溢或下溢,则该值会回绕(如果加法,进位标志也会受到影响)。
终止
虚拟机在以下时间终止
- 一
HLT到达指令 - 访问了不存在的内存地址
- 程序计数器在内存外部运行(请注意,即使VM被提供了完整的256字节内存,它也不会回绕)
地址模式
- 隐式 -指令没有参数,隐含操作数
- 立即数-操作数是指令之后的字节
- 相对 -(仅用于分支)指令后的字节被签名(2的补码),并确定执行分支后要添加到程序计数器的偏移量。
0是以下指令的位置 - 绝对 -指令后的字节是操作数的地址
- 索引 -指令加号
X(寄存器)之后的字节是操作数的地址
使用说明
每条指令都由一个操作码(一个字节)组成,在寻址模式下是即时,相对,绝对和索引第二个参数字节。当虚拟CPU执行一条指令时,它会相应地(由1或2)递增程序计数器。
此处显示的所有操作码均为十六进制。
LDA-将操作数加载到A- 操作码:立即:
00,绝对:02,索引:04 - 标志:
Z,N
- 操作码:立即:
STA-存储A到操作数- 操作码:立即:
08,绝对:0a,索引:0c
- 操作码:立即:
LDX-将操作数加载到X- 操作码:立即:
10,绝对:12,索引:14 - 标志:
Z,N
- 操作码:立即:
STX-存储X到操作数- 操作码:立即:
18,绝对:1a,索引:1c
- 操作码:立即:
AND-按位与的A和操作数并入A- 操作码:立即:
30,绝对:32,索引:34 - 标志:
Z,N
- 操作码:立即:
ORA-按位或的A和操作数并入A- 操作码:立即:
38,绝对:3a,索引:3c - 标志:
Z,N
- 操作码:立即:
EOR-按位XOR(异或)的A和操作数到A- 操作码:立即:
40,绝对:42,索引:44 - 标志:
Z,N
- 操作码:立即:
LSR-逻辑右移,将操作数的所有位右移一位,位0进位- 操作码:立即:
48,绝对:4a,索引:4c - 标志:
Z,N,C
- 操作码:立即:
ASL-算术左移,将操作数的所有位左移一位,第7位进位- 操作码:立即:
50,绝对:52,索引:54 - 标志:
Z,N,C
- 操作码:立即:
ROR-向右旋转,将操作数的所有位向右移一位,进位到位7,位0进位- 操作码:立即:
58,绝对:5a,索引:5c - 标志:
Z,N,C
- 操作码:立即:
ROL-向左旋转,将操作数的所有位向左移一位,进位转到位0,位7进位- 操作码:立即:
60,绝对:62,索引:64 - 标志:
Z,N,C
- 操作码:立即:
ADC-带进位加法,操作数加进位加法A,进位设置为溢出- 操作码:立即:
68,绝对:6a,索引:6c - 标志:
Z,N,C
- 操作码:立即:
INC-将操作数加1- 操作码:立即:
78,绝对:7a,索引:7c - 标志:
Z,N
- 操作码:立即:
DEC-将操作数减一- 操作码:立即:
80,绝对:82,索引:84 - 标志:
Z,N
- 操作码:立即:
CMP-A通过从中减去操作数来与操作数比较A,忘记结果。溢出时清除进位,否则设置- 操作码:立即:
88,绝对:8a,索引:8c - 标志:
Z,N,C
- 操作码:立即:
CPX-比较X-同CMP为X- 操作码:立即:
90,绝对:92,索引:94 - 标志:
Z,N,C
- 操作码:立即:
HLT-终止- 操作码:隐式:
c0
- 操作码:隐式:
INX-加X一- 操作码:隐式:
c8 - 标志:
Z,N
- 操作码:隐式:
DEX-减X一- 操作码:隐式:
c9 - 标志:
Z,N
- 操作码:隐式:
SEC-设置进位标志- 操作码:隐式:
d0 - 标志:
C
- 操作码:隐式:
CLC-清除进位标志- 操作码:隐式:
d1 - 标志:
C
- 操作码:隐式:
BRA-总是分支- 操作码:相对:
f2
- 操作码:相对:
BNE-如果Z清除标志则分支- 操作码:相对:
f4
- 操作码:相对:
BEQ-如果Z设置了标志则分支- 操作码:相对:
f6
- 操作码:相对:
BPL-如果N清除标志则分支- 操作码:相对:
f8
- 操作码:相对:
BMI-如果N设置了标志则分支- 操作码:相对:
fa
- 操作码:相对:
BCC-如果C清除标志则分支- 操作码:相对:
fc
- 操作码:相对:
BCS-如果C设置了标志则分支- 操作码:相对:
fe
- 操作码:相对:
操作码
如果发现任何操作码未映射到上述列表中的有效指令,则VM的行为是不确定的。
根据Jonathan Allan的要求,您可以选择自己的一组操作码,而不是“ 说明”部分中显示的操作码。如果这样做,则必须将完整的映射添加到答案中上面使用的操作码中。
映射应该是带有对的十六进制文件<official opcode> <your opcode>,例如,如果您替换了两个操作码:
f4 f5
10 11
换行在这里无关紧要。
测试用例(官方操作码)
// some increments and decrements
pc: 0
ram: 10 10 7a 01 c9 f4 fb
output: 10 20 7a 01 c9 f4 fb
// a 16bit addition
pc: 4
ram: e0 08 2a 02 02 00 6a 02 0a 00 02 01 6a 03 0a 01
output: 0a 0b 2a 02 02 00 6a 02 0a 00 02 01 6a 03 0a 01
// a 16bit multiplication
pc: 4
ram: 5e 01 28 00 10 10 4a 01 5a 00 fc 0d 02 02 d1 6a 21 0a 21 02 03 6a 22 0a 22 52
02 62 03 c9 f8 e6 c0 00 00
output: 00 00 00 00 10 10 4a 01 5a 00 fc 0d 02 02 d1 6a 21 0a 21 02 03 6a 22 0a 22 52
02 62 03 c9 f8 e6 c0 b0 36
我可能会在以后添加更多的测试用例。
参考和测试
为了帮助自己进行实验,这里有一些(完全不是高尔夫)参考实现 -它可以stderr在运行时将跟踪信息(包括反汇编的指令)输出到并转换操作码。
推荐的获取来源的方式:
git clone https://github.com/zirias/gvm --branch challenge --single-branch --recurse-submodules
或结帐分支challenge并git submodule update --init --recursive在克隆后进行操作,以获取我的自定义构建系统。
使用GNU make构建工具(只需输入make,或者gmake在您的系统上,默认的make不是GNU make)。
用法:gvm [-s startpc] [-h] [-t] [-c convfile] [-d] [-x] <initial_ram
-s startpc-初始程序计数器,默认为0-h-输入为十六进制(否则为二进制)-t-跟踪执行到stderr-c convfile-根据给出的映射转换操作码convfile-d-将结果内存转储为二进制数据-x-将结果内存转储为十六进制initial_ram-初始RAM内容,十六进制或二进制
请注意,转换功能将在运行时修改操作码的程序上失败。
免责声明:以上规则和规范仅代表挑战,而非此工具。这尤其适用于操作码转换功能。如果您认为此处提供的工具存在规范错误,请在评论中进行报告:)
BRA(“总是分支”)没有在控制流中引入分支,是否不应该将其称为JMP?
BRA存在于诸如65C02和MC 68000之类的后续芯片设计中(6502没有这样的指令)JMP。区别在于BRA使用相对寻址和JMP绝对寻址。因此,我只是遵循了这些设计-确实,这听起来并不合逻辑;)