Skip to content

About

QtSPIM inspired GUI for unicorn

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

Unicorn UI

An educational desktop CPU debugger inspired by QtSPIM, written in Python and PyQt6. Unicorn executes instructions, Capstone disassembles them, and pyelftools reads ELF executables; pefile reads PE and macholib supplies Mach-O structures. The bundled BSD SPIM r764 assembler supplies full SPIM syntax; the original Keystone frontend remains available as a subset backend. Guest CPU execution always uses Unicorn.

Current features

  • MIPS32 in little- and big-endian configurations; x86 32-bit/x86-64; ARM32 A32/Thumb/Thumb-2 and ARM64 A64; PowerPC32/64 in both byte orders; Motorola 68000/020/030/040/060 (FPU on 020/030/040).
  • Run, Pause, backend Step, Reset, and Run to Cursor in a dedicated Qt worker.
  • Address breakpoints: toggle, enable/disable, clear, continue and repeated hits.
  • Register/disassembly tables with PC, changed values, symbols and source lines.
  • Assembly editor with line numbers, syntax highlighting, breakpoint gutter, execution/error highlighting, Open, Save and Save As.
  • Static MIPS32/x86/ARM32/PowerPC32/m68k ELF32 and x86-64/ARM64/PowerPC64 ELF64 loading with PT_LOAD permissions, zero-filled BSS, entry points, symbols and GP initialization from _gp or PT_MIPS_REGINFO.
  • x86/ARM and legacy PowerPC PE32/PE32+ loading with headers, section permissions, zero-filled tails, exports and optional HIGHLOW/DIR64 and ARM MOV32 base relocations.
  • Thin/universal ARM/PowerPC Mach-O loading with segment permissions, BSS and symbols.
  • Reset restores registers, mapped bytes and map topology without disk reads.
  • SPIM console syscalls, interactive input, float/double I/O, heap allocation and exit.
  • File services 13–16 with binary reads/writes, guest descriptors and a program directory.
  • Syscalls in branch delay slots, including waiting for input and branch-likely annulment.
  • Memory/stack windows with address navigation, SP following, bytes, words and ASCII.
  • GUI register and memory editing, including live disassembly after code edits.
  • FPU raw bits, IEEE singles/doubles, FR=0 pairs/FR=1 registers and FCSR inspection/editing.
  • Standalone delay-slot breakpoints, saved branch continuation and likely annulment.
  • SPIM terminal MMIO with polled I/O and interrupts, CP0 Count/Compare timer, guest exception handlers, mfc0/mtc0 and eret/rfe.
  • Optional bundled SPIM startup/standard exception handler.
  • Dockable panels with saved layouts.

Failed assembly or executable loading preserves the previously loaded machine. Editing source clears stale source markers until it has been assembled again.

Install and run

Requires Python 3.12+ and a graphical desktop. Targets macOS (Apple Silicon/Intel), Ubuntu 26.04 Desktop and Windows 10/11 x64. See platform setup and verification for exact dependencies, Windows build instructions, CI coverage and remaining native desktop checks. Use Python 3.12 when native wheels for a newer Python are unavailable.

python3.12 -m venv .venv
source .venv/bin/activate  # Windows: .venv\Scripts\activate
python -m pip install -e '.[dev]'
python scripts/build_spim_assembler.py  # requires C++, flex and bison
unicorn-ui

Alternatively, use uv venv --python 3.12 and uv pip install -e '.[dev]'. PyQt6 distribution is subject to its GPL/commercial licensing terms.

SPIM assembler build

Run python scripts/build_spim_assembler.py once after installing Python dependencies. It builds little- and big-endian assembly adapters from the bundled SPIM sources. A C++ compiler, flex and bison are required; macOS Command Line Tools supply them. For an installed package, unicorn-ui-build-spim runs the same build. The adapters use a portable assembly-only frontend. POSIX console code, timers and the original SPIM interpreter are not linked. Windows uses MinGW-w64 GCC and .exe helpers; see the platform guide. CXX or --cxx selects GCC/Clang. Missing adapters produce a setup diagnostic and preserve the loaded program. The toolbar also offers Keystone subset, which needs no SPIM adapter.

Keystone on Apple Silicon

The PyPI Keystone 0.9.2 source package can install without a native library. Install the Homebrew library and link it to the active environment's binding:

brew install keystone
.venv/bin/python scripts/configure_keystone.py

The helper only creates a link inside the active Python environment. On other platforms the PyPI wheel normally bundles the native library. A separately installed library can be configured with --library /path/to/libkeystone.so. If assembly cannot initialize Keystone, the app reports a setup error and keeps its existing loaded program available. Do not replace it with OpenStack Keystone.

