600行代码实现一个chip-8虚拟机

默认分类·爬虫与逆向 · 2024-03-02 · 4025 人浏览

介绍

CHIP-8 是一种解释型语言,设计之初就是为了编写简单的小游戏。我猜是作者嫌老机器的汇编语言太复杂繁琐,从而自己设计了一门汇编语言,并且摆脱硬件的束缚,在模拟器上运行。其实这个思想和 Java 等基于虚拟机的高级语言也是类似的,提供方便程序员编写的指令集,在硬件之上空架一层虚拟机,实现 “Write Once, Run Everywhere”。

我们的目的是在python中实现一个chip8虚拟机

Image Description

CHIP8 虚拟机的结构

和现代的计算机一样,虚拟起也有内存,CPU,寄存器等计算机的“基础零件”:

  • Memory:CHIP-8 最多有 4096 字节的内存
CHIP-8 解释器本身占用这些机器上的前 512 字节内存空间。因此,为原始系统编写的大多数程序都从内存位置 512 (0x200) 开始,并且不会访问位置 512 (0x200) 以下的任何内存。最上面的 256 个字节 (0xF00-0xFFF) 保留用于显示刷新,下面的 96 个字节 (0xEA0-0xEFF) 保留用于调用堆栈、内部使用和其他变量。
  • Program Counter:16 位的 PC,记录当前程序指令运行的内存位置,因为需要访问最多 4K 的内存(0xFFF)
  • Stack:16 位地址的堆栈,用于调用函数和返回。栈调用深度最初设计位 12 层,可以自行调整。
  • Registers:

    • 16 个 8 位数据寄存器(data register),名为 V0 至 VF。 VF 寄存器兼作某些指令的标志;因此,应该避免这种情况。在加法运算中,VF 是进位标志,而在减法运算中,VF 是“无借位”标志。在绘制指令中,VF 在像素冲突时设置。
    • 一个 16 位索引寄存器(index register),用于记录内存地址
  • Timers

    • 8 位延迟定时器,以 60 Hz(每秒 60 次)的速率递减,直至达到 0
    • 8 位声音定时器,当其值非零时,会发出蜂鸣声。
  • Display:64 x 32 像素(或 128 x 64 对于 SUPER-CHIP)单色,即黑或白
  • Inputs:16 个输入键,与前 16 个十六进制值匹配:0 到 F。

上面就是 CHIP-8 虚拟机的全部“硬件”了,接下来就是实现用软件模拟硬件。

导入模块

import pygame
import sys
import random
import os

我们需要实现图形界面、系统操作、随机数生成器和CHIP8指令集

寄存器

class Register:
    def __init__(self, bits):
        self.value = 0
        self.bits = bits

    def checkCarry(self):
        hexValue = hex(self.value)[2:]
        if len(hexValue) > self.bits / 4:
            self.value = int(hexValue[-int(self.bits / 4):], 16)
            return 1
        return 0
    
    def checkBorrow(self):
        if self.value < 0:
            self.value = abs(self.value)
            return 0
        return 1
    
    def readValue(self):
        return hex(self.value)
    
    def setValue(self, value):
        self.value = value

Register 类表示一个寄存器,包含寄存器的值和位数。checkCarrycheckBorrow 方法用于检查进位和借位。

延迟计时器

CHIP-8 有两个独立的定时器寄存器:延迟定时器和声音定时器。 大小为 1 个字节,只要它们的值大于 0,它们就应该每秒减少 60 次(即 60 Hz ),并且与执行指令的速度无关。 也就是说无论代码怎么执行,即使进入了死循环,定时器也需要以 60 Hz 的频率运行。

class DelayTimer:
    def __init__(self):
        self.timer = 0
    
    def countDown(self):
        if self.timer > 0:
            self.timer -= 1

    def setTimer(self, value):
        self.timer = value
    
    def readTimer(self):
        return self.timer

这样可以设计一个最基础的延迟计时器,包含计时器的值和倒计时方法。

蜂鸣器

class SoundTimer(DelayTimer):
    def __init__(self):
        DelayTimer.__init__(self)

    def beep(self):
        if self.timer > 1:
            os.system('play --no-show-progress --null --channels 1 synth %s triangle %f' % (self.timer / 60, 440))
            self.timer = 0

SoundTimer 类继承自 DelayTimer,可以利用系统延时器播放声音。

Stack 类

