From 7edddd5731ecbff1abf6bd296d02955121f8ebc7 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 27 Sep 2026 21:33:45 +0000 Subject: [PATCH 1/2] Show flags and hi/lo in regs; fix stale examples and docs regs now shows the registers students need to follow compares, branches, and multiplies: MIPS hi and lo, ARM64 nzcv, and x86-64 rflags (CF, ZF, SF, OF), with change markers like the general-purpose registers. The RISC-V fibonacci and matrix_multiply examples exited through the HTIF tohost mechanism left over from the Sail backend, unlike every other example; they now use the exit syscall and fibonacci prints its result. The ARM examples README described an old workflow (install a cross toolchain, load by path, wrong base address) and is removed. The Console Guide gets a register table for all four ISAs and loses its redundant Tips section; transcripts are regenerated from the rebuilt examples. Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 12 + README.md | 28 +-- docs/user/console-guide.md | 151 +++++-------- examples/arm/README.md | 226 ------------------- examples/arm/linker.ld | 2 +- examples/mips/linker.ld | 2 +- examples/riscv/fibonacci/fibonacci | Bin 1904 -> 1400 bytes examples/riscv/fibonacci/fibonacci.s | 43 ++-- examples/riscv/linker.ld | 8 +- examples/riscv/matrix_multiply/matrix_mult | Bin 2312 -> 1784 bytes examples/riscv/matrix_multiply/matrix_mult.s | 30 +-- mapachespim/console.py | 112 +++++---- mapachespim/engines.py | 11 +- mapachespim/isa.py | 20 ++ mapachespim/unicorn_backend.py | 14 ++ tests/test_program_correctness.py | 64 +++--- tests/test_simulator_parts.py | 2 +- tests/test_special_registers.py | 100 ++++++++ 18 files changed, 345 insertions(+), 480 deletions(-) delete mode 100644 examples/arm/README.md create mode 100644 tests/test_special_registers.py diff --git a/CHANGELOG.md b/CHANGELOG.md index d996742..ceb839b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,18 @@ ## Unreleased +### Console +- `regs` shows MIPS `hi` and `lo`, and the condition flags on ARM64 (`nzcv: N Z C V`) and + x86-64 (`rflags: CF ZF SF OF`), with ★ when they change. New `Simulator.get_special_regs()` + and `get_flags()`. +- `regs` and `pc` ask you to load a program first instead of showing an unset machine. + +### Examples and docs +- The RISC-V `fibonacci` and `matrix_multiply` examples exit with the exit syscall, like the + other ISAs, instead of the HTIF `tohost` mechanism; `fibonacci` now prints its result. +- Removed the out-of-date `examples/arm/README.md`. The Console Guide has a register table for + all four ISAs in place of the RISC-V-only one. + ### Code structure - Each ISA is described once, in `mapachespim/isa.py`; the simulator, disassembler, ELF loader and writer, assembler, console, and command-line tools all read their ISA-specific facts from it. diff --git a/README.md b/README.md index beabbb9..999e79b 100644 --- a/README.md +++ b/README.md @@ -76,28 +76,28 @@ Welcome to MapacheSPIM. Type help or ? to list commands, or quickstart for a tut (mapachespim) load riscv/fibonacci Loaded riscv/fibonacci (RISCV) Entry point: 0x0000000080000000 -Source info: fibonacci.s (32 address mappings) +Source info: fibonacci.s (34 address mappings) (mapachespim) break fibonacci Breakpoint set at fibonacci (0x80000038) (mapachespim) run -Breakpoint hit at 0x0000000080000038 after 6 instructions +Breakpoint hit at 0x0000000080000038 after 4 instructions PC = 0x0000000080000038 (mapachespim) list fibonacci.s: - 81: # sp+16: saved a0 (original n value) - 82: # Total: 24 bytes - 83: # ============================================================================ - 84: fibonacci: - 85: # Base case 1: if n == 0, return 0 - 86> beqz a0, base_case_zero # <-- PC: 0x80000038 - 87: - 88: # Base case 2: if n == 1, return 1 - 89: li t0, 1 - 90: beq a0, t0, base_case_one + 75: # sp+16: saved ra (return address) + 76: # Total: 24 bytes + 77: # ============================================================================ + 78: fibonacci: + 79: # Base case 1: if n == 0, return 0 + 80> beqz a0, base_case_zero # <-- PC: 0x80000038 + 81: + 82: # Base case 2: if n == 1, return 1 + 83: li t0, 1 + 84: beq a0, t0, base_case_one (mapachespim) step [0x80000038] 0x63040504 beqz a0, 0x48 @@ -107,8 +107,8 @@ fibonacci.s: (mapachespim) regs -x0 (zero) = 0x0000000000000000 x1 ( ra) = 0x0000000080000018 ★ -x2 ( sp) = 0x0000000080181000 ★ x3 ( gp) = 0x0000000000000000 +x0 (zero) = 0x0000000000000000 x1 ( ra) = 0x0000000080000010 ★ +x2 ( sp) = 0x0000000083effff8 x3 ( gp) = 0x0000000000000000 x4 ( tp) = 0x0000000000000000 x5 ( t0) = 0x0000000000000001 ★ x6 ( t1) = 0x0000000000000000 x7 ( t2) = 0x0000000000000000 x8 ( s0) = 0x0000000000000000 x9 ( s1) = 0x0000000000000000 diff --git a/docs/user/console-guide.md b/docs/user/console-guide.md index 869fff2..68bb9f8 100644 --- a/docs/user/console-guide.md +++ b/docs/user/console-guide.md @@ -91,8 +91,8 @@ mapachespim riscv/fibonacci (mapachespim) regs x0 (zero) = 0x0000000000000000 x1 ( ra) = 0x0000000000000000 - x2 ( sp) = 0x0000000080181000 ★ x3 ( gp) = 0x0000000000000000 - x4 ( tp) = 0x0000000000000000 x5 ( t0) = 0x0000000000000000 + x2 ( sp) = 0x0000000083effff8 x3 ( gp) = 0x0000000000000000 + x4 ( tp) = 0x0000000000000000 x5 ( t0) = 0x0000000080100000 ★ x6 ( t1) = 0x0000000000000000 x7 ( t2) = 0x0000000000000000 x8 ( s0) = 0x0000000000000000 x9 ( s1) = 0x0000000000000000 x10 ( a0) = 0x0000000000000000 x11 ( a1) = 0x0000000000000000 @@ -110,6 +110,18 @@ mapachespim riscv/fibonacci pc = 0x0000000080000004 ``` + On MIPS, `regs` also shows `hi` and `lo`, where `mult` and `div` put their results. On ARM64 + and x86-64 it shows the condition flags that compare instructions set and conditional branches + test. For example, on x86-64 after `movq $3, %rax` and `cmpq $5, %rax` (3 − 5 is negative and + borrows): + ``` + pc = 0x000000000040000f + rflags: CF=1 ZF=0 SF=1 OF=0 ★ + ``` + ARM64 shows the same four flags as `nzcv: N=1 Z=0 C=0 V=0`. + +- `stepreg [n]` - Step, then show the registers, like `step` followed by `regs` (alias: `sr`) + - `pc` - Show just the program counter ``` (mapachespim) pc @@ -129,10 +141,10 @@ mapachespim riscv/fibonacci ``` (mapachespim) mem 0x80000000 64 - 0x80000000: 17 11 18 00 13 01 01 00 97 02 10 00 93 82 82 ff |................| - 0x80000010: 03 a5 02 00 ef 00 40 02 97 02 10 00 93 82 c2 fe |......@.........| - 0x80000020: 23 a0 a2 00 93 02 10 00 17 03 19 00 13 03 83 fd |#...............| - 0x80000030: 23 30 53 00 6f 00 00 00 63 04 05 04 93 02 10 00 |#0S.o...c.......| + 0x80000000: 97 02 10 00 93 82 02 00 03 a5 02 00 ef 00 c0 02 |................| + 0x80000010: 97 02 10 00 93 82 42 ff 23 a0 a2 00 93 08 10 00 |......B.#.......| + 0x80000020: 73 00 00 00 13 05 a0 00 93 08 b0 00 73 00 00 00 |s...........s...| + 0x80000030: 93 08 a0 00 73 00 00 00 63 04 05 04 93 02 10 00 |....s...c.......| ``` - `list [location]` - Display source code (requires debug symbols) (alias: `l`) @@ -147,16 +159,16 @@ mapachespim riscv/fibonacci (mapachespim) list fibonacci fibonacci.s: - 81: # sp+16: saved a0 (original n value) - 82: # Total: 24 bytes - 83: # ============================================================================ - 84: fibonacci: - 85: # Base case 1: if n == 0, return 0 - 86: beqz a0, base_case_zero - 87: - 88: # Base case 2: if n == 1, return 1 - 89: li t0, 1 - 90: beq a0, t0, base_case_one + 75: # sp+16: saved ra (return address) + 76: # Total: 24 bytes + 77: # ============================================================================ + 78: fibonacci: + 79: # Base case 1: if n == 0, return 0 + 80: beqz a0, base_case_zero + 81: + 82: # Base case 2: if n == 1, return 1 + 83: li t0, 1 + 84: beq a0, t0, base_case_one ``` Programs loaded from `.s` files always have source info. @@ -183,18 +195,13 @@ mapachespim riscv/fibonacci ``` (mapachespim) info symbols - Symbols (11 total): + Symbols (6 total): 0x80000000 _start - 0x80000034 exit_loop 0x80000038 fibonacci 0x80000080 base_case_zero 0x80000088 base_case_one 0x80100000 fib_input 0x80100004 fib_result - 0x80180000 _stack_bottom - 0x80181000 _stack_start - 0x80190000 tohost - 0x80190040 fromhost ``` - `info sections` - List all ELF sections (alias: `info sec`) @@ -206,8 +213,6 @@ mapachespim riscv/fibonacci ---------------------------------------------------------------------- .text 0x80000000 144 AX .data 0x80100000 8 WA - .bss 0x80180000 4096 WA - .tohost 0x80190000 72 A Flags: W=Write, A=Alloc, X=Execute Use 'mem
' to view section contents (e.g., mem .data) @@ -236,13 +241,13 @@ Welcome to MapacheSPIM. Type help or ? to list commands, or quickstart for a tut (mapachespim) load riscv/fibonacci Loaded riscv/fibonacci (RISCV) Entry point: 0x0000000080000000 -Source info: fibonacci.s (32 address mappings) +Source info: fibonacci.s (34 address mappings) (mapachespim) break fibonacci Breakpoint set at fibonacci (0x80000038) (mapachespim) run -Breakpoint hit at 0x0000000080000038 after 6 instructions +Breakpoint hit at 0x0000000080000038 after 4 instructions PC = 0x0000000080000038 (mapachespim) step @@ -250,8 +255,8 @@ PC = 0x0000000080000038 (mapachespim) regs -x0 (zero) = 0x0000000000000000 x1 ( ra) = 0x0000000080000018 ★ -x2 ( sp) = 0x0000000080181000 ★ x3 ( gp) = 0x0000000000000000 +x0 (zero) = 0x0000000000000000 x1 ( ra) = 0x0000000080000010 ★ +x2 ( sp) = 0x0000000083effff8 x3 ( gp) = 0x0000000000000000 x4 ( tp) = 0x0000000000000000 x5 ( t0) = 0x0000000080100000 ★ x6 ( t1) = 0x0000000000000000 x7 ( t2) = 0x0000000000000000 x8 ( s0) = 0x0000000000000000 x9 ( s1) = 0x0000000000000000 @@ -273,8 +278,8 @@ pc = 0x000000008000003c Breakpoint removed at 0x0000000080000038 (mapachespim) continue -Program completed (tohost) after 455 instructions -PC = 0x0000000080000034 +13 +Program exited with code 0 after 458 instructions (mapachespim) mem fib_input 64 @@ -287,28 +292,28 @@ PC = 0x0000000080000034 Goodbye! ``` -The last `mem` shows the program's data: `fib_input` (7) in the first word, and the result it -computed, `fib_result` (13, or `0d` in hex), in the next. Words are stored little-endian, so the +The program prints its result, 13, and the last `mem` shows its data: `fib_input` (7) in the first +word, and the result it stored in `fib_result` (13, or `0d` in hex) in the next. Words are stored little-endian, so the low byte comes first. -## RISC-V Register ABI Names - -The console shows both numeric (x0-x31) and ABI names: - -| Reg | ABI Name | Description | -|-----|----------|-------------| -| x0 | zero | Hard-wired zero | -| x1 | ra | Return address | -| x2 | sp | Stack pointer | -| x3 | gp | Global pointer | -| x4 | tp | Thread pointer | -| x5-7 | t0-t2 | Temporaries | -| x8 | s0/fp | Saved / frame pointer | -| x9 | s1 | Saved register | -| x10-11 | a0-a1 | Function args/return values | -| x12-17 | a2-a7 | Function arguments | -| x18-27 | s2-s11 | Saved registers | -| x28-31 | t3-t6 | Temporaries | +## Register Names + +`regs` shows each register by its conventional name. What those registers are for, by ISA: + +| Use | RISC-V | MIPS | ARM64 | x86-64 | +|-----|--------|------|-------|--------| +| Arguments | `a0`-`a7` | `$a0`-`$a3` | `x0`-`x7` | `rdi`, `rsi`, `rdx`, `rcx`, `r8`, `r9` | +| Return value | `a0` | `$v0` | `x0` | `rax` | +| Return address | `ra` | `$ra` | `x30` (lr) | pushed on the stack by `call` | +| Stack pointer | `sp` | `$sp` | `sp` | `rsp` | +| Frame pointer | `s0` (fp) | `$fp` | `x29` | `rbp` | +| Temporaries (caller-saved) | `t0`-`t6` | `$t0`-`$t9` | `x9`-`x15` | `rax`, `rcx`, `rdx`, `rsi`, `rdi`, `r8`-`r11` | +| Saved (callee-saved) | `s0`-`s11` | `$s0`-`$s7` | `x19`-`x28` | `rbx`, `rbp`, `r12`-`r15` | +| Always zero | `zero` | `$zero` | `xzr` | - | +| Syscall number | `a7` | `$v0` | `x8` | `rax` | +| Other | | `hi`, `lo` | `nzcv` flags | `rflags` flags | + +RISC-V registers can also be written by number (`x0`-`x31`), and `regs` shows both. ## Command Line Options @@ -338,44 +343,6 @@ echo 7 | mapachespim -e fib.s > output.txt - **Enter** on an empty line - Repeat the last command (handy for `step`) - **Up/Down arrows** - Command history -## Tips - -1. **Setting multiple breakpoints**: Set breakpoints before running - ``` - break 0x80000010 - break 0x80000100 - info break - run - ``` - -2. **Examining function calls**: Set breakpoint at function entry - ``` - break 0x80000050 # Function entry point - run - regs # Check arguments in a0-a7 - ``` - -3. **Memory inspection**: Use hex addresses or section names - ``` - mem 0x80000000 # By address - mem .text # Code section - mem .data # Data section - mem .rodata # Read-only data (strings, constants) - info sections # List all available sections - ``` - -4. **Source code viewing**: Requires debug info (automatic when you load a `.s` file) - ``` - list # Show source around current PC - list fibonacci # Show source around function - list 25 # Show source around line 25 - ``` - -5. **Single-stepping**: Use `step n` for multiple steps - ``` - step 10 # Execute 10 instructions at once - ``` - ## Assembling Programs The simplest way is to `load` your `.s` file; MapacheSPIM assembles it with debug info. To produce @@ -399,10 +366,10 @@ MapacheSPIM is similar to SPIM but has key differences: 3. **Delay slots**: On MIPS, the assembler fills each branch and jump delay slot with a `nop` (like GNU `as` in its default `.set reorder` mode), and the simulator executes delay slots as real hardware does. Write `.set noreorder` to fill delay slots yourself -4. **Same code as GNU as**: RISC-V and MIPS programs assemble to exactly what GNU `as` produces, +4. **Same code as GNU as**: programs assemble to exactly what GNU `as` produces, so disassembly matches what you see in textbooks and `objdump`. One deliberate exception follows SPIM instead: on MIPS, the two-operand `div $t0, $t1` / `divu $t0, $t1` is the real instruction - (results in HI and LO), whereas GNU `as` treats it as `div $t0, $t0, $t1` + (results in `hi` and `lo`, which `regs` shows), whereas GNU `as` treats it as `div $t0, $t0, $t1` 5. **Runtime checks**: MIPS `div`/`rem` with three operands trap on division by zero and on overflow, as in SPIM, and the console reports "Division by zero" 6. **Source display**: `list` shows your source code next to the program counter @@ -420,7 +387,3 @@ MapacheSPIM is similar to SPIM but has key differences: **Breakpoint not hit:** - Verify address is correct: `mem ` - Check program actually reaches that address - -**Ctrl-C doesn't work:** -- Signal handling may take 1-2 instructions -- Try again if needed diff --git a/examples/arm/README.md b/examples/arm/README.md deleted file mode 100644 index f563460..0000000 --- a/examples/arm/README.md +++ /dev/null @@ -1,226 +0,0 @@ -# ARM64 Assembly Examples for MapacheSPIM - -Educational ARM64 (AArch64) assembly examples designed to teach computer architecture concepts using the Unicorn Engine emulator. - -## Prerequisites - -### ARM64 Toolchain - -You need an ARM64 cross-compilation toolchain to build these examples: - -**macOS (Homebrew):** -```bash -brew install aarch64-elf-gcc -``` - -**Linux (Debian/Ubuntu):** -```bash -sudo apt-get install gcc-aarch64-linux-gnu binutils-aarch64-linux-gnu -# Then build with: make PREFIX=aarch64-linux-gnu- -``` - -**Alternative toolchains:** -- ARM GNU Toolchain: https://developer.arm.com/downloads/-/arm-gnu-toolchain-downloads -- LLVM/Clang with ARM support - -## Building Examples - -### Build All Examples -```bash -make -``` - -### Build Individual Examples -```bash -make hello_asm # Basic I/O and syscalls -make guess_game # Interactive number guessing -make fibonacci # Recursive Fibonacci -make array_stats # Array processing -make matrix_multiply # Matrix multiplication -``` - -### Clean Build Artifacts -```bash -make clean -``` - -## Running Examples - -```bash -# Start MapacheSPIM -mapachespim - -# Load and run an example -(mapachespim) load examples/arm/fibonacci/fibonacci -(mapachespim) run -``` - -## Examples Overview - -### 1. Hello Assembly (`hello_asm/hello_asm.s`) -**Difficulty:** Beginner - -**Learning Objectives:** -- Basic ARM64 instruction syntax -- String output with syscalls -- Simple arithmetic operations -- Program structure and exit - -### 2. Guess Game (`guess_game/guess_game.s`) -**Difficulty:** Beginner+ - -**Learning Objectives:** -- User input with read_int syscall -- Conditional branching (cbz, cmp, beq, blt, bgt) -- Loop structures -- Interactive programs - -### 3. Fibonacci (`fibonacci/fibonacci.s`) -**Difficulty:** Intermediate - -**Learning Objectives:** -- Recursive function calls -- Stack frame management -- ARM64 calling convention -- Register save/restore with stp/ldp - -**ARM64 Calling Convention:** -- x0-x7: Argument/result registers -- x8: Syscall number register -- x9-x15: Temporary registers -- x19-x28: Callee-saved registers -- x29: Frame pointer (FP) -- x30: Link register (LR) - return address -- sp: Stack pointer (16-byte aligned) - -### 4. Array Statistics (`array_stats/array_stats.s`) -**Difficulty:** Intermediate - -**Learning Objectives:** -- Array traversal in memory -- Load/store instructions (ldr/str) -- Calculating sum, min, max, average -- Working with data sections - -### 5. Matrix Multiply (`matrix_multiply/matrix_mult.s`) -**Difficulty:** Advanced - -**Learning Objectives:** -- Nested loop structures -- 2D array indexing -- Memory layout for matrices -- Complex pointer arithmetic - -## ARM64 Assembly Quick Reference - -### Register Names -- **x0-x30**: 64-bit general-purpose registers -- **w0-w30**: 32-bit versions (lower half of x registers) -- **sp**: Stack pointer -- **pc**: Program counter (not directly accessible) - -### Common Instructions -```assembly -mov x0, #42 // Move immediate 42 to x0 -add x2, x0, x1 // x2 = x0 + x1 -sub x2, x0, x1 // x2 = x0 - x1 -lsl x2, x0, #2 // x2 = x0 << 2 (multiply by 4) -ldr x0, [x1] // Load from address in x1 to x0 -str x0, [x1] // Store x0 to address in x1 -adr x0, label // Load PC-relative address of label -b label // Unconditional branch -bl function // Branch with link (call function) -ret // Return (branch to x30/LR) -cbz x0, label // Branch if x0 is zero -cmp x0, #1 // Compare x0 with 1 -beq label // Branch if equal -svc #0 // Supervisor call (syscall) -``` - -### Stack Operations -```assembly -// Save two registers (16-byte aligned) -stp x19, x30, [sp, #-16]! - -// Restore two registers -ldp x19, x30, [sp], #16 - -// Allocate stack space -sub sp, sp, #32 - -// Deallocate stack space -add sp, sp, #32 -``` - -### Function Call Example -```assembly -my_function: - // Prologue: save registers - stp x29, x30, [sp, #-16]! // Save FP and LR - mov x29, sp // Set up frame pointer - - // Function body - // ... your code ... - - // Epilogue: restore and return - ldp x29, x30, [sp], #16 // Restore FP and LR - ret // Return to caller -``` - -## Syscalls - -MapacheSPIM uses SPIM-compatible syscalls: - -| # | Name | Arguments | Description | -|---|------|-----------|-------------| -| 1 | print_int | x0 = integer | Print integer | -| 4 | print_string | x0 = address | Print null-terminated string | -| 5 | read_int | | Read integer into x0 | -| 10 | exit | | Exit program | -| 11 | print_char | x0 = char | Print ASCII character | - -**Syscall Convention:** Place syscall number in x8, arguments in x0-x2, then execute `svc #0`. - -## Differences from RISC-V - -If you're familiar with RISC-V assembly: - -| Concept | RISC-V | ARM64 | -|---------|--------|-------| -| Syscall | `ecall` | `svc #0` | -| Return | `ret` (pseudo-instr) | `ret` | -| Call | `call label` | `bl label` | -| Jump | `j label` | `b label` | -| Load immediate | `li rd, imm` | `mov xd, #imm` | -| Shift left | `slli rd, rs, n` | `lsl xd, xs, #n` | -| Return address | `ra` (x1) | `x30` (LR) | -| Syscall number | `a7` (x17) | `x8` | - -## Linker Script - -The `linker.ld` file defines the memory layout: -- **Base address**: 0x80000000 -- **Memory size**: 64MB RAM -- **Sections**: `.text`, `.rodata`, `.data`, `.bss` -- **Stack**: 1MB at end of RAM - -## Resources - -- [ARM Architecture Reference Manual](https://developer.arm.com/documentation/) -- [ARM Assembly Language](https://developer.arm.com/documentation/den0024/latest) -- [MapacheSPIM Documentation](../../docs/) -- [Examples Guide](../README.md) - -## Troubleshooting - -### Toolchain Not Found -``` -ERROR: aarch64-elf-as not found -``` -**Solution**: Install the ARM64 toolchain (see Prerequisites) - -### Wrong Toolchain Prefix -If you have a different toolchain (e.g., `aarch64-linux-gnu-`), override the PREFIX: -```bash -make PREFIX=aarch64-linux-gnu- -``` diff --git a/examples/arm/linker.ld b/examples/arm/linker.ld index 369d8be..37e5de1 100644 --- a/examples/arm/linker.ld +++ b/examples/arm/linker.ld @@ -1,5 +1,5 @@ /* ============================================================================ - * ARM64 Linker Script for Bare-Metal Programs (Sail Emulator) + * ARM64 Linker Script for Bare-Metal Programs (GNU ld) * ============================================================================ */ diff --git a/examples/mips/linker.ld b/examples/mips/linker.ld index 34cc806..c1f3e6f 100644 --- a/examples/mips/linker.ld +++ b/examples/mips/linker.ld @@ -1,5 +1,5 @@ /* ============================================================================ - * MIPS Linker Script for Bare-Metal Programs (Unicorn Emulator) + * MIPS Linker Script for Bare-Metal Programs (GNU ld) * ============================================================================ * * Uses traditional MIPS user-space memory layout: diff --git a/examples/riscv/fibonacci/fibonacci b/examples/riscv/fibonacci/fibonacci index 759c0087538000a18c81c1e72f3fa9b8a80c59da..61a3e5b05a278e57f9589cb04e84b0ad20437d0d 100644 GIT binary patch delta 360 zcmeys_k(MKhLQy{0~|OoSTHa#I52QBurV+KMOlG(1H(iwGe(Drjryk3nFJUnH!(3V zFJ)qQ&v1YV!guL@7aD99%lv#H>77fsJkQI%b2( zYAiyM3P8gdfUas_03ig>pB%^{&S*P%CQClk28PL8tagkJlRa6rCx2lTVe(*{%*Ce0 zC_VWjt2k!_$V)(AJz0=Vo>3hvHW@0G2o^J*ypc_u^9NMS7c6EA7V}_&=yd^$xlT4@ Xmr#cat$@fdumfo}Am)JbC7?6_9K1ki literal 1904 zcmb_cy>HV{5Wi(Kmez#eoB;r~2nef|o&I5uO> zLfzcnZ_Pi41apN}#UebkURrgwdC=YfO~6gLJ~8h(Z2ChFt& z;)cTQ^UGgy$@!d+AkCEn5SMTr+0{zBp(HJs{e1Klch?|2}@67Onwiukm6i3$;U zKAN9MOhs>xl!B~!ztlS=$M}j>pDf)!%0ZPCBKAYQV|0hcg}!)PE}=s)C132r(Yqwy zEydCK(>Or$(@78_>Z$lk3YJ#qUqxJ;B`r`U)TTcXd+QXwE&1ZSl)3e}@O+)fNzqRv z-z%xSr}MjrmbEK`N0R@lgm_QqKR$(T%cL7;@Sowhoa$KQj^sOX!bu#Zrp`k{M*jyc C4XF$O diff --git a/examples/riscv/fibonacci/fibonacci.s b/examples/riscv/fibonacci/fibonacci.s index e40e7d2..dd83ce7 100644 --- a/examples/riscv/fibonacci/fibonacci.s +++ b/examples/riscv/fibonacci/fibonacci.s @@ -27,20 +27,10 @@ fib_result: .word 0 -.section .bss - # Stack space (4KB) - .align 4 - _stack_bottom: - .space 4096 - _stack_start: - .section .text .globl _start _start: - # Initialize stack pointer (required for recursive calls) - la sp, _stack_start # sp = top of stack - # Load the input value la t0, fib_input lw a0, 0(t0) # a0 = n (input argument) @@ -52,14 +42,18 @@ _start: la t0, fib_result sw a0, 0(t0) # save result to memory - # Exit program using HTIF (write to tohost) - li t0, 1 # exit code 1 (success) - la t1, tohost # load address of tohost - sd t0, 0(t1) # write to tohost to signal exit + # Print result (it is already in a0, the print_int argument) + li a7, 1 # syscall 1 = print_int + ecall -exit_loop: - # Infinite loop - emulator will detect tohost write and stop - j exit_loop + # Print newline + li a0, 10 + li a7, 11 # syscall 11 = print_char + ecall + + # Exit via syscall + li a7, 10 # syscall 10 = exit + ecall # ============================================================================ # Function: fibonacci @@ -76,9 +70,9 @@ exit_loop: # Saved: ra (return address), s0 (saved register) # # Stack frame layout (when needed): -# sp+0: saved s0 (saved register for intermediate results) -# sp+8: saved ra (return address) -# sp+16: saved a0 (original n value) +# sp+0: saved a0 (original n value) +# sp+8: saved s0 (saved register for intermediate results) +# sp+16: saved ra (return address) # Total: 24 bytes # ============================================================================ fibonacci: @@ -151,12 +145,3 @@ base_case_one: # Fibonacci sequence reference: # F(0)=0, F(1)=1, F(2)=1, F(3)=2, F(4)=3, F(5)=5, F(6)=8, F(7)=13 # ============================================================================ - -# HTIF (Host-Target Interface) section for Sail emulator -.section .tohost,"aw",@progbits -.align 6 -.globl tohost -tohost: .dword 0 -.align 6 -.globl fromhost -fromhost: .dword 0 diff --git a/examples/riscv/linker.ld b/examples/riscv/linker.ld index bb6f17d..97db26f 100644 --- a/examples/riscv/linker.ld +++ b/examples/riscv/linker.ld @@ -1,5 +1,5 @@ /* ============================================================================ - * RISC-V Linker Script for Bare-Metal Programs (Sail Emulator) + * RISC-V Linker Script for Bare-Metal Programs (GNU ld) * ============================================================================ */ @@ -33,12 +33,6 @@ SECTIONS *(COMMON) } > RAM - /* HTIF at fixed address - critical for Sail emulator */ - . = 0x80001000; - .tohost ALIGN(64) : { - *(.tohost) - } > RAM - /* Stack at end of memory */ . = ORIGIN(RAM) + LENGTH(RAM) - 1M; _stack_start = .; diff --git a/examples/riscv/matrix_multiply/matrix_mult b/examples/riscv/matrix_multiply/matrix_mult index aabd170c6daba27b2a3014de9eb390fcffb35829..b07126009ea2fd16737cd76e60abc0f8e97c8b09 100644 GIT binary patch delta 511 zcmeAW`oTLv!*K@-0~|OoSTHa#I52QBurV+KMOlG(16-D&0m=l^Z=jqv6Bmjz&0(C_ zq^Bs(D!?Gj%E~YuNKbA($|%kTLgzh^kWFqvZkL-FQNMsvo=SC}-I-Y`u5$E3_y zI$4R?id9EPPeE>S3bPHjUP)?234>lrVoBoUoy@M28Cj$!JF)~$Uc{n2`2>q!JtKn( z1Kb&42k3xFMg|WwadW762%5MvR6GSu+z%?g22DHyDt-ne4zwH&lA#h;&@|*uHe}1^ zf`uB;pR3r+fZ;Rw5u4WJ6ebZyj>!wz)fuHHCo+jMN=$wTWUGVNoS{q*qbnz`WS3_( z26C7u7cvQRUVw`Gg2ip&;tI?V^(A0&S0IN8q+VkNR9pcRfk40x#N0s40pb9G1cYJ$ E06(fn=l}o! literal 2312 zcmcgt&rcIU6n@jCrC=Qy&;NVG(95gYa@6Ehn%Oc@oeCf=4?|bi?nVmPCxifm{ zLQDwI5`#@(FCc7$I+U@5($tT@5Hw+b0Fp8{j#wrfBA^_UK=vAE-6TPfJQ^|+V*t{D zvXm|{taX(4V4K&)Jgu3SR=SZ#cF5~yp3-AX(^~U7+q=NT{ppOS!Hh5cevG=lG%7k1 z=qD1!7YIZ>mWEEF8%(42E1SCtE{=tqt!!=)vV#vHJJc((XWPMSibMTg9L$yk)N%dw zwj4~Xe`_%fw#PS{%@o?&f43Q@oVDd|#@h02s6Vq{EwwIcuTqjbu(D|kw7Fm&?T3Mt z4Po@R!P?4aV<5F9>NOMGuOEJf{0j5mi|p!KVHz_2qkgvo^PYb<1_jgx7r_`7qLy0} zxc=BQ?huuYwi=g&H3y(tQ{9@eC9$<|BbzXaQ4Zx2hq(F(p!r`!v_m@4E;@vfJVEOk z#-@%-QZFTt3lSi7+4if&`Fwe<*fXWj>E z()Mkz3Z936JL7r;Ifc3Dyj>_%og3OKR;FBSmx>h!EN{m1tG-=8OY4O)`7Hbqw*H!e zHYOnN`S!&1e8Kg7x2!$Hsy;aLML%D1-C3PYn^&AIEr2`cJ5>%9CnrmevWu09hMme} zUb;Y5StdzSQh`5js_K^IchV|w#(hK;mUds?BZ|-}RSb?taFg+|2!2H3baHpzC#N)? z-ie=)xPU=@-$@_fJmW(V{4(R$A~^ae0%jsO`Vz-u7)X;lPig^gBKR%FKSuETjDL#Y zj}T7-{af{YC==l+<0BFL1>zm*A04R7f74U_sL}nw5|GZQ!m1z#NYRQLh3+0orOGK$ z=Rm(N`W?~HDE}Y^lGsANs`K`H!SBgl{0pWm& z`dzsv_uy|Z|6Jpsf62x3yK79M^zw$>B&R{5YaPK@xOZXVDud%8B&{g_;vRfm=$HHO cuV6gf`!M-6Z}bzMklrhNVr8u None: Usage: step [n] - Executes n instructions (default 1) and displays the program - counter for each step. If a breakpoint is hit, execution stops. + Executes n instructions (default 1), showing each one before it + runs. If a breakpoint is hit, execution stops. Arguments: n - Number of instructions to execute (optional, default=1) @@ -418,11 +418,9 @@ def do_step(self, arg: str) -> None: step 5 # Execute 5 instructions s 10 # Execute 10 instructions (using alias) - Tips: - - Use 'step 1' to carefully trace through code - - Use 'step 10' to quickly skip over known-good code - - After stepping, use 'regs' to see register changes - - Set breakpoints before stepping to stop at key locations + Pressing Enter on an empty line repeats the last command, so after + one 'step' each Enter steps again. Use 'stepreg' to see the + registers after each step. """ if not self._require_program(): return @@ -617,7 +615,7 @@ def do_reset(self, arg: str) -> None: except Exception as e: self.print_error(f"Error reloading {self.loaded_file}: {e}") return - self.prev_regs = self.sim.get_all_regs() + self.prev_regs = self._reg_snapshot() print( f"Reset {self.loaded_file}. PC = {self.sim.get_pc():#018x}", file=self.stdout, @@ -740,6 +738,13 @@ def _format_reg_value(self, value: int, show_mode: str, leading_zeros_mode: str) return sign + prefix + formatted + def _reg_snapshot(self) -> List[int]: + """Every register value 'regs' shows except the PC: the general-purpose + registers, then any special registers, then the flags""" + flags = self.sim.get_flags() + extra = [flags] if flags is not None else [] + return self.sim.get_all_regs() + self.sim.get_special_regs() + extra + def _change_marker(self) -> str: """Marker for changed registers; '*' where the terminal can't show ★""" encoding = getattr(self.stdout, "encoding", None) or "utf-8" @@ -758,8 +763,10 @@ def do_regs(self, arg: str) -> None: Shows all general-purpose registers with their ABI names plus the program counter (PC). Register count and names vary by ISA: - RISC-V: 32 registers (x0-x31) - - ARM64: 32 registers (x0-x30, sp) - - x86-64: 16 registers (rax, rcx, rdx, etc.) + - MIPS: 32 registers ($zero-$ra), plus hi and lo (set by mult and div) + - ARM64: 32 registers (x0-x30, sp), plus the nzcv flags + - x86-64: 16 registers (rax, rcx, rdx, etc.), plus the rflags flags + CF, ZF, SF, and OF Display format can be controlled with arguments or 'set' command. @@ -788,6 +795,9 @@ def do_regs(self, arg: str) -> None: - Use 'regs peek' to view without resetting the change baseline - Use 'status' to see current ISA """ + if not self._require_program(): + return + # Parse arguments for temporary overrides and peek mode show_mode = self.regs_base leading_zeros_mode = self.regs_leading_zeros @@ -810,7 +820,7 @@ def do_regs(self, arg: str) -> None: return self._print_block_start() - regs = self.sim.get_all_regs() + regs = self._reg_snapshot() pc = self.sim.get_pc() # Determine the width needed for values based on format @@ -823,49 +833,50 @@ def do_regs(self, arg: str) -> None: else: value_width = 18 - # Format registers in 2 columns (or 1 if binary is too wide) - cols = 1 if show_mode == "binary" else 2 + def star(index: int) -> str: + """The change marker for regs[index], if it changed since last shown""" + changed = ( + self.show_reg_changes + and self.prev_regs is not None + and index < len(self.prev_regs) + and self.prev_regs[index] != regs[index] + ) + return f" {self._change_marker()} " if changed else " " + + # Label each general-purpose register by ISA, e.g. "x5 (t0)" or "rax", + # then the special registers (MIPS hi and lo) by name num_regs = self.sim.get_register_count() - prefix = self.sim.spec.registers.number_prefix if self.sim.get_isa() is not None else None + registers = self.sim.spec.registers + labels = [] + for reg_num in range(num_regs): + abi_name = self.sim.get_reg_name(reg_num) + if registers.number_prefix: + labels.append(f"{registers.number_prefix}{reg_num:<2} ({abi_name:>4})") + else: + labels.append(f"{abi_name:>3}") + labels += [f"{name:>3}" for name in registers.special] - reg_lines = [] - for i in range(0, num_regs, cols): + # Format registers in 2 columns (or 1 if binary is too wide) + cols = 1 if show_mode == "binary" else 2 + for i in range(0, len(labels), cols): line_parts = [] - for j in range(cols): - if i + j < num_regs: - reg_num = i + j - abi_name = self.sim.get_reg_name(reg_num) - value = regs[reg_num] - - # Format the value - formatted_value = self._format_reg_value(value, show_mode, leading_zeros_mode) - - # Check if this register changed with the last instruction - star = ( - f" {self._change_marker()} " - if ( - self.show_reg_changes - and self.prev_regs is not None - and reg_num < len(self.prev_regs) - and self.prev_regs[reg_num] != value - ) - else " " - ) - - # Format register name based on ISA - if prefix: # e.g. "x5 (t0)" - label = f"{prefix}{reg_num:<2} ({abi_name:>4})" - else: # just the name, e.g. "rax" - label = f"{abi_name:>3}" - line_parts.append(f"{label} = {formatted_value:<{value_width}}{star}") - reg_lines.append(" ".join(line_parts)) - - for line in reg_lines: - print(line, file=self.stdout) + for index in range(i, min(i + cols, len(labels))): + formatted_value = self._format_reg_value(regs[index], show_mode, leading_zeros_mode) + line_parts.append( + f"{labels[index]} = {formatted_value:<{value_width}}{star(index)}" + ) + print(" ".join(line_parts), file=self.stdout) # Format PC formatted_pc = self._format_reg_value(pc, show_mode, leading_zeros_mode) print(f"\npc = {formatted_pc}", file=self.stdout) + + # The flags, one bit each, e.g. "nzcv: N=0 Z=1 C=1 V=0" + if registers.flags is not None: + flags = regs[-1] + bits = " ".join(f"{name}={(flags >> bit) & 1}" for name, bit in registers.flags.bits) + marker = star(len(regs) - 1).rstrip() + print(f"{registers.flags.name}: {bits}{marker}", file=self.stdout) self._print_block_end() # Update the snapshot for change tracking (unless peek mode) @@ -887,10 +898,13 @@ def do_pc(self, arg: str) -> None: pc # See new PC value Tips: - - PC increments by 4 for each instruction (32-bit encoding) + - PC advances by the size of each instruction (always 4 bytes on + RISC-V, MIPS, and ARM64; 1 to 15 bytes on x86-64) - Jump/branch instructions change PC non-sequentially - Use 'mem ' to see instructions at PC """ + if not self._require_program(): + return pc = self.sim.get_pc() print(f"pc = {pc:#018x}", file=self.stdout) diff --git a/mapachespim/engines.py b/mapachespim/engines.py index 6d143f5..f702574 100644 --- a/mapachespim/engines.py +++ b/mapachespim/engines.py @@ -8,7 +8,7 @@ from __future__ import annotations from dataclasses import dataclass -from typing import Dict, Tuple +from typing import Dict, Optional, Tuple try: import unicorn @@ -44,6 +44,12 @@ class EngineBinding: cs_arch: int cs_mode: int + special_regs: Tuple[int, ...] = () + """Unicorn register constant for each of the ISA's ``RegisterFile.special`` registers.""" + + flags_reg: Optional[int] = None + """Unicorn register constant for the ISA's ``RegisterFile.flags`` register.""" + ENGINES: Dict[ISA, EngineBinding] = { ISA.RISCV: EngineBinding( @@ -63,6 +69,7 @@ class EngineBinding: gpr_regs=tuple(getattr(mips_const, f"UC_MIPS_REG_{n}") for n in range(32)), cs_arch=capstone.CS_ARCH_MIPS, cs_mode=capstone.CS_MODE_MIPS32 | capstone.CS_MODE_BIG_ENDIAN, + special_regs=(mips_const.UC_MIPS_REG_HI, mips_const.UC_MIPS_REG_LO), ), ISA.ARM: EngineBinding( uc_arch=unicorn.UC_ARCH_ARM64, @@ -75,6 +82,7 @@ class EngineBinding: + (arm64_const.UC_ARM64_REG_SP,), cs_arch=capstone.CS_ARCH_ARM64, cs_mode=capstone.CS_MODE_ARM, + flags_reg=arm64_const.UC_ARM64_REG_NZCV, ), ISA.X86_64: EngineBinding( uc_arch=unicorn.UC_ARCH_X86, @@ -87,6 +95,7 @@ class EngineBinding: ), cs_arch=capstone.CS_ARCH_X86, cs_mode=capstone.CS_MODE_64, + flags_reg=x86_const.UC_X86_REG_RFLAGS, ), } diff --git a/mapachespim/isa.py b/mapachespim/isa.py index edf1a44..89e0051 100644 --- a/mapachespim/isa.py +++ b/mapachespim/isa.py @@ -35,6 +35,17 @@ class ISA(IntEnum): UNKNOWN = -1 +@dataclass(frozen=True) +class StatusFlags: + """A condition-code register, shown by the flags it holds rather than its raw value.""" + + name: str + """Name of the register (e.g. "rflags").""" + + bits: Tuple[Tuple[str, int], ...] + """(flag name, bit number) of each flag to show, in display order.""" + + @dataclass(frozen=True) class RegisterFile: """The general-purpose registers, numbered as the simulator API uses them.""" @@ -48,6 +59,12 @@ class RegisterFile: number_prefix: Optional[str] = None """Prefix for numbered display, e.g. "x" shows "x5 (t0)"; None shows only the name.""" + special: Tuple[str, ...] = () + """Other registers that instructions write and ``regs`` shows (e.g. MIPS hi and lo).""" + + flags: Optional[StatusFlags] = None + """The condition codes that compare and branch instructions use, if the ISA has them.""" + @property def count(self) -> int: return len(self.names) @@ -252,6 +269,7 @@ def _arm64_trap(word: int) -> Optional[str]: registers=RegisterFile( names=_MIPS_REGISTERS, zero_register=0, + special=("hi", "lo"), # results of mult and div ), syscall_abi=SyscallABI(number=2, arg0=4, result=2), # $v0, $a0, $v0 syscall_instruction=InstructionPattern(4, 0xFFFFFFFF, 0x0000000C), # syscall @@ -276,6 +294,7 @@ def _arm64_trap(word: int) -> Optional[str]: registers=RegisterFile( names=tuple(f"x{n}" for n in range(31)) + ("sp",), number_prefix="x", + flags=StatusFlags("nzcv", (("N", 31), ("Z", 30), ("C", 29), ("V", 28))), ), syscall_abi=SyscallABI(number=8, arg0=0, result=0), # x8, x0, x0 syscall_instruction=InstructionPattern(4, 0xFFE0001F, 0xD4000001), # svc #imm @@ -296,6 +315,7 @@ def _arm64_trap(word: int) -> Optional[str]: layout=X86_64_LAYOUT, registers=RegisterFile( names=_X86_64_REGISTERS, + flags=StatusFlags("rflags", (("CF", 0), ("ZF", 6), ("SF", 7), ("OF", 11))), ), syscall_abi=SyscallABI(number=0, arg0=7, result=0), # rax, rdi, rax syscall_instruction=InstructionPattern(2, 0xFFFF, 0x050F), # syscall (0f 05) diff --git a/mapachespim/unicorn_backend.py b/mapachespim/unicorn_backend.py index 928cba8..abf60ef 100644 --- a/mapachespim/unicorn_backend.py +++ b/mapachespim/unicorn_backend.py @@ -523,6 +523,20 @@ def get_all_regs(self) -> List[int]: """All general-purpose register values, by register number""" return [self.get_reg(i) for i in range(self.get_register_count())] + def get_special_regs(self) -> List[int]: + """Values of the ISA's other registers (e.g. MIPS hi and lo), in the + order of ``spec.registers.special``""" + if self._uc is None: + return [] + return [self._uc.reg_read(reg) for reg in self._engine.special_regs] + + def get_flags(self) -> Optional[int]: + """The condition-code register (e.g. x86 rflags, ARM64 nzcv), or None if + the ISA has none; ``spec.registers.flags`` says which bits are which""" + if self._uc is None or self._engine.flags_reg is None: + return None + return self._uc.reg_read(self._engine.flags_reg) + # --- Memory --- def read_mem(self, addr: int, length: int) -> bytes: diff --git a/tests/test_program_correctness.py b/tests/test_program_correctness.py index 8db0362..3161ce4 100644 --- a/tests/test_program_correctness.py +++ b/tests/test_program_correctness.py @@ -6,14 +6,17 @@ actually executes programs correctly and produces the right answers! """ +import os import sys +import tempfile import unittest from pathlib import Path # Add parent directory to path sys.path.insert(0, str(Path(__file__).parent.parent)) -from mapachespim import Simulator, StepResult +from mapachespim import Simulator, StepResult, StopReason +from mapachespim.toolchain import assemble class TestFibonacciCorrectness(unittest.TestCase): @@ -107,17 +110,36 @@ def test_fibonacci_completes_not_timeout(self): class TestToHostMechanism(unittest.TestCase): - """Test HTIF (Host-Target Interface) tohost/fromhost mechanism""" - - FIBONACCI_PATH = 'examples/riscv/fibonacci/fibonacci' + """Test HTIF (Host-Target Interface) tohost/fromhost mechanism, which some + RISC-V test programs use to exit instead of a syscall""" + + SOURCE = """ +.text +.globl _start +_start: + li t0, 1 # exit code 1 (success) + la t1, tohost + sd t0, 0(t1) # write to tohost to signal exit +exit_loop: + j exit_loop + +.section .tohost,"aw",@progbits +.align 6 +.globl tohost +tohost: .dword 0 +""" def setUp(self): self.sim = Simulator() + result = assemble(self.SOURCE, "riscv64") + self.assertTrue(result.success, result.errors) + with tempfile.NamedTemporaryFile(delete=False) as f: + f.write(result.elf_bytes) + self.addCleanup(os.unlink, f.name) + self.sim.load_elf(f.name) def test_tohost_written_on_exit(self): """Verify program writes to tohost to signal completion""" - self.sim.load_elf(self.FIBONACCI_PATH) - # Get tohost address tohost_addr = self.sim.lookup_symbol('tohost') self.assertIsNotNone(tohost_addr, "Could not find 'tohost' symbol") @@ -127,33 +149,15 @@ def test_tohost_written_on_exit(self): tohost_value_before = int.from_bytes(tohost_before, byteorder='little', signed=False) self.assertEqual(tohost_value_before, 0, "tohost should be 0 initially") - # Run program - steps = self.sim.run(max_steps=10000) - self.assertLess(steps, 10000, "Program should complete") + # Run program: the simulator stops at the tohost write rather than + # spinning in exit_loop + result = self.sim.run_until(10000) + self.assertEqual(result.reason, StopReason.TOHOST) - # After running, tohost should be non-zero (exit code written) + # After running, tohost holds the exit code tohost_after = self.sim.read_mem(tohost_addr, 8) tohost_value_after = int.from_bytes(tohost_after, byteorder='little', signed=False) - - self.assertNotEqual(tohost_value_after, 0, - "tohost should be non-zero after program exit (exit code should be written)") - - print(f" ✓ tohost written: {tohost_value_after:#x}") - - def test_tohost_exit_code(self): - """Verify exit code written to tohost""" - self.sim.load_elf(self.FIBONACCI_PATH) - - tohost_addr = self.sim.lookup_symbol('tohost') - self.sim.run(max_steps=10000) - - tohost_bytes = self.sim.read_mem(tohost_addr, 8) - tohost_value = int.from_bytes(tohost_bytes, byteorder='little', signed=False) - - # fibonacci.s writes exit code 1 to tohost - # The actual value might be encoded (HTIF protocol) - # For now, just verify it's non-zero - self.assertGreater(tohost_value, 0, "Exit code should be positive") + self.assertEqual(tohost_value_after, 1) class TestMatrixMultiplyCorrectness(unittest.TestCase): diff --git a/tests/test_simulator_parts.py b/tests/test_simulator_parts.py index 630b8b4..e55e808 100644 --- a/tests/test_simulator_parts.py +++ b/tests/test_simulator_parts.py @@ -72,7 +72,7 @@ def _sim(self): def test_runs_to_exit(self, capsys): sim = self._sim() result = sim.run_until() - assert result.reason == StopReason.TOHOST # fibonacci signals completion via tohost + assert result.reason == StopReason.EXIT assert result.steps > 10 def test_step_limit(self): diff --git a/tests/test_special_registers.py b/tests/test_special_registers.py new file mode 100644 index 0000000..a7f65f1 --- /dev/null +++ b/tests/test_special_registers.py @@ -0,0 +1,100 @@ +""" +The registers 'regs' shows beyond the general-purpose ones: MIPS hi/lo and +the condition flags of x86-64 (rflags) and ARM64 (nzcv). +""" + +import io + +import pytest + +from mapachespim.console import MapacheSPIMConsole + + +def run_regs(tmp_path, source, steps): + """Load source, step it, and return the output of 'regs'""" + path = tmp_path / "prog.s" + path.write_text(source) + console = MapacheSPIMConsole(verbose=False) + console.stdout = io.StringIO() + console.onecmd(f"load {path}") + console.onecmd(f"step {steps}") + console.stdout = io.StringIO() + console.onecmd("regs") + return console, console.stdout.getvalue() + + +MIPS_MULT_DIV = """ +.isa mips32 +.text +_start: + li $t0, 6 + li $t1, 7 + mult $t0, $t1 + li $t2, 20 + div $t2, $t0 +""" + + +def test_mips_hi_lo_after_mult(tmp_path): + console, out = run_regs(tmp_path, MIPS_MULT_DIV, 3) + assert console.sim.get_special_regs() == [0, 42] + assert " lo = 0x000000000000002a ★" in out + + +def test_mips_hi_lo_after_div(tmp_path): + console, out = run_regs(tmp_path, MIPS_MULT_DIV, 5) + assert console.sim.get_special_regs() == [2, 3] # remainder, quotient + assert " hi = 0x0000000000000002 ★" in out + assert " lo = 0x0000000000000003 ★" in out + + +@pytest.mark.parametrize( + "compare_with, expected", + [ + (3, "nzcv: N=0 Z=1 C=1 V=0"), # equal + (5, "nzcv: N=1 Z=0 C=0 V=0"), # less than + ], +) +def test_arm64_nzcv(tmp_path, compare_with, expected): + source = f".isa arm64\n.text\n_start:\n mov x0, #3\n cmp x0, #{compare_with}\n" + _, out = run_regs(tmp_path, source, 2) + assert expected in out + + +@pytest.mark.parametrize( + "instructions, expected", + [ + ("movq $3, %rax\n cmpq $3, %rax", "rflags: CF=0 ZF=1 SF=0 OF=0"), + ("movq $3, %rax\n cmpq $5, %rax", "rflags: CF=1 ZF=0 SF=1 OF=0"), + ("movq $0x7fffffffffffffff, %rax\n addq $1, %rax", "rflags: CF=0 ZF=0 SF=1 OF=1"), + ], +) +def test_x86_rflags(tmp_path, instructions, expected): + source = f".isa x86_64\n.text\n_start:\n {instructions}\n" + _, out = run_regs(tmp_path, source, 2) + assert expected in out + + +def test_flags_marked_only_when_changed(tmp_path): + source = ".isa x86_64\n.text\n_start:\n cmpq $0, %rax\n movq $1, %rbx\n" + console, out = run_regs(tmp_path, source, 1) + assert "ZF=1 SF=0 OF=0 ★" in out + console.onecmd("step") + console.stdout = io.StringIO() + console.onecmd("regs") + assert "ZF=1 SF=0 OF=0\n" in console.stdout.getvalue() + + +def test_riscv_has_no_flags_or_special_registers(tmp_path): + source = ".isa riscv64\n.text\n_start:\n li a0, 1\n" + console, out = run_regs(tmp_path, source, 1) + assert console.sim.get_special_regs() == [] + assert console.sim.get_flags() is None + assert "flags" not in out and "nzcv" not in out + + +def test_regs_without_a_program(): + console = MapacheSPIMConsole(verbose=False) + console.stdout = io.StringIO() + console.onecmd("regs") + assert "No program loaded" in console.stdout.getvalue() From 70ce81ac6db941ea1d93cbb37db8c7f7c1842f91 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 27 Sep 2026 21:47:50 +0000 Subject: [PATCH 2/2] Show source lines when stepping; size MIPS32 values; match x86 syntax step prints each source line before the machine instructions it assembled to, so pseudo-instructions like la are visibly one line becoming several instructions. MIPS32 registers, the PC, and addresses in console messages are shown at their real width (8 hex digits, 32 binary digits, 11-character decimal) instead of padded to 64 bits. x86-64 instructions are disassembled in the syntax the source used. syntax_by_line() reuses the assembler's per-instruction AT&T/Intel decision (including .att_syntax/.intel_syntax); instructions that read the same either way, and ELF files without source, fall back to the program's usual syntax (Intel when there is no source). Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 5 + README.md | 6 +- docs/user/console-guide.md | 17 +++- docs/user/quick-start.md | 10 +- mapachespim/console.py | 129 +++++++++++++++++------- mapachespim/disassembler.py | 17 +++- mapachespim/toolchain/x86.py | 43 +++++++- mapachespim/unicorn_backend.py | 15 +-- tests/test_special_registers.py | 6 +- tests/test_step_display.py | 170 ++++++++++++++++++++++++++++++++ 10 files changed, 357 insertions(+), 61 deletions(-) create mode 100644 tests/test_step_display.py diff --git a/CHANGELOG.md b/CHANGELOG.md index ceb839b..7f2544b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,11 @@ x86-64 (`rflags: CF ZF SF OF`), with ★ when they change. New `Simulator.get_special_regs()` and `get_flags()`. - `regs` and `pc` ask you to load a program first instead of showing an unset machine. +- `step` shows each source line before the machine instructions it assembled to. +- MIPS32 registers and addresses are shown at their real width: 8 hex digits (and 32 bits in + `regs binary`) instead of 16. +- x86-64 instructions are disassembled in the syntax they were written in, AT&T or Intel, by + `step`, `disasm`, and error messages (Intel for ELF files without source). ### Examples and docs - The RISC-V `fibonacci` and `matrix_multiply` examples exit with the exit syscall, like the diff --git a/README.md b/README.md index 999e79b..e9dc629 100644 --- a/README.md +++ b/README.md @@ -100,9 +100,11 @@ fibonacci.s: 84: beq a0, t0, base_case_one (mapachespim) step +fibonacci.s:80: beqz a0, base_case_zero [0x80000038] 0x63040504 beqz a0, 0x48 (mapachespim) step +fibonacci.s:83: li t0, 1 [0x8000003c] 0x93021000 addi t0, zero, 1 (mapachespim) regs @@ -202,6 +204,7 @@ The standalone assembler writes an ELF file if you want one: `mapachespim-as -g ### Enhanced Step Display Every step shows: +- The source line, when a new one starts (`fibonacci.s:83: li t0, 1`) - The address (`0x80000030`) - The instruction bytes (`0x13050005`) - The disassembly (`addi x10, x0, 0x5`) @@ -290,7 +293,8 @@ relaxation (x86-64 short and long jumps). This is checked continuously by random tests that assemble hundreds of thousands of instructions with both and compare the results. x86-64 accepts both AT&T syntax (`movq $1, %rax`) and Intel syntax (`mov rax, 1`), detected per -instruction, as well as the `.intel_syntax` / `.att_syntax` directives. +instruction, as well as the `.intel_syntax` / `.att_syntax` directives. `step` and `disasm` show +each instruction in the syntax it was written in. Loading an ELF file detects its ISA automatically. diff --git a/docs/user/console-guide.md b/docs/user/console-guide.md index 68bb9f8..737f58f 100644 --- a/docs/user/console-guide.md +++ b/docs/user/console-guide.md @@ -47,7 +47,8 @@ mapachespim riscv/fibonacci ``` ### Execution -- `step [n]` - Execute 1 or n instructions (alias: `s`) +- `step [n]` - Execute 1 or n instructions (alias: `s`). Each source line is shown before the + machine instructions it assembled to (a pseudo-instruction like `la` may become several). ``` (mapachespim) step # Execute 1 instruction (mapachespim) step 10 # Execute 10 instructions @@ -173,6 +174,19 @@ mapachespim riscv/fibonacci Programs loaded from `.s` files always have source info. +- `disasm [addr|label] [n]` - Disassemble n instructions (default 10) from the PC or the given + place (alias: `d`). A `>` marks the PC. On x86-64, instructions are shown in the syntax they + were written in, AT&T or Intel (Intel for ELF files without source). + For example, with `riscv/fibonacci` loaded: + ``` + (mapachespim) disasm fibonacci 4 + + [0x80000038] beqz a0, 0x48 + [0x8000003c] addi t0, zero, 1 + [0x80000040] beq a0, t0, 0x48 + [0x80000044] addi sp, sp, -0x18 + ``` + ### Breakpoints - `break ` - Set breakpoint at an address or label (alias: `b`) ``` @@ -251,6 +265,7 @@ Breakpoint hit at 0x0000000080000038 after 4 instructions PC = 0x0000000080000038 (mapachespim) step +fibonacci.s:80: beqz a0, base_case_zero [0x80000038] 0x63040504 beqz a0, 0x48 (mapachespim) regs diff --git a/docs/user/quick-start.md b/docs/user/quick-start.md index 0bf1e70..68ac352 100644 --- a/docs/user/quick-start.md +++ b/docs/user/quick-start.md @@ -76,15 +76,17 @@ The `>` marks the line the program counter (PC) is on. Step through a few instru ``` (mapachespim) step 3 +hello_asm.s:61: la a0, hello_msg # la = "Load Address" - puts address of hello_msg into a0 [0x80000000] 0x17051000 auipc a0, 0x100 <_start> [0x80000004] 0x13050500 mv a0, a0 <_start+4> +hello_asm.s:62: li a7, 4 # li = "Load Immediate" - puts the value 4 into a7 [0x80000008] 0x93084000 addi a7, zero, 4 <_start+8> ``` -Each line shows the address, the instruction's bytes, the instruction, and where it is relative to -the nearest label. Notice that the single `la` in the source became two machine instructions -(`auipc` and `addi`, which the disassembler shows as `mv` because its immediate is 0): `la` is a -*pseudo-instruction*. +Each source line is followed by the machine instructions it became. Each instruction shows its +address, its bytes, the instruction, and where it is relative to the nearest label. Notice that the +single `la` in the source became two machine instructions (`auipc` and `addi`, which the +disassembler shows as `mv` because its immediate is 0): `la` is a *pseudo-instruction*. Run the rest of the program: diff --git a/mapachespim/console.py b/mapachespim/console.py index 01c01eb..955448e 100644 --- a/mapachespim/console.py +++ b/mapachespim/console.py @@ -15,9 +15,9 @@ import tempfile from pathlib import Path from types import FrameType -from typing import Any, Dict, Generator, List, Optional, Set +from typing import Any, Dict, Generator, List, Optional, Set, Tuple -from . import Simulator, StopReason +from . import ISA, Simulator, StopReason from .debug_info import SourceInfo, parse_line_info from .isa import find_spec, isa_names @@ -55,6 +55,8 @@ class MapacheSPIMConsole(cmd.Cmd): regs_base: str regs_leading_zeros: str source_info: SourceInfo + _x86_syntax: Dict[Tuple[str, int], str] + _x86_default_att: bool _ALIASES: Dict[str, str] @@ -97,6 +99,10 @@ def __init__(self, verbose: bool = False) -> None: # Source code information (DWARF debug info) self.source_info = SourceInfo() + # x86-64: which syntax each source line is in, so disassembly matches it + self._x86_syntax = {} + self._x86_default_att = False + # Set up signal handler for Ctrl-C signal.signal(signal.SIGINT, self._handler_sigint) @@ -227,7 +233,7 @@ def _load_elf_file( pc = self.sim.get_pc() isa_name = self.sim.get_isa_name() print(f"Loaded {display_name} ({isa_name})", file=self.stdout) - print(f"Entry point: {pc:#018x}", file=self.stdout) + print(f"Entry point: {self._hex_word(pc)}", file=self.stdout) if not keep_breakpoints: self.breakpoints.clear() @@ -244,8 +250,31 @@ def _load_elf_file( ) else: print("Debug info present but source files not found", file=self.stdout) + self._find_x86_syntax() return True + def _find_x86_syntax(self) -> None: + """For x86-64, note whether each source line is AT&T or Intel syntax, so + instructions are disassembled in the syntax they were written in. + Instructions without a telling source line use the program's usual syntax.""" + self._x86_syntax = {} + self._x86_default_att = False + if self.sim.get_isa() != ISA.X86_64: + return + from .toolchain.x86 import syntax_by_line + + for filename, lines in self.source_info.source_cache.items(): + for line_num, syntax in syntax_by_line("\n".join(lines)).items(): + self._x86_syntax[(filename, line_num)] = syntax + att_lines = sum(1 for syntax in self._x86_syntax.values() if syntax == "att") + self._x86_default_att = att_lines > len(self._x86_syntax) - att_lines + + def _att_syntax_at(self, pc: int) -> bool: + """Whether to disassemble the instruction at pc in AT&T syntax (x86-64 only)""" + location = self.source_info.get_location(pc) + syntax = self._x86_syntax.get(location) if location is not None else None + return syntax == "att" if syntax is not None else self._x86_default_att + def _assemble_and_load( self, source: Path, @@ -433,17 +462,25 @@ def do_step(self, arg: str) -> None: return # Execute instructions + last_location = None for i in range(n_steps): pc = self.sim.get_pc() # Check for breakpoint (but skip if this is the first step and we're already at a breakpoint) if i > 0 and pc in self.breakpoints: - print(f"Breakpoint hit at {pc:#018x}", file=self.stdout) + print(f"Breakpoint hit at {self._hex_word(pc)}", file=self.stdout) break # Show the instruction before executing it, so any output it # produces (e.g. a print syscall) appears after it self._end_program_output() + # The source line, before the first of the instructions it assembled to + location = self.source_info.get_location(pc) + if location is not None and location != last_location: + source_line = self._format_source_line(location) + if source_line: + print(source_line, file=self.stdout) + last_location = location print(self._format_instruction(pc), file=self.stdout) self.stdout.flush() @@ -540,7 +577,7 @@ def do_run(self, arg: str) -> None: print(f"Interrupted after {result.steps} instructions", file=self.stdout) elif result.reason == StopReason.BREAKPOINT: print( - f"Breakpoint hit at {result.pc:#018x} after {result.steps} instructions", + f"Breakpoint hit at {self._hex_word(result.pc)} after {result.steps} instructions", file=self.stdout, ) elif result.reason is not None: @@ -549,7 +586,7 @@ def do_run(self, arg: str) -> None: print(f"Executed {result.steps} instructions (max limit reached)", file=self.stdout) self._end_program_output() if result.steps > 0 and not self.sim.exited: - print(f"PC = {self.sim.get_pc():#018x}", file=self.stdout) + print(f"PC = {self._hex_word(self.sim.get_pc())}", file=self.stdout) def _stop_before(self, pc: int) -> Optional[StopReason]: """Stop a run at a breakpoint or when the user pressed Ctrl-C""" @@ -617,7 +654,7 @@ def do_reset(self, arg: str) -> None: return self.prev_regs = self._reg_snapshot() print( - f"Reset {self.loaded_file}. PC = {self.sim.get_pc():#018x}", + f"Reset {self.loaded_file}. PC = {self._hex_word(self.sim.get_pc())}", file=self.stdout, ) @@ -626,7 +663,7 @@ def do_reset(self, arg: str) -> None: def _format_instruction(self, pc: int) -> str: """Format one instruction as '[addr] 0xbytes disasm '""" try: - text, size = self.sim.disasm_with_size(pc) + text, size = self.sim.disasm_with_size(pc, self._att_syntax_at(pc)) raw = self.sim.read_mem(pc, size) instr_hex = "".join(f"{b:02x}" for b in raw) except Exception: @@ -685,36 +722,51 @@ def _report_stop(self, reason: StopReason, pc: int, steps: Optional[int] = None) print(f"Error: {detail}", file=self.stdout) print(f" {self._format_instruction(pc)}", file=self.stdout) location = self.source_info.get_location(pc) - if location: - filename, line_num = location - lines = self.source_info.get_source_lines(filename, line_num, 1) - if lines: - print(f" {filename}:{line_num}: {lines[0][1].strip()}", file=self.stdout) + source_line = self._format_source_line(location) if location else None + if source_line: + print(f" {source_line}", file=self.stdout) + + def _format_source_line(self, location: Tuple[str, int]) -> Optional[str]: + """Format a source line as 'file.s:12: text', or None if it can't be read""" + filename, line_num = location + lines = self.source_info.get_source_lines(filename, line_num, 1) + if not lines: + return None + return f"{filename}:{line_num}: {lines[0][1].strip()}" # --- State Inspection --- + def _word_bits(self) -> int: + """Width of the loaded ISA's registers (32 for MIPS32, else 64)""" + return self.sim.spec.word_bits if self.sim.get_isa() is not None else 64 + + def _hex_word(self, value: int) -> str: + """A register-sized value in full-width hex: 8 digits on 32-bit ISAs, 16 on 64-bit""" + return f"{value:#0{self._word_bits() // 4 + 2}x}" + + def _decimal_width(self) -> int: + """Characters in the widest signed decimal register value (e.g. -2147483648)""" + return len(str(-(1 << (self._word_bits() - 1)))) + def _format_reg_value(self, value: int, show_mode: str, leading_zeros_mode: str) -> str: """Format a register value according to display settings""" + bits = self._word_bits() + value &= (1 << bits) - 1 sign = "" # only used by signed decimal - if show_mode == "hex": - # Format as hex with 0x prefix - formatted = f"{value:016x}" - prefix = "0x" - elif show_mode == "decimal": - # Signed decimal, since that is how students think of values - # like -1 (max 20 digits for 64-bit) - bits = self.sim.spec.word_bits if self.sim.get_isa() is not None else 64 + if show_mode == "decimal": + # Signed decimal, since that is how students think of values like -1 if value & (1 << (bits - 1)): value -= 1 << bits sign = "-" if value < 0 else "" - formatted = f"{abs(value):0{20 - len(sign)}d}" + formatted = f"{abs(value):0{self._decimal_width() - len(sign)}d}" prefix = "" elif show_mode == "binary": # Format as binary with 0b prefix - formatted = f"{value:064b}" + formatted = f"{value:0{bits}b}" prefix = "0b" else: - formatted = f"{value:016x}" + # Format as hex with 0x prefix + formatted = f"{value:0{bits // 4}x}" prefix = "0x" # Handle 'default' mode: show for hex/binary, dot for decimal @@ -824,14 +876,13 @@ def do_regs(self, arg: str) -> None: pc = self.sim.get_pc() # Determine the width needed for values based on format - if show_mode == "hex": - value_width = 18 # 0x + 16 hex digits - elif show_mode == "decimal": - value_width = 20 # max 20 decimal digits for 64-bit + bits = self._word_bits() + if show_mode == "decimal": + value_width = self._decimal_width() elif show_mode == "binary": - value_width = 66 # 0b + 64 binary digits + value_width = bits + 2 # 0b + digits else: - value_width = 18 + value_width = bits // 4 + 2 # 0x + digits def star(index: int) -> str: """The change marker for regs[index], if it changed since last shown""" @@ -874,9 +925,11 @@ def star(index: int) -> str: # The flags, one bit each, e.g. "nzcv: N=0 Z=1 C=1 V=0" if registers.flags is not None: flags = regs[-1] - bits = " ".join(f"{name}={(flags >> bit) & 1}" for name, bit in registers.flags.bits) + flag_bits = " ".join( + f"{name}={(flags >> bit) & 1}" for name, bit in registers.flags.bits + ) marker = star(len(regs) - 1).rstrip() - print(f"{registers.flags.name}: {bits}{marker}", file=self.stdout) + print(f"{registers.flags.name}: {flag_bits}{marker}", file=self.stdout) self._print_block_end() # Update the snapshot for change tracking (unless peek mode) @@ -906,7 +959,7 @@ def do_pc(self, arg: str) -> None: if not self._require_program(): return pc = self.sim.get_pc() - print(f"pc = {pc:#018x}", file=self.stdout) + print(f"pc = {self._hex_word(pc)}", file=self.stdout) def do_mem(self, arg: str) -> None: """Display memory contents in hex dump format @@ -1074,7 +1127,9 @@ def do_disasm(self, arg: str) -> None: instr_addr = addr for _ in range(count): try: - disasm, size = self.sim.disasm_with_size(instr_addr) + disasm, size = self.sim.disasm_with_size( + instr_addr, self._att_syntax_at(instr_addr) + ) except Exception as e: print(f"[{instr_addr:#010x}] ", file=self.stdout) break @@ -1375,9 +1430,9 @@ def do_delete(self, arg: str) -> None: return if addr in self.breakpoints: self.breakpoints.remove(addr) - print(f"Breakpoint removed at {addr:#018x}", file=self.stdout) + print(f"Breakpoint removed at {self._hex_word(addr)}", file=self.stdout) else: - print(f"No breakpoint at {addr:#018x}", file=self.stdout) + print(f"No breakpoint at {self._hex_word(addr)}", file=self.stdout) def do_clear(self, arg: str) -> None: """Clear all breakpoints @@ -1435,7 +1490,7 @@ def do_status(self, arg: str) -> None: isa_name = self.sim.get_isa_name() pc = self.sim.get_pc() print(f"ISA: {isa_name}", file=self.stdout) - print(f"PC: {pc:#018x}", file=self.stdout) + print(f"PC: {self._hex_word(pc)}", file=self.stdout) print(f"Breakpoints: {len(self.breakpoints)}", file=self.stdout) self._print_block_end() diff --git a/mapachespim/disassembler.py b/mapachespim/disassembler.py index f064d1a..2f4fd96 100644 --- a/mapachespim/disassembler.py +++ b/mapachespim/disassembler.py @@ -2,9 +2,9 @@ from __future__ import annotations -from typing import Tuple +from typing import Optional, Tuple -from capstone import Cs +from capstone import CS_ARCH_X86, CS_OPT_SYNTAX_ATT, Cs from .engines import engine_for from .isa import ISASpec @@ -18,16 +18,24 @@ def __init__(self, spec: ISASpec) -> None: self._spec = spec self._cs = Cs(engine.cs_arch, engine.cs_mode) self._cs.detail = False # We don't need detailed instruction info + self._cs_att: Optional[Cs] = None + if engine.cs_arch == CS_ARCH_X86: + self._cs_att = Cs(engine.cs_arch, engine.cs_mode) + self._cs_att.detail = False + self._cs_att.syntax = CS_OPT_SYNTAX_ATT @property def max_size(self) -> int: """Bytes needed to be sure of decoding any one instruction.""" return self._spec.max_instruction_size - def disassemble(self, code: bytes, addr: int) -> Tuple[str, int]: + def disassemble(self, code: bytes, addr: int, att: bool = False) -> Tuple[str, int]: """ Disassemble the instruction at the start of ``code`` (located at ``addr``). + On x86-64, ``att`` shows it in AT&T syntax (``movq $5, %rdi``) rather + than Intel syntax (``mov rdi, 5``); other ISAs ignore it. + Returns: (text, size). Bytes that do not decode are shown as ``.word`` (for fixed-width ISAs) or ``.byte`` (for variable-length ones), with the @@ -37,7 +45,8 @@ def disassemble(self, code: bytes, addr: int) -> Tuple[str, int]: if len(code) < spec.min_instruction_size: return ("", spec.min_instruction_size) - for instr in self._cs.disasm(code, addr, count=1): + cs = self._cs_att if att and self._cs_att is not None else self._cs + for instr in cs.disasm(code, addr, count=1): return (f"{instr.mnemonic} {instr.op_str}".strip(), instr.size) if spec.fixed_width: diff --git a/mapachespim/toolchain/x86.py b/mapachespim/toolchain/x86.py index f22529d..ef6cc56 100644 --- a/mapachespim/toolchain/x86.py +++ b/mapachespim/toolchain/x86.py @@ -602,6 +602,44 @@ def _looks_att(ops_text: str) -> bool: return "%" in ops_text or "$" in ops_text or ops_text.lstrip().startswith("*") +def _is_att(mnemonic: str, ops_text: str, syntax: str) -> bool: + """Whether an instruction is read as AT&T syntax (otherwise Intel).""" + if syntax != "auto": + return syntax == "att" + # A size suffix ("imulq d0") only exists in AT&T syntax. + return _looks_att(ops_text) or not ops_text or _split_mnemonic(mnemonic)[1] is not None + + +def syntax_by_line(source: str) -> Dict[int, str]: + """ + The syntax, "att" or "intel", that each instruction in x86-64 source is + written in, keyed by line number, as the assembler reads it (following + .att_syntax / .intel_syntax, else per instruction). + + Instructions that read the same in both syntaxes are left out: those + without operands, and those whose operands are only labels or numbers + (like "jmp loop") where no directive says which syntax is meant. + """ + from .directives import DirectiveParser, LineType + + parser = DirectiveParser("x86_64") + syntaxes: Dict[int, str] = {} + for section in parser.parse(source).values(): + for line in section.lines: + if line.line_type != LineType.INSTRUCTION or not line.instruction: + continue + parts = line.instruction.strip().split(None, 1) + if len(parts) < 2: + continue + att = _is_att(parts[0].lower(), parts[1], line.syntax) + if line.syntax == "auto" and not att: + operands = _split(parts[1]) + if not any("[" in op or _reg_or_none(op) is not None for op in operands): + continue + syntaxes[line.line_number] = "att" if att else "intel" + return syntaxes + + def encode_instruction( text: str, ctx: Context, @@ -619,10 +657,7 @@ def encode_instruction( if mnemonic in _NO_OPERANDS and not ops_text: return _NO_OPERANDS[mnemonic] - att = syntax == "att" or (syntax == "auto" and (_looks_att(ops_text) or not ops_text)) - if syntax == "auto" and not att and _split_mnemonic(mnemonic)[1] is not None: - # A size suffix ("imulq d0") only exists in AT&T syntax. - att = True + att = _is_att(mnemonic, ops_text, syntax) base, suffix = _split_mnemonic(mnemonic) if att else (mnemonic, None) is_branch = base in ("jmp", "call") or (base.startswith("j") and base[1:] in CONDITIONS) raw = _split(ops_text) if ops_text else [] diff --git a/mapachespim/unicorn_backend.py b/mapachespim/unicorn_backend.py index abf60ef..db232d2 100644 --- a/mapachespim/unicorn_backend.py +++ b/mapachespim/unicorn_backend.py @@ -562,12 +562,13 @@ def write_mem(self, addr: int, data: Union[bytes, str]) -> bool: # --- Disassembly --- - def disasm_with_size(self, addr: int) -> Tuple[str, int]: + def disasm_with_size(self, addr: int, att: bool = False) -> Tuple[str, int]: """ Disassemble the instruction at ``addr`` and return its size in bytes Callers that walk a range of instructions must advance by the size, - since x86-64 instructions vary in length. + since x86-64 instructions vary in length. On x86-64, ``att`` selects + AT&T syntax instead of Intel syntax. """ if self._disasm is None: raise RuntimeError("Simulator not initialized") @@ -575,14 +576,14 @@ def disasm_with_size(self, addr: int) -> Tuple[str, int]: size = self._disasm.max_size while size > 0: try: - return self._disasm.disassemble(self.read_mem(addr, size), addr) + return self._disasm.disassemble(self.read_mem(addr, size), addr, att) except RuntimeError: size -= 1 - return self._disasm.disassemble(b"", addr) + return self._disasm.disassemble(b"", addr, att) - def disasm(self, addr: int) -> str: - """Disassemble the instruction at ``addr``""" - return self.disasm_with_size(addr)[0] + def disasm(self, addr: int, att: bool = False) -> str: + """Disassemble the instruction at ``addr`` (in AT&T syntax on x86-64 if ``att``)""" + return self.disasm_with_size(addr, att)[0] # --- Symbols and sections --- diff --git a/tests/test_special_registers.py b/tests/test_special_registers.py index a7f65f1..bdbe23f 100644 --- a/tests/test_special_registers.py +++ b/tests/test_special_registers.py @@ -38,14 +38,14 @@ def run_regs(tmp_path, source, steps): def test_mips_hi_lo_after_mult(tmp_path): console, out = run_regs(tmp_path, MIPS_MULT_DIV, 3) assert console.sim.get_special_regs() == [0, 42] - assert " lo = 0x000000000000002a ★" in out + assert " lo = 0x0000002a ★" in out def test_mips_hi_lo_after_div(tmp_path): console, out = run_regs(tmp_path, MIPS_MULT_DIV, 5) assert console.sim.get_special_regs() == [2, 3] # remainder, quotient - assert " hi = 0x0000000000000002 ★" in out - assert " lo = 0x0000000000000003 ★" in out + assert " hi = 0x00000002 ★" in out + assert " lo = 0x00000003 ★" in out @pytest.mark.parametrize( diff --git a/tests/test_step_display.py b/tests/test_step_display.py new file mode 100644 index 0000000..a3eea58 --- /dev/null +++ b/tests/test_step_display.py @@ -0,0 +1,170 @@ +""" +How the console shows code and values: the source line when stepping, +register widths that match the ISA, and x86-64 disassembly in the syntax +the program was written in. +""" + +import io + +from mapachespim.console import MapacheSPIMConsole +from mapachespim.toolchain import assemble +from mapachespim.toolchain.x86 import syntax_by_line + + +def load(tmp_path, source, name="prog.s"): + """A console with source loaded, and its output cleared""" + path = tmp_path / name + path.write_text(source) + console = MapacheSPIMConsole(verbose=False) + console.stdout = io.StringIO() + console.onecmd(f"load {path}") + console.stdout = io.StringIO() + return console + + +def run(console, command): + """Run a console command and return what it printed""" + console.stdout = io.StringIO() + console.onecmd(command) + return console.stdout.getvalue() + + +# --- Source lines when stepping --- + +RISCV_LA = """.isa riscv64 +.data +msg: .asciz "hi" +.text +_start: + la a0, msg # two instructions + li a7, 10 + ecall +""" + + +def test_step_shows_source_line_before_its_instructions(tmp_path): + console = load(tmp_path, RISCV_LA) + lines = run(console, "step 3").splitlines() + assert lines[0] == "prog.s:6: la a0, msg # two instructions" + assert "auipc a0" in lines[1] + assert lines[2].startswith("[0x80000004]") # the rest of la: no repeated source line + assert lines[3] == "prog.s:7: li a7, 10" + assert "addi a7, zero, 0xa" in lines[4] + + +def test_each_step_command_shows_its_source_line(tmp_path): + console = load(tmp_path, RISCV_LA) + run(console, "step") + lines = run(console, "step").splitlines() + assert lines[0] == "prog.s:6: la a0, msg # two instructions" + assert lines[1].startswith("[0x80000004]") + + +# --- Register and address widths --- + +MIPS_NEGATIVE = """.isa mips32 +.text +_start: + li $t0, -1 + li $t1, 5 +""" + + +def test_mips32_registers_are_8_hex_digits(tmp_path): + console = load(tmp_path, MIPS_NEGATIVE) + run(console, "step 2") + out = run(console, "regs") + assert " t0 = 0xffffffff ★" in out + assert " t1 = 0x00000005 ★" in out + assert "pc = 0x00400008" in out + assert run(console, "pc").strip() == "pc = 0x00400008" + + +def test_mips32_decimal_and_binary_are_32_bits(tmp_path): + console = load(tmp_path, MIPS_NEGATIVE) + run(console, "step 2") + assert " t0 = .........-1 ★" in run(console, "regs decimal") + assert " t0 = 0b" + "1" * 32 + " " in run(console, "regs binary") + + +def test_mips32_load_shows_8_digit_entry_point(tmp_path): + path = tmp_path / "prog.s" + path.write_text(MIPS_NEGATIVE) + console = MapacheSPIMConsole(verbose=False) + console.stdout = io.StringIO() + console.onecmd(f"load {path}") + assert "Entry point: 0x00400000\n" in console.stdout.getvalue() + + +def test_64_bit_registers_are_16_hex_digits(tmp_path): + console = load(tmp_path, RISCV_LA) + assert "pc = 0x0000000080000000" in run(console, "regs") + + +# --- x86-64 disassembly syntax --- + +X86_ATT = """.isa x86_64 +.text +_start: + movq $5, %rdi + call sq + movq $10, %rax + syscall +sq: + movq %rdi, %rax + ret +""" + +X86_INTEL = """.isa x86_64 +.text +_start: + mov rdi, 5 + call sq + mov rax, 10 + syscall +sq: + mov rax, rdi + ret +""" + + +def test_att_source_disassembles_as_att(tmp_path): + console = load(tmp_path, X86_ATT) + out = run(console, "step 3") + assert "movq $5, %rdi" in out + assert "callq 0x" in out # a bare label, shown in the program's syntax + assert "movq %rdi, %rax" in out + assert "movq $0xa, %rax" in run(console, "disasm _start 3") + + +def test_intel_source_disassembles_as_intel(tmp_path): + console = load(tmp_path, X86_INTEL) + out = run(console, "step 3") + assert "mov rdi, 5" in out + assert "mov rax, rdi" in out + assert "mov rax, 0xa" in run(console, "disasm _start 3") + + +def test_elf_without_source_disassembles_as_intel(tmp_path): + result = assemble(X86_ATT, "x86_64") + assert result.success, result.errors + elf = tmp_path / "prog" + elf.write_bytes(result.elf_bytes) + console = MapacheSPIMConsole(verbose=False) + console.stdout = io.StringIO() + console.onecmd(f"load {elf}") + assert "mov rdi, 5" in run(console, "step") + + +def test_syntax_by_line(): + source = """.isa x86_64 +_start: movq $1, %rax # 2: AT&T by its operands + mov rax, 1 # 3: Intel by its operands + jmp _start # 4: reads the same either way + ret # 5: no operands +.intel_syntax noprefix + jmp _start # 7: Intel by directive +.att_syntax + imulq 8(%rsp) # 9: AT&T by directive +""" + assert syntax_by_line(source) == {2: "att", 3: "intel", 7: "intel", 9: "att"}