Assembly workflow

  1. Choose File → Open Assembly and select examples/addition.s or examples/data-and-branches.s, or create/edit a source in the Source tab.
  2. Select byte order in the toolbar. Opening source automatically assembles it in the currently selected byte order; press F7 after edits or byte-order changes.
  3. Press F10 to step or F5 to run. The addition example produces $t2 = 15.
  4. Double-click a disassembly row, press F9, or click an executable source line's gutter to toggle a breakpoint. Source markers refer to the loaded source version, so reassemble after editing before placing source breakpoints.
  5. Use View → Breakpoints to enable/disable breakpoints. Reset retains persistent breakpoints; loading a new program clears them.

SPIM assembler is the default. It uses the actual r764 grammar, directives, expressions, pseudo-instruction expansion, small-data allocation and $gp optimizations, including .ktext, .kdata, .extern, .comm, .lcomm, repeated data, absolute/symbolic loads/stores, comparison/rotation/arithmetic pseudos, and the instruction vocabulary accepted by that SPIM revision. It is SPIM syntax; GNU and MARS-only directives are not added. SPIM's scanner treats adjacent decimal numbers such as 1,2 as a float, so use spaces in integer lists: .word 1, 2, 3. Use break 0 for a guest breakpoint exception. Standard data begins at 0x10010000, with the first 64 KiB reserved for $gp small data.

SPIM bare assembler uses SPIM's bare-machine assembly configuration: no pseudo-instructions or $gp allocation; data starts at 0x10000000.

Keystone subset retains the previous Python frontend: .text, .data, .globl/.global, .byte, .half, .word, .float, .double, .ascii, .asciiz, .space, .align, .set noreorder, simple bounded expressions, li, fixed two-word la, move, b, and nop. Its data starts at 0x10000000. Unsupported forms produce diagnostics. Keystone native library setup applies to this mode and both x86 and ARM modes.

Use explicit branch delay slots, for example:

    b done
    nop              # executes before the branch reaches done
    li $t0, 99       # skipped
done:
    move $s0, $t2

Branch delay slots are explicit in both modes. Place an instruction after every branch/jump and avoid branches inside slots, which MIPS defines as unpredictable. By default, entry is main, then _start, then the text base; reaching the end of user text completes source execution. Enable Run → SPIM startup and standard exception handler before assembling to load the bundled exceptions.s, start at __start, call main and exit when it returns. Leave this option off for a custom .ktext 0x80000180 handler. Source markers cover expanded instructions.

Memory, stack and register editing

Open View → Memory or View → Stack. Enter a hexadecimal/decimal address and press Go / Refresh. Each bounded window shows 256 bytes, endian-aware 32-bit words and ASCII; ?? marks holes. Choose 8, 16 or 32 bytes per row. Follow SP keeps the stack window centered near SP. Device inspection never consumes terminal input.

Double-click a register value or memory byte while stopped. Register values accept hexadecimal or signed/unsigned decimal; bytes accept hexadecimal. $zero and FIR are read-only. MIPS PC must be word aligned; x86 EIP/RIP are byte addressed. Byte edits require mapped RAM, can patch text, and refresh its disassembly. Edits are disabled while running or waiting for syscall input. Reset restores the original register/memory/code baseline.

View → Floating-point registers shows raw bits, singles and doubles. FR=0 uses even/odd low/high pairs; FR=1 presents independent 64-bit registers. Edit any supported cell directly, including NaN/infinity. View → CP0 / FPU control shows Status, Cause, EPC, BadVAddr, Count, Compare, Config, FCSR and FIR. The FCSR tooltip decodes rounding mode, flags, enables, causes and FCC0–FCC7. Try examples/fpu-inspection.s.

MMIO and guest exception handlers

Enable Run → Terminal MMIO. Registers at 0xFFFF0000, +4, +8, +12 provide receiver control/data and transmitter control/data. Bit 0 means ready; bit 1 in each control enables its interrupt. Reading receiver data consumes one byte. Writing ready transmitter data emits its low byte to the Program Console. Byte/halfword/word accesses at these register addresses are supported. Transmitter readiness returns after ten instruction hooks; this is a deterministic device model, not a physical timing model. The separate Send to MMIO field can queue UTF-8 text while running. Try examples/mmio-echo.s.

A mapped executable handler at 0x80000180 receives overflow, traps, guest break, reserved instructions, address/bus errors, FPU exceptions and enabled interrupts. Cause records ExcCode and BD; EPC records the branch PC for a slot fault and the faulting PC otherwise; BadVAddr records bad addresses. EXL prevents recursive handler delivery. mfc0/mtc0 access the SPIM CP0 registers; eret clears EXL and returns to EPC. Receiver/transmitter interrupts use IP3/IP2. CP0 Count ticks at 100 Hz during execution; matching Compare asserts IP7, and writing Compare clears it. Masks, IE and EXL control delivery. This educational environment has a flat virtual memory map; it does not implement a hardware MMU/TLB or a Linux kernel. Try examples/guest-exceptions.s. The bundled standard handler follows SPIM's own behavior, including its documented limitation when skipping delay-slot faults; custom handlers should inspect BD when advancing EPC.

