diff --git a/CHANGELOG.md b/CHANGELOG.md index d996742..7f2544b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,23 @@ ## 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. +- `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 + 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..e9dc629 100644 --- a/README.md +++ b/README.md @@ -76,39 +76,41 @@ 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 +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 -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 @@ -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 869fff2..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 @@ -91,8 +92,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 +111,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 +142,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,20 +160,33 @@ 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. +- `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`) ``` @@ -183,18 +209,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 +227,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,22 +255,23 @@ 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 +fibonacci.s:80: beqz a0, base_case_zero [0x80000038] 0x63040504 beqz a0, 0x48 (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 +293,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 +307,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 +358,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 +381,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 +402,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/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/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 759c008..61a3e5b 100644 Binary files a/examples/riscv/fibonacci/fibonacci and b/examples/riscv/fibonacci/fibonacci differ 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 aabd170..b071260 100644 Binary files a/examples/riscv/matrix_multiply/matrix_mult and b/examples/riscv/matrix_multiply/matrix_mult differ diff --git a/examples/riscv/matrix_multiply/matrix_mult.s b/examples/riscv/matrix_multiply/matrix_mult.s index 4fe79f6..fb6b3db 100644 --- a/examples/riscv/matrix_multiply/matrix_mult.s +++ b/examples/riscv/matrix_multiply/matrix_mult.s @@ -45,9 +45,6 @@ .globl _start _start: - # Initialize stack pointer (required for function calls) - la sp, _stack_start # sp = top of stack - # Load base addresses of matrices into registers la a0, matrix_a # a0 = address of matrix A la a1, matrix_b # a1 = address of matrix B @@ -56,14 +53,9 @@ _start: # Call matrix multiply function jal ra, matrix_multiply - # 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 - -exit_loop: - # Infinite loop - emulator will detect tohost write and stop - j exit_loop + # Exit program using syscall + li a7, 10 # syscall 10 = exit + ecall # ============================================================================ # Function: matrix_multiply @@ -182,19 +174,3 @@ end_outer: # [ 84 69 54 ] (4*9+5*6+6*3=84, 4*8+5*5+6*2=69, 4*7+5*4+6*1=54) # [138 114 90 ] (7*9+8*6+9*3=138, 7*8+8*5+9*2=114, 7*7+8*4+9*1=90) # ============================================================================ - -# Stack space in .bss section -.section .bss - .align 4 - _stack_bottom: - .space 4096 - _stack_start: - -# 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/mapachespim/console.py b/mapachespim/console.py index 43ea1df..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) @@ -223,11 +229,11 @@ def _load_elf_file( self.loaded_source = source self._elf_path = elf_path # Registers at the entry point are the baseline for ★ change markers - self.prev_regs = self.sim.get_all_regs() + self.prev_regs = self._reg_snapshot() 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, @@ -404,8 +433,8 @@ def do_step(self, arg: str) -> 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 +447,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 @@ -435,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() @@ -542,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: @@ -551,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,9 +652,9 @@ 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}", + f"Reset {self.loaded_file}. PC = {self._hex_word(self.sim.get_pc())}", file=self.stdout, ) @@ -628,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: @@ -687,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 @@ -740,6 +790,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 +815,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 +847,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,62 +872,64 @@ 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 - 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""" + 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 " " - # Format registers in 2 columns (or 1 if binary is too wide) - cols = 1 if show_mode == "binary" else 2 + # 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] + 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}: {flag_bits}{marker}", file=self.stdout) self._print_block_end() # Update the snapshot for change tracking (unless peek mode) @@ -887,12 +951,15 @@ 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) + print(f"pc = {self._hex_word(pc)}", file=self.stdout) def do_mem(self, arg: str) -> None: """Display memory contents in hex dump format @@ -1060,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 @@ -1361,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 @@ -1421,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/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/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 928cba8..db232d2 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: @@ -548,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") @@ -561,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_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..bdbe23f --- /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 = 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 = 0x00000002 ★" in out + assert " lo = 0x00000003 ★" 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() 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"}