Skip to content

Commit 8c60804

Browse files
authored
Merge pull request #53 from HeatCrab/u-mode/basic-support
Add basic U-mode support with ecall-based syscalls
2 parents afa6148 + 814b636 commit 8c60804

File tree

18 files changed

+598
-96
lines changed

18 files changed

+598
-96
lines changed

.ci/run-app-tests.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ else
4747
fi
4848

4949
# Filter excluded apps
50-
EXCLUDED_APPS=""
50+
EXCLUDED_APPS="umode"
5151
if [ -n "$EXCLUDED_APPS" ]; then
5252
FILTERED_APPS=""
5353
for app in $APPS; do

.ci/run-functional-tests.sh

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ TOOLCHAIN_TYPE=${TOOLCHAIN_TYPE:-gnu}
1111
declare -A FUNCTIONAL_TESTS
1212
FUNCTIONAL_TESTS["mutex"]="Fairness: PASS,Mutual Exclusion: PASS,Data Consistency: PASS,Overall: PASS"
1313
FUNCTIONAL_TESTS["semaphore"]="Overall: PASS"
14+
FUNCTIONAL_TESTS["umode"]="PASS: sys_tid() returned,PASS: sys_uptime() returned,[EXCEPTION] Illegal instruction"
1415
#FUNCTIONAL_TESTS["test64"]="Unsigned Multiply: PASS,Unsigned Divide: PASS,Signed Multiply: PASS,Signed Divide: PASS,Left Shifts: PASS,Logical Right Shifts: PASS,Arithmetic Right Shifts: PASS,Overall: PASS"
1516
#FUNCTIONAL_TESTS["suspend"]="Suspend: PASS,Resume: PASS,Self-Suspend: PASS,Overall: PASS"
1617

