|
| 1 | +# HAL: RISC-V Calling Convention |
| 2 | + |
| 3 | +## Overview |
| 4 | +Linmo kernel strictly adheres to the RISC-V calling convention for RV32I architecture. |
| 5 | +This document describes how the standard calling convention is implemented and extended within the Linmo kernel context, |
| 6 | +particularly for context switching, interrupt handling, and task management. |
| 7 | + |
| 8 | +## C Datatypes and Alignment (RV32I) |
| 9 | + |
| 10 | +| C type | Description | Bytes | Alignment | |
| 11 | +| ------------- | ------------------------ | ----- | --------- | |
| 12 | +| `char` | Character value/byte | 1 | 1 | |
| 13 | +| `short` | Short integer | 2 | 2 | |
| 14 | +| `int` | Integer | 4 | 4 | |
| 15 | +| `long` | Long integer | 4 | 4 | |
| 16 | +| `long long` | Long long integer | 8 | 8 | |
| 17 | +| `void*` | Pointer | 4 | 4 | |
| 18 | +| `float` | Single-precision float | 4 | 4 | |
| 19 | +| `double` | Double-precision float | 8 | 8 | |
| 20 | + |
| 21 | +Note: Linmo currently targets RV32I (integer-only) and does not use floating-point instructions. |
| 22 | + |
| 23 | +## Register Usage in Linmo |
| 24 | + |
| 25 | +### Standard Register Classifications |
| 26 | + |
| 27 | +#### Caller-Saved Registers (Volatile) |
| 28 | +These registers may be modified by function calls and must be saved by the caller if needed across calls: |
| 29 | + |
| 30 | +| Register | ABI Name | Description | Linmo Usage | |
| 31 | +| -------- | -------- | -------------------------------- | ----------- | |
| 32 | +| x1 | `ra` | Return address | Function returns, ISR context | |
| 33 | +| x5–7 | `t0–2` | Temporaries | Scratch registers | |
| 34 | +| x10–11 | `a0–1` | Function arguments/return values | Syscall args/returns | |
| 35 | +| x12–17 | `a2–7` | Function arguments | Extended syscall args | |
| 36 | +| x28–31 | `t3–6` | Temporaries | Scratch registers | |
| 37 | + |
| 38 | +#### Callee-Saved Registers (Non-Volatile) |
| 39 | +These registers must be preserved across function calls: |
| 40 | + |
| 41 | +| Register | ABI Name | Description | Linmo Usage | |
| 42 | +| -------- | -------- | ---------------------------- | ----------- | |
| 43 | +| x2 | `sp` | Stack pointer | Task stack management | |
| 44 | +| x8 | `s0/fp` | Saved register/frame pointer | General purpose | |
| 45 | +| x9 | `s1` | Saved register | General purpose | |
| 46 | +| x18–27 | `s2–11` | Saved registers | General purpose | |
| 47 | + |
| 48 | +#### Special Registers |
| 49 | +| Register | ABI Name | Description | Linmo Usage | |
| 50 | +| -------- | -------- | --------------- | ----------- | |
| 51 | +| x0 | `zero` | Hard-wired zero | Constant zero | |
| 52 | +| x3 | `gp` | Global pointer | Kernel globals access | |
| 53 | +| x4 | `tp` | Thread pointer | Thread-local storage | |
| 54 | + |
| 55 | +## Context Switching in Linmo |
| 56 | + |
| 57 | +### Task Context Structure (`jmp_buf`) |
| 58 | + |
| 59 | +Linmo's `jmp_buf` stores the minimal context required for task switching: |
| 60 | + |
| 61 | +```c |
| 62 | +typedef uint32_t jmp_buf[17]; |
| 63 | +``` |
| 64 | + |
| 65 | +Layout (32-bit word indices): |
| 66 | +``` |
| 67 | +[0-11]: s0-s11 (Callee-saved registers) |
| 68 | +[12]: gp (Global pointer) |
| 69 | +[13]: tp (Thread pointer) |
| 70 | +[14]: sp (Stack pointer) |
| 71 | +[15]: ra (Return address) |
| 72 | +[16]: mstatus (Machine status CSR) |
| 73 | +``` |
| 74 | + |
| 75 | +### Why Only Callee-Saved Registers? |
| 76 | + |
| 77 | +The RISC-V calling convention guarantees that: |
| 78 | +- Callee-saved registers (`s0-s11`, `sp`) are preserved across function calls |
| 79 | +- Caller-saved registers (`t0-t6`, `a0-a7`, `ra`) may be modified by callees |
| 80 | + |
| 81 | +Since task switches occur at well-defined points (yield, block, preemption), we only need to save callee-saved registers plus essential control state. Caller-saved registers are either: |
| 82 | +- Already saved by the compiler before function calls |
| 83 | +- Not significant at task switch boundaries |
| 84 | + |
| 85 | +### Context Switching Functions |
| 86 | + |
| 87 | +#### Standard C Library Functions |
| 88 | +```c |
| 89 | +int32_t setjmp(jmp_buf env); /* Save execution context only */ |
| 90 | +void longjmp(jmp_buf env, int32_t val); /* Restore execution context only */ |
| 91 | +``` |
| 92 | +- Use elements [0-15] of `jmp_buf` |
| 93 | +- Standard C semantics for non-local jumps |
| 94 | +- No processor state management |
| 95 | +
|
| 96 | +#### HAL Context Switching Functions |
| 97 | +```c |
| 98 | +int32_t hal_context_save(jmp_buf env); /* Save context + processor state */ |
| 99 | +void hal_context_restore(jmp_buf env, int32_t val); /* Restore context + processor state */ |
| 100 | +``` |
| 101 | +- Use all elements [0-16] of `jmp_buf` |
| 102 | +- Include `mstatus` for interrupt state preservation |
| 103 | +- Used by the kernel scheduler for task switching |
| 104 | + |
| 105 | +## Interrupt Service Routine (ISR) Context |
| 106 | + |
| 107 | +### Full Context Preservation |
| 108 | + |
| 109 | +The ISR in `boot.c` performs a complete context save of all registers: |
| 110 | + |
| 111 | +``` |
| 112 | +Stack Frame Layout (128 bytes, offsets from sp): |
| 113 | + 0: ra, 4: gp, 8: tp, 12: t0, 16: t1, 20: t2 |
| 114 | + 24: s0, 28: s1, 32: a0, 36: a1, 40: a2, 44: a3 |
| 115 | + 48: a4, 52: a5, 56: a6, 60: a7, 64: s2, 68: s3 |
| 116 | + 72: s4, 76: s5, 80: s6, 84: s7, 88: s8, 92: s9 |
| 117 | + 96: s10, 100:s11, 104:t3, 108: t4, 112: t5, 116: t6 |
| 118 | +120: mcause, 124: mepc |
| 119 | +``` |
| 120 | + |
| 121 | +Why full context save in ISR? |
| 122 | +- ISRs can preempt tasks at any instruction boundary |
| 123 | +- Caller-saved registers may contain live values not yet spilled by compiler |
| 124 | +- Ensures ISR can call any C function without corrupting task state |
| 125 | +- Provides complete transparency to interrupted code |
| 126 | + |
| 127 | +### ISR Stack Requirements |
| 128 | + |
| 129 | +Each task stack must reserve space for the ISR frame: |
| 130 | +```c |
| 131 | +#define ISR_STACK_FRAME_SIZE 128 /* 32 registers × 4 bytes */ |
| 132 | +``` |
| 133 | +
|
| 134 | +This "red zone" is reserved at the top of every task stack to guarantee ISR safety. |
| 135 | +
|
| 136 | +## Function Calling in Linmo |
| 137 | +
|
| 138 | +### Kernel Function Calls |
| 139 | +
|
| 140 | +Standard RISC-V calling convention applies: |
| 141 | +
|
| 142 | +```c |
| 143 | +/* Example: mo_task_spawn(entry, stack_size) */ |
| 144 | +/* a0 = entry, a1 = stack_size, return value in a0 */ |
| 145 | +int32_t result = mo_task_spawn(task_function, 2048); |
| 146 | +``` |
| 147 | + |
| 148 | +### System Call Interface |
| 149 | + |
| 150 | +Linmo uses standard function calls (not trap instructions) for system services: |
| 151 | +- Arguments passed in `a0-a7` registers |
| 152 | +- Return values in `a0` |
| 153 | +- No special calling convention required |
| 154 | + |
| 155 | +### Task Entry Points |
| 156 | + |
| 157 | +When a new task starts: |
| 158 | +```c |
| 159 | +void task_function(void) { |
| 160 | + /* ra contains this function address */ |
| 161 | + /* sp points to task's stack (16-byte aligned) */ |
| 162 | + /* gp points to kernel globals */ |
| 163 | + /* tp points to thread-local storage area */ |
| 164 | + |
| 165 | + /* Task code here */ |
| 166 | +} |
| 167 | +``` |
| 168 | +
|
| 169 | +## Stack Management |
| 170 | +
|
| 171 | +### Stack Layout |
| 172 | +
|
| 173 | +Each task has its own stack with this layout: |
| 174 | +
|
| 175 | +``` |
| 176 | +High Address |
| 177 | ++------------------+ <- stack_base + stack_size |
| 178 | +| ISR Red Zone | <- 128 bytes reserved for ISR |
| 179 | +| (128 bytes) | |
| 180 | ++------------------+ <- Initial SP (16-byte aligned) |
| 181 | +| | |
| 182 | +| Task Stack | <- Grows downward |
| 183 | +| (Dynamic) | |
| 184 | +| | |
| 185 | ++------------------+ <- stack_base |
| 186 | +Low Address |
| 187 | +``` |
| 188 | +
|
| 189 | +### Stack Alignment |
| 190 | +- 16-byte alignment: Required by RISC-V ABI for stack pointer |
| 191 | +- 4-byte alignment: Minimum for all memory accesses on RV32I |
| 192 | +- Stack grows downward (towards lower addresses) |
| 193 | +
|
| 194 | +### Stack Protection |
| 195 | +When `CONFIG_STACK_PROTECTION` is enabled: |
| 196 | +```c |
| 197 | +#define STACK_CANARY 0x33333333U |
| 198 | +
|
| 199 | +/* Canaries placed at stack boundaries */ |
| 200 | +*(uint32_t *)stack_base = STACK_CANARY; /* Low guard */ |
| 201 | +*(uint32_t *)(stack_base + stack_size - 4) = STACK_CANARY; /* High guard */ |
| 202 | +``` |
| 203 | + |
| 204 | +## Assembly Function Interface |
| 205 | + |
| 206 | +### Calling Assembly from C |
| 207 | +```c |
| 208 | +/* C declaration */ |
| 209 | +extern void assembly_function(uint32_t arg1, uint32_t arg2); |
| 210 | + |
| 211 | +/* Assembly implementation must follow RISC-V ABI */ |
| 212 | +``` |
| 213 | +
|
| 214 | +### Calling C from Assembly |
| 215 | +```assembly |
| 216 | +.globl assembly_calls_c |
| 217 | +assembly_calls_c: |
| 218 | + # Arguments already in a0, a1 per calling convention |
| 219 | + call c_function # Standard call |
| 220 | + # Return value now in a0 |
| 221 | + ret |
| 222 | +``` |
| 223 | + |
| 224 | +### Naked Functions |
| 225 | +For low-level kernel functions that manage their own prologue/epilogue: |
| 226 | + |
| 227 | +```c |
| 228 | +__attribute__((naked)) void _isr(void) { |
| 229 | + asm volatile( |
| 230 | + /* Manual register save/restore */ |
| 231 | + "addi sp, sp, -128\n" |
| 232 | + /* ... save registers ... */ |
| 233 | + "call do_trap\n" |
| 234 | + /* ... restore registers ... */ |
| 235 | + "addi sp, sp, 128\n" |
| 236 | + "mret\n" |
| 237 | + ); |
| 238 | +} |
| 239 | +``` |
| 240 | +
|
| 241 | +## Performance Considerations |
| 242 | +
|
| 243 | +### Register Pressure |
| 244 | +- Callee-saved registers: `s0-s11` (12 registers) - preserved across calls |
| 245 | +- Caller-saved registers: `t0-t6`, `a0-a7`, `ra` (15 registers) - may be used freely |
| 246 | +
|
| 247 | +The abundance of caller-saved registers in RISC-V reduces spill pressure compared to architectures like x86. |
| 248 | +
|
| 249 | +### Context Switch Cost |
| 250 | +Minimal context (jmp_buf): |
| 251 | +- 17 × 32-bit loads/stores = 68 bytes |
| 252 | +- Essential for cooperative scheduling |
| 253 | +
|
| 254 | +Full context (ISR): |
| 255 | +- 32 × 32-bit loads/stores = 128 bytes |
| 256 | +- Required for preemptive interrupts |
| 257 | +
|
| 258 | +### Function Call Overhead |
| 259 | +Standard RISC-V function call: |
| 260 | +```assembly |
| 261 | +call function # 1 instruction (may expand to 2) |
| 262 | +# ... function body ... |
| 263 | +ret # 1 instruction |
| 264 | +``` |
| 265 | + |
| 266 | +Minimal overhead due to dedicated return address register (`ra`). |
| 267 | + |
| 268 | +## Debugging and Stack Traces |
| 269 | + |
| 270 | +### Stack Frame Walking |
| 271 | +With frame pointer enabled (`-fno-omit-frame-pointer`): |
| 272 | +```c |
| 273 | +void print_stack_trace(void) { |
| 274 | + uint32_t *fp = (uint32_t *)read_csr_s0(); /* s0 = frame pointer */ |
| 275 | + |
| 276 | + while (fp) { |
| 277 | + uint32_t ra = *(fp - 1); /* Return address */ |
| 278 | + uint32_t prev_fp = *(fp - 2); /* Previous frame pointer */ |
| 279 | + |
| 280 | + printf("PC: 0x%08x\n", ra); |
| 281 | + fp = (uint32_t *)prev_fp; |
| 282 | + } |
| 283 | +} |
| 284 | +``` |
| 285 | +
|
| 286 | +### Register Dump in Panic |
| 287 | +```c |
| 288 | +void panic_dump_context(void) { |
| 289 | + /* ISR context is available on stack during trap handling */ |
| 290 | + printf("ra=0x%08x sp=0x%08x gp=0x%08x tp=0x%08x\n", |
| 291 | + saved_ra, saved_sp, saved_gp, saved_tp); |
| 292 | + /* ... dump all saved registers ... */ |
| 293 | +} |
| 294 | +``` |
| 295 | + |
| 296 | +## Compliance and Validation |
| 297 | + |
| 298 | +### ABI Compliance Checks |
| 299 | +- GCC validation: Use `-mabi=ilp32` for RV32I |
| 300 | +- Stack alignment: Verified at task creation and context switches |
| 301 | +- Register preservation: Validated by context switching tests |
| 302 | +- Calling convention: Ensured by compiler and manual assembly review |
| 303 | + |
| 304 | +### Testing |
| 305 | +```c |
| 306 | +/* Validate calling convention compliance */ |
| 307 | +void test_calling_convention(void) { |
| 308 | + /* Call functions with various argument patterns */ |
| 309 | + test_no_args(); |
| 310 | + test_scalar_args(1, 2, 3, 4, 5, 6, 7, 8, 9); /* > 8 args use stack */ |
| 311 | + test_return_values(); |
| 312 | + test_callee_saved_preservation(); |
| 313 | +} |
| 314 | +``` |
| 315 | +
|
| 316 | +## References |
| 317 | +- [RISC-V Calling Convention Specification](https://riscv.org/wp-content/uploads/2024/12/riscv-calling.pdf) |
0 commit comments