TonicBoxOS · TB32 reference

as - the TB32 assembler

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.

At a glance. Fixed 32-bit instructions, little-endian. Sixteen registers 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.

00Registers

Sixteen 32-bit registers plus the program counter. Any register can be named r0-r15; a few carry ABI roles and have aliases.

regaliasrole
r0zeroHard-wired zero - reads as 0, writes are discarded. This is why mov/nop exist as they do.
r1-r6Argument / return registers. A call passes its arguments here and returns its result in r1.
r7By convention holds the syscall number for the sys instruction.
r8-r12General purpose.
r13spStack pointer. The stack grows downward; push/pop adjust it by 4.
r14fpFrame pointer.
r15lrLink register. call/callr write the return address here; ret jumps to it.
pcProgram counter (not part of the register file). Advances by 4 each instruction unless a branch redirects it.

01Encoding

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).

form31:2524:2120:1716:0
Ropcoderdrs1rs2 [16:13]
Iopcoderdrs1imm16 [16:1]
Jopcodeimm25 [24:0]
REGopcode-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:

02Arithmetic & logic

Register form (R): op rd, rs1, rs2 computes rd = rs1 op rs2. Arithmetic wraps mod 232.

opformeffectenc
add subrd, rs1, rs2add / subtract10 11
and or xorrd, rs1, rs2bitwise12-14
sll srl srard, rs1, rs2shift left / right-logical / right-arithmetic by rs2 & 3115-17
slt slturd, rs1, rs2set rd = 1 if rs1 < rs2 (signed / unsigned), else 018 19
mulrd, rs1, rs2low 32 bits of the product1A
div remrd, rs1, rs2signed quotient / remainder (divide by zero faults)1F 2B
divu remurd, rs1, rs2unsigned quotient / remainder (divide by zero faults)1B 1C
cmprs1, rs2set flags from rs1 - rs2 (no register written)1D
tstrs1, rs2set Z/N from rs1 & rs2 (no register written)1E

Immediate form (I): op rd, rs1, imm computes rd = rs1 op imm16.

opformeffectenc
addird, rs1, immadd sign-extended immediate20
andi ori xorird, rs1, immbitwise with zero-extended immediate21-23
slli srli sraird, rs1, immshift by imm & 3124-26
slti sltiurd, rs1, immset-less-than against the sign-extended immediate (signed / unsigned compare)27 28
luird, immrd = imm16 << 1629
cmpirs1, immset flags from rs1 - sign-extended immediate2A

03Loads & stores

Address is rs1 + the sign-extended offset, written [base, offset] (the offset is optional). Loads read into rd; stores write rd out.

opformeffectenc
lb lburd, [rs1, imm]load byte, sign- / zero-extended30 31
lh lhurd, [rs1, imm]load halfword, sign- / zero-extended32 33
lwrd, [rs1, imm]load word34
sb sh swrd, [rs1, imm]store low byte / halfword / word of rd38-3A
; load, add, store back
lw   r1, [sp, 8]
addi r1, r1, 1
sw   r1, [sp, 8]

04Flow & conditions

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.

flagmeaning after cmp a, b
Zset when a == b
Nsign bit of a - b
Cset when a >= b unsigned (no borrow)
Vsigned overflow of a - b
opformtaken whenenc
bralabelalways (unconditional)40
beq bnelabelequal / not equal (Z / !Z)41 42
blt bgelabelsigned < / >= (N≠V / N=V)43 44
bltu bgeulabelunsigned < / >= (!C / C)45 46
calllabelset lr to the next instruction, then branch (PC-relative)48
callrrs1set lr, then jump to the address in rs149
jmprs1jump to the address in rs1 (no link)4B
retjump to lr4A

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.

05System & faults

opeffectenc
syssystem call. Number in r7, arguments in r1-r6, result in r1. See the syscall reference.50
hlthalt the machine.51
brkbreakpoint. 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):

codefault
1misaligned instruction fetch (pc not a multiple of 4)
2memory access the machine rejected - out of bounds, or an unmapped or protected page
3illegal / unknown opcode
4divide or remainder by zero

06Privileged extension (TB32-V)

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.

moderuns
Hthe hypervisor
VSa guest kernel (virtualized supervisor)
VUa guest user process (virtualized user)
S / Ua 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.

opformeffectenc
csrrrd, csrread a control register: rd = CSR[csr]53
csrwcsr, rswrite a control register: CSR[csr] = rs54
sretreturn from a supervisor trap - restore mode and pc from the S bank55
hretreturn from a hypervisor trap56
hcallhypercall - trap from a guest into the hypervisor57

Control & status registers. Each level has its own bank; when virtualized, the supervisor bank is the guest's. Selected registers:

csrnumholds
sstatus100supervisor status: interrupt-enable, previous privilege, and user-memory access (SUM)
sflags101the condition flags (Z/N/C/V), so they can be saved across a context switch
sie sip104 144supervisor interrupt enable / pending
stvec105supervisor trap vector
sepc scause stval141-143saved pc / cause / aux, written on a trap to S
stimecmp14Dsupervisor timer compare
satp180stage-1 page-table base (a guest's own paging)
hstatus600hypervisor status, including the virtualization bit
hedeleg602which guest exceptions are delegated to the guest kernel
htvec605hypervisor trap vector
hepc hcause htval htinst641-64Asaved pc / cause / faulting guest-physical address / trapping instruction
htimecmp64Dhypervisor timer compare (preemptive scheduling)
hgatp680stage-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.

07Pseudo-instructions

Conveniences the assembler expands to real instructions.

writeexpands to
nopadd r0, r0, r0
mov rd, rsadd rd, rs, r0
li rd, immlui rd, hi16 ; ori rd, rd, lo16 - a full 32-bit constant (a label is relocated at load)
push raddi sp, sp, -4 ; sw r, [sp, 0]
pop rlw r, [sp, 0] ; addi sp, sp, 4
j labelbra label (unconditional)

08Assembler syntax

One instruction or directive per line. Operands are comma-separated. Comments run to end of line after ;, //, or #.

directiveeffect
.text .rodata .data .bssselect a section (also .section NAME). .text is based at 0x1000; sections are placed in order, word-aligned.
.equ / .set NAME, VALdefine an assemble-time constant.
.align Npad 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 Nemit N zero bytes.
.globl NAMEaccepted (all labels are already global); no effect.
.entry LABELset 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"