chip8虚拟机提供了一个栈机,我们可以使用python的队列来实现

class Stack:
    def __init__(self):
        self.stack = []
    
    def push(self, value):
        self.stack.append(value)
    
    def pop(self):
        return self.stack.pop()

Stack 类表示一个堆栈,包含入栈和出栈方法。

Emulator 类

Emulator 初始化内存、寄存器、堆栈、计时器、键盘映射和显示屏。 我们之前导入的pygame 这会儿可以用于图形界面

chip8使用的是点阵字体,我们可以用16进制数,每一位来表示一个像素

class Emulator:
    def __init__(self):
        self.Memory = [0x0] * 4096
        fonts = [ 
            0xF0, 0x90, 0x90, 0x90, 0xF0, # 0
            0x20, 0x60, 0x20, 0x20, 0x70, # 1
            0xF0, 0x10, 0xF0, 0x80, 0xF0, # 2
            0xF0, 0x10, 0xF0, 0x10, 0xF0, # 3
            0x90, 0x90, 0xF0, 0x10, 0x10, # 4
            0xF0, 0x80, 0xF0, 0x10, 0xF0, # 5
            0xF0, 0x80, 0xF0, 0x90, 0xF0, # 6
            0xF0, 0x10, 0x20, 0x40, 0x40, # 7
            0xF0, 0x90, 0xF0, 0x90, 0xF0, # 8
            0xF0, 0x90, 0xF0, 0x10, 0xF0, # 9
            0xF0, 0x90, 0xF0, 0x90, 0x90, # A
            0xE0, 0x90, 0xE0, 0x90, 0xE0, # B
            0xF0, 0x80, 0x80, 0x80, 0xF0, # C
            0xE0, 0x90, 0x90, 0x90, 0xE0, # D
            0xF0, 0x80, 0xF0, 0x80, 0xF0, # E
            0xF0, 0x80, 0xF0, 0x80, 0x80  # F
        ]
        for i in range(len(fonts)):
            self.Memory[i] = fonts[i]

        self.Registers = [Register(8) for _ in range(16)]
        self.IRegister = Register(16)
        self.ProgramCounter = 0x200
        self.stack = Stack()
        self.delayTimer = DelayTimer()
        self.soundTimer = SoundTimer()
        pygame.init()
        pygame.time.set_timer(pygame.USEREVENT+1, int(1000 / 60))
        
        self.keys = [False] * 16
        self.keyDict = {
            49 : 1, 50 : 2, 51 : 3, 52 : 0xc,
            113 : 4, 119 : 5, 101 : 6, 114 : 0xd,
            97 : 7, 115 : 8, 100 : 9, 102 : 0xe,
            122 : 0xa, 120 : 0, 99 : 0xb, 118 : 0xf
        }

        self.grid = [[0] * 64 for _ in range(32)]
        self.emptyGrid = self.grid[:]
        self.zeroColor = [0, 0, 50]
        self.oneColor = [255, 255, 255]

        self.size = 10
        width = 64
        height = 32
        self.screen = pygame.display.set_mode([width * self.size, height * self.size])
        self.screen.fill(self.oneColor)
        pygame.display.flip()

execOpcode 方法

