What is SVC as Syscall Dispatcher-Embedded C Training Institute in Hyderabad

Need Help or More Information?

Questions about this free C programming course, embedded C training or career guidance? Get in touch.

Example 14 — STM32F407 Discovery

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.

SVC call from Thread mode into the kernel handler
Thread mode (user/application)
int32_t add_numbers(x,y)
{
/* x→R0, y→R1 per
AAPCS calling conv */
SVC #36
MOV res, R0
return res
}
SVC #36
result in R0
Handler mode (kernel)
SVC_Handler (naked)
MRS R0, MSP
B SVC_Handler_c
SVC_Handler_c(frame)
arg0 = frame[0] (R0)
arg1 = frame[1] (R1)
svc = *(pc-2)
switch(svc):
36: res=arg0+arg1
frame[0] = res (→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 #36 fires → 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;
}
Why does this work? AAPCS places function parameters in R0-R3. Since the SVC instruction is inside the function body (after parameter setup), R0 and R1 still hold x and y when SVC executes. The exception hardware pushes R0 and R1 to the stack, preserving them for the handler to read.
============================================================

3. SVC Dispatch Table — Operations 36 to 39

SVC NumberOperationFormulaHex Opcode
36Addframe[0] + frame[1]0xDF24
37Subtractframe[0] – frame[1]0xDF25
38Multiplyframe[0] * frame[1]0xDF26
39Divideframe[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

Unprivileged tasks and the privileged kernel
Unprivileged Thread mode
Application Task
• Cannot access MPU-
protected registers
• Cannot modify
CONTROL/NVIC
• Can only call
user-space code
and SVC wrappers
SVC #N
EXC_RETURN
Privileged Handler mode
RTOS Kernel
• Full hardware access
• Manages task states
• Controls interrupts
• Allocates memory
• Performs I/O
• Returns result in R0
============================================================

8. Interview Questions & Answers

Q1. How does a SVC wrapper function pass arguments to the kernel handler?
Per the ARM AAPCS, function arguments are placed in R0-R3 by the compiler. When SVC executes, the hardware pushes all of R0-R3 (plus R12, LR, PC, xPSR) onto the stack as the exception frame. The kernel handler receives a pointer to this frame and reads frame[0]=R0 (arg1), frame[1]=R1 (arg2), etc. No special assembly is needed for argument passing — the calling convention handles it automatically.
Q2. How does the SVC handler return a value to the caller?
By writing the return value to frame[0] (the slot for stacked R0). When the exception exits, the hardware pops the exception frame and restores R0 from frame[0]. The caller’s next instruction after the SVC can then read R0, which now contains the kernel’s return value. This mirrors how a normal function returns a value in R0 per AAPCS.
Q3. Why is the SVC number in the opcode rather than passed in a register?
Embedding the service number in the opcode (SVC #N) frees up all argument registers (R0-R3) for actual parameters. If the SVC number were passed in R0, you’d only have R1-R3 for arguments (3 parameters instead of 4). The opcode approach lets the handler extract the number independently from the argument registers, giving maximum flexibility.
Q4. What happens if SVC is called with an invalid SVC number?
Nothing happens automatically — the processor always transfers to SVC_Handler regardless of the SVC number. The handler C code must validate the number. If invalid, the handler should write an error code (like -1 or 0xFFFFFFFF) to frame[0] to indicate failure. There is no hardware exception for unknown SVC numbers; that would be a UsageFault (UNDEFINSTR) only for truly invalid opcodes, not for a valid SVC with an unknown immediate.
Q5. Can SVC be used to pass 64-bit arguments? How?
Yes, with two approaches:
(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?
A regular function call (BL) runs in the same privilege level as the caller. SVC always transitions to Handler mode (privileged) via the exception mechanism. This means unprivileged Thread mode code can safely request privileged services (kernel operations) without permanently elevating its own privilege level. The kernel can validate and gatekeep each request.
Q7. In the example, why are SVC numbers 36-39 chosen instead of 0-3?
SVC 0-31 are often reserved for RTOS kernel services (yield, delay, create task, delete task, etc.). Starting application-defined services at 36 leaves room for the RTOS reserved range. This is a convention chosen by the developer — there is no hardware significance to any particular SVC number. Real systems define an agreed-upon allocation table in a header file.
Q8. How does a function pointer dispatch table (for SVC) compare to a switch statement?
A switch statement compiles to a jump table or series of comparisons — simple but less extensible.
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?
Almost identically. Linux on ARM uses the SVC instruction (formerly SWI) with a syscall number to request kernel services. The kernel’s SVC handler reads the syscall number, looks it up in a table, and dispatches to the implementation. Arguments are passed in R0-R6. The return value is placed in R0. Linux defines 400+ syscall numbers (open=5, read=3, write=4, etc.). The Cortex-M SVC mechanism is the embedded equivalent of this POSIX syscall layer.
Q10. What prevents a malicious task from modifying the exception frame to forge a return value?
In a system with MPU (Memory Protection Unit), the task’s stack region is mapped as user-accessible but the kernel stack and scheduler data are in a protected region. However, a task can potentially read/write its own PSP-based exception frame (since it owns that stack). This is why security-critical systems should:
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

Leave a Reply

Your email address will not be published. Required fields are marked *