x86 and x86-64

Choose x86 32-bit or x86-64 in the architecture toolbar, then open an x86 source file or press F7. File → Load x86 32-bit/64-bit demo provides runnable examples. Try examples/x86-addition.s, examples/x86-64-addition.s and examples/x86-stack.s. Intel instructions use Keystone; x86 assembly runs in a bounded subprocess so a native assembler failure cannot terminate the GUI.

The x86 frontend supports labels, forward/backward branches and Keystone's branch relaxation, .text, .data, .globl/.global, .intel_syntax noprefix, matching .code32/.code64, and data directives .byte, .word/.short (16 bits), .long/.int (32 bits), .quad (64 bits), .ascii, .asciz/.asciiz, .space/.zero, and .align (power of two). Use # or ; for comments. Text contains instructions and labels; data directives belong in .data. This is a documented Intel frontend, not full NASM/GAS source compatibility.

Run/Pause/Step/Reset, breakpoints, Run to Cursor, source mapping, editable CPU/RAM, flags, disassembly, and stack inspection work in both modes. Steps execute one x86 instruction-count boundary; REP string operations can remain at the same PC while making progress. Branches have no delay slots. HLT completes a bare program, with PC left at the halt instruction. Source images also complete at text end. Code edits re-decode variable instruction boundaries; Reset restores original code and disassembly. Breakpoints remain absolute addresses and hit only instruction starts that the CPU actually reaches.

Registers include GPRs, EIP/RIP and 32-bit EFLAGS. View → Control / vector registers shows MXCSR and 128-bit XMM registers (8 in x86, 16 in x86-64), with raw editing. EFLAGS/MXCSR tooltips explain their fields. Memory/stack windows accept 64-bit addresses and show architecture-sized words; x86-64 RSP starts above 4 GiB. The MIPS floating-pair dock, SPIM assembler options, standard handler and terminal MMIO are disabled for an x86 machine.

x86 currently emulates a flat bare CPU, without Linux/Windows guest services, a Windows runtime loader, real-mode/16-bit execution, or OS exception handlers. int 0x80, syscall and sysenter stop with an explicit OS-environment diagnostic; other interrupts/exceptions stop with their native CPU diagnostic. Static ELF loading does not imply that an executable requiring OS services can run to completion.

ARM32 and ARM64

Choose ARM32 (A32) or ARM64 (A64) in the architecture toolbar, then press F7 to assemble source. File → Load ARM32/ARM64 demo provides addition examples; examples/arm32-addition.s and examples/arm64-addition.s produce R0/X0 = 15. Both modes use little endian. A32/A64 instructions are four bytes; Thumb supports two- and four-byte instructions through the Thumb / Thumb-2 toolbar option. Run, Step, breakpoints, Run to Cursor, register/memory edits, and Reset use the shared debugger. ARM64 has a 64-bit memory/stack view and a 16-byte-aligned initial stack above 4 GiB.

ARM32 displays R0–R12, SP, LR, PC and CPSR. ARM64 displays X0–X30, SP, PC and NZCV; W-register instructions follow Unicorn's zero-extension semantics. Flags tooltips show N/Z/C/V. The Control / vector registers dock presents raw 128-bit Q registers (16 in ARM32, 32 in ARM64), floating-point status/control and FP access controls. SIMD/VFP access starts enabled. ARM32 starts in supervisor A32 state; ARM64 uses Unicorn's initial EL1 state. These are flat bare CPUs without process startup, an MMU setup, OS services, SPIM syscalls or guest exception handlers.

Source accepts Keystone ARM instruction syntax, labels, forward/backward branches, .text, .data, .globl/.global, data .align exponents, .byte, .short/ .hword, .word/.long/.int (four bytes), .quad/.xword (eight bytes), .ascii/.asciz/.asciiz, and .space/.zero. ARM32 also accepts .arm and .syntax unified. # denotes an immediate; use //, @ or ; for comments. Text contains instructions and labels, with data directives in .data. Use explicit MOV/MOVW/MOVT or ADR instructions instead of ldr reg, =value literal-pool pseudos. Full GNU assembler directives/macros are not implemented.

Source execution completes at the end of its text. ARM64 hlt #imm is also a completion hook. svc stops with an OS-environment diagnostic when its condition is satisfied; a false A32 condition skips it. Jazelle and big-endian ARM are unsupported. CPSR T edits and guest BX/BLX interworking switch between A32 and Thumb, with canonical even PC/breakpoint addresses and mode-aware disassembly. ELF32/EM_ARM and ELF64/EM_AARCH64 load automatically through Open ELF. ARM32 accepts EABI5 or unspecified ABI flags; Thumb entry bit 0 selects Thumb. ELF mapping symbols $a/$t/$d identify A32, Thumb and inline data. Observed guest state changes refine disassembly when metadata is absent. BE8 flags are rejected.

