CPU State at Power-On
When reset is released the RISC-V CPU is in a precisely defined state mandated by the privileged specification. None of this is QEMU-specific — every RISC-V implementation follows these rules.
| Register / CSR | Reset Value | Reason |
PC | 0x1000 | MROM base — implementation-defined, QEMU virt uses 0x1000 |
| Privilege | M-mode | Highest level, must configure lower modes before entering them |
mstatus | 0x0000000A00000000 | SXL=64-bit, UXL=64-bit; MIE=0 (interrupts off) |
misa | 0x8000000000141101 | MXL=10 → 64-bit; extensions I,M,A,C,S,U |
mie | 0 | All M-mode interrupts disabled |
satp | 0 | Virtual memory off — bare physical addressing |
x0 | Always 0 | Hardwired zero by definition |
x1–x31 | Undefined (QEMU zeros them) | Spec does not require initialisation |
QEMU virt Memory Map
| Address | Size | Device |
0x00001000 | 0xF000 | MROM — reset vector lives here ← |
0x02000000 | 0x10000 | CLINT (mtime @ 0x200BFF8, mtimecmp hart0 @ 0x2004000) |
0x0C000000 | 0x4000000 | PLIC |
0x10000000 | 0x100 | UART0 (NS16550A) — write bytes here for terminal output |
0x80000000 | up to 128 MB | DRAM — your code runs here |
Reset Vector Disassembly
QEMU writes this exact sequence into MROM before simulation starts. The instruction bytes are hardcoded in hw/riscv/virt.c; only the two data words are patched at runtime.
0x1000: 97 02 00 00 auipc t0, 0 # t0 = 0x1000 (current PC)
0x1004: 13 86 82 02 addi a1, t0, 40 # a1 = 0x1028 (DTB address)
0x1008: 73 25 40 f1 csrr a0, mhartid # a0 = hart ID (usually 0)
0x100c: 83 2b 82 01 ld t0, 24(t0) # t0 = *(0x1018) = kernel entry
0x1010: 67 80 02 00 jr t0 # PC = 0x80000000
0x1014: 00 00 00 00 <padding>
0x1018: .dword kernel_entry # written by QEMU from ELF e_entry
0x1020: .dword fdt_addr # written by QEMU (top of RAM)
0x1028: d0 0d fe ed ... <DTB blob> # Flattened Device Tree
After these 5 instructions execute, the register state on entry to your firmware is:
| Register | Value | Set by |
a0 | 0 (hart ID) | csrr a0, mhartid — instruction 3 |
a1 | DTB address (~0x87E00000) | addi a1, t0, 40 — instruction 2 |
t0 | 0x80000000 | ld t0, 24(t0) — instruction 4 (now trashed) |
PC | 0x80000000 | jr t0 — instruction 5 |
| Privilege | M-mode | CPU never changed privilege |
32-bit Instructions ≠ 32-bit Mode
Every instruction in the reset vector is 4 bytes wide. It is natural to conclude the CPU is in 32-bit mode — but this is wrong. These are two completely independent properties.
| Property | Width | Controlled by |
| Instruction encoding width | Always 32-bit (base ISA) | Fixed by RISC-V ISA spec — does not change with XLEN |
| Register width (XLEN) | 32 / 64 / 128-bit | misa.MXL — set to 64-bit at reset on QEMU virt |
This is identical to x86-64: a modern 64-bit Intel CPU fetches instructions that may be 1–15 bytes wide, but its registers are 64-bit. Nobody says "x86-64 is in 1-byte mode" when they see a 1-byte NOP. Instruction width and register width are orthogonal.
Proof that the reset vector runs in 64-bit mode
The ld instruction at 0x100c loads 8 bytes (64 bits) from memory. This instruction does not exist in RV32 — executing it on a 32-bit core causes an Illegal Instruction fault. Its presence and successful execution is definitive proof of 64-bit mode.
| Instruction | Loads | RV32? | RV64? |
lw | 32 bits | ✅ Yes | ✅ Yes |
ld | 64 bits | ❌ Illegal | ✅ Yes — proof of RV64 |
Decoding ld t0, 24(t0) → 0x0182b283
# I-type instruction format (all loads are I-type):
# bits 31:20 19:15 14:12 11:7 6:0
# imm[11:0] rs1 funct3 rd opcode
0x0182b283 in binary:
000000011000 00101 011 00101 0000011
├──────────┤ ├───┤ ├─┤ ├───┤ ├─────┤
imm = 24(0x18) rs1=t0 011 rd=t0 LOAD
funct3 = 011 = LD (doubleword) # Only valid on RV64 — illegal on RV32
# Effect: t0 = Memory[t0 + 24] = Memory[0x1018] = 0x80000000
MISA CSR — Decoding 0x8000000000141101
# Read in GDB: -exec info registers misa
misa = 0x8000000000141101
bits 63:62 = 10 → MXL = 2 → XLEN = 64-bit (RV64)
bit 20 = 1 → U-mode supported
bit 18 = 1 → S-mode supported
bit 12 = 1 → M extension (multiply/divide)
bit 8 = 1 → I base integer ISA
bit 2 = 1 → C extension (compressed 16-bit instructions)
bit 0 = 1 → A extension (atomics)
ISA string: RV64IMAC + User mode + Supervisor mode
# Matches: -march=rv64imac in the Makefile
mstatus Reset Value — 0x0000000A00000000
The lower 32 bits are all zero, which is why tools that show only 32-bit registers display 0x00000000. The meaningful bits are in the upper 32:
0x0000000A00000000
bits 37:36 = 10 → SXL = 2 → S-mode XLEN = 64-bit
bits 35:34 = 10 → UXL = 2 → U-mode XLEN = 64-bit
bit 12:11 = 0 → MPP = 0 = U-mode (previous privilege)
bit 3 = 0 → MIE = 0 → M-mode interrupts DISABLED at reset
# zsbl.S must explicitly set MIE before any interrupt can fire:
csrr t0, mstatus
ori t0, t0, (1 << 3) # set MIE
csrw mstatus, t0
-bios none vs Default QEMU
| QEMU Flags | What runs between reset vector and your code | Privilege on entry |
-bios none -kernel x.elf | Nothing — reset vector jumps directly to your ELF entry | M-mode (Project 1) |
-bios zsbl.elf -device loader,file=super.elf | Your zsbl.S in M-mode, then mret to S-mode | M→S-mode (Project 2) |
(default) -kernel vmlinux | OpenSBI (built-in) in M-mode, then mret to S-mode for Linux | S-mode (Linux) |
Inspect the Reset Vector in GDB
# Start QEMU paused, connect GDB, then:
-exec x/7i 0x1000 # disassemble 7 instructions from 0x1000
-exec x/2gx 0x1018 # show kernel_entry and fdt_addr data words
-exec x/4xb 0x1028 # verify DTB magic: 0xd0 0x0d 0xfe 0xed
# Step through manually:
-exec stepi # auipc → t0 = 0x1000
-exec info registers t0 a0 a1
-exec stepi # addi → a1 = 0x1028
-exec stepi # csrr → a0 = 0
-exec stepi # ld → t0 = 0x80000000
-exec stepi # jr → PC = 0x80000000
# Now load symbols and debug your code:
-exec file main.elf
-exec break _start
-exec continue
Common Misconceptions
| Misconception | Verdict | Reality |
| Instructions are 32-bit wide → CPU is in 32-bit mode | FALSE | Instruction encoding is always 32-bit in base ISA. XLEN=64 is set by misa.MXL=10. |
| CPU starts at address 0x0 | FALSE | CPU starts at 0x1000 (MROM base). Address 0x0 is DEBUG region — not executable. |
| -bios none means no firmware runs | MISLEADING | The 5-instruction MROM stub still runs. -bios none skips OpenSBI only. |
| mstatus=0 at reset — CPU unconfigured | MISLEADING | Lower 32 bits are 0 but SXL=UXL=10 in bits 37:34 confirm 64-bit for all modes. |
| mtime runs at 10 MHz (per DTB) | FALSE | DTB reports 10 MHz but QEMU mtime actually ticks at 1 MHz. Measure empirically. |
| a0 and a1 are set by your _start code | FALSE | Set by the QEMU reset vector (csrr + addi) before your code runs. They arrive pre-populated. |
| MROM reset vector can be modified | FALSE | MROM is read-only. Writing to 0x1000 causes a store access fault. |
| satp=0 means the MMU is missing | TRUE BUT | satp=0 means virtual memory is disabled, using physical addressing. MMU is present but off. Correct for M-mode firmware. |