SVC系统调用
2026/10/1大约 4 分钟
在非特权级下,函数也可以访问特权级才能访问访问的资源->在异常处理函数里面访问
所以调用这个函数就相当于触发一次异常,在异常中处理访问资源
第一层:用户应用层
提供了几个接口,在受限的环境下,可以通过这几个接口来访问内核资源
#include <stdio.h>
#include "ARMCM3.h"
#define SYSCALL_ENABLE_IRQ 0
#define SYSCALL_PRINT_NUM 1
#define SYSCALL_SLEEP 2
// 用户态 API:申请睡眠
int sys_sleep(void) {
return syscall(SYSCALL_SLEEP, 0, 0);
}
// 用户态 API:申请开关中断
int sys_enable_irq(int enable) {
return syscall(SYSCALL_ENABLE_IRQ, enable, 0);
}
// 用户态 API:申请打印
int sys_print_num(char *fmt, int num) {
return syscall(SYSCALL_PRINT_NUM, (int)fmt, num);
}
// 应用入口
int main(void) {
NVIC_EnableIRQ(Interrupt0_IRQn); // 注意:此操作必须在进入用户模式前完成(特权态)
switch_user(); // 切换到非特权模式
int res = sys_sleep();
res = sys_enable_irq(1);
res = sys_print_num("%d\n", 32);
for (;;) {
sys_print_num("Hello, world\n", 0); // 用户态下不能直接 printf
}
return 0;
}第二层:系统调用接口层
// 统一的系统调用入口,打包参数并触发 SVC 异常
static inline int syscall(int id, int arg1, int arg2) {
register int r0 __ASM("r0") = id;
register int r1 __ASM("r1") = arg1;
register int r2 __ASM("r2") = arg2;
__asm volatile (
"svc #0x80\n"
: "+r"(r0)
: "r"(r1), "r"(r2)
: "memory"
);
return r0; // 内核层修改栈帧里的 r0,异常返回时自动带回
}保存函数传入的参数的值到通用寄存器里面,一会儿进入异常会保存上下文,通用寄存器的值会被改变,但是SP只会继续向下生长。通过SP指针指向的堆栈地址就可以实现栈回溯

第三层:异常边界层
// 硬件自动压栈的寄存器结构
typedef struct _stack_frame_t {
uint32_t r0;
uint32_t r1;
uint32_t r2;
uint32_t r3;
uint32_t r12;
uint32_t lr;
uint32_t pc;
uint32_t xpsr;
} stack_frame_t;
// 进入用户模式(非特权态)
void switch_user(void) {
__set_CONTROL(__get_CONTROL() | 0x1);
__ISB(); // 切换 CONTROL 后必须执行指令同步屏障
}
// SVC 异常入口:极简裸汇编,把 MSP 传给 C 函数
__attribute__((naked)) void SVC_Handler(void) {
__asm volatile (
"mrs r0, msp\n"
"b SVC_C_Handler\n"
);
}把MSP给R0(注意,到这里,之前我调用函数的上下文包括R0 R1 R2这些保存的函数参数都应经被保存在MSP指向的栈里面了),这里R0又被覆盖为栈的地址,作为SVC里调用的函数的参数

第四层:内核特权服务层
// SVC 的 C 处理函数(运行在特权模式)
void SVC_C_Handler(stack_frame_t *frame) {
// 修正:SVC 是 16 位指令,PC-2 处读取 8 位立即数
uint8_t imm = *(uint8_t *)(frame->pc - 2);
// 只处理 svc #0x80(低 8 位是 0x80)
if (imm != 0x80) {
return;
}
// 根据 R0 中的系统调用号进行分发
switch (frame->r0) {
case SYSCALL_SLEEP: {
for (int i = 0; i < 100000; i++) {
__NOP();
}
frame->r0 = 1; // 修改栈帧中的 R0 作为返回值
break;
}
case SYSCALL_PRINT_NUM: {
printf((char *)frame->r1, frame->r2);
frame->r0 = 2;
break;
}
case SYSCALL_ENABLE_IRQ: {
if (frame->r1) {
__enable_irq();
} else {
__disable_irq();
}
frame->r0 = 3;
break;
}
default:
break;
}
}这里要找SVC #xxx 这条指令的立即数xxx是多少,这里是我触发SVC异常时写入的立即数。
用PC去查找,发现PC还在它的高地址处,所以得减2。这里可以看见一条Thumb指令是16位的2字节,具体是2还是4得具体调试才知道,所见即所得