For Thumb source, select Thumb / Thumb-2 and try examples/thumb-addition.s. .thumb, .thumb_func (without an argument), .code 16 and .syntax unified are accepted. A32 accepts .arm/.code 32. A source file uses one selected state; changing states within one source is not implemented. Thumb code addresses in data expressions become tagged function pointers; debugger symbols remain even. Try examples/thumb-addition.elf and break at entry + 4 to inspect R0 = 15. False IT conditions follow Unicorn; a taken svc reports the missing OS runtime. Reset restores CPSR, bytes and original disassembly.

Try the independent static examples/arm32-addition.elf or examples/arm64-addition.elf. Set a breakpoint at entry + 8 and Run to inspect R0/X0 = 15. These ELF fixtures loop there and require no OS runtime.

PowerPC32 and PowerPC64

Choose PowerPC32 or PowerPC64, in big- or little-endian mode, in the architecture selector and assemble with F7. File → Load PowerPC32/64 demo provides addition examples; examples/powerpc32-addition.s and powerpc64-addition.s produce R3 = 15. Both modes use four-byte instructions and the shared Run/Step/breakpoint/edit/reset workflow. The stack pointer is R1 and initially 16-byte aligned. PowerPC64 supports 64-bit register values and memory addresses above 4 GiB.

The register table displays R0–R31, PC, LR, CTR, CR and XER. CR tooltips explain its eight condition fields; XER shows carry/overflow flags. The Control / floating-point registers dock provides MSR, FPSCR and 32 raw 64-bit FPRs, with IEEE double values in tooltips. Floating-point execution starts enabled. Hex edits and Reset support all exposed registers and mapped RAM.

Source uses Keystone PowerPC syntax. Both r3/f1/cr0 and numeric register operands are accepted. Labels, forward branches, .text/.data, globals, data alignment and the shared integer/string/space directives are supported. .word is four bytes, .short two, .quad eight; .float/.double accept numeric literals. Instructions, data and label metadata use the selected byte order. # and ; begin comments. Full GNU macros/directives and relocation suffixes are not implemented. Source execution completes at the end of text; sc reports a missing OS environment.

Open ELF recognizes either byte order for ELF32/EM_PPC and ELF64/EM_PPC64 static ET_EXEC images. PPC64 ELFv1 entry descriptors resolve to code and initialize R2 (TOC) and R11 (environment). Function symbols in .opd resolve to code addresses for the debugger. Direct-entry PPC64 images, including ELFv2, initialize R12 to the global entry; their code must establish any needed TOC. ABI-unspecified files use .opd when the entry lies there, otherwise a direct code entry. ELF segment permissions, BSS and reset follow the common loader. Try powerpc32-addition.elf, powerpc64-addition.elf or powerpc64-elfv1.elf in examples, with a breakpoint at entry + 8 to inspect R3 = 15. Regenerate them with python scripts/generate_powerpc_examples.py.

PowerPC runs a flat bare CPU with Unicorn's virtual TLB. MSR edits/state changes that enable address translation, switch endian or clear PPC64's 64-bit state are rejected. Both byte orders use Unicorn's supported big-endian constructor; MSR LE selects little-endian instruction fetch/data access. Keystone assembles PPC code in big endian, then the frontend reverses each four-byte instruction for LE images; label metadata is decoded separately. This also supports PPC32 LE despite Keystone's constructor limitation. CPU execution and memory bytes remain native to Unicorn. VLE ELF sections are rejected; AltiVec/VSX register presentation, OS/exception handlers, dynamic linking and complete processor-model compatibility are not implemented. CPU instruction support follows Unicorn's models; prefixed eight-byte Power ISA instructions are outside this frontend.

Motorola 68k (68000/020/030/040/060)

Choose Motorola 68000 in the architecture selector and assemble with F7. File → Load Motorola 68000 demo produces D0 = 15. Try examples/m68000-addition.s or m68000-stack.s. Unicorn uses its original 68000 CPU model and Capstone uses the matching decoder. Instructions are big endian, word aligned, and variable length (up to ten bytes). Run, Step, breakpoints, Run to Cursor, register/memory editing, source markers and Reset share the debugger.

Keystone has no m68k assembler. Source assembly uses a GNU m68k cross-assembler and linker, discovered on PATH or through standard Homebrew locations. Install brew install m68k-elf-binutils on macOS. Other platforms need GNU m68k-elf-as/ m68k-elf-ld or m68k-linux-gnu-as/m68k-linux-gnu-ld. Set UNICORN_UI_M68K_TOOL_PREFIX to an alternative executable prefix, such as /opt/m68k/bin/m68k-elf-. Missing tools produce an actionable diagnostic; raw CPU and ELF loading work without them. Python wheel installation does not bundle GNU tools. Source assembly tests require this toolchain.