@@ -75,7 +76,8 @@ test_functional_app() {
7576
IFS=',' read -ra PASS_CRITERIA <<< "$expected_passes"
7677

7778
# Check for crashes first
78-
if echo "$output" | grep -qiE "(trap|exception|fault|panic|illegal|segfault)"; then
79+
# Special case: umode test expects an illegal instruction exception
80+
if [ "$test" != "umode" ] && echo "$output" | grep -qiE "(trap|exception|fault|panic|illegal|segfault)"; then
7981
echo "[!] Crash detected"
8082

8183
# Mark all criteria as crashed

Documentation/hal-calling-convention.md

Lines changed: 24 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -109,13 +109,14 @@ void hal_context_restore(jmp_buf env, int32_t val); /* Restore context + process
109109
The ISR in `boot.c` performs a complete context save of all registers:
110110

111111
```
112-
Stack Frame Layout (128 bytes, offsets from sp):
112+
Stack Frame Layout (144 bytes, 33 words × 4 bytes, offsets from sp):
113113
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
114+
24: s0, 28: s1, 32: a0, 36: a1, 40: a2, 44: a3
115115
48: a4, 52: a5, 56: a6, 60: a7, 64: s2, 68: s3
116116
72: s4, 76: s5, 80: s6, 84: s7, 88: s8, 92: s9
117117
96: s10, 100:s11, 104:t3, 108: t4, 112: t5, 116: t6
118-
120: mcause, 124: mepc
118+
120: mcause, 124: mepc, 128: mstatus
119+
132-143: padding (12 bytes for 16-byte alignment)
119120
```
120121

121122
Why full context save in ISR?
@@ -128,7 +129,7 @@ Why full context save in ISR?
128129

129130
Each task stack must reserve space for the ISR frame:
130131
```c
131-
#define ISR_STACK_FRAME_SIZE 128 /* 32 registers × 4 bytes */
132+
#define ISR_STACK_FRAME_SIZE 144 /* 33 words × 4 bytes, 16-byte aligned */
132133
```
133134
134135
This "red zone" is reserved at the top of every task stack to guarantee ISR safety.
@@ -147,10 +148,20 @@ int32_t result = mo_task_spawn(task_function, 2048);
147148

148149
### System Call Interface
149150

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
151+
Linmo provides system calls through the RISC-V trap mechanism for privilege
152+
boundary crossing. User mode tasks invoke system calls using the environment
153+
call instruction, which triggers a synchronous exception handled by the kernel.
154+
155+
System call convention:
156+
- Arguments passed in `a0-a7` registers before trap
157+
- System call number in `a7` register
158+
- Trap handler preserves all registers except return value
159+
- Return value delivered in `a0` register after trap return
160+
- Standard RISC-V calling convention maintained across privilege boundary
161+
162+
The trap-based interface allows user mode tasks to safely access kernel
163+
services without requiring privileged instruction execution. The kernel
164+
validates all parameters and mediates access to protected resources.
154165

155166
### Task Entry Points
156167

@@ -174,9 +185,9 @@ Each task has its own stack with this layout:
174185
175186
```
176187
High Address
177-
+------------------+ <- stack_base + stack_size
178-
| ISR Red Zone | <- 128 bytes reserved for ISR
179-
| (128 bytes) |
188+
+------------------+ <- stack_base + stack_size
189+
| ISR Red Zone | <- 144 bytes reserved for ISR
190+
| (144 bytes) |
180191
+------------------+ <- Initial SP (16-byte aligned)
181192
| |
182193
| Task Stack | <- Grows downward
@@ -251,8 +262,8 @@ Minimal context (jmp_buf):
251262
- 17 × 32-bit loads/stores = 68 bytes
252263
- Essential for cooperative scheduling
253264
254-
Full context (ISR):
255-
- 32 × 32-bit loads/stores = 128 bytes
265+
Full context (ISR):
266+
- 33 × 32-bit loads/stores = 144 bytes (includes padding for alignment)
256267
- Required for preemptive interrupts
257268
258269
### Function Call Overhead

Documentation/hal-riscv-context-switch.md

Lines changed: 69 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,11 @@ State Preservation:
9696
- Nested interrupts are handled correctly by hardware's automatic state stacking
9797

9898
### Task Initialization
99-
New tasks are initialized with proper processor state:
99+
Task initialization differs between cooperative and preemptive modes due to
100+
their distinct context management approaches.
101+
102+
In cooperative mode, tasks use lightweight context structures for voluntary
103+
yielding. New tasks are initialized with execution context only:
100104

101105
```c
102106
void hal_context_init(jmp_buf *ctx, size_t sp, size_t ss, size_t ra)
@@ -109,7 +113,58 @@ void hal_context_init(jmp_buf *ctx, size_t sp, size_t ss, size_t ra)
109113
}
110114
```
111115
112-
This ensures new tasks start with interrupts enabled in machine mode.
116+
This lightweight approach uses standard calling conventions where tasks
117+
return control through normal function returns.
118+
119+
Preemptive mode requires interrupt frame structures to support trap-based
120+
context switching and privilege mode transitions. Task initialization builds
121+
a complete interrupt service routine frame:
122+
123+
```c
124+
void *hal_build_initial_frame(void *stack_top,
125+
void (*task_entry)(void),
126+
int user_mode)
127+
{
128+
/* Place frame in stack with initial reserve below for proper startup */
129+
uint32_t *frame = (uint32_t *) ((uint8_t *) stack_top - 256 -
130+
ISR_STACK_FRAME_SIZE);
131+
132+
/* Initialize all general purpose registers to zero */
133+
for (int i = 0; i < 32; i++)
134+
frame[i] = 0;
135+
136+
/* Compute thread pointer: aligned to 64 bytes from _end */
137+
uint32_t tp_val = ((uint32_t) &_end + 63) & ~63U;
138+
139+
/* Set essential pointers */
140+
frame[FRAME_GP] = (uint32_t) &_gp; /* Global pointer */
141+
frame[FRAME_TP] = tp_val; /* Thread pointer */
142+
143+
/* Configure processor state for task entry:
144+
* - MPIE=1: Interrupts will enable when task starts
145+
* - MPP: Target privilege level (user or machine mode)
146+
* - MIE=0: Keep interrupts disabled during frame restoration
147+
*/
148+
uint32_t mstatus_val =
149+
MSTATUS_MPIE | (user_mode ? MSTATUS_MPP_USER : MSTATUS_MPP_MACH);
150+
frame[FRAME_MSTATUS] = mstatus_val;
151+
152+
/* Set entry point */
153+
frame[FRAME_EPC] = (uint32_t) task_entry;
154+
155+
return frame; /* Return frame base as initial stack pointer */
156+
}
157+
```
158+
159+
The interrupt frame layout reserves space for all register state, control
160+
registers, and alignment padding. When the scheduler first dispatches this
161+
task, the trap return mechanism restores the frame and transfers control to
162+
the entry point with the configured privilege level.
163+
164+
Key differences from cooperative mode include full register state allocation
165+
rather than minimal callee-saved registers, trap return semantics rather than
166+
function return, support for privilege level transitions through MPP
167+
configuration, and proper interrupt state initialization through MPIE bit.
113168

114169
## Implementation Details
115170

@@ -168,10 +223,19 @@ New Task Creation:
168223
4. Processor state initialized with interrupts enabled
169224
170225
First Task Launch:
171-
1. `hal_dispatch_init` transfers control from kernel to first task
226+
227+
**Cooperative Mode**:
228+
1. `hal_dispatch_init` receives lightweight context structure
172229
2. Global interrupts enabled just before task execution
173-
3. Timer interrupts activated for preemptive scheduling
174-
4. Task begins execution at its entry point
230+
3. Control transfers to first task through standard function call
231+
4. Task begins execution and voluntarily yields control
232+
233+
**Preemptive Mode**:
234+
1. `hal_dispatch_init` receives interrupt frame pointer
235+
2. Timer interrupt enabled for periodic preemption
236+
3. Dispatcher loads frame and executes trap return instruction
237+
4. Hardware restores registers and transitions to configured privilege level
238+
5. Task begins execution and can be preempted by timer
175239
176240
Context Switch Cycle:
177241
1. Timer interrupt triggers scheduler entry

Makefile

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ deps += $(LIB_OBJS:%.o=%.o.d)
2929
APPS := coop echo hello mqueues semaphore mutex cond \
3030
pipes pipes_small pipes_struct prodcons progress \
3131
rtsched suspend test64 timer timer_kill \
32-
cpubench test_libc
32+
cpubench test_libc umode
3333

3434
# Output files for __link target
3535
IMAGE_BASE := $(BUILD_DIR)/image
@@ -66,9 +66,9 @@ $(APPS): %: rebuild $(BUILD_APP_DIR)/%.o linmo
6666
# Link target - creates all output files
6767
__link: $(IMAGE_FILES)
6868

69-
$(IMAGE_BASE).elf: $(BUILD_APP_DIR)/*.o $(BUILD_DIR)/liblinmo.a
69+
$(IMAGE_BASE).elf: $(BUILD_APP_DIR)/*.o $(BUILD_DIR)/liblinmo.a $(ENTRY_OBJ)
7070
$(VECHO) " LD\t$@\n"
71-
$(Q)$(LD) $(LDFLAGS) -T$(LDSCRIPT) -Map $(IMAGE_BASE).map -o $@ $(BUILD_APP_DIR)/*.o -L$(BUILD_DIR) -llinmo
71+
$(Q)$(LD) $(LDFLAGS) -T$(LDSCRIPT) -Map $(IMAGE_BASE).map -o $@ $(BUILD_APP_DIR)/*.o $(ENTRY_OBJ) -L$(BUILD_DIR) -llinmo
7272

7373
$(IMAGE_BASE).lst: $(IMAGE_BASE).elf
7474
$(VECHO) " DUMP\t$@\n"

app/umode.c

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
#include <linmo.h>
2+
3+
/* U-mode Validation Task
4+
*
5+
* Integrates two tests into a single task flow to ensure sequential execution:
6+
* 1. Phase 1: Mechanism Check - Verify syscalls work.
7+
* 2. Phase 2: Security Check - Verify privileged instructions trigger a trap.
8+
*/
9+
void umode_validation_task(void)
10+
{
11+
/* --- Phase 1: Mechanism Check (Syscalls) --- */
12+
umode_printf("[umode] Phase 1: Testing Syscall Mechanism\n");
13+
14+
/* Test 1: sys_tid() - Simplest read-only syscall. */
15+
int my_tid = sys_tid();
16+
if (my_tid > 0) {
17+
umode_printf("[umode] PASS: sys_tid() returned %d\n", my_tid);
18+
} else {
19+
umode_printf("[umode] FAIL: sys_tid() failed (ret=%d)\n", my_tid);
20+
}
21+
22+
/* Test 2: sys_uptime() - Verify value transmission is correct. */
23+
int uptime = sys_uptime();
24+
if (uptime >= 0) {
25+
umode_printf("[umode] PASS: sys_uptime() returned %d\n", uptime);
26+
} else {
27+
umode_printf("[umode] FAIL: sys_uptime() failed (ret=%d)\n", uptime);
28+
}
29+
30+
/* Note: Skipping sys_tadd for now, as kernel user pointer checks might
31+
* block function pointers in the .text segment, avoiding distraction.
32+
*/
33+
34+
/* --- Phase 2: Security Check (Privileged Access) --- */
35+
umode_printf("[umode] ========================================\n");
36+
umode_printf("[umode] Phase 2: Testing Security Isolation\n");
37+
umode_printf(
38+
"[umode] Action: Attempting to read 'mstatus' CSR from U-mode.\n");
39+
umode_printf("[umode] Expect: Kernel Panic with 'Illegal instruction'.\n");
40+
umode_printf("[umode] ========================================\n");
41+
42+
/* CRITICAL: Delay before suicide to ensure logs are flushed from
43+
* buffer to UART.
44+
*/
45+
sys_tdelay(10);
46+
47+
/* Privileged Instruction Trigger */
48+
uint32_t mstatus;
49+
asm volatile("csrr %0, mstatus" : "=r"(mstatus));
50+
51+
/* If execution reaches here, U-mode isolation failed (still has
52+
* privileges).
53+
*/
54+
umode_printf(
55+
"[umode] FAIL: Privileged instruction executed! (mstatus=0x%lx)\n",
56+
(long) mstatus);
57+
58+
/* Spin loop to prevent further execution. */
59+
while (1)
60+
sys_tyield();
61+
}
62+
63+
int32_t app_main(void)
64+
{
65+
umode_printf("[Kernel] Spawning U-mode validation task...\n");
66+
67+
/* app_main is called from kernel context during bootstrap.
68+
* Use mo_task_spawn_user to create the validation task in user mode.
69+
* This ensures privilege isolation is properly tested.
70+
*/
71+
mo_task_spawn_user(umode_validation_task, DEFAULT_STACK_SIZE);
72+
73+
/* Return 1 to enable preemptive scheduler */
74+
return 1;
75+
}

arch/riscv/boot.c

Lines changed: 19 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -94,10 +94,10 @@ __attribute__((naked, section(".text.prologue"))) void _entry(void)
9494
}
9595

