Rtos-custom_SVC_handler
Written on
What is SVC?
Service/ Supervisor/ System calls are calls made by user processes/threads to
access system level resources by calling an software interrupt. Usually
in arm it is known as SWI/SVC, in x86 it's syscall. In linux,
this instruction is used to pass control to kernel space. The instruction that
runs after calling this interrupt is known as the service call handler.
Examples of SVC?
In freertos, the svc handler's work is to initialize the psp to point to first
task's stack and put 0xFFFFFFFD in Link register, so as to return in
Thread mode using psp as stack pointer. The following example shows the exact
implementation for Cortex-M3 port of Freertos.
__asm volatile (
" ldr r3, =pxCurrentTCB \n" /* Restore the context.*/
" ldr r1, [r3] \n" /* Get the pxCurrentTCB
address.*/
" ldr r0, [r1] \n" /* The first item in
pxCurrentTCB is the
task top of stack.*/
" ldmia r0!, {r4-r11} \n" /* Pop the registers that are
not automatically saved
on exception entry and
the critical nesting
count.*/
" msr psp, r0 \n" /* Restore the task stack
pointer.*/
" isb \n"
" mov r0, #0 \n"
" msr basepri, r0 \n"
" orr r14, #0xd \n"
" bx r14 \n"
" \n"
" .ltorg \n"
);
Main
__attribute__((optimize("O0"))) /*So that s is not optimised out*/
void print(char * s){
/*0 has no meaning for now*/
__asm volatile ("svc 0\n");
s = s;/*to prevent Werror-unused*/
}
int main(){
stdout_uart_init();
char * a = "hello\n";
while(1){
print(a);
sleep_ms(1000);
}
}
Edition 1
In armv6, SVC_Handler is named as isr_svcall.
__attribute__((naked)) /*tell compiler to not add its logic inside*/
void isr_svcall(){
__asm volatile("push {lr}\n");
__asm volatile("bl puts\n");
__asm volatile("pop {r0}\n");
__asm volatile("mov lr,r0\n");
__asm volatile("bx lr\n");
}
Some problems here, First it depends on r0 being intact, if an interrupt
triggers just between svc 0 and isr_svcall r0 will be gone to
the heavens, which will cause puts to take a dangling pointer.
Edition 2
Now we want to pull the argument from stack so as to protect it from any interrupt in between.
__attribute__((naked)) /*tell compiler to not add its logic inside*/
void isr_svcall(){
__asm volatile("pop {r0}\n");
__asm volatile("push {lr}\n");
__asm volatile("bl puts\n");
__asm volatile("pop {r0}\n");
__asm volatile("mov lr,r0\n");
__asm volatile("push {r0}\n");
__asm volatile("bx lr\n");
}
but this has a fatal flaw, It will only work for cases when we haven't started running ofoff threads. So let's move on to 3rd edition.
Edition 3
Now we want to gather the first argument, so to fetch that we actually need to gather the r0 of the interrupted context, now to know the context we need to know if we were in priveleged context(using msp) or unpriveleged(using psp), so we use the lr's 4th bit to make that decision. After which we take the sp, get the stacked r0.
__attribute__((naked)) /*tell compiler to not add its logic inside*/
void isr_svcall(){
//store msp/psp in r1
__asm volatile("mov r0,#16\n"
"mov r1,lr\n"
"and r0,r1\n"
"cmp r0,#0\n"
"beq PSP\n"
"mrs r1,msp\n"
"b DONE\n"
"PSP: mrs r1,psp\n"
"DONE:\n"
"pop {r0}\n"
"push {r0}\n"
"b puts");
}
Edition 4
Now we want to gather the svc number, this number is stored along with the svc instruction so to fetch that we actually need to gather the pc of the interrupted context, now to know the context we need to know if we were in priveleged context(using msp) or unpriveleged(using psp), so we use the lr's 4th bit to make that decision. After which we take the sp, get the stacked pc, as that pc is 4 bytes ahead of svc instruction and svc number is 2 byte ahead of svc, we get the svc number's pointer by doing pc - 2, and then read the byte at address [pc-2].
__attribute__((naked)) /*tell compiler to not add its logic inside*/
void isr_svcall(){
//store msp/psp in r1
__asm volatile("mov r0,#16\n"
"mov r1,lr\n"
"and r0,r1\n"
"cmp r0,#0\n"
"beq PSP\n"
"mrs r1,msp\n"
"b DONE\n"
"PSP: mrs r1,psp\n"
"DONE:\n"
"ldr r12,[r12,#24]\n"
"ldrb r12,[r12,#-2]\n"
"your_asm_code_here");
}
Well, this is too counter intuitive, and IMO not practical. There is and IPC scratch register, the R12. We can store SVC number there. And also if you see, you need too much assembly to read r12 and to manually push, pop so many of regs.
Edition 5
The piece de resistance, the final implementation solves multiple problems, with least assembly to do the thing. This implementation is very similar to linux syscall implementation.
/**
* SPDX-License-Identifier: MIT
* Copyright (c) 2025 Sidharth Seela
*/
#include <stdio.h>
#include <stdlib.h>
#include"pico/stdlib.h"
enum {
PUTS = 1<<4 | 1,
};
struct __attribute__((packed)) stacked_bank{
uint32_t r0;
uint32_t r1;
uint32_t r2;
uint32_t r3;
uint32_t r12;
uint32_t lr;
uint32_t return_address;
uint32_t xpsr;
};
void isr_svcall_c(struct stacked_bank *bank){
//try to pop out regs by encoding sycall param count and then
//use function pointer.
switch(bank->r12){
case PUTS:
puts((char *)bank->r0);
break;
default:
__asm volatile("bkpt 1");
break;
}
}
__attribute__((naked))
void isr_svcall(){
__asm volatile("mov r0,#16\n"
"mov r1,lr\n"
"and r0,r1\n"
"cmp r0,#0\n"
"beq PSP\n"
"mrs r0,msp\n"
"b DONE\n"
"PSP: mrs r0,psp\n"
"DONE: b isr_svcall_c\n"
);
}
void syscall_1(uint32_t sys, uint32_t a){
__asm volatile ("mov r0, %1\n"
"mov r12,%0\n"
"svc 0\n"
:
:"r"(sys),"r"(a)
:"r0","r12","memory");
}
void syscall_2(uint32_t sys, uint32_t a, uint32_t b){
__asm volatile ("mov r0, %1\n"
"mov r1, %2\n"
"mov r12,%0\n"
"svc 0\n"
:
:"r"(sys),"r"(a),"r"(b)
:"r0","r1","r12","memory");
}
void SYS_PUTS(char *s){
syscall_1(PUTS,(uint32_t)s);
}
int main(){
stdout_uart_init();
char * a = "hello %d\n";
extern char __StackLimit;
printf("%p\n",(void*) &__StackLimit);
while(1){
SYS_PUTS(a);
sleep_ms(1000);
}
}
Checkout more in this Wadix's blogpost
Result
I learnt quite a lot about how syscalls work, register saving. Quite interesting experiment really.
Fun, Sidharth