GNU Motorola/Sun syntax accepts bare or %-prefixed registers, # immediates, moveq #5,d0, addi.l #10,d0, (a0) and GNU address forms. .text, .data, .bss, .rodata, globals, integer/string/space/alignment directives and native labels/relocations are supported. .word is two bytes; .long four. |, ; or // begin comments in this frontend; a leading # line is also a comment. Full vasm/Devpac dialect compatibility is not supplied. File inclusion and macro/repeat expansion directives are rejected. Data links at 0x00100000, text at 0x00400000. The assembler enforces the selected CPU (-m68000, -m68020, etc.). The 020/030/040 targets enable native 68881-style floating-point assembly. Native processes have time limits and POSIX CPU/file limits; source and segments are bounded to 1 MiB. Failed assembly preserves the active session.

The register table presents D0–D7, A0–A7, PC and 16-bit SR. A7 is the active stack pointer; USP is available in Control / FPU / stack registers. SR tooltips show X/N/Z/V/C, supervisor state and interrupt priority. Native MOVE SR probes preserve lazy condition flags when reading snapshots. SR edits use Unicorn's status setter; user/supervisor stack switching is supported, with both stacks initialized in the configured debugger stack. Reserved SR bits and debugger trace-mode edits are rejected. Original 68000 has no FPU, so its FPU/vector panes stay disabled.

TRAP #n reports a missing OS runtime. STOP #sr applies its native SR/privilege behavior and then reports CPU halted; no following instruction executes. Other CPU exceptions stop with diagnostics, without routing through a guest vector table. Source completes at the end of its text. Open ELF recognizes big-endian ELF32/EM_68K static executables. EF_M68K_M68000 selects 68000; unflagged classic ELF defaults to 68020. GNU does not distinguish 020/030/040/060 in these ELF flags: select a later CPU before Open ELF to override the model. ColdFire, CPU32 flags, LE m68k, dynamic linking and m68k PE/Mach-O are not implemented. The flat software memory map does not reproduce a physical 24-bit bus, board devices or an Amiga/Atari/Mac OS environment.

examples/m68000-addition.elf is an independent bare-code fixture. Break at entry + 8 to inspect D0 = 15. Regenerate examples with python scripts/generate_m68k_examples.py.

Choose Motorola 68020 + FPU, 68030 + FPU, or 68040 + FPU for later instructions, full indexed addressing, and Unicorn's native floating-point engine. Motorola 68060 (integer) uses the matching CPU/decoder but disables FPU source assembly: the installed Unicorn 2.1.4 backend raises line-F exceptions for basic FPU instructions on that model. A separate 68010 model is unavailable in Unicorn. These targets are backend emulations, not complete board or OS implementations.

The Control / FPU / stack registers pane adds SFC, DFC, CACR and (on FPU targets) FP0–FP7, FPCR and FPSR. Double-click a hex value to edit it. FP registers retain all 80 bits: 16 sign/exponent bits followed by 64 significand bits; memory uses Motorola's 12-byte extended format with two padding bytes. Hover to see a decimal approximation or FPCR precision/rounding and FPSR condition flags. For example, extended 1.0 is 0x3FFF8000000000000000, and 15.0 is 0x4002F000000000000000. Native FMOVEM probes preserve guest flags, FP status, GPRs, PC and memory, and do not trigger breakpoints. Reset restores the complete FPU context along with memory.

Try examples/m68020-fpu.s or examples/m68020-fpu.elf: D0 and FP0 finish at 15, and result holds an extended value. Normal native FPU arithmetic, comparisons, conditional branches and memory transfers work. FPIAR is omitted because Unicorn returns zero and ignores its writes; complete FP exception delivery, all original 68881/68882 instructions, per-chip FPU fidelity and OS trap emulation are not promised. These limitations follow the installed backend, rather than a Python reimplementation of the CPU. See the upstream 68k translator for its FPIAR and floating-point exception limitations.

ELF workflow

Choose File → Open ELF. Byte order and entry come from the file, not the assembly toolbar. The ELF must be static ET_EXEC: ELF32/EM_MIPS, ELF32/EM_386, ELF64/EM_X86_64, ELF32/EM_ARM, ELF64/EM_AARCH64, ELF32/EM_PPC, ELF64/EM_PPC64 or ELF32/EM_68K. x86 and ARM require little endian; PowerPC accepts both byte orders; m68k is big endian. MIPS accepts the classic MIPS I/II and MIPS32/r2 flag families. MIPS64, MIPS16, microMIPS, PIE/shared objects, dynamic linking and runtime relocations are rejected. Non-overlapping segment bytes may share a page; page permissions are combined. ELF images do not receive synthetic text/data mappings or an automatic end-of- file stop; use breakpoints/steps to inspect them. The configured stack is added.

Try examples/addition-little.elf or examples/addition-big.elf. These deliberately small hand-built fixtures (see tests/elf_fixture.py) have main at 0x00020004 and done at 0x00020010. Set a breakpoint at done, then Run to inspect $t2 = 15. The ELF loops at done until paused; it does not require an OS or syscall service. Symbol names appear in the disassembly table.