cpu最重要的就是指令集了,chip8的指令集非常精简,一共只有35条指令,execOpcode 方法根据操作码执行相应的指令。

    def execOpcode(self, opcode):
        if opcode[0] == '0':
            if opcode[1] != '0':
                print("ROM attempts to run RCA 1802 program at <0x" + opcode[1:] + '>')
            else:
                if opcode == '00e0':
                    self.clear()
                elif opcode == '00ee':
                    self.ProgramCounter = self.stack.pop()
        elif opcode[0] == '1':
            self.ProgramCounter = int(opcode[1:], 16) - 2
        elif opcode[0] == '2':
            self.stack.push(self.ProgramCounter)
            self.ProgramCounter = int(opcode[1:], 16) - 2
        elif opcode[0] == '3':
            vNum = int(opcode[1], 16)
            targetNum = int(opcode[2:], 16)
            if self.Registers[vNum].value == targetNum:
                self.ProgramCounter += 2
        elif opcode[0] == '4':
            vNum = int(opcode[1], 16)
            targetNum = int(opcode[2:], 16)
            if self.Registers[vNum].value != targetNum:
                self.ProgramCounter += 2
        elif opcode[0] == '5':
            v1 = int(opcode[1], 16)
            v2 = int(opcode[2], 16)
            if self.Registers[v1].value == self.Registers[v2].value:
                self.ProgramCounter += 2
        elif opcode[0] == '6':
            vNum = int(opcode[1], 16)
            targetNum = int(opcode[2:], 16)
            self.Registers[vNum].value = targetNum
        elif opcode[0] == '7':
            vNum = int(opcode[1], 16)
            targetNum = int(opcode[2:], 16)
            self.Registers[vNum].value += targetNum
            self.Registers[vNum].checkCarry()
        elif opcode[0] == '8':
            if opcode[3] == '0':
                v1 = int(opcode[1], 16)
                v2 = int(opcode[2], 16)
                self.Registers[v1].value = self.Registers[v2].value
            elif opcode[3] == '1':
                v1 = int(opcode[1], 16)
                v2 = int(opcode[2], 16)
                self.Registers[v1].value = self.Registers[v1].value | self.Registers[v2].value
            elif opcode[3] == '2':
                v1 = int(opcode[1], 16)
                v2 = int(opcode[2], 16)
                self.Registers[v1].value = self.Registers[v1].value & self.Registers[v2].value
            elif opcode[3] == '3':
                v1 = int(opcode[1], 16)
                v2 = int(opcode[2], 16)
                self.Registers[v1].value = self.Registers[v1].value ^ self.Registers[v2].value
            elif opcode[3] == '4':
                v1 = int(opcode[1], 16)
                v2 = int(opcode[2], 16)
                self.Registers[v1].value += self.Registers[v2].value
                self.Registers[0xf].value = self.Registers[v1].checkCarry()
            elif opcode[3] == '5':
                v1 = int(opcode[1], 16)
                v2 = int(opcode[2], 16)
                self.Registers[v1].value -= self.Registers[v2].value
                self.Registers[0xf].value = self.Registers[v1].checkBorrow()
            elif opcode[3] == '6':
                v1 = int(opcode[1], 16)
                leastBit = int(bin(self.Registers[v1].value)[-1])
                self.Registers[v1].value = self.Registers[v1].value >> 1
                self.Registers[0xf].value = leastBit
            elif opcode[3] == '7':
                v1 = int(opcode[1], 16)
                v2 = int(opcode[2], 16)
                self.Registers[v1].value = self.Registers[v2].value - self.Registers[v1].value
                self.Registers[0xf].value = self.Registers[v1].checkBorrow()
            elif opcode[3] == 'e':
                v1 = int(opcode[1], 16)
                mostBit = int(bin(self.Registers[v1].value)[2])
                self.Registers[v1].value = self.Registers[v1].value << 1
                self.Registers[0xf].value = mostBit
        elif opcode[0] == '9':
            v1 = int(opcode[1], 16)
            v2 = int(opcode[2], 16)
            if self.Registers[v1].value != self.Registers[v2].value:
                self.ProgramCounter += 2
        elif opcode[0] == 'a':
            addr = int(opcode[1:], 16)
            self.IRegister.value = addr
        elif opcode[0] == 'b':
            addr = int(opcode[1:], 16)
            self.ProgramCounter = self.Registers[0].value + addr - 2
        elif opcode[0] == 'c':
            vNum = int(opcode[1], 16)
            targetNum = int(opcode[2:], 16)
            rand = random.randint(0, 255)
            self.Registers[vNum].value = targetNum & rand
        elif opcode[0] == 'd':
            Vx = int(opcode[1], 16)
            Vy = int(opcode[2], 16)
            N  = int(opcode[3], 16)
            addr = self.IRegister.value
            sprite = self.Memory[addr: addr + N]
            for i in range(len(sprite)):
                if type(sprite[i]) == str:
                     sprite[i] = int(sprite[i], 16)
            if self.draw(self.Registers[Vx].value, self.Registers[Vy].value, sprite):
                self.Registers[0xf].value = 1
            else:
                self.Registers[0xf].value = 0
        elif opcode[0] == 'e':
            if opcode[2:] == '9e':
                Vx = int(opcode[1], 16)
                key = self.Registers[Vx].value
                if self.keys[key]:
                    self.ProgramCounter += 2
            elif opcode[2:] == 'a1':
                Vx = int(opcode[1], 16)
                key = self.Registers[Vx].value
                if not self.keys[key]:
                    self.ProgramCounter += 2
        elif opcode[0] == 'f':
            if opcode[2:] == '07':
                Vx = int(opcode[1], 16)
                self.Registers[Vx].value = self.delayTimer.readTimer()
            elif opcode[2:] == '0a':
                Vx = int(opcode[1], 16)
                key = None
                while True:
                    self.keyHandler()
                    isKeyDown = False
                    for i in range(len(self.keys)):
                        if self.keys[i]:
                            key = i
                            isKeyDown = True
                    if isKeyDown:
                        break
                self.Registers[Vx].value = key
            elif opcode[2:] == '15':
                Vx = int(opcode[1], 16)
                value = self.Registers[Vx].value
                self.delayTimer.setTimer(value)
            elif opcode[2:] == '18':
                Vx = int(opcode[1], 16)
                value = self.Registers[Vx].value
                self.soundTimer.setTimer(value)
            elif opcode[2:] == '1e':
                Vx = int(opcode[1], 16)
                self.IRegister.value += self.Registers[Vx].value
            elif opcode[2:] == '29':
                Vx = int(opcode[1], 16)
                value = self.Registers[Vx].value
                self.IRegister.value = value * 5
            elif opcode[2:] == '33':
                Vx = int(opcode[1], 16)
                value = str(self.Registers[Vx].value)
                fillNum = 3 - len(value)
                value = '0' * fillNum + value
                for i in range(len(value)):
                    self.Memory[self.IRegister.value + i] = int(value[i])
            elif opcode[2:] == '55':
                Vx = int(opcode[1], 16)
                for i in range(0, Vx + 1):
                    self.Memory[self.IRegister.value + i] = self.Registers[i].value
            elif opcode[2:] == '65':
                Vx = int(opcode[1], 16)
                for i in range(0, Vx + 1):
                    self.Registers[i].value = self.Memory[self.IRegister.value + i]
        self.ProgramCounter += 2

