TB32 is the CPU underneath everything: a small, fixed-width 32-bit RISC. This is its instruction set -
what objdump and tbdbg disassemble, what cc emits, and
what as turns into a runnable object. Around 50 instructions in four encoding formats, plus an
optional privileged extension (TB32-V) for virtualization.
r0-r15; r0 is hard-wired to zero. Loads and stores address a byte memory the
machine provides - on TonicBoxOS a per-process, demand-paged virtual address space. Comparisons set flags;
conditional branches read them. The sys instruction
is the only door to the kernel.
Sixteen 32-bit registers plus the program counter. Any register can be named r0-r15;
a few carry ABI roles and have aliases.
| reg | alias | role |
|---|---|---|
r0 | zero | Hard-wired zero - reads as 0, writes are discarded. This is why mov/nop exist as they do. |
r1-r6 | Argument / return registers. A call passes its arguments here and returns its result in r1. | |
r7 | By convention holds the syscall number for the sys instruction. | |
r8-r12 | General purpose. | |
r13 | sp | Stack pointer. The stack grows downward; push/pop adjust it by 4. |
r14 | fp | Frame pointer. |
r15 | lr | Link register. call/callr write the return address here; ret jumps to it. |
pc | Program counter (not part of the register file). Advances by 4 each instruction unless a branch redirects it. |
Every instruction is one 32-bit word. The top 7 bits are the opcode; the rest is one of four field layouts. Register fields are 4 bits (0-15).
| form | 31:25 | 24:21 | 20:17 | 16:0 |
|---|---|---|---|---|
| R | opcode | rd | rs1 | rs2 [16:13] |
| I | opcode | rd | rs1 | imm16 [16:1] |
| J | opcode | imm25 [24:0] | ||
| REG | opcode | - | rs1 | - |
Loads, stores, and all immediate ops use the I form. ret/sys/hlt/brk
(and the privileged sret/hret/hcall) are opcode-only (every other field zero). A couple of details worth knowing when you read disassembly:
addi, slti, sltiu, cmpi and every load/store offset sign-extend the 16-bit immediate; andi, ori, xori zero-extend it.lui loads the high half: rd = imm16 << 16. A full 32-bit constant is lui + ori (see li below).Register form (R): op rd, rs1, rs2 computes rd = rs1 op rs2. Arithmetic wraps mod 232.
| op | form | effect | enc |
|---|---|---|---|
add sub | rd, rs1, rs2 | add / subtract | 10 11 |
and or xor | rd, rs1, rs2 | bitwise | 12-14 |
sll srl sra | rd, rs1, rs2 | shift left / right-logical / right-arithmetic by rs2 & 31 | 15-17 |
slt sltu | rd, rs1, rs2 | set rd = 1 if rs1 < rs2 (signed / unsigned), else 0 | 18 19 |
mul | rd, rs1, rs2 | low 32 bits of the product | 1A |
div rem | rd, rs1, rs2 | signed quotient / remainder (divide by zero faults) | 1F 2B |
divu remu | rd, rs1, rs2 | unsigned quotient / remainder (divide by zero faults) | 1B 1C |
cmp | rs1, rs2 | set flags from rs1 - rs2 (no register written) | 1D |
tst | rs1, rs2 | set Z/N from rs1 & rs2 (no register written) | 1E |
Immediate form (I): op rd, rs1, imm computes rd = rs1 op imm16.
| op | form | effect | enc |
|---|---|---|---|
addi | rd, rs1, imm | add sign-extended immediate | 20 |
andi ori xori | rd, rs1, imm | bitwise with zero-extended immediate | 21-23 |
slli srli srai | rd, rs1, imm | shift by imm & 31 | 24-26 |
slti sltiu | rd, rs1, imm | set-less-than against the sign-extended immediate (signed / unsigned compare) | 27 28 |
lui | rd, imm | rd = imm16 << 16 | 29 |
cmpi | rs1, imm | set flags from rs1 - sign-extended immediate | 2A |
Address is rs1 + the sign-extended offset, written [base, offset] (the offset is optional).
Loads read into rd; stores write rd out.
| op | form | effect | enc |
|---|---|---|---|
lb lbu | rd, [rs1, imm] | load byte, sign- / zero-extended | 30 31 |
lh lhu | rd, [rs1, imm] | load halfword, sign- / zero-extended | 32 33 |
lw | rd, [rs1, imm] | load word | 34 |
sb sh sw | rd, [rs1, imm] | store low byte / halfword / word of rd | 38-3A |
; load, add, store back
lw r1, [sp, 8]
addi r1, r1, 1
sw r1, [sp, 8]
TB32 branches on flags, ARM-style. A cmp/cmpi sets all four; a
tst sets Z and N. A conditional branch then tests them - the branch itself carries no operands
but the PC-relative target.
| flag | meaning after cmp a, b |
|---|---|
| Z | set when a == b |
| N | sign bit of a - b |
| C | set when a >= b unsigned (no borrow) |
| V | signed overflow of a - b |
| op | form | taken when | enc |
|---|---|---|---|
bra | label | always (unconditional) | 40 |
beq bne | label | equal / not equal (Z / !Z) | 41 42 |
blt bge | label | signed < / >= (N≠V / N=V) | 43 44 |
bltu bgeu | label | unsigned < / >= (!C / C) | 45 46 |
call | label | set lr to the next instruction, then branch (PC-relative) | 48 |
callr | rs1 | set lr, then jump to the address in rs1 | 49 |
jmp | rs1 | jump to the address in rs1 (no link) | 4B |
ret | jump to lr | 4A |
Because ret just jumps to whatever is in lr, a routine that spills lr to
the stack and later reloads it is where control flow can be hijacked - the basis of the ROP path in the
challenge.
| op | effect | enc |
|---|---|---|
sys | system call. Number in r7, arguments in r1-r6, result in r1. See the syscall reference. | 50 |
hlt | halt the machine. | 51 |
brk | breakpoint. A no-op during normal execution; the debugger patches instructions to brk to stop the program. | 52 |
A fault stops the process with a trap code (visible in tbdbg):
| code | fault |
|---|---|
| 1 | misaligned instruction fetch (pc not a multiple of 4) |
| 2 | memory access the machine rejected - out of bounds, or an unmapped or protected page |
| 3 | illegal / unknown opcode |
| 4 | divide or remainder by zero |
Everything above is the unprivileged core: one flat privilege level, with the CPU handing faults straight back to whatever runs it. TB32-V is an optional extension that makes TB32 virtualizable - privilege levels, control registers, in-CPU traps, two-stage paging, and timers - modeled on the RISC-V hypervisor extension. A machine opts into TB32-V by running the privileged executor: it is what the tb32hv hypervisor is built on, and TonicBoxOS now runs as a guest kernel under it - so the OS you use exercises the full extension, not just the core.
Privilege modes. Three levels - user (U), supervisor (S), hypervisor (H) - plus a
virtualization bit. When it is set, the running S/U pair is a guest: VS (guest kernel) and VU
(guest user). A trap moves up the ladder; sret/hret return down it.
| mode | runs |
|---|---|
| H | the hypervisor |
| VS | a guest kernel (virtualized supervisor) |
| VU | a guest user process (virtualized user) |
| S / U | a supervisor / user running without virtualization |
Privileged instructions. csrr and csrw reuse the I
form, with the 16-bit immediate carrying the control-register number. Accessing a register above the current
privilege traps.
| op | form | effect | enc |
|---|---|---|---|
csrr | rd, csr | read a control register: rd = CSR[csr] | 53 |
csrw | csr, rs | write a control register: CSR[csr] = rs | 54 |
sret | return from a supervisor trap - restore mode and pc from the S bank | 55 | |
hret | return from a hypervisor trap | 56 | |
hcall | hypercall - trap from a guest into the hypervisor | 57 |
Control & status registers. Each level has its own bank; when virtualized, the supervisor bank is the guest's. Selected registers:
| csr | num | holds |
|---|---|---|
sstatus | 100 | supervisor status: interrupt-enable, previous privilege, and user-memory access (SUM) |
sflags | 101 | the condition flags (Z/N/C/V), so they can be saved across a context switch |
sie sip | 104 144 | supervisor interrupt enable / pending |
stvec | 105 | supervisor trap vector |
sepc scause stval | 141-143 | saved pc / cause / aux, written on a trap to S |
stimecmp | 14D | supervisor timer compare |
satp | 180 | stage-1 page-table base (a guest's own paging) |
hstatus | 600 | hypervisor status, including the virtualization bit |
hedeleg | 602 | which guest exceptions are delegated to the guest kernel |
htvec | 605 | hypervisor trap vector |
hepc hcause htval htinst | 641-64A | saved pc / cause / faulting guest-physical address / trapping instruction |
htimecmp | 64D | hypervisor timer compare (preemptive scheduling) |
hgatp | 680 | stage-2 page-table base (guest-physical to host-physical) |
Traps & delegation. A trap records its cause, pc, and an aux value, switches up a
privilege level, and jumps to that level's vector. hedeleg lets a guest user's syscalls and
stage-1 page faults be delivered straight to the guest kernel, while the guest kernel's privileged
operations, device access, and stage-2 faults trap to the hypervisor. Interrupt causes are marked by the
high bit; a hypervisor timer (htimecmp) can preempt a running guest.
Two-stage translation. With a guest running, every address is translated twice:
stage-1 (satp, the guest's own page tables) maps guest-virtual to
guest-physical, then stage-2 (hgatp, the hypervisor's tables) maps
guest-physical to host-physical. Stage-2 is where isolation lives - a guest cannot reach memory its stage-2
does not map, whatever its own tables say - and an unmapped guest-physical address faults to the hypervisor,
which is how a guest's virtual devices (a UART, a timer) are trapped and emulated.
Page permissions. Each stage-1 page grants read/write/execute and carries a
user (U) bit. User mode may only reach U pages; supervisor mode may not read or write a U page
unless sstatus.SUM is set - which is how a kernel copies a process's syscall buffers across the
privilege boundary - and it never executes one, so kernel and user memory stay separated within a single
address space.
Conveniences the assembler expands to real instructions.
| write | expands to |
|---|---|
nop | add r0, r0, r0 |
mov rd, rs | add rd, rs, r0 |
li rd, imm | lui rd, hi16 ; ori rd, rd, lo16 - a full 32-bit constant (a label is relocated at load) |
push r | addi sp, sp, -4 ; sw r, [sp, 0] |
pop r | lw r, [sp, 0] ; addi sp, sp, 4 |
j label | bra label (unconditional) |
One instruction or directive per line. Operands are comma-separated. Comments run to end of line after
;, //, or #.
r0-r15, or the aliases zero sp fp lr.[base, offset]; the offset is optional ([sp] is [sp, 0]).0x hex, 0o octal, 0b binary; a label; or label ± offset.name: defines one; reference it by name in a branch, li, or .word. _start is the default entry point.| directive | effect |
|---|---|
.text .rodata .data .bss | select a section (also .section NAME). .text is based at 0x1000; sections are placed in order, word-aligned. |
.equ / .set NAME, VAL | define an assemble-time constant. |
.align N | pad to an N-byte boundary. |
.word / .byte v, ... | emit 32-bit words / bytes (a .word may be a label, relocated at load). |
.asciz / .string / .ascii "..." | emit string bytes; .asciz/.string append a NUL. |
.space / .skip N | emit N zero bytes. |
.globl NAME | accepted (all labels are already global); no effect. |
.entry LABEL | set the entry point (default _start). |
Assemble with as file.s -o out in the terminal.
cc emits .s, so cc x.c -o x then studying the disassembly is the fast
way to see what any C compiles to.
; minimal program: write "hi\n" and exit .text _start: li r7, 1 ; write li r1, 1 ; fd = stdout li r2, msg li r3, 3 ; len sys li r7, 11 ; exit li r1, 0 sys .rodata msg: .ascii "hi\n"