PE workflow

Choose File → Open PE… to load an x86 or ARM .exe, .dll, .sys or .efi image. Machine headers select x86, x86-64, A32 ARM, Thumb/ARMNT, ARM64 or legacy little-endian PowerPC32 automatically. Headers and sections map at the preferred image base with section permissions and zero-filled virtual tails. The debugger uses the PE entry point and displays export names. Its configured stack is added; PE stack-reserve fields do not create a Windows process environment. DLLs without an entry point use the first file-backed executable section for inspection, with a warning. Images without executable code are rejected.

Try examples/bare-pe-x86-32.exe or examples/bare-pe-x86-64.exe, then Run: EAX/RAX becomes 15 and HLT stops execution. These are Unicorn-only fixtures, not programs intended for native Windows execution. Regenerate them with python scripts/generate_pe_examples.py.

Imports, delay imports, TLS callbacks, DLL initialization and .NET execution are not emulated. Such images can load for inspection, with warnings in the console and program tooltip; calling an unresolved import will not execute a Windows API. Only export symbols are loaded, without PDB/COFF debug information. Certificate and overlay bytes are not mapped, and signatures are not verified.

The Python loader also supports an alternate, 64 KiB-aligned base through load_pe(path, load_base=...). Rebasing requires a valid relocation table and supports HIGHLOW for PE32, DIR64 for PE32+, ARM MOV32A and Thumb MOV32T MOVW/MOVT pairs. PowerPC PE also supports HIGH, LOW and paired HIGHADJ relocations for split address immediates. Other relocation types are rejected. The GUI uses the preferred base. The loader validates headers, section ranges, alignment, directories and entry bytes before replacing the active machine.

ARM and PowerPC Mach-O workflow

Choose File → Open Mach-O… to load an ARM32, ARM64, PowerPC32 or PowerPC64 image. ARM is little endian; PowerPC follows the Mach-O header byte order (legacy Mac images normally use BE). Thin and 32/64-bit universal containers are supported. Universal files select an 64-bit slice when available, preferring ARM on equal word widths. load_macho(path, bits=32, family="ppc") selects a PowerPC32 slice; family="arm" selects ARM through the Python API. Segment linked addresses, R/W/X permissions, zero-filled tails and nlist symbol names are retained. __PAGEZERO remains unmapped. Entry comes from LC_MAIN or LC_THREAD/LC_UNIXTHREAD; ARM thread metadata selects A32/Thumb state and PowerPC thread metadata supplies SRR0 entry, without restoring a native process stack. Mach-O Thumb function descriptors guide decoding.

Executable/preload images require an entry; dylibs/bundles without one can load for inspection at their first executable segment, with a warning. Relocatable objects, unsupported CPUs, big-endian ARM and SG_HIGHVM segments are rejected. Mach-O loads at its linked base, without ASLR sliding, binding/rebasing, chained fixups, dyld, TLS, constructors, Darwin syscalls or Apple runtime initialization. Affected images display loader warnings. ARM64e loads for inspection with a warning; pointer authentication/Apple ABI initialization is not supplied. Code signatures are range-checked but not verified. DWARF/dSYM and export tries are not consumed.

Try examples/bare-thumb.macho, examples/bare-arm32.macho, examples/bare-arm64.macho, or examples/bare-arm-universal.macho. Break at entry + 4 for Thumb, or entry + 8 for A32/A64, to inspect R0/X0 = 15. Matching bare-*.exe examples exercise ARM PE loading. These are Unicorn-only fixtures; do not run them as native host programs. Regenerate all ARM executable examples with python scripts/generate_arm_examples.py.

PowerPC PE and Mach-O examples

Open PE supports legacy Windows NT POWERPC/POWERPCFP PE32 images in little endian. PE32+ PowerPC is not a standardized supported target. Entry can point to code or a file-backed eight-byte descriptor holding absolute code/TOC addresses; the loader resolves descriptor entry and initializes R2, including optional rebasing. Exports remain faithful to their file addresses, including data/descriptor exports. Windows imports and startup remain unimplemented.

Try examples/bare-powerpc32.exe or powerpc32-pe-descriptor.exe, and break at entry + 8 to inspect R3 = 15. Mach-O examples include bare-powerpc32-big.macho, bare-powerpc64-big.macho, their -little.macho variants, and bare-powerpc-universal.macho. powerpc32-little.elf and powerpc64-little.elf exercise LE ELF loading. File → Load PowerPC32/64 little-endian demo runs source examples directly. These are Unicorn-only fixtures, without a native OS runtime. Regenerate them with python scripts/generate_powerpc_examples.py.

Controls

