Skip to content

Commit a1576cf

Browse files
authored
Merge branch 'sysprog21:main' into main
2 parents 82524de + ff94b81 commit a1576cf

23 files changed

+2225
-1121
lines changed
Lines changed: 317 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,317 @@
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

Comments
 (0)