图形显示配置

    def execution(self):
        index = self.ProgramCounter
        high = self.hexHandler(self.Memory[index])
        low = self.hexHandler(self.Memory[index + 1])
        opcode = high + low
        self.execOpcode(opcode)

    def draw(self, Vx, Vy, sprite):
        collision = False
        spriteBits = []
        for i in sprite:
            binary = bin(i)
            line = list(binary[2:])
            fillNum = 8 - len(line)
            line = ['0'] * fillNum + line
            spriteBits.append(line)
        for i in range(len(spriteBits)):
            for j in range(8):
                try:
                    if self.grid[Vy + i][Vx + j] == 1 and int(spriteBits[i][j]) == 1:
                        collision = True
                    self.grid[Vy + i][Vx + j] = self.grid[Vy + i][Vx + j] ^ int(spriteBits[i][j])
                except:
                    continue
        return collision
    
    def clear(self):
        for i in range(len(self.grid)):
            for j in range(len(self.grid[0])):
                self.grid[i][j] = 0

    def readProg(self, filename):
        rom = self.convertProg(filename)
        offset = int('0x200', 16)
        for i in rom:
            self.Memory[offset] = i
            offset += 1
    
    def convertProg(self, filename):
        rom = []
        with open(filename, 'rb') as f:
            wholeProgram = f.read()
            for i in wholeProgram:
                opcode = i
                rom.append(opcode)
        return rom
    
    def hexHandler(self, Num):
        newHex = hex(Num)[2:]
        if len(newHex) == 1:
            newHex = '0' + newHex
        return newHex

    def keyHandler(self):
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                sys.exit()
            elif event.type == pygame.USEREVENT+1:
                self.delayTimer.countDown()
            elif event.type == pygame.KEYDOWN:
                try:
                    targetKey = self.keyDict[event.key]
                    self.keys[targetKey] = True
                except: pass
            elif event.type == pygame.KEYUP:
                try:
                    targetKey = self.keyDict[event.key]
                    self.keys[targetKey] = False
                except: pass

    def mainLoop(self):
        clock = pygame.time.Clock()
        while True:
            clock.tick(300)
            self.keyHandler()
            self.soundTimer.beep()
            self.execution()
            self.display()
    
    def display(self):
        for i in range(0, len(self.grid)):
            for j in range(0, len(self.grid[0])):
                cellColor = self.zeroColor
                if self.grid[i][j] == 1:
                    cell

项目地址

https://github.com/dream2333/chip8-emulator

Theme Jasmine by Kent Liao