Action Shortcut / interaction
Run / Continue F5
Pause F6
Backend step F10
Reset machine Ctrl/Cmd+Shift+F5
Assemble and load source F7
Toggle breakpoint F9 / double-click disassembly / source gutter
Run to Cursor Ctrl/Cmd+F10 / disassembly context menu
New / Open / Save source Standard platform shortcuts
Copy address / instruction Disassembly context menu

A breakpoint stops before execution. Continue bypasses that particular stop once and can hit the same address again on the next loop iteration. Run to Cursor creates a temporary target, cleared on any stop, explicit pause, fault or reset. The diagnostics panel shows debugger messages; the separate Program Console shows guest stdout and accepts input when a syscall requests it.

SPIM syscalls and interactive programs

Open examples/spim-console.s, then Run. Enter an integer in the Program Console and press Enter or Send, then enter your name. The example allocates heap memory, stores/prints the integer, reads/prints a string and exits with status zero. examples/spim-floats.s demonstrates floating-point input/output. Both examples work in either byte order. View → Program Console reveals the console when hidden; an input request also brings it forward automatically.

$v0 Service Arguments / result
1 Print signed integer $a0
2 Print float $f12 (8 fractional digits, like SPIM)
3 Print double Paired $f12/$f13 (18 significant digits)
4 Print NUL-terminated string Pointer in $a0
5 Read signed decimal integer Result in $v0
6 Read float Result in $f0
7 Read double Result in paired $f0/$f1
8 Read string Buffer $a0, capacity $a1 including trailing NUL
9 Allocate heap bytes (sbrk) Size $a0, previous break returned in $v0
10 Exit Exit status 0
11 Print character Low byte of $a0
12 Read character Next input byte returned in $v0
13 Open file Filename $a0, flags $a1; descriptor in $v0
14 Read bytes Descriptor $a0, buffer $a1, count $a2; count/EOF/error in $v0
15 Write bytes Descriptor $a0, buffer $a1, count $a2; count/error in $v0
16 Close file Descriptor $a0; 0 on success, -1 on error
17 Exit with status Signed $a0

PC stays at the syscall while input is pending. Valid input completes it exactly once and advances PC once. Run resumes automatically; input requested by Step completes only that syscall and remains paused. Pause while waiting cancels automatic resume. Invalid numeric input keeps the request active so it can be corrected. Reset/new load cancels old requests, clears console/stdin/exit state, and restores the initial heap. Late input from an old request cannot affect a new run.

Input is line-based in the GUI and receives a newline on Enter. String input copies at most capacity - 1 bytes, includes a newline if it fits, and always adds NUL. Capacity <= 0 does nothing; capacity 1 writes only NUL without waiting. Unused input bytes remain buffered for subsequent reads, including read-character. Numeric input consumes a whole line and must be valid decimal text; unlike SPIM's permissive C conversions, invalid prefixes/overflow are reported for correction. Strings/characters are byte streams rendered as UTF-8 (invalid bytes use replacement characters). Enter alone supplies a newline to read-character. Input is in a separate field and is not echoed into guest stdout.

The initial heap is page-aligned beyond static writable segments and at least 0x10010000. Allocations round up to four bytes, map zero-initialized RW pages as needed, and have a 16 MiB total limit. Negative sizes and collisions are diagnostics. String transfers/input buffers are capped at 1 MiB. The GUI input field accepts up to 64 Ki characters; its transcript retains up to 1 Mi characters and 2,000 blocks. The worker batches output rather than emitting per-character signals.

Floating-point services use Unicorn's real FPU words; doubles require the default paired-register FR=0 mode, verified using guest loads/stores in both byte orders. The optional Floating-point registers dock shows raw 32-bit words with change highlighting. .float/.double accept decimal floating-point literals and align naturally to four/eight bytes. Full SPIM floating-point pseudos are not implemented.

File I/O

Open examples/spim-files.s and choose File → Program Directory… before Run. The example creates demo-output.txt, writes it, reopens/reads it and sends the bytes to the Program Console. It puts file services in branch delay slots.

Relative filenames default to the opened source/ELF file's directory, or the application working directory for untitled assembly and the built-in demo. Program Directory overrides that default for the app session. The active path appears in the console. Paths must resolve within it; parent escapes and symlinks leading outside fail. Only regular files are opened.

Portable MARS open flags are 0 = read, 1 = create/truncate/write, and 9 = create/append/write. $a2 mode is ignored. Other flag words return -1; SPIM's host-specific native flag combinations are not portable across OSes. Guest descriptors start at 3 and are independent of host descriptor numbers. The limit is 64 open files. File errors, invalid descriptors/counts return -1 (0xFFFFFFFF). Invalid guest buffers fault before file I/O. Read returns a byte count or 0 at EOF, with no added NUL; Write returns the actual byte count and preserves embedded NUL bytes. Transfers are capped at 1 MiB.