9696
/* Size of the full trap context frame saved on the stack by the ISR.
97-
* 30 GPRs (x1, x3-x31) + mcause + mepc = 32 registers * 4 bytes = 128 bytes.
98-
* This provides a 16-byte aligned full context save.
97+
* 30 GPRs (x1, x3-x31) + mcause + mepc + mstatus = 33 words * 4 bytes = 132
98+
* bytes. Round up to 144 bytes for 16-byte alignment.
9999
*/
100-
#define ISR_CONTEXT_SIZE 128
100+
#define ISR_CONTEXT_SIZE 144
101101

102102
/* Low-level Interrupt Service Routine (ISR) trampoline.
103103
*
@@ -154,11 +154,15 @@ __attribute__((naked, aligned(4))) void _isr(void)
154154
"sw t6, 29*4(sp)\n"
155155

156156
/* Save trap-related CSRs and prepare arguments for do_trap */
157-
"csrr a0, mcause\n" /* Arg 1: cause */
158-
"csrr a1, mepc\n" /* Arg 2: epc */
159-
"mv a2, sp\n" /* Arg 3: isr_sp (current stack frame) */
157+
"csrr a0, mcause\n"
158+
"csrr a1, mepc\n"
159+
"csrr a2, mstatus\n" /* For context switching in privilege change */
160+
160161
"sw a0, 30*4(sp)\n"
161162
"sw a1, 31*4(sp)\n"
163+
"sw a2, 32*4(sp)\n"
164+
165+
"mv a2, sp\n" /* a2 = isr_sp */
162166