完整代码
#include <stdio.h>
#include "ARMCM3.h"
typedef struct _stack_frame_t {
uint32_t r0;
uint32_t r1;
uint32_t r2;
uint32_t r3;
uint32_t r12;
uint32_t lr;
uint32_t pc;
uint32_t xpsr;
} stack_frame_t;
#define SYSCALL_ENABLE_IRQ 0
#define SYSCALL_PRINT_NUM 1
#define SYSCALL_SLEEP 2
// SVC 的 C 处理函数
void SVC_C_Handler (stack_frame_t * frame) {
// 修正:PC-2 处是 16 位的 SVC 指令,立即数只有 8 位,应读 uint8_t 或 uint16_t
uint8_t imm = *(uint8_t *)(frame->pc - 2);
// 原代码判断 imm != 0 就 return,这里保持原逻辑(即只处理 svc #0x80,因为低8位是0x80)
if (imm != 0x80) {
return;
}
switch (frame->r0) {
case SYSCALL_SLEEP: {
for (int i = 0; i < 100000; i++) {
__NOP();
}
frame->r0 = 1;
break;
}
case SYSCALL_PRINT_NUM: {
printf((char *)frame->r1, frame->r2);
frame->r0 = 2;
break;
}
case SYSCALL_ENABLE_IRQ: {
if (frame->r1) {
__enable_irq();
} else {
__disable_irq();
}
frame->r0 = 3;
break;
}
default:
break;
}
}
// 极简的 SVC 入口汇编:只需把 MSP 传给 C 函数
__attribute__((naked)) void SVC_Handler (void) {
__asm volatile (
"mrs r0, msp\n"
"b SVC_C_Handler\n"
);
}
// 简化 1:使用统一的内联汇编宏,消除 syscall_0/1/2 的重复代码
// 只要参数不超过 3 个,都可以用这个模式
static inline int syscall(int id, int arg1, int arg2) {
register int r0 __ASM("r0") = id;
register int r1 __ASM("r1") = arg1;
register int r2 __ASM("r2") = arg2;
__asm volatile (
"svc #0x80\n"
: "+r"(r0)
: "r"(r1), "r"(r2)
: "memory"
);
return r0; // SVC_C_Handler 修改了栈帧里的 r0,返回时自动带回
}
// 简化 2:具体的 API 直接用 C 语言调用,无需内联汇编
int sys_sleep(void) {
return syscall(SYSCALL_SLEEP, 0, 0);
}
int sys_enable_irq(int enable) {
return syscall(SYSCALL_ENABLE_IRQ, enable, 0);
}
int sys_print_num(char *fmt, int num) {
return syscall(SYSCALL_PRINT_NUM, (int)fmt, num);
}
// 简化 3:使用 CMSIS 标准函数切换用户模式
void switch_user(void) {
__set_CONTROL(__get_CONTROL() | 0x1);
__ISB(); // 切换 CONTROL 后需要指令同步屏障
}
int main (void) {
NVIC_EnableIRQ(Interrupt0_IRQn);
switch_user();
int res = sys_sleep();
res = sys_enable_irq(1);
res = sys_print_num("%d\n", 32);
for (;;) {
// 注意:在非特权模式下,直接调用 printf 会触发 HardFault(因为要访问外设/内存)
// 这里应该用 sys_print_num 代替,原代码这里是有问题的
sys_print_num("Hello, world\n", 0);
}
return 0;
}