Descriptor 0 reads console stdin; 1 and 2 write to the Program Console (stdout/stderr are merged). Reserved console descriptors cannot be closed. End input signals EOF: buffered bytes drain first, then fd-0 reads return 0, character reads return -1, numeric reads return zero and string reads produce an empty NUL-terminated string. Reset clears EOF and buffered input.

Exit, natural completion, faults, Reset, new program load, directory changes and application shutdown close owned file handles. Reset does not undo disk writes. File contents persist; descriptors and emulated machine/input state are reset.

Syscalls in branch delay slots

Try examples/spim-delay-input.s. Normal branches execute their syscall slot whether taken or not. A false branch-likely annuls it, including all syscall side effects and input requests. Jumps, register jumps, link instructions and floating-point branches retain Unicorn's target/condition/link behavior.

While a delay-slot syscall waits for input, PC points at the syscall and link registers already reflect the executed branch. Input resumes at the saved native continuation, not at syscall PC + 4, without executing the branch again. Step normally executes branch and slot together. A standalone slot breakpoint stops after the branch's condition and link effects but before slot effects. Continue or Step executes the slot once and installs the saved continuation. An annulled likely slot does not hit its breakpoint. Branch conditions are not reevaluated after edits/input. Original bytes and scratch state are restored before every visible stop, syscall and input request.

Important limits

  • CPU execution and floating-point arithmetic follow Unicorn's architecture models; SPIM parser compatibility does not guarantee every SPIM CPU extension behaves identically. Branches inside delay slots remain architecturally unpredictable.
  • MARS services beyond 17, host-specific file open flags, non-regular files, Linux syscalls, a hardware MMU/TLB and full operating-system emulation are unsupported.
  • MIPS double console services retain SPIM's FR=0 pair convention. FR=1 is supported for FPU inspection/editing, not double syscalls.
  • Configurable raw-file loading, watchpoints, conditional breakpoints and step-over/out remain future work. Platform setup, automated compatibility checks and native desktop validation status are described in the platform guide.
  • ELF/PE/Mach-O files and mapped images are limited to 64 MiB; source to 1 MiB. The SPIM adapter caps each segment at 4 MiB; the Keystone subset caps sections at 1 MiB. Disassembly displays up to 1 MiB per executable segment. Images overlapping segment bytes, the configured stack, or an enabled MMIO mapping are rejected.
  • Reset stores full in-memory baseline copies; host file writes persist.

Verification and design

unicorn-ui-demo          # headless real-Unicorn LE and BE addition demo
python -m pytest        # core and offscreen GUI integration tests
ruff check .
unicorn-ui --smoke-test  # native window startup and cooperative shutdown

Tests cover breakpoint continuation/loops, explicit delay-slot constraints, ELF segment/entry/symbol/BSS validation, assembly/pseudos/data/source diagnostics, SPIM I/O/FPU/heap/exit/error/reset semantics, real binary file I/O in temporary directories, native delay-slot continuation/annulment, and the complete desktop open/assemble/step/run/reset/input and failed-load workflows. Additional tests exercise SPIM grammar and pseudos, endian data/relocations, GUI edits and code refresh, IEEE displays, delay-slot breakpoints, CP0 handler return/BD/EPC, MMIO interrupts, output ordering and side-effect-free memory inspection.

See SPIM analysis and architecture.

The x86 regression suite covers short/near branch relaxation, source spans, calls/returns, stack operations, breakpoint loops/continuation, EFLAGS and SSE register widths, ELF32/ELF64 high-address mapping/BSS/reset, OS-service diagnostics, native assembly isolation, and switching architectures through the GUI.

The PE regression suite covers PE32/PE32+ execution, exports, BSS, permissions, rebasing, malformed images, low-alignment sections, certificates, reset, GUI architecture selection and preserving the session after a failed load.

The ARM regression suite covers A32/A64 stepping, conditional execution, flags, branches/calls/returns, stack operations, SIMD/VFP access, register/memory editing, static ELF and BSS/reset, Thumb/IT/interworking, source locations and GUI switching. Additional tests cover ARM PE MOV32 relocations, thin/universal Mach-O entry/symbol/ segment validation, malformed candidates and preserving sessions after failed loads.

PowerPC tests cover both word widths, branches/LR/CTR/CR, endian data and label relocations, FPR arithmetic/edit/reset, high addresses, ELFv1 descriptors and symbols, direct ELF entries, MSR constraints and GUI mode switching/failed loads.

PowerPC format tests also cover LE floating memory operations, PE descriptors and HIGH/LOW/HIGHADJ rebasing, Mach-O symbols/thread state/universal selection in both byte orders, GUI automatic mode selection and preserving sessions on failed loads.

Motorola 68000 tests cover GNU assembly/diagnostics, variable instruction spans, branch flags, BSR/RTS, SR/USP stack switching, partial register writes, endian load/store, STOP privilege/halt behavior, traps, static ELF/BSS/reset and GUI edits.

About

QtSPIM inspired GUI for unicorn

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages