Need Help or More Information?
Questions about this free C programming course, embedded C training or career guidance? Get in touch.
SVC as Syscall Dispatcher
Build a complete system call mechanism using SVC numbers to dispatch arithmetic operations. Learn how arguments flow through the exception stack frame and how this pattern scales to a full RTOS syscall table.
1. The Syscall Pattern — SVC as API Gateway
The core idea: client code in Thread mode calls a thin wrapper function that passes its arguments in R0/R1 (per AAPCS), then executes SVC #N where N identifies the service. The kernel-side handler receives the same R0/R1 via the exception stack frame, performs the privileged operation, and writes the result back to stacked R0.
2. How Function Arguments Reach the SVC Handler
The AAPCS (ARM Architecture Procedure Call Standard) specifies:
- First argument → R0
- Second argument → R1
- Third argument → R2
- Fourth argument → R3
- Additional arguments → on the stack
When add_numbers(40, -90) is called:
- R0 = 40 (first argument)
- R1 = -90 (second argument)
SVC #36fires → hardware pushes these onto the stack- Exception frame: frame[0]=40, frame[1]=-90
- Handler reads frame[0] and frame[1] to get the operands
/* AAPCS argument passing: compiler auto-loads R0, R1 */
int32_t add_numbers(int32_t x, int32_t y)
{
/* Compiler has placed x in R0, y in R1 per AAPCS */
int32_t res;
__asm volatile("SVC #36"); /* trigger SVC, R0&R1 get pushed to frame */
__asm volatile("MOV %0, R0" : "=r"(res) ::); /* R0 = result from handler */
return res;
}
3. SVC Dispatch Table — Operations 36 to 39
| SVC Number | Operation | Formula | Hex Opcode |
|---|---|---|---|
| 36 | Add | frame[0] + frame[1] | 0xDF24 |
| 37 | Subtract | frame[0] – frame[1] | 0xDF25 |
| 38 | Multiply | frame[0] * frame[1] | 0xDF26 |
| 39 | Divide | frame[0] / frame[1] | 0xDF27 |
4. Complete Working Example
/* ================================================================
* SVC SYSCALL DISPATCHER DEMO
* Four arithmetic operations dispatched via SVC numbers 36-39
* Arguments in R0/R1 via exception stack frame
* ================================================================ */
#include <stdint.h>
#include <stdio.h>
/* ── SVC wrapper functions — thin user-mode API ─────────────────
Each function:
1. Receives args in R0/R1 per AAPCS
2. Executes SVC #N to trigger the kernel
3. Reads result from R0 after SVC returns
──────────────────────────────────────────────────────────────── */
int32_t add_numbers(int32_t x, int32_t y)
{
int32_t res;
__asm volatile("SVC #36");
__asm volatile("MOV %0, R0" : "=r"(res) ::);
return res;
}
int32_t sub_numbers(int32_t x, int32_t y)
{
int32_t res;
__asm volatile("SVC #37");
__asm volatile("MOV %0, R0" : "=r"(res) ::);
return res;
}
int32_t mul_numbers(int32_t x, int32_t y)
{
int32_t res;
__asm volatile("SVC #38");
__asm volatile("MOV %0, R0" : "=r"(res) ::);
return res;
}
int32_t div_numbers(int32_t x, int32_t y)
{
int32_t res;
__asm volatile("SVC #39");
__asm volatile("MOV %0, R0" : "=r"(res) ::);
return res;
}
int main(void)
{
int32_t res;
res = add_numbers(40, -90);
printf("40 + (-90) = %ld\n", res); /* -50 */
res = sub_numbers(25, 150);
printf("25 - 150 = %ld\n", res); /* -125 */
res = mul_numbers(374, 890);
printf("374 * 890 = %ld\n", res); /* 332860 */
res = div_numbers(67, -3);
printf("67 / (-3) = %ld\n", res); /* -22 (integer division) */
for(;;);
}
/* ── Naked SVC entry: capture exact SP before anything changes ── */
__attribute__((naked)) void SVC_Handler(void)
{
__asm("MRS R0, MSP");
__asm("B SVC_Handler_c");
}
/* ── C handler: extract SVC number and dispatch ─────────────────
frame layout: [R0][R1][R2][R3][R12][LR][PC][xPSR]
frame[0] = arg0 (x), frame[1] = arg1 (y) ── */
void SVC_Handler_c(uint32_t *frame)
{
int32_t arg0 = (int32_t)frame[0]; /* stacked R0 = first argument */
int32_t arg1 = (int32_t)frame[1]; /* stacked R1 = second argument */
int32_t result = 0;
/* Extract SVC number from the opcode at (stacked_PC - 2) */
uint8_t *pc = (uint8_t*)frame[6]; /* stacked PC */
uint8_t svc_num = *(pc - 2); /* low byte of SVC opcode */
printf("SVC_Handler: svc=%d arg0=%ld arg1=%ld\n", svc_num, arg0, arg1);
switch(svc_num)
{
case 36: result = arg0 + arg1; break;
case 37: result = arg0 - arg1; break;
case 38: result = arg0 * arg1; break;
case 39:
if (arg1 == 0) {
printf("Error: divide by zero\n");
result = 0;
} else {
result = arg0 / arg1;
}
break;
default:
printf("Unknown SVC: %d\n", svc_num);
result = -1;
}
/* Write result to stacked R0 — caller gets this in R0 on return */
frame[0] = (uint32_t)result;
}
============================================================
5. Scaling to a Real RTOS Syscall Table
In a production RTOS, this pattern extends to dozens of kernel services. FreeRTOS, Zephyr, and similar RTOSes use exactly this mechanism for privileged/unprivileged separation:
/* ── RTOS-style syscall table using function pointers ── */
typedef void (*svc_handler_t)(uint32_t *frame);
/* Kernel service implementations */
void svc_task_yield(uint32_t *frame) { schedule_next_task(); }
void svc_task_delay(uint32_t *frame) { task_block(frame[0]); } /* R0=ticks */
void svc_mutex_take(uint32_t *frame) { frame[0] = mutex_acquire((void*)frame[0]); }
void svc_queue_send(uint32_t *frame) { frame[0] = queue_push((void*)frame[0], (void*)frame[1]); }
void svc_get_tick (uint32_t *frame) { frame[0] = g_tick_count; }
/* Dispatch table indexed by SVC number */
svc_handler_t svc_table[] = {
svc_task_yield, /* SVC #0 */
svc_task_delay, /* SVC #1 */
svc_mutex_take, /* SVC #2 */
svc_queue_send, /* SVC #3 */
svc_get_tick, /* SVC #4 */
/* ... */
};
#define SVC_TABLE_SIZE (sizeof(svc_table) / sizeof(svc_table[0]))
void SVC_Handler_c(uint32_t *frame)
{
uint8_t svc_num = *((uint8_t*)frame[6] - 2);
if (svc_num < SVC_TABLE_SIZE) {
svc_table[svc_num](frame); /* dispatch via function pointer */
} else {
/* Unknown syscall — return error code in R0 */
frame[0] = (uint32_t)-1;
}
}
============================================================
6. Passing More Than 2 Arguments via SVC
For operations requiring more than 4 arguments, you can pass a struct pointer in R0:
/* ── Passing struct pointer for complex syscalls ── */
typedef struct {
uint32_t operation;
int32_t arg0;
int32_t arg1;
int32_t arg2;
int32_t result;
} svc_params_t;
int32_t complex_operation(svc_params_t *params)
{
/* params pointer is in R0 per AAPCS */
__asm volatile("SVC #50");
return params->result; /* handler fills result field */
}
void svc_50_handler(uint32_t *frame)
{
/* frame[0] = R0 = pointer to svc_params_t */
svc_params_t *params = (svc_params_t*)frame[0];
switch(params->operation) {
case 0: params->result = params->arg0 + params->arg1 + params->arg2; break;
default: params->result = -1;
}
/* No need to modify frame[0] — result is in the struct */
}
============================================================
7. SVC-Based Privilege Model
8. Interview Questions & Answers
Q1. How does a SVC wrapper function pass arguments to the kernel handler?
Q2. How does the SVC handler return a value to the caller?
Q3. Why is the SVC number in the opcode rather than passed in a register?
Q4. What happens if SVC is called with an invalid SVC number?
Q5. Can SVC be used to pass 64-bit arguments? How?
(1) For a single 64-bit value: use R0+R1 pair (arg0 in R0=low word, arg1 in R1=high word), which AAPCS actually specifies for 64-bit integers.
(2) For multiple complex arguments: pass a pointer to a struct in R0. The struct in SRAM holds all arguments and the result. The kernel reads from and writes to the struct through the pointer.
Q6. What is the difference between SVC and a regular function call in terms of privilege?
Q7. In the example, why are SVC numbers 36-39 chosen instead of 0-3?
Q8. How does a function pointer dispatch table (for SVC) compare to a switch statement?
A function pointer table (array of function pointers indexed by SVC number) offers: O(1) dispatch for any SVC number, easy addition of new syscalls without modifying the dispatcher, and the ability to register handlers at runtime. However, it uses more RAM (the pointer table) and has a slight indirect-call overhead. For embedded systems with fixed syscall sets, either works well.
Q9. How does this SVC syscall pattern relate to Linux system calls?
Q10. What prevents a malicious task from modifying the exception frame to forge a return value?
1) Validate all SVC arguments inside the handler before acting on them.
2) Never trust frame[0]-frame[3] as valid kernel addresses.
3) Use the MPU to restrict tasks from accessing kernel memory even indirectly via bogus pointers passed as SVC arguments.
STM32F407 Deep Dive Series — Example 14: SVC Operations & Syscall Dispatcher
ARM Cortex-M4 TRM · AAPCS · ARMv7-M Architecture Reference Manual
Need Help or More Information?
Questions about this free C programming course, embedded C training or career guidance? Get in touch.

2 Comments