From 483a3f24163341853503151a85596d2651f10e64 Mon Sep 17 00:00:00 2001 From: Anton Lydike Date: Wed, 19 May 2021 12:14:43 +0200 Subject: [PATCH] Priv: [wip] implementing privileged architecture --- riscemu/priv/CSR.py | 101 +++++++++++++++++++++++++++++++ riscemu/priv/ElfLoader.py | 9 +++ riscemu/priv/Exceptions.py | 23 +++++++ riscemu/priv/PrivCPU.py | 121 +++++++++++++++++++++++++++++++++++++ riscemu/priv/PrivRV32I.py | 79 ++++++++++++++++++++++++ riscemu/priv/__init__.py | 15 +++++ riscemu/priv/__main__.py | 27 +++++++++ 7 files changed, 375 insertions(+) create mode 100644 riscemu/priv/CSR.py create mode 100644 riscemu/priv/ElfLoader.py create mode 100644 riscemu/priv/Exceptions.py create mode 100644 riscemu/priv/PrivCPU.py create mode 100644 riscemu/priv/PrivRV32I.py create mode 100644 riscemu/priv/__init__.py create mode 100644 riscemu/priv/__main__.py diff --git a/riscemu/priv/CSR.py b/riscemu/priv/CSR.py new file mode 100644 index 0000000..35e7277 --- /dev/null +++ b/riscemu/priv/CSR.py @@ -0,0 +1,101 @@ +from typing import Dict, Union +from collections import defaultdict + +MSTATUS_OFFSETS = { + 'uie': 0, + 'sie': 1, + 'mie': 3, + 'upie': 4, + 'spie': 5, + 'mpie': 7, + 'spp': 8, + 'mpp': 11, + 'fs': 13, + 'xs': 15, + 'mpriv': 17, + 'sum': 18, + 'mxr': 19, + 'tvm': 20, + 'tw': 21, + 'tsr': 22, + 'sd': 31 +} +""" +Offsets for all mstatus bits +""" + +MSTATUS_LEN_2 = ('mpp', 'fs', 'xs') +""" +All mstatus parts that have length 2. All other mstatus parts have length 1 +""" + + +class CSR: + """ + This holds all Control and Status Registers (CSR) + """ + regs: Dict[int, int] + """ + All Control and Status Registers are stored here + """ + + name_to_addr: Dict[str, int] = { + 'mstatus': 0x300, + 'misa': 0x301, + 'mie': 0x304, + 'mtvec': 0x305, + 'mepc': 0x341, + 'mcause': 0x342, + 'mtval': 0x343, + 'mip': 0x344, + 'mvendorid': 0xF11, + 'marchid': 0xF12, + 'mimpid': 0xF13, + 'mhartid': 0xF14, + } + """ + Translation for named registers + """ + + def __init__(self): + self.regs = defaultdict(lambda: 0) + + def set(self, addr: Union[str, int], val: int): + if isinstance(addr, str): + if not addr in self.name_to_addr: + print("Unknown CSR register {}".format(addr)) + addr = self.name_to_addr[addr] + self.regs[addr] = val + + def get(self, addr: Union[str, int]): + if isinstance(addr, str): + if not addr in self.name_to_addr: + print("Unknown CSR register {}".format(addr)) + addr = self.name_to_addr[addr] + return self.regs[addr] + + # mstatus properties + def set_mstatus(self, name: str, val: int): + """ + Set mstatus bits using this helper. mstatus is a 32 bit register, holding various machine status flags + Setting them by hand is super painful, so this helper allows you to set specific bits. + + Please make sure your supplied value has the correct width! + + :param name: + :param val: + :return: + """ + size = 2 if name in MSTATUS_LEN_2 else 1 + off = MSTATUS_OFFSETS[name] + mask = (2**size - 1) << off + old_val = self.get('mstatus') + erased = old_val & (~mask) + new_val = erased | (val << off) + self.set('mstatus', new_val) + + def get_mstatus(self, name): + size = 2 if name in MSTATUS_LEN_2 else 1 + off = MSTATUS_OFFSETS[name] + mask = (2**size - 1) << off + return (self.get('mstatus') & mask) >> off diff --git a/riscemu/priv/ElfLoader.py b/riscemu/priv/ElfLoader.py new file mode 100644 index 0000000..d61446b --- /dev/null +++ b/riscemu/priv/ElfLoader.py @@ -0,0 +1,9 @@ +from ..Executable import Executable, MemorySection, MemoryFlags + +# This requires pyelftools package! + +class ElfExecutable(Executable): + def __init__(self, name): + from elftools.elf.elffile import ELFFile + + with open(f) diff --git a/riscemu/priv/Exceptions.py b/riscemu/priv/Exceptions.py new file mode 100644 index 0000000..6a2eafa --- /dev/null +++ b/riscemu/priv/Exceptions.py @@ -0,0 +1,23 @@ +from typing import Optional + + +class CpuTrap(BaseException): + code: int + interrupt: int + mtval: int + + def __init__(self, interrupt: int, code: int, mtval=0): + assert 0 <= interrupt <= 1 + + self.interrupt = interrupt + self.code = code + self.mtval = mtval + + @property + def mcause(self): + return (self.code << 31) + self.interrupt + + +class IllegalInstructionTrap(CpuTrap): + def __init__(self): + super().__init__(0, 2, 0) diff --git a/riscemu/priv/PrivCPU.py b/riscemu/priv/PrivCPU.py new file mode 100644 index 0000000..b0e2a63 --- /dev/null +++ b/riscemu/priv/PrivCPU.py @@ -0,0 +1,121 @@ +""" +RiscEmu (c) 2021 Anton Lydike + +SPDX-License-Identifier: MIT +""" + +from riscemu.CPU import * +from enum import IntEnum +from riscemu.Executable import LoadedMemorySection +from .Exceptions import * +from .CSR import CSR +from .PrivRV32I import PrivRV32I +from ..instructions.RV32M import RV32M + +from collections import defaultdict + +from typing import Union + +if typing.TYPE_CHECKING: + from riscemu import Executable, LoadedExecutable, LoadedInstruction + from riscemu.instructions.InstructionSet import InstructionSet + + +class PrivModes(IntEnum): + USER = 0 + SUPER = 1 + MACHINE = 3 + + +class PrivCPU(CPU): + """ + This is a CPU that has different modes, instruction sets and registers. + + It should support M and U Mode, but no U-Mode Traps. + + This allows us to + """ + + csr: CSR + + INS_XLEN = 1 + """ + Size of an instruction in memory. Should be 4, but since our loading code is shit, instruction take up + the equivalent of "1 byte" (this is actually impossible) + """ + + def __init__(self, conf): + super().__init__(conf, [PrivRV32I, RV32M]) + self.mode: PrivModes = PrivModes.MACHINE + + # set up CSR + self.csr = CSR() + # TODO: Actually populate the CSR with real data (vendorID, heartID, machine implementation etc) + self.csr.set('mhartid', 0) # core id + self.csr.set('mimpid', 1) # implementation id + self.csr.set('misa', ()) # available ISA + + def _run(self, verbose=False): + if self.pc <= 0: + return False + ins = None + try: + while not self.exit: + try: + self.cycle += 1 + ins = self.mmu.read_ins(self.pc) + if verbose: + print(FMT_CPU + " Running 0x{:08X}:{} {}".format(self.pc, FMT_NONE, ins)) + self.pc += 1 + self.run_instruction(ins) + except CpuTrap as trap: + mie = self.csr.get_mstatus('mie') + if not mie: + print("Caught trap while mie={}!".format(mie)) + # TODO: handle this a lot better + continue + # raise trap + + # caught a trap! + self.csr.set('mepc', self.pc) # store MEPC + self.csr.set_mstatus('mpp', self.mode) # save mpp + self.csr.set_mstatus('mpie', mie) # save mie + self.csr.set_mstatus('mie', 0) # disable further interrupts + self.csr.set('mcause', trap.mcause) # store cause + + # set mtval csr + self.csr.set('mtval', trap.mtval) + + # set priv mode to machine + self.mode = PrivModes.MACHINE + + # trap vector + mtvec = self.csr.get('mtvec') + if mtvec & 3 == 1: + # vectored mode! + self.pc = (mtvec >> 2) + (self.INS_XLEN * trap.code) + else: + # standard mode + self.pc = (mtvec >> 2) + + + except RiscemuBaseException as ex: + if not isinstance(ex, LaunchDebuggerException): + print(FMT_ERROR + "[CPU] excpetion caught at 0x{:08X}: {}:".format(self.pc - 1, ins) + FMT_NONE) + print(ex.message()) + self.pc -= 1 + + if self.active_debug: + print(FMT_CPU + "[CPU] Returning to debugger!" + FMT_NONE) + return + if self.conf.debug_on_exception: + launch_debug_session(self, self.mmu, self.regs, + "Exception encountered, launching debug:") + + if self.exit: + print() + print(FMT_CPU + "Program exited with code {}".format(self.exit_code) + FMT_NONE) + sys.exit(self.exit_code) + else: + print() + print(FMT_CPU + "Program stopped without exiting - perhaps you stopped the debugger?" + FMT_NONE) diff --git a/riscemu/priv/PrivRV32I.py b/riscemu/priv/PrivRV32I.py new file mode 100644 index 0000000..dc56df9 --- /dev/null +++ b/riscemu/priv/PrivRV32I.py @@ -0,0 +1,79 @@ +""" +RiscEmu (c) 2021 Anton Lydike + +SPDX-License-Identifier: MIT +""" + +from riscemu.instructions.RV32I import * +from ..Exceptions import INS_NOT_IMPLEMENTED +from riscemu.priv.PrivCPU import PrivModes, PrivCPU +from .Exceptions import * + + +class PrivRV32I(RV32I): + cpu: PrivCPU + """ + This is an extension of RV32I, written for the PrivCPU class + """ + + def instruction_csrrw(self, ins: 'LoadedInstruction'): + rd, off, rs = self.parse_crs_ins(ins) + if rd != 'zero': + old_val = int_from_bytes(self.cpu.csr.read(off, 4)) + self.regs.set(rd, old_val) + self.cpu.csr.write(off, 4, int_to_bytes(rs)) + + def instruction_csrrs(self, ins: 'LoadedInstruction'): + INS_NOT_IMPLEMENTED(ins) + + def instruction_csrrc(self, ins: 'LoadedInstruction'): + INS_NOT_IMPLEMENTED(ins) + + def instruction_csrrsi(self, ins: 'LoadedInstruction'): + INS_NOT_IMPLEMENTED(ins) + + def instruction_csrrwi(self, ins: 'LoadedInstruction'): + INS_NOT_IMPLEMENTED(ins) + + def instruction_csrrci(self, ins: 'LoadedInstruction'): + INS_NOT_IMPLEMENTED(ins) + + def instruction_mret(self, ins: 'LoadedInstruction'): + if self.cpu.mode != PrivModes.MACHINE: + print("MRET not inside machine level code!") + raise IllegalInstructionTrap() + # retore mie + mpie = self.cpu.csr.get_mstatus('mpie') + self.cpu.csr.set_mstatus('mie', mpie) + # restore priv + mpp = self.cpu.csr.get_mstatus('mpp') + self.cpu.mode = PrivModes(mpp) + # restore pc + mepc = self.cpu.csr.get('mepc') + self.cpu.pc = mepc + + def instruction_uret(self, ins: 'LoadedInstruction'): + raise IllegalInstructionTrap() + + def instruction_sret(self, ins: 'LoadedInstruction'): + raise IllegalInstructionTrap() + + def instruction_scall(self, ins: 'LoadedInstruction'): + """ + Overwrite the scall from userspace RV32I + """ + if self.cpu.mode == PrivModes.USER: + raise CpuTrap(0, 8) # ecall from U mode + elif self.cpu.mode == PrivModes.SUPER: + raise CpuTrap(0, 9) # ecall from S mode - should not happen + elif self.cpu.mode == PrivModes.MACHINE: + raise CpuTrap(0, 11) # ecall from M mode + + + + + + + def parse_crs_ins(self, ins: 'LoadedInstruction'): + ASSERT_LEN(ins.args, 3) + return ins.get_reg(0), ins.get_imm(1), self.get_reg_content(ins, 2) diff --git a/riscemu/priv/__init__.py b/riscemu/priv/__init__.py new file mode 100644 index 0000000..26e488f --- /dev/null +++ b/riscemu/priv/__init__.py @@ -0,0 +1,15 @@ +""" +RiscEmu (c) 2021 Anton Lydike + +SPDX-License-Identifier: MIT + +The priv Module holds everything necessary for emulating privileged risc-v assembly + + +Running priv is only preferable to the normal userspace emulator, if you actually want to emulate the whole system. + +Syscalls will have to be intercepted by your assembly code. + + +The PrivCPU Implements the Risc-V M/U Model, meaning there is machine mode and user mode. No PMP or paging is available. +""" \ No newline at end of file diff --git a/riscemu/priv/__main__.py b/riscemu/priv/__main__.py new file mode 100644 index 0000000..dd38576 --- /dev/null +++ b/riscemu/priv/__main__.py @@ -0,0 +1,27 @@ +from .PrivCPU import * + +from ..Tokenizer import RiscVInput +from ..ExecutableParser import ExecutableParser + +import sys + +if __name__ == '__main__': + + files = sys.argv + + cpu = PrivCPU(RunConfig()) + + try: + loaded_exe = None + for file in files: + tk = cpu.get_tokenizer(RiscVInput.from_file(file)) + tk.tokenize() + loaded_exe = cpu.load(ExecutableParser(tk).parse()) + # run the last loaded executable + cpu.run_loaded(loaded_exe) + except RiscemuBaseException as e: + print("Error while parsing: {}".format(e.message())) + import traceback + + traceback.print_exception(type(e), e, e.__traceback__) + sys.exit(1)