163167
/* Call the high-level C trap handler.
164168
* Returns: a0 = SP to use for restoring context (may be different
@@ -169,9 +173,13 @@ __attribute__((naked, aligned(4))) void _isr(void)
169173
/* Use returned SP for context restore (enables context switching) */
170174
"mv sp, a0\n"
171175

172-
/* Restore context. mepc might have been modified by the handler */
173-
"lw a1, 31*4(sp)\n"
174-
"csrw mepc, a1\n"
176+
/* Restore mstatus from frame[32] */
177+
"lw t0, 32*4(sp)\n"
178+
"csrw mstatus, t0\n"
179+
180+
/* Restore mepc from frame[31] (might have been modified by handler) */
181+
"lw t1, 31*4(sp)\n"
182+
"csrw mepc, t1\n"
175183
"lw ra, 0*4(sp)\n"
176184
"lw gp, 1*4(sp)\n"
177185
"lw tp, 2*4(sp)\n"
@@ -208,7 +216,7 @@ __attribute__((naked, aligned(4))) void _isr(void)
208216

209217
/* Return from trap */
210218
"mret\n"
211-
: /* no outputs */
212-
: "i"(ISR_CONTEXT_SIZE)
219+
: /* no outputs */
220+
: "i"(ISR_CONTEXT_SIZE) /* +16 for mcause, mepc, mstatus */
213221
: "memory");
214222
}

arch/riscv/build.mk

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,14 @@ HAL_OBJS := boot.o hal.o muldiv.o
7474
HAL_OBJS := $(addprefix $(BUILD_KERNEL_DIR)/,$(HAL_OBJS))
7575
deps += $(HAL_OBJS:%.o=%.o.d)
7676

77+
# Architecture-specific syscall entry point requiring direct linkage.
78+
# Archives only extract objects when symbols are unresolved. Since the generic
79+
# syscall dispatcher provides a weak symbol, the archive mechanism would skip
80+
# the strong override. Direct linking ensures the architecture-specific
81+
# implementation takes precedence at link time.
82+
ENTRY_OBJ := $(BUILD_KERNEL_DIR)/entry.o
83+
deps += $(ENTRY_OBJ).d
84+
7785
$(BUILD_KERNEL_DIR)/%.o: $(ARCH_DIR)/%.c | $(BUILD_DIR)
7886
$(VECHO) " CC\t$@\n"
7987
$(Q)$(CC) $(CFLAGS) -o $@ -c -MMD -MF $@.d $<

0 commit comments

Comments
 (0)