diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 2b66235..e72f99c 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -19,7 +19,7 @@ jobs: strategy: fail-fast: false matrix: - os: [windows-2019, ubuntu-22.04, macos-13] + os: [windows-2019, windows-2022, ubuntu-22.04, ubuntu-24.04, macos-13] python-version: ['3.10', '3.11', '3.12'] name: ${{ matrix.os }} / ${{ matrix.python-version }} runs-on: ${{ matrix.os }} @@ -27,9 +27,9 @@ jobs: windows-2019-3-10: ${{ join(steps.*.outputs.windows-2019-3-10,'') }} windows-2019-3-11: ${{ join(steps.*.outputs.windows-2019-3-11,'') }} windows-2019-3-12: ${{ join(steps.*.outputs.windows-2019-3-12,'') }} - ubuntu-22.04-3-10: ${{ join(steps.*.outputs.ubuntu-22.04-3-10,'') }} - ubuntu-22.04-3-11: ${{ join(steps.*.outputs.ubuntu-22.04-3-11,'') }} - ubuntu-22.04-3-12: ${{ join(steps.*.outputs.ubuntu-22.04-3-12,'') }} + ubuntu-22-04-3-10: ${{ join(steps.*.outputs.ubuntu-22-04-3-10,'') }} + ubuntu-22-04-3-11: ${{ join(steps.*.outputs.ubuntu-22-04-3-11,'') }} + ubuntu-22-04-3-12: ${{ join(steps.*.outputs.ubuntu-22-04-3-12,'') }} macos-13-3-10: ${{ join(steps.*.outputs.macos-13-3-10,'') }} macos-13-3-11: ${{ join(steps.*.outputs.macos-13-3-11,'') }} macos-13-3-12: ${{ join(steps.*.outputs.macos-13-3-12,'') }} @@ -47,62 +47,17 @@ jobs: version: 'latest' - name: "Install Pre-requisite (Linux)" - if: matrix.os == 'ubuntu-22.04' + if: startsWith(matrix.os, 'ubuntu') shell: bash run: | sudo apt update - sudo apt upgrade -y - sudo apt install -y build-essential libegl1 libgl1-mesa-glx - - # - name: "Install Pre-requisite (macOS)" - # if: matrix.os == 'macos-13' - # run: | - # env - - # - name: "Install Pre-requisite (Windows)" - # if: matrix.os == 'windows-2019' - # shell: pwsh - # run: | - # env + sudo apt install -y build-essential libegl1 - run: rye fmt - run: rye lint - run: rye test - run: rye build --wheel --out ./build - - # - name: Build artifact - # shell: bash - # run: | - # mkdir build - # mkdir build/bin - # python --version - # python -m pip --version - # python -m pip install --upgrade pip setuptools wheel - # python -m pip install --user --upgrade .[all] - - # - name: "Post build Cemu (Windows)" - # if: matrix.os == 'windows-2019' - # shell: pwsh - # run: | - # Copy-Item $env:APPDATA\Python\Python*\Scripts\cemu.exe build\bin\ - - # - name: "Post build Cemu (Linux)" - # if: matrix.os == 'ubuntu-22.04' - # shell: bash - # run: | - # cp -v ~/.local/bin/cemu build/bin/ - - # - name: "Post build Cemu (macOS)" - # if: matrix.os == 'macos-13' - # shell: bash - # run: | - # cp -v ~/.local/bin/cemu build/bin/ || cp -v /Users/runner/Library/Python/${{ matrix.python-version }}/bin/cemu build/bin/ - - # - name: "Run tests" - # run: | - # python -m pytest tests/ - - name: Publish artifact id: publish_artifact uses: actions/upload-artifact@v4 @@ -112,35 +67,39 @@ jobs: - name: Populate the successful output (Windows) id: output_success_windows - if: ${{ matrix.os == 'windows-2019' && success() }} + if: ${{ startsWith(matrix.os, 'windows') && success() }} shell: pwsh run: | + $osVersion = "${{ matrix.os }}" -replace "\.", "-" $pyVersion = "${{ matrix.python-version }}" -replace "\.", "-" - echo "${{ matrix.os }}-$pyVersion=✅ ${{ matrix.os }} ${{ matrix.python-version }}" | Out-File -FilePath $env:GITHUB_OUTPUT -Encoding utf8 -Append + echo "${osVersion}-$pyVersion=✅ ${{ matrix.os }} ${{ matrix.python-version }}" | Out-File -FilePath $env:GITHUB_OUTPUT -Encoding utf8 -Append - name: Populate the successful output (Other) id: output_success_other - if: ${{matrix.os != 'windows-2019' && success() }} + if: ${{startsWith(matrix.os, 'windows') == false && success() }} shell: bash run: | + osVersion="$(echo -n ${{ matrix.os }} | tr . -)" pyVersion="$(echo -n ${{ matrix.python-version }} | tr . -)" - echo "${{ matrix.os }}-${pyVersion}=✅ ${{ matrix.os }} ${{ matrix.python-version }}" >> $GITHUB_OUTPUT + echo "${osVersion}-${pyVersion}=✅ ${{ matrix.os }} ${{ matrix.python-version }}" >> $GITHUB_OUTPUT - name: Populate the failure output (Windows) id: output_failure_windows - if: ${{matrix.os == 'windows-2019' && failure() }} + if: ${{startsWith(matrix.os, 'windows') && failure() }} shell: pwsh run: | + $osVersion = "${{ matrix.os }}" -replace "\.", "-" $pyVersion = "${{ matrix.python-version }}" -replace "\.", "-" - echo "${{ matrix.os }}-${pyVersion}=❌ ${{ matrix.os }} ${{ matrix.python-version }}" | Out-File -FilePath $env:GITHUB_OUTPUT -Encoding utf8 -Append + echo "${osVersion}-${pyVersion}=❌ ${{ matrix.os }} ${{ matrix.python-version }}" | Out-File -FilePath $env:GITHUB_OUTPUT -Encoding utf8 -Append - name: Populate the failure output (Other) id: output_failure_other - if: ${{matrix.os != 'windows-2019' && failure() }} + if: ${{startsWith(matrix.os, 'windows') && failure() }} shell: bash run: | + osVersion="$(echo -n ${{ matrix.os }} | tr . -)" pyVersion="$(echo -n ${{ matrix.python-version }} | tr . -)" - echo "${{ matrix.os }}-$pyVersion=❌ ${{ matrix.os }} ${{ matrix.python-version }}" >> $GITHUB_OUTPUT + echo "${osVersion}-$pyVersion=❌ ${{ matrix.os }} ${{ matrix.python-version }}" >> $GITHUB_OUTPUT notify: env: @@ -167,9 +126,9 @@ jobs: ${{ needs.build.outputs.windows-2019-3-10 }} ${{ needs.build.outputs.windows-2019-3-11 }} ${{ needs.build.outputs.windows-2019-3-12 }} - ${{ needs.build.outputs.ubuntu-22.04-3-10 }} - ${{ needs.build.outputs.ubuntu-22.04-3-11 }} - ${{ needs.build.outputs.ubuntu-22.04-3-12 }} + ${{ needs.build.outputs.ubuntu-22-04-3-10 }} + ${{ needs.build.outputs.ubuntu-22-04-3-11 }} + ${{ needs.build.outputs.ubuntu-22-04-3-12 }} ${{ needs.build.outputs.macos-13-3-10 }} ${{ needs.build.outputs.macos-13-3-11 }} ${{ needs.build.outputs.macos-13-3-12 }} diff --git a/src/cemu/__main__.py b/src/cemu/__main__.py index c8cf38b..f257840 100644 --- a/src/cemu/__main__.py +++ b/src/cemu/__main__.py @@ -20,9 +20,7 @@ def setup_remote_debug(port: int = cemu.const.DEBUG_DEBUGPY_PORT): def main(argv: list[str]): - parser = argparse.ArgumentParser( - prog=cemu.const.PROGNAME, - description=cemu.const.DESCRIPTION) + parser = argparse.ArgumentParser(prog=cemu.const.PROGNAME, description=cemu.const.DESCRIPTION) parser.add_argument("filename") parser.add_argument("--debug", action="store_true") parser.add_argument("--attach", action="store_true") @@ -46,6 +44,7 @@ def main(argv: list[str]): if __name__ == "__main__": import sys + path = pathlib.Path(__file__).absolute().parent.parent sys.path.append(str(path)) main(sys.argv) diff --git a/src/cemu/arch/__init__.py b/src/cemu/arch/__init__.py index fdf0db5..216c2f9 100644 --- a/src/cemu/arch/__init__.py +++ b/src/cemu/arch/__init__.py @@ -1,19 +1,24 @@ +from dataclasses import dataclass import enum import importlib +import pathlib from typing import Optional, TYPE_CHECKING import capstone import keystone import unicorn +import cemu.errors from cemu.const import SYSCALLS_PATH -from ..ui.utils import popup, PopupType +from cemu.log import dbg, error +from cemu.utils import DISASSEMBLY_DEFAULT_BASE_ADDRESS + if TYPE_CHECKING: import cemu.core -class Endianness(enum.Enum): +class Endianness(enum.IntEnum): LITTLE_ENDIAN = 1 BIG_ENDIAN = 2 @@ -27,7 +32,7 @@ def __int__(self) -> int: return self.value -class Syntax(enum.Enum): +class Syntax(enum.IntEnum): INTEL = 1 ATT = 2 @@ -71,26 +76,29 @@ def syscalls(self): if not self.__context: import cemu.core + assert cemu.core.context self.__context = cemu.core.context assert isinstance(self.__context, cemu.core.GlobalContext) if not self.__syscalls: - syscall_dir = SYSCALLS_PATH / str(self.__context.os) + syscall_dir = SYSCALLS_PATH / str(self.__context.os).lower() try: fpath = syscall_dir / (self.syscall_filename + ".csv") except ValueError as e: - popup(str(e), PopupType.Error, "No Syscall File Error") + error(f"No Syscall File Error: {e}") return {} self.__syscalls = {} - if fpath.exists(): - with fpath.open("r") as fd: - for row in fd.readlines(): - row = [x.strip() for x in row.strip().split(",")] - syscall_number = int(row[0]) - syscall_name = row[1].lower() - self.__syscalls[syscall_name] = self.syscall_base + syscall_number + if not fpath.exists(): + raise FileNotFoundError(fpath) + + with fpath.open("r") as fd: + for row in fd.readlines(): + row = [x.strip() for x in row.strip().split(",")] + syscall_number = int(row[0]) + syscall_name = row[1].lower() + self.__syscalls[syscall_name] = self.syscall_base + syscall_number return self.__syscalls @@ -264,3 +272,118 @@ def is_sparc64(a: Architecture): def is_ppc(a: Architecture): return isinstance(a, PowerPC) + + +def format_address(addr: int, arch: Optional[Architecture] = None) -> str: + """Format an address to string, aligned to the given architecture + + Args: + addr (int): _description_ + arch (Optional[Architecture], optional): _description_. Defaults to None. + + Raises: + ValueError: _description_ + + Returns: + str: _description_ + """ + if arch is None: + import cemu.core + + if not cemu.core.context: + ptrsize = 8 + else: + ptrsize = cemu.core.context.architecture.ptrsize + else: + ptrsize = arch.ptrsize + + match ptrsize: + case 2: + return f"{addr:#04x}" + case 4: + return f"{addr:#08x}" + case 8: + return f"{addr:#016x}" + case _: + raise ValueError(f"Invalid pointer size value of {ptrsize}") + + +@dataclass +class Instruction: + address: int + mnemonic: str + operands: str + bytes: bytes + + @property + def size(self): + return len(self.bytes) + + @property + def end(self) -> int: + return self.address + self.size + + def __str__(self): + return f'Instruction({self.address:#x}, "{self.mnemonic} {self.operands}")' + + +def disassemble(raw_data: bytes, count: int = -1, base: int = DISASSEMBLY_DEFAULT_BASE_ADDRESS) -> list[Instruction]: + """Disassemble the code given as raw data, with the given architecture. + + Args: + raw_data (bytes): the raw byte code to disassemble + arch (Architecture): the architecture to use for disassembling + count (int, optional): the maximum number of instruction to disassemble. Defaults to -1. + base (int, optional): the disassembled code base address. Defaults to DISASSEMBLY_DEFAULT_BASE_ADDRESS + + Returns: + str: the text representation of the disassembled code + """ + assert cemu.core.context + arch = cemu.core.context.architecture + insns: list[Instruction] = [] + for idx, ins in enumerate(arch.cs.disasm(raw_data, base)): + insn = Instruction(ins.address, ins.mnemonic, ins.op_str, ins.bytes) + insns.append(insn) + if idx == count: + break + + dbg(f"{insns=}") + return insns + + +def disassemble_file(fpath: pathlib.Path) -> list[Instruction]: + return disassemble(fpath.read_bytes()) + + +def assemble(code: str, base_address: int = DISASSEMBLY_DEFAULT_BASE_ADDRESS) -> list[Instruction]: + """ + Helper function to assemble code receive in parameter `asm_code` using Keystone. + + @param code : assembly code in bytes (multiple instructions must be separated by ';') + @param base_address : (opt) the base address to use + + @return a list of Instruction + """ + assert cemu.core.context + arch = cemu.core.context.architecture + + # + # Compile the entire given code + # + bytecode, assembled_insn_count = arch.ks.asm(code, as_bytes=True, addr=base_address) + if not bytecode or assembled_insn_count == 0: + raise cemu.errors.AssemblyException("Not instruction compiled") + + assert isinstance(bytecode, bytes) + + # + # Decompile it and return the stuff + # + insns = disassemble(bytecode, base=base_address) + dbg(f"{insns=}") + return insns + + +def assemble_file(fpath: pathlib.Path) -> list[Instruction]: + return assemble(fpath.read_text()) diff --git a/src/cemu/cli/repl.py b/src/cemu/cli/repl.py index df8aadd..04d6010 100644 --- a/src/cemu/cli/repl.py +++ b/src/cemu/cli/repl.py @@ -26,7 +26,9 @@ @bindings.add("c-c") def _(event): + assert cemu.core.context if cemu.core.context.emulator.is_running: + assert cemu.core.context cemu.core.context.emulator.set(EmulatorState.FINISHED) pass @@ -35,6 +37,8 @@ class CEmuRepl: def __init__(self, args): super(CEmuRepl, self).__init__() assert cemu.core.context + assert cemu.core.context + assert cemu.core.context assert isinstance(cemu.core.context, cemu.core.GlobalContext) self.history_filepath = pathlib.Path().home() / ".cemu_history" @@ -42,9 +46,11 @@ def __init__(self, args): self.keep_running = False self.prompt = "(cemu)> " self.__background_emulator_thread = EmulationRunner() + assert cemu.core.context cemu.core.context.emulator.set_threaded_runner(self.__background_emulator_thread) # register the callbacks for the emulator + assert cemu.core.context emu = cemu.core.context.emulator # emu.add_state_change_cb( # EmulatorState.NOT_RUNNING, self.update_layout_not_running @@ -65,6 +71,7 @@ def __init__(self, args): def run_forever(self): self.keep_running = True + assert cemu.core.context emu = cemu.core.context.emulator while self.keep_running: # @@ -163,8 +170,10 @@ def run_forever(self): case "arch": match args[0]: case "get": + assert cemu.core.context print(f"Current architecture {cemu.core.context.architecture}") case "set": + assert cemu.core.context cemu.core.context.architecture = cemu.arch.Architectures.find(args[1]) case "regs": @@ -253,6 +262,7 @@ def run_forever(self): return def bottom_toolbar(self) -> str: + assert cemu.core.context return f"{str(cemu.core.context.emulator)} [{str(cemu.core.context.architecture)}]" @@ -263,6 +273,7 @@ def run(self): """ Runs the emulation """ + assert cemu.core.context emu = cemu.core.context.emulator if not emu.vm: error("VM is not ready") diff --git a/src/cemu/core.py b/src/cemu/core.py index 1e2a7d4..359f5f4 100644 --- a/src/cemu/core.py +++ b/src/cemu/core.py @@ -76,7 +76,7 @@ def root(self, root: cemu.ui.main.CEmuWindow): # # The global application context. This **must** defined for cemu to operate # -context: Union[GlobalContext, GlobalGuiContext] +context: Optional[Union[GlobalContext, GlobalGuiContext]] = None def CemuGui() -> None: @@ -93,8 +93,10 @@ def CemuGui() -> None: cemu.log.dbg("Creating GUI context") context = GlobalGuiContext() + default_style_sheet = cemu.const.DEFAULT_STYLE_PATH.read_text() + app = QApplication(sys.argv) - app.setStyleSheet(cemu.const.DEFAULT_STYLE_PATH.open().read()) + app.setStyleSheet(default_style_sheet) app.setWindowIcon(QIcon(str(cemu.const.ICON_PATH.absolute()))) context.root = cemu.ui.main.CEmuWindow(app) sys.exit(app.exec()) diff --git a/src/cemu/emulator.py b/src/cemu/emulator.py index 29f0c4e..6e71af7 100644 --- a/src/cemu/emulator.py +++ b/src/cemu/emulator.py @@ -1,7 +1,7 @@ import collections from enum import IntEnum, unique from multiprocessing import Lock -from typing import Any, Callable, Optional +from typing import Any, Callable, Optional, TYPE_CHECKING import unicorn @@ -21,6 +21,10 @@ from .ui.utils import popup, PopupType +if TYPE_CHECKING: + import cemu.arch + + @unique class EmulatorState(IntEnum): INVALID = 0 @@ -59,6 +63,7 @@ def __getitem__(self, key: str) -> int: Returns: int: the register value """ + assert cemu.core.context emu = cemu.core.context.emulator if emu.state in (EmulatorState.RUNNING, EmulatorState.IDLE, EmulatorState.FINISHED) and key in self.data.keys(): val = emu.get_register_value(key) @@ -98,6 +103,7 @@ def reset(self): self.vm = None self.code = b"" self.sections = MEMORY_MAP_DEFAULT_LAYOUT[:] + assert cemu.core.context self.registers = EmulationRegisters({name: 0 for name in cemu.core.context.architecture.registers}) self.start_addr = 0 self.set(EmulatorState.NOT_RUNNING) @@ -116,6 +122,7 @@ def get_register_value(self, regname: str) -> Optional[int]: if not self.vm: return None + assert cemu.core.context arch = cemu.core.context.architecture ur = arch.uc_register(regname) val = self.vm.reg_read(ur) @@ -130,14 +137,18 @@ def pc(self) -> int: """ Returns the current value of $pc """ + assert cemu.core.context # return self.get_register_value(cemu.core.context.architecture.pc) + assert cemu.core.context return self.registers[cemu.core.context.architecture.pc] def sp(self) -> int: """ Returns the current value of $sp """ + assert cemu.core.context # return self.get_register_value(cemu.core.context.architecture.sp) + assert cemu.core.context return self.registers[cemu.core.context.architecture.sp] def setup(self) -> None: @@ -152,6 +163,7 @@ def setup(self) -> None: info("Setting up emulation environment...") + assert cemu.core.context arch = cemu.core.context.architecture self.vm = arch.uc self.vm.hook_add(unicorn.UC_HOOK_BLOCK, self.hook_block) @@ -159,6 +171,7 @@ def setup(self) -> None: self.vm.hook_add(unicorn.UC_HOOK_INTR, self.hook_interrupt) # type: ignore self.vm.hook_add(unicorn.UC_HOOK_MEM_WRITE, self.hook_mem_access) self.vm.hook_add(unicorn.UC_HOOK_MEM_READ, self.hook_mem_access) + assert cemu.core.context if is_x86(cemu.core.context.architecture): self.vm.hook_add( unicorn.UC_HOOK_INSN, @@ -217,6 +230,7 @@ def __populate_vm_registers(self) -> bool: if not self.vm: return False + assert cemu.core.context arch = cemu.core.context.architecture # @@ -296,6 +310,7 @@ def __refresh_registers_from_vm(self) -> bool: if not self.vm or not self.is_running: return False + assert cemu.core.context arch = cemu.core.context.architecture for regname in self.registers.keys(): value = self.vm.reg_read(arch.uc_register(regname)) @@ -310,10 +325,11 @@ def __generate_text_bytecode(self) -> bool: Returns: bool True if all went well, False otherwise. """ + assert cemu.core.context dbg(f"[vm::setup] Generating assembly code for {cemu.core.context.architecture.name}") try: - insns = cemu.utils.assemble(self.codelines, base_address=self.start_addr) + insns = cemu.arch.assemble(self.codelines, base_address=self.start_addr) if len(insns) == 0: raise Exception("no instruction") except Exception as e: @@ -356,11 +372,11 @@ def __populate_text_section(self) -> bool: self.vm.mem_write(text_section.address, self.code) return True - def next_instruction(self, code: bytes, addr: int) -> Optional[cemu.utils.Instruction]: + def next_instruction(self, code: bytes, addr: int) -> Optional[cemu.arch.Instruction]: """ Returns a string disassembly of the first instruction from `code`. """ - for insn in cemu.utils.disassemble(code, 1, addr): + for insn in cemu.arch.disassemble(code, 1, addr): return insn return None @@ -376,7 +392,8 @@ def hook_code(self, emu: unicorn.Uc, address: int, size: int, user_data: Any) -> return False code = self.vm.mem_read(address, size) - insn: cemu.utils.Instruction = self.next_instruction(code, address) + insn = self.next_instruction(code, address) + assert isinstance(insn, cemu.arch.Instruction) if self.use_step_mode: dbg(f"[vm::runtime] Stepping @ {insn}") diff --git a/src/cemu/examples/aarch64_execve_bin_sh.asm b/src/cemu/examples/aarch64_sys_exec_bin_sh.asm similarity index 100% rename from src/cemu/examples/aarch64_execve_bin_sh.asm rename to src/cemu/examples/aarch64_sys_exec_bin_sh.asm diff --git a/src/cemu/examples/arm_sys_exec_bin_sh.asm b/src/cemu/examples/arm_sys_exec_bin_sh.asm index 3aedc76..4eda262 100644 --- a/src/cemu/examples/arm_sys_exec_bin_sh.asm +++ b/src/cemu/examples/arm_sys_exec_bin_sh.asm @@ -1,11 +1,11 @@ -# -# ARM sys_exec("/bin/sh") shellcode -# -# @_hugsy_ -# -ldr r0, ="nib/" +;;; +;;; ARM sys_exec("/bin/sh") shellcode +;;; +;;; @_hugsy_ +;;; +ldr r0, =0x2f62696e str r0, [sp] -ldr r0, ="hs//" +ldr r0, =0x2f2f7368 str r0, [sp, 4] mov r0, sp mov r1, 0 diff --git a/src/cemu/examples/mips_sys_exec_bin_sh.asm b/src/cemu/examples/mips_sys_exec_bin_sh.asm index 13e7c1d..338dc33 100644 --- a/src/cemu/examples/mips_sys_exec_bin_sh.asm +++ b/src/cemu/examples/mips_sys_exec_bin_sh.asm @@ -3,9 +3,9 @@ ;;; ;;; @_hugsy_ ;;; -li $v0, "nib/" +li $v0, 0x2f62696e sw $v0, 0($sp) -li $v0, "hs//" +li $v0, 0x2f2f7368 sw $v0, 4($sp) li $v0, __NR_SYS_execve move $a0, $sp diff --git a/src/cemu/examples/sparc64_sys_exec_bin_sh.asm b/src/cemu/examples/sparc64_sys_exec_bin_sh.asm index 3424e54..bd82cf2 100644 --- a/src/cemu/examples/sparc64_sys_exec_bin_sh.asm +++ b/src/cemu/examples/sparc64_sys_exec_bin_sh.asm @@ -2,12 +2,12 @@ ;;; ;;; @_hugsy_ ;;; - mov 0x6e69622f, %g1 - st %g1, [ %sp ] - mov 0x0068732f, %g1 - st %g1, [ %sp + 4 ] - mov %g1, %o0 - clr %o1 - clr %o2 - mov 11, %g1 - t 0x6d +mov 0x6e69622f, %g1 +st %g1, [ %sp ] +mov 0x0068732f, %g1 +st %g1, [ %sp + 4 ] +mov %g1, %o0 +clr %o1 +clr %o2 +mov 11, %g1 +t 0x6d diff --git a/src/cemu/examples/x86_32_sys_exec_bin_sh.asm b/src/cemu/examples/x86_32_sys_exec_bin_sh.asm index f02415c..079264a 100644 --- a/src/cemu/examples/x86_32_sys_exec_bin_sh.asm +++ b/src/cemu/examples/x86_32_sys_exec_bin_sh.asm @@ -9,8 +9,8 @@ ;;; eax = sys_execve mov eax, __NR_SYS_execve ;;; write /bin/sh @esp -mov dword ptr [esp], "nib/" -mov dword ptr [esp+4], "hs//" +mov dword ptr [esp], 0x2f62696e +mov dword ptr [esp+4], 0x2f2f7368 ;;; ebx = @/bin/sh mov ebx, esp ;;; nullify the other args diff --git a/src/cemu/examples/x86_64_sys_exec_bin_sh.asm b/src/cemu/examples/x86_64_sys_exec_bin_sh.asm index 76ac8e6..314f9e8 100644 --- a/src/cemu/examples/x86_64_sys_exec_bin_sh.asm +++ b/src/cemu/examples/x86_64_sys_exec_bin_sh.asm @@ -10,7 +10,7 @@ ;;; rax = sys_execve mov rax, __NR_SYS_execve ;;; write /bin/sh @rsp -mov rsi, "hs//nib/" # or 0x68732f2f6e69622f +mov rsi, 0x68732f2f6e69622f mov [rsp], rsi ;;; rdi = @/bin/sh mov rdi, rsp @@ -18,4 +18,4 @@ mov rdi, rsp xor rsi, rsi xor rdx, rdx ;;; trigger syscall -syscall \ No newline at end of file +syscall diff --git a/src/cemu/exports.py b/src/cemu/exports.py index f820156..079095b 100644 --- a/src/cemu/exports.py +++ b/src/cemu/exports.py @@ -6,7 +6,7 @@ from .arch import Architecture, is_x86_32, is_x86_64 from .memory import MemoryPermission, MemorySection -from .utils import generate_random_string +import cemu.utils def parse_as_lief_pe_permission(perm: MemoryPermission, extra: Any = None) -> int: @@ -42,10 +42,10 @@ def build_pe_executable(text: bytes, memory_layout: List[MemorySection], arch: A is_x64 = is_x86_64(arch) if is_x64: - basename = "cemu-pe-amd64-{:s}".format(generate_random_string(5)) + basename = "cemu-pe-amd64-{:s}".format(cemu.utils.generate_random_string(5)) pe = PE.Binary(basename, PE.PE_TYPE.PE32_PLUS) else: - basename = "cemu-pe-i386-{:s}".format(generate_random_string(5)) + basename = "cemu-pe-i386-{:s}".format(cemu.utils.generate_random_string(5)) pe = PE.Binary(basename, PE.PE_TYPE.PE32) # adding sections diff --git a/src/cemu/memory.py b/src/cemu/memory.py index de04f6c..1d9ab3d 100644 --- a/src/cemu/memory.py +++ b/src/cemu/memory.py @@ -215,7 +215,7 @@ def content(self) -> Optional[bytes]: if not self.file_source: return None - data = self.file_source.open("rb").read() + data = self.file_source.read_bytes() if len(data) > self.size: raise AttributeError("Insufficient space") return data diff --git a/src/cemu/plugins/__init__.py b/src/cemu/plugins/__init__.py index 03c95fb..671ab79 100644 --- a/src/cemu/plugins/__init__.py +++ b/src/cemu/plugins/__init__.py @@ -26,7 +26,9 @@ def __str__(self): @property def rootWindow(self) -> CEmuWindow: + assert cemu.core.context assert isinstance(cemu.core.context, cemu.core.GlobalGuiContext) + assert cemu.core.context return cemu.core.context.root diff --git a/src/cemu/plugins/pyconsole/console.py b/src/cemu/plugins/pyconsole/console.py index 08e86ee..50ecaf6 100644 --- a/src/cemu/plugins/pyconsole/console.py +++ b/src/cemu/plugins/pyconsole/console.py @@ -202,6 +202,7 @@ def keyPressEvent(self, event): @property def emu(self) -> cemu.emulator.Emulator: + assert cemu.core.context return cemu.core.context.emulator @property @@ -210,4 +211,5 @@ def vm(self): @property def arch(self): + assert cemu.core.context return cemu.core.context.architecture diff --git a/src/cemu/settings.py b/src/cemu/settings.py index 931f77d..6270609 100644 --- a/src/cemu/settings.py +++ b/src/cemu/settings.py @@ -76,8 +76,7 @@ def __create_default_config_file(self) -> None: """ Deploy a new config file as ~/.cemu.ini """ - with self.__config_filename.open("w") as cfg: - cfg.write(cemu.const.TEMPLATE_CONFIG.open().read()) + self.__config_filename.write_text(cemu.const.TEMPLATE_CONFIG.read_text()) return def __contains__(self, key: str) -> bool: diff --git a/src/cemu/shortcuts.py b/src/cemu/shortcuts.py index e607fa6..a4ffd56 100644 --- a/src/cemu/shortcuts.py +++ b/src/cemu/shortcuts.py @@ -44,6 +44,7 @@ def load(self) -> bool: Load the shortcuts dict from either the config file if the value exists, or the defaults """ + assert cemu.core.context settings = cemu.core.context.settings for key in self._defaults: default_shortcut, description = self._defaults[key] diff --git a/src/cemu/ui/codeeditor.py b/src/cemu/ui/codeeditor.py index 8ce7904..078eba8 100644 --- a/src/cemu/ui/codeeditor.py +++ b/src/cemu/ui/codeeditor.py @@ -29,7 +29,7 @@ if typing.TYPE_CHECKING: from cemu.ui.main import CEmuWindow -from ..utils import assemble +from ..arch import assemble from .highlighter import Highlighter from .utils import get_cursor_position @@ -160,6 +160,7 @@ def __init__(self, parent, *args, **kwargs): self.setWidget(widget) def onUpdateText(self): + assert cemu.core.context cemu.core.context.emulator.codelines = self.getCleanContent() def onCursorPositionChanged(self): @@ -189,6 +190,7 @@ def remove_comments(lines: list[str]) -> list[str]: def parse_syscalls(lines: list[str]) -> list[str]: parsed = [] + assert cemu.core.context syscalls = cemu.core.context.architecture.syscalls syscall_names = syscalls.keys() for line in lines: diff --git a/src/cemu/ui/command.py b/src/cemu/ui/command.py index 8df42e4..15a8a8a 100644 --- a/src/cemu/ui/command.py +++ b/src/cemu/ui/command.py @@ -6,7 +6,7 @@ from PyQt6.QtWidgets import QDockWidget, QHBoxLayout, QPushButton, QWidget import cemu.core -from cemu import utils +import cemu.arch from cemu.emulator import Emulator, EmulatorState from cemu.log import dbg, info from cemu.ui.utils import PopupType, popup @@ -58,6 +58,7 @@ def __init__(self, parent: CEmuWindow, *args, **kwargs): # # Emulator state callback # + assert cemu.core.context self.emulator: Emulator = cemu.core.context.emulator self.emulator.add_state_change_cb(EmulatorState.NOT_RUNNING, self.onNotRunningUpdateCommandButtons) @@ -113,7 +114,7 @@ def onClickCheckCode(self) -> None: if not code: raise ValueError("Empty code") - insns = utils.assemble(code) + insns = cemu.arch.assemble(code) title = "Success" msg = f"Your code is syntaxically valid, {len(insns)} instructions compiled" popup_style = PopupType.Information diff --git a/src/cemu/ui/highlighter.py b/src/cemu/ui/highlighter.py index 7aeef3e..4cce9de 100644 --- a/src/cemu/ui/highlighter.py +++ b/src/cemu/ui/highlighter.py @@ -68,7 +68,11 @@ def __init__(self, parent, mode): def highlightBlock(self, text): cb = self.currentBlock() pos = cb.position() - text = self.document().toPlainText() + "\n" + doc = self.document() + if not doc: + cemu.log.dbg("Missing doc") + return + text = doc.toPlainText() + "\n" highlight(text, self.lexer, self.formatter) for i in range(len(text)): try: diff --git a/src/cemu/ui/main.py b/src/cemu/ui/main.py index 9566a80..5f19341 100644 --- a/src/cemu/ui/main.py +++ b/src/cemu/ui/main.py @@ -20,6 +20,7 @@ ) import cemu.core +import cemu.arch import cemu.exports import cemu.plugins import cemu.utils @@ -53,7 +54,9 @@ class CEmuWindow(QMainWindow): def __init__(self, app: QApplication, *args, **kwargs): super(CEmuWindow, self).__init__() self.currentAction: Optional[QAction] = None + assert cemu.core.context assert cemu.core.context is not None + assert cemu.core.context assert isinstance(cemu.core.context, cemu.core.GlobalGuiContext) self.rootWindow: CEmuWindow = self @@ -64,6 +67,7 @@ def __init__(self, app: QApplication, *args, **kwargs): # self.signals = {} Unused? self.current_file: Optional[pathlib.Path] = None self.__background_emulator_thread: EmulationRunner = EmulationRunner() + assert cemu.core.context cemu.core.context.emulator.set_threaded_runner(self.__background_emulator_thread) self.shortcuts: ShortcutManager = ShortcutManager() @@ -100,6 +104,7 @@ def __init__(self, app: QApplication, *args, **kwargs): self.__app.aboutToQuit.connect(self.onAboutToQuit) # register the callbacks for the emulator + assert cemu.core.context emu = cemu.core.context.emulator emu.add_state_change_cb(EmulatorState.NOT_RUNNING, self.update_layout_not_running) emu.add_state_change_cb(EmulatorState.RUNNING, self.update_layout_running) @@ -107,6 +112,7 @@ def __init__(self, app: QApplication, *args, **kwargs): emu.add_state_change_cb(EmulatorState.FINISHED, self.update_layout_step_finished) # show everything + assert cemu.core.context start_in_full_screen = cemu.core.context.settings.getboolean("Global", "StartInFullScreen") if start_in_full_screen: self.showMaximized() @@ -129,6 +135,7 @@ def __del__(self): def changeEvent(self, event): if event.type() == QEvent.Type.WindowStateChange: + assert cemu.core.context cemu.core.context.settings.set("Global", "StartInFullScreen", str(self.isMaximized())) super().changeEvent(event) @@ -136,7 +143,9 @@ def onAboutToQuit(self): """ Overriding the aboutToSignal handler """ + assert cemu.core.context if cemu.core.context.settings.getboolean("Global", "SaveConfigOnExit"): + assert cemu.core.context cemu.core.context.settings.save() ok("Settings saved...") return @@ -161,18 +170,23 @@ def LoadExtraPlugins(self) -> int: return nb_added def setMainWindowProperty(self) -> None: + assert cemu.core.context width = cemu.core.context.settings.getint("Global", "WindowWidth", 800) + assert cemu.core.context heigth = cemu.core.context.settings.getint("Global", "WindowHeight", 600) self.resize(width, heigth) self.refreshWindowTitle() # center the window frame_geometry = self.frameGeometry() - screen = self.screen().availableGeometry().center() - frame_geometry.moveCenter(screen) + screen = self.screen() + if screen: + p = screen.availableGeometry().center() + frame_geometry.moveCenter(p) self.move(frame_geometry.topLeft()) # apply the style + assert cemu.core.context style = cemu.core.context.settings.get("Theme", "QtStyle", "Cleanlooks") self.__app.setStyle(style) return @@ -205,10 +219,16 @@ def addMenuItem( def setMainWindowMenuBar(self): self.statusBar() menubar = self.menuBar() + if not menubar: + popup("No menubar found") + return + + assert cemu.core.context maxRecentFiles = cemu.core.context.settings.getint("Global", "MaxRecentFiles") # Create "File" menu options fileMenu = menubar.addMenu("&File") + assert fileMenu # "Open File" submenu openAsmAction = self.addMenuItem( @@ -327,11 +347,14 @@ def setMainWindowMenuBar(self): # Add Architecture menu bar archMenu = menubar.addMenu("&Architecture") + assert archMenu for abi in sorted(Architectures.keys()): archSubMenu = archMenu.addMenu(abi.upper()) + assert archSubMenu for arch in Architectures[abi]: label = f"{arch.name:s} / Endian: {str(arch.endianness)} / Syntax: {str(arch.syntax)}" self.archActions[label] = QAction(QIcon(), label, self) + assert cemu.core.context if arch == cemu.core.context.architecture: self.archActions[label].setEnabled(False) self.currentAction = self.archActions[label] @@ -342,6 +365,7 @@ def setMainWindowMenuBar(self): # Add the View Window menu bar viewWindowsMenu = menubar.addMenu("&View") + assert viewWindowsMenu toggleFocusMode = self.addMenuItem( "Toggle Focus Mode", self.toggleFocusMode, @@ -367,6 +391,7 @@ def setMainWindowMenuBar(self): # Add Help menu bar helpMenu = menubar.addMenu("&Help") + assert helpMenu shortcutAction = self.addMenuItem( "Shortcuts", self.showShortcutPopup, @@ -434,7 +459,7 @@ def loadFile(self, fpath: pathlib.Path) -> None: KeyError: if the architecture from the file metadata is invalid """ dbg(f"Trying to load '{fpath}'") - content = fpath.open().read() + content = fpath.read_text() try: res = cemu.utils.get_metadata_from_stream(content) @@ -474,7 +499,7 @@ def loadCode(self, title, filter, run_disassembler): if run_disassembler: with tempfile.NamedTemporaryFile("w", suffix=".asm", delete=False) as fd: - disassembled_instructions = cemu.utils.disassemble_file(fpath) + disassembled_instructions = cemu.arch.disassemble_file(fpath) fd.write(os.linesep.join([f"{insn.mnemonic}, {insn.operands}" for insn in disassembled_instructions])) fpath = pathlib.Path(fd.name) @@ -507,12 +532,12 @@ def saveCode(self, title, filter, run_assembler): if run_assembler: raw_assembly = self.get_codeview_content() - insns: list[cemu.utils.Instruction] = cemu.utils.assemble(raw_assembly) + insns: list["cemu.arch.Instruction"] = cemu.arch.assemble(raw_assembly) raw_bytecode = b"".join([insn.bytes for insn in insns]) - fpath.open("wb").write(raw_bytecode) + fpath.write_bytes(raw_bytecode) else: raw_bytecode = self.get_codeview_content() - fpath.open("w").write(raw_bytecode) + fpath.write_text(raw_bytecode) ok(f"Saved as '{fpath}'") return @@ -537,10 +562,10 @@ def saveCodeBin(self): return self.saveCode("Save Raw Binary Pane As", "Raw binary files (*.raw)", True) def saveAsCFile(self): - template = (TEMPLATE_PATH / "linux" / "template.c").open("r").read() + template = (TEMPLATE_PATH / "linux" / "template.c").read_text("r") output: list[str] = [] lines = self.get_codeview_content().splitlines() - insns = cemu.utils.assemble(self.get_codeview_content()) + insns = cemu.arch.assemble(self.get_codeview_content()) for i, insn in enumerate(insns): hexa = ", ".join([f"{b:#02x}" for b in insn.bytes]) line = f"/* {i:#08x} */ {hexa} // {lines[i]}" @@ -551,6 +576,7 @@ def saveAsCFile(self): return with picked_file_path.open("w") as fd: + assert cemu.core.context body = template % ( cemu.core.context.architecture.name, len(insns), @@ -562,17 +588,16 @@ def saveAsCFile(self): def saveAsAsmFile(self) -> None: """Write the content of the ASM pane to disk""" - template = (TEMPLATE_PATH / "linux" / "template.asm").open("r").read() + template = (TEMPLATE_PATH / "linux" / "template.asm").read_text() code = self.get_codeview_content() picked_file_path = self.pick_file("Save As Generated Assembly File", "Assembly files (*.asm *.s)") if picked_file_path is None: return - with picked_file_path.open("w") as fd: - body = template % (cemu.core.context.architecture.name, code) - fd.write(body) - ok(f"Saved as '{fd.name}'") + assert cemu.core.context + picked_file_path.write_text(template % (cemu.core.context.architecture.name, code)) + ok(f"Saved as '{picked_file_path}'") return def generate_pe(self) -> None: @@ -580,9 +605,10 @@ def generate_pe(self) -> None: memory_layout = self.get_memory_layout() code = self.get_codeview_content() try: - insns = cemu.utils.assemble(code) + insns = cemu.arch.assemble(code) if len(insns) > 0: asm_code = b"".join([x.bytes for x in insns]) + assert cemu.core.context pe = cemu.exports.build_pe_executable(asm_code, memory_layout, cemu.core.context.architecture) info(f"PE file written as '{pe}'") except Exception as e: @@ -594,9 +620,10 @@ def generate_elf(self) -> None: memory_layout = self.get_memory_layout() code = self.get_codeview_content() try: - insns = cemu.utils.assemble(code) + insns = cemu.arch.assemble(code) if len(insns) > 0: asm_code = b"".join([x.bytes for x in insns]) + assert cemu.core.context elf = cemu.exports.build_pe_executable(asm_code, memory_layout, cemu.core.context.architecture) info(f"ELF file written as '{elf}'") except Exception as e: @@ -610,9 +637,14 @@ def onUpdateArchitecture(self, arch: Architecture, endian: Optional[Endianness] arch (Architecture): the newly selected architecture """ label = f"{arch.name:s} / Endian: {str(arch.endianness)} / Syntax: {str(arch.syntax)}" + if self.currentAction is None: + dbg("No current action defined") + return self.currentAction.setEnabled(True) + assert cemu.core.context cemu.core.context.architecture = arch if endian: + assert cemu.core.context cemu.core.context.architecture.endianness = endian info(f"Switching to '{label}'") self.__regsWidget.updateGrid() @@ -623,6 +655,7 @@ def onUpdateArchitecture(self, arch: Architecture, endian: Optional[Endianness] def refreshWindowTitle(self) -> None: """Refresh the main window title bar""" + assert cemu.core.context title = f"{TITLE} ({cemu.core.context.architecture})" if self.current_file: title += f": {self.current_file.name}" @@ -651,12 +684,14 @@ def showShortcutPopup(self): wid.setMinimumWidth(800) wid.setLayout(grid) - msgbox.layout().addWidget(wid) + msgbox_layout = msgbox.layout() + if msgbox_layout: + msgbox_layout.addWidget(wid) msgbox.exec() return def about_popup(self): - templ = (TEMPLATE_PATH / "about.html").open().read() + templ = (TEMPLATE_PATH / "about.html").read_text() desc = templ.format(author=AUTHOR, version=VERSION, project_link=URL, issues_link=ISSUE_LINK) msgbox = QMessageBox(self) msgbox.setIcon(QMessageBox.Icon.Information) @@ -675,6 +710,7 @@ def updateRecentFileActions(self, insert_file=None): settings.setValue("recentFileList", []) files = settings.value("recentFileList") + assert cemu.core.context maxRecentFiles = cemu.core.context.settings.getint("Default", "MaxRecentFiles") if insert_file: @@ -722,23 +758,28 @@ def get_memory_layout(self) -> list[MemorySection]: """ Returns the memory layout as defined by the __mapWidget values as a structured list. """ + assert cemu.core.context return cemu.core.context.emulator.sections def update_layout_not_running(self): - self.statusBar().showMessage("Not running") - return + statusBar = self.statusBar() + assert statusBar + statusBar.showMessage("Not running") def update_layout_running(self): - self.statusBar().showMessage("Running") - return + statusBar = self.statusBar() + assert statusBar + statusBar.showMessage("Running") def update_layout_step_running(self): - self.statusBar().showMessage("Idle (Step Mode)") - return + statusBar = self.statusBar() + assert statusBar + statusBar.showMessage("Idle (Step Mode)") def update_layout_step_finished(self): - self.statusBar().showMessage("Finished") - return + statusBar = self.statusBar() + assert statusBar + statusBar.showMessage("Finished") class EmulationRunner: @@ -748,6 +789,7 @@ def run(self): """ Runs the emulation """ + assert cemu.core.context emu = cemu.core.context.emulator if not emu.vm: error("VM is not ready") diff --git a/src/cemu/ui/mapping.py b/src/cemu/ui/mapping.py index 2057cf1..649a0fd 100644 --- a/src/cemu/ui/mapping.py +++ b/src/cemu/ui/mapping.py @@ -19,7 +19,7 @@ from cemu.emulator import Emulator, EmulatorState from cemu.log import error from cemu.memory import MemorySection -from cemu.utils import format_address +from cemu.arch import format_address from .utils import popup @@ -42,7 +42,9 @@ def __init__(self, parent: "CEmuWindow"): self.MemoryMapTableWidget.setColumnWidth(3, 120) self.MemoryMapTableWidget.setSelectionBehavior(QTableWidget.SelectionBehavior.SelectRows) self.MemoryMapTableWidget.setHorizontalHeaderLabels(["Start", "End", "Name", "Permission"]) - self.MemoryMapTableWidget.verticalHeader().setVisible(False) + vHeader = self.MemoryMapTableWidget.verticalHeader() + if vHeader: + vHeader.setVisible(False) layout.addWidget(self.MemoryMapTableWidget) # add/remove buttons @@ -64,6 +66,7 @@ def __init__(self, parent: "CEmuWindow"): # # Emulator state callback # + assert cemu.core.context self.emu: Emulator = cemu.core.context.emulator self.emu.add_state_change_cb(EmulatorState.NOT_RUNNING, self.onNotRunningUpdateMemoryMap) self.emu.add_state_change_cb(EmulatorState.RUNNING, self.onRunningDisableMemoryMapGrid) @@ -111,13 +114,14 @@ def onDeleteSectionButtonClicked(self) -> None: Callback associated with the click of the "Remove Section" button """ selection = self.MemoryMapTableWidget.selectionModel() - if not selection.hasSelection(): + if not selection or not selection.hasSelection(): return indexes = [x.row() for x in selection.selectedRows()] for idx in range(len(self.emu.sections) - 1, 0, -1): if idx in indexes: + assert cemu.core.context del cemu.core.context.emulator.sections[idx] self.redraw_memory_map_table() return @@ -169,6 +173,7 @@ def add_or_edit_section_popup(self) -> None: msgbox.setWindowTitle("Add section") layout = msgbox.layout() + assert layout wid.setLayout(grid) wid.setMinimumWidth(400) layout.addWidget(wid) @@ -181,10 +186,12 @@ def add_or_edit_section_popup(self) -> None: address = int(startAddressEdit.text(), 0) size = int(sizeEdit.text(), 0) + assert cemu.core.context if name in (x.name for x in cemu.core.context.emulator.sections): error("section name already exists") return + assert cemu.core.context memory_set = (set(range(x.address, x.address + x.size)) for x in cemu.core.context.emulator.sections) current_set = set(range(address, address + size)) for m in memory_set: @@ -201,6 +208,7 @@ def add_or_edit_section_popup(self) -> None: section_perm.append("EXEC") try: section = MemorySection(name, address, size, "|".join(section_perm)) + assert cemu.core.context cemu.core.context.emulator.sections.append(section) self.redraw_memory_map_table() except ValueError as ve: @@ -211,6 +219,7 @@ def add_or_edit_section_popup(self) -> None: section_perm.append("EXEC") try: section = MemorySection(name, address, size, "|".join(section_perm)) + assert cemu.core.context cemu.core.context.emulator.sections.append(section) self.redraw_memory_map_table() except ValueError as ve: diff --git a/src/cemu/ui/memory.py b/src/cemu/ui/memory.py index 4985f9d..31a9758 100644 --- a/src/cemu/ui/memory.py +++ b/src/cemu/ui/memory.py @@ -59,6 +59,7 @@ def __init__(self, parent: CEmuWindow, *args, **kwargs): # # Emulator state callback # + assert cemu.core.context emu: Emulator = cemu.core.context.emulator emu.add_state_change_cb(EmulatorState.IDLE, self.onIdleRefreshMemoryEditor) emu.add_state_change_cb(EmulatorState.FINISHED, self.onFinishedClearMemoryEditor) @@ -66,7 +67,9 @@ def __init__(self, parent: CEmuWindow, *args, **kwargs): return def updateEditor(self) -> None: + assert cemu.core.context arch = cemu.core.context.architecture + assert cemu.core.context emu = cemu.core.context.emulator if not emu.vm: self.editor.setText("VM not initialized") diff --git a/src/cemu/ui/registers.py b/src/cemu/ui/registers.py index a95765f..3449721 100644 --- a/src/cemu/ui/registers.py +++ b/src/cemu/ui/registers.py @@ -17,7 +17,8 @@ DEFAULT_REGISTER_VIEW_REGISTER_FONT_SIZE, ) from cemu.emulator import Emulator, EmulatorState -from cemu.utils import format_address +from cemu.arch import format_address +import cemu.emulator class RegistersWidget(QDockWidget): @@ -28,9 +29,13 @@ def __init__(self, parent, *args, **kwargs): self.__old_register_values = {} layout = QVBoxLayout() self.RegisterTableWidget = QTableWidget(10, 2) - self.RegisterTableWidget.horizontalHeader().setStretchLastSection(True) + hHeader = self.RegisterTableWidget.horizontalHeader() + if hHeader: + hHeader.setStretchLastSection(True) self.RegisterTableWidget.setHorizontalHeaderLabels(["Register", "Value"]) - self.RegisterTableWidget.verticalHeader().setVisible(False) + vHeader = self.RegisterTableWidget.verticalHeader() + if vHeader: + vHeader.setVisible(False) self.RegisterTableWidget.setColumnWidth(0, 80) layout.addWidget(self.RegisterTableWidget) @@ -45,6 +50,7 @@ def __init__(self, parent, *args, **kwargs): # # Emulator state callback # + assert cemu.core.context emu: Emulator = cemu.core.context.emulator emu.add_state_change_cb(EmulatorState.IDLE, self.onIdleRefreshRegisterGrid) emu.add_state_change_cb(EmulatorState.FINISHED, self.onFinishedRefreshRegisterGrid) @@ -56,7 +62,9 @@ def updateGrid(self) -> None: VM CPU registers """ + assert cemu.core.context emu: Emulator = cemu.core.context.emulator + assert cemu.core.context arch = cemu.core.context.architecture registers = arch.registers self.RegisterTableWidget.setRowCount(len(registers)) @@ -93,16 +101,21 @@ def updateGrid(self) -> None: # # Propagate the change to the emulator # - cemu.core.context.emulator.registers = self.getRegisterValuesFromGrid() + assert cemu.core.context + cemu.core.context.emulator.registers = cemu.emulator.EmulationRegisters(self.getRegisterValuesFromGrid()) return def getRegisterValuesFromGrid(self) -> dict[str, int]: """Returns the current values of the registers, as shown by the widget grid""" regs = {} + assert cemu.core.context registers = cemu.core.context.emulator.registers.keys() for i in range(len(registers)): - name = self.RegisterTableWidget.item(i, 0).text() - value = self.RegisterTableWidget.item(i, 1).text() + item1 = self.RegisterTableWidget.item(i, 0) + name = item1.text() if item1 else "" + + item2 = self.RegisterTableWidget.item(i, 1) + value = item2.text() if item2 else "0" regs[name] = int(value, 16) return regs diff --git a/src/cemu/utils.py b/src/cemu/utils.py index 7c00477..8975823 100644 --- a/src/cemu/utils.py +++ b/src/cemu/utils.py @@ -1,15 +1,12 @@ import os -import pathlib + import random import string -from dataclasses import dataclass from typing import Optional, TYPE_CHECKING if TYPE_CHECKING: import cemu.arch -import cemu.core -import cemu.errors -import cemu.utils + from cemu.const import COMMENT_MARKER, PROPERTY_MARKER from cemu.log import dbg @@ -35,6 +32,17 @@ def hexdump( Returns: str: _description_ """ + import cemu.arch + + if not isinstance(source, bytes): + raise ValueError("source must be of type `bytes`") + + if len(separator) != 1: + raise ValueError("separator must be a single character") + + if (alignment & 1) == 1: + raise ValueError("alignment must be a multiple of two") + result: list[str] = [] for i in range(0, len(source), alignment): chunk = source[i : i + alignment] @@ -44,125 +52,18 @@ def hexdump( if show_raw: result.append(hexa) else: - result.append(f"{format_address(base)} {hexa} {text}") + result.append(f"{cemu.arch.format_address(base+i)} {hexa} {text}") return os.linesep.join(result) -def format_address(addr: int, arch: Optional[cemu.arch.Architecture] = None) -> str: - """Format an address to string, aligned to the given architecture - - Args: - addr (int): _description_ - arch (Optional[Architecture], optional): _description_. Defaults to None. - - Raises: - ValueError: _description_ - - Returns: - str: _description_ - """ - if arch is None: - arch = cemu.core.context.architecture - - if arch.ptrsize == 2: - return f"{addr:#04x}" - elif arch.ptrsize == 4: - return f"{addr:#08x}" - elif arch.ptrsize == 8: - return f"{addr:#016x}" - else: - raise ValueError(f"Invalid value for '{arch.ptrsize=}'") - - -@dataclass -class Instruction: - address: int - mnemonic: str - operands: str - bytes: bytes - - @property - def size(self): - return len(self.bytes) - - @property - def end(self) -> int: - return self.address + self.size - - def __str__(self): - return f'Instruction({self.address:#x}, "{self.mnemonic} {self.operands}")' - - -def disassemble(raw_data: bytes, count: int = -1, base: int = DISASSEMBLY_DEFAULT_BASE_ADDRESS) -> list[Instruction]: - """Disassemble the code given as raw data, with the given architecture. - - Args: - raw_data (bytes): the raw byte code to disassemble - arch (Architecture): the architecture to use for disassembling - count (int, optional): the maximum number of instruction to disassemble. Defaults to -1. - base (int, optional): the disassembled code base address. Defaults to DISASSEMBLY_DEFAULT_BASE_ADDRESS - - Returns: - str: the text representation of the disassembled code - """ - arch = cemu.core.context.architecture - insns: list[Instruction] = [] - for idx, ins in enumerate(arch.cs.disasm(raw_data, base)): - insn = Instruction(ins.address, ins.mnemonic, ins.op_str, ins.bytes) - insns.append(insn) - if idx == count: - break - - dbg(f"{insns=}") - return insns - - -def disassemble_file(fpath: pathlib.Path) -> list[Instruction]: - with fpath.open("rb") as f: - return disassemble(f.read()) - - -def assemble(code: str, base_address: int = DISASSEMBLY_DEFAULT_BASE_ADDRESS) -> list[Instruction]: - """ - Helper function to assemble code receive in parameter `asm_code` using Keystone. - - @param code : assembly code in bytes (multiple instructions must be separated by ';') - @param base_address : (opt) the base address to use - - @return a list of Instruction - """ - arch = cemu.core.context.architecture - - # - # Compile the entire given code - # - bytecode, assembled_insn_count = arch.ks.asm(code, as_bytes=True, addr=base_address) - if not bytecode or assembled_insn_count == 0: - raise cemu.errors.AssemblyException("Not instruction compiled") - - assert isinstance(bytecode, bytes) - - # - # Decompile it and return the stuff - # - insns = disassemble(bytecode, base=base_address) - dbg(f"{insns=}") - return insns - - -def assemble_file(fpath: pathlib.Path) -> list[Instruction]: - with fpath.open("r") as f: - return assemble(f.read()) - - def ishex(x: str) -> bool: if x.lower().startswith("0x"): x = x[2:] return all([c in string.hexdigits for c in x]) -def generate_random_string(length: int) -> str: +def generate_random_string(length: int, charset: str = string.ascii_letters + string.digits) -> str: """Returns a random string Args: @@ -171,13 +72,15 @@ def generate_random_string(length: int) -> str: Returns: str: _description_ """ - charset = string.ascii_letters + string.digits + if length < 1: + raise ValueError("invalid length") + return "".join(random.choice(charset) for _ in range(length)) def get_metadata_from_stream( content: str, -) -> Optional[tuple[cemu.arch.Architecture, cemu.arch.Endianness]]: +) -> Optional[tuple["cemu.arch.Architecture", "cemu.arch.Endianness"]]: """Parse a file content to automatically extract metadata. Metadata can only be passed in the file header, and *must* be a commented line (i.e. starting with `;;; `) followed by the property marker (i.e. `@@@`). Both the architecture and endianess *must* be provided @@ -190,23 +93,30 @@ def get_metadata_from_stream( content (str): _description_ Returns: - Optional[tuple[str, str]]: _description_ + Optional[tuple[Architecture, Endianness]]: _description_ Raises: KeyError: - if an architecture metadata is found, but invalid - if an endianess metadata is found, but invalid """ - arch: Optional[cemu.arch.Architecture] = None - endian: Optional[cemu.arch.Endianness] = None + import cemu.arch + + arch: Optional["cemu.arch.Architecture"] = None + endian: Optional["cemu.arch.Endianness"] = None for line in content.splitlines(): + # if already set, don't bother continuing + if arch and endian: + return (arch, endian) + + # validate the line format part = line.strip().split() - if len(part) < 4: - return None + if len(part) != 3: + continue - if (part[0] != COMMENT_MARKER) and (arch and endian): - return (arch, endian) + if part[0] != COMMENT_MARKER: + continue if not part[1].startswith(PROPERTY_MARKER): continue @@ -216,16 +126,17 @@ def get_metadata_from_stream( if metadata_type == "architecture" and not arch: arch = cemu.arch.Architectures.find(metadata_value) - dbg(f"Forcing architecture '{arch}'") + dbg(f"Setting architecture from metadata to '{arch}'") continue if metadata_type == "endianness" and not endian: - if metadata_value == "little": - endian = cemu.arch.Endianness.LITTLE_ENDIAN - elif metadata_value == "big": - endian = cemu.arch.Endianness.BIG_ENDIAN - else: - continue - dbg(f"Forcing endianness '{endian}'") + match metadata_value: + case "little": + endian = cemu.arch.Endianness.LITTLE_ENDIAN + case "big": + endian = cemu.arch.Endianness.BIG_ENDIAN + case _: + raise ValueError + dbg(f"Setting endianness from metadata to '{endian}'") return None diff --git a/tests/test_arch.py b/tests/test_arch.py new file mode 100644 index 0000000..4ac5090 --- /dev/null +++ b/tests/test_arch.py @@ -0,0 +1,84 @@ +import pathlib +import cemu +import cemu.arch +import cemu.core + +assert cemu.__package__ +assert len(cemu.__package__) >= 1 + +CURRENT_FILE = pathlib.Path(__file__) +CURRENT_DIR = CURRENT_FILE.parent +CEMU_PACKAGE_ROOT = CURRENT_DIR / "../src/cemu" +CEMU_EXAMPLES_FOLDER = CEMU_PACKAGE_ROOT / "examples" + + +def test_endianness_basic(): + assert int(cemu.arch.Endianness.LITTLE_ENDIAN) == 1 + assert int(cemu.arch.Endianness.BIG_ENDIAN) == 2 + assert str(cemu.arch.Endianness.BIG_ENDIAN) == "Big Endian" + assert str(cemu.arch.Endianness.LITTLE_ENDIAN) == "Little Endian" + + +def test_syntax_basic(): + assert int(cemu.arch.Syntax.INTEL) == 1 + assert str(cemu.arch.Syntax.INTEL) == "INTEL" + assert int(cemu.arch.Syntax.ATT) == 2 + assert str(cemu.arch.Syntax.ATT) == "ATT" + + +def test_architecture_manager(): + archs = cemu.arch.Architectures + assert isinstance(archs, dict) + + +def test_assemble_file(): + cemu.core.context = cemu.core.GlobalContext() + + def parse_syscalls(lines: list[str]) -> list[str]: + parsed = [] + assert cemu.core.context + syscalls = cemu.core.context.architecture.syscalls + syscall_names = syscalls.keys() + for line in lines: + for sysname in syscall_names: + pattern = f"__NR_SYS_{sysname}" + if pattern in line: + line = line.replace(pattern, str(syscalls[sysname])) + parsed.append(line) + return parsed + + # Values: + # ['generic', 'x86_32', 'x86_64', 'x86', 'arm', 'aarch64', 'mips', 'mips64', 'sparc', 'sparc64'] + + for tc in ( + "aarch64", + "arm", + "mips", + "sparc", + "x86_32", + "x86_64", + ): + cemu.core.context.architecture = cemu.arch.Architectures.find(tc) + fpath = CEMU_EXAMPLES_FOLDER / f"{tc}_sys_exec_bin_sh.asm" + code = ";".join(parse_syscalls([x.strip() for x in fpath.read_text().splitlines() if not x.strip().startswith(";;; ")])) + print(f"trying {tc} -> {cemu.core.context.architecture.syscalls}") + insns = cemu.arch.assemble(code) + assert len(insns) > 0 + + +def test_disassemble_file(): + cemu.core.context = cemu.core.GlobalContext() + + # Values: + # ['generic', 'x86_32', 'x86_64', 'x86', 'arm', 'aarch64', 'mips', 'mips64', 'sparc', 'sparc64'] + + for tc in ("arm", "sparc", "x86"): + cemu.core.context.architecture = cemu.arch.Architectures.find(tc) + insns = cemu.arch.disassemble_file(CEMU_EXAMPLES_FOLDER / f"{tc}_nops.raw") + assert len(insns) == 10 + + +def test_disassemble(): + cemu.core.context = cemu.core.GlobalContext() + insns = cemu.arch.disassemble(b"\xcc") + assert len(insns) == 1 diff --git a/tests/test_basic.py b/tests/test_basic.py index 0e541d7..32045cf 100644 --- a/tests/test_basic.py +++ b/tests/test_basic.py @@ -179,6 +179,7 @@ def test_matrix_execute_assembly_regs(self): # # (Re-)Initialize the context # + assert cemu.core.context cemu.core.context.architecture = tc.arch self.emu.reset() self.emu.sections = MEMORY_MAP_DEFAULT_LAYOUT[:] diff --git a/tests/test_utils.py b/tests/test_utils.py new file mode 100644 index 0000000..8a82df6 --- /dev/null +++ b/tests/test_utils.py @@ -0,0 +1,61 @@ +import string +import pytest + +import cemu.core +import cemu.utils +import cemu.arch + + +def test_get_metadata_from_stream(): + raw = r""" + ;;; @@@architecture x86_64 + ;;; @@@endianness little + """ + res = cemu.utils.get_metadata_from_stream(raw) + assert res and len(res) == 2 + assert isinstance(res[0], cemu.arch.Architecture) + assert cemu.arch.is_x86_64(res[0]) + assert isinstance(res[1], cemu.arch.Endianness) + assert res[1] == cemu.arch.Endianness.LITTLE_ENDIAN + + +def test_generate_random_string(): + assert len(cemu.utils.generate_random_string(5)) == 5 + assert isinstance(cemu.utils.generate_random_string(5), str) + with pytest.raises(ValueError): + cemu.utils.generate_random_string(-5) + + res = cemu.utils.generate_random_string(500, charset=string.ascii_letters) + assert all(filter(lambda x: x in string.ascii_letters, res)) + + +def test_ishex(): + assert not cemu.utils.ishex("0Xasd") + assert cemu.utils.ishex("0123") + assert not cemu.utils.ishex("0123asd") + assert not cemu.utils.ishex("0x!!0123asd") + assert not cemu.utils.ishex("0123fff==") + assert cemu.utils.ishex("0123abcdef") + + +def test_hexdump(): + cemu.core.context = cemu.core.GlobalContext() + cemu.core.context.architecture = cemu.arch.Architectures.find("x86_32") + assert cemu.utils.hexdump(b"aaaa") == "0x000000 61 61 61 61 aaaa" + cemu.core.context.architecture = cemu.arch.Architectures.find("x86_64") + assert cemu.utils.hexdump(b"aaaa") == "0x00000000000000 61 61 61 61 aaaa" + + with pytest.raises(ValueError): + cemu.utils.hexdump(b"aaaa", separator="") + + assert cemu.utils.hexdump(b"\x41\x41\xff\xfe") == "0x00000000000000 41 41 FF FE AA.." + + with pytest.raises(ValueError): + cemu.utils.hexdump(b"A" * 0x20, alignment=3) + + assert cemu.utils.hexdump(b"\x41\x41\xff\xfe", base=0x41414141_41414141) == "0x4141414141414141 41 41 FF FE AA.." + res = cemu.utils.hexdump(b"A" * 0x20, base=0x41414141_41414141).splitlines() + + assert len(res) == 2 + assert res[0] == "0x4141414141414141 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 AAAAAAAAAAAAAAAA" + assert res[1] == "0x4141414141414151 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 AAAAAAAAAAAAAAAA"