用户态发起系统调用
当用户态代码调用系统调用时,将会触发一次异常,从而进入内核态。ARM64提供了汇编指令svc,实现该功能。其中x8寄存器存储了系统调用号,参数一般存在x0寄存器到x5寄存器。
以下案例是绕过libc,直接通过内联方法调用系统调用的案例:
arduino
int main() {
// 手动调用 write 系统调用(ARM64)
// 参数:x0=1(STDOUT), x1=字符串地址, x2=长度, x8=64(write 系统调用号)
const char *msg = "Raw Syscall: Hello ARM64\n";
int len = 21; // 字符串长度
asm volatile (
"mov x8, #64\n" // x8 = write 系统调用号(64)
"svc #0\n" // 触发系统调用(ARM64 核心指令)
: // 输出寄存器(无)
: "r"(1), "r"(msg), "r"(len) // 输入寄存器:x0=1, x1=msg, x2=len
: "x8", "memory" // 告诉编译器:x8 和内存会被修改,避免优化
);
// 手动调用 exit 系统调用(x8=93, x0=0)
asm volatile (
"mov x8, #93\n" // x8 = exit 系统调用号(93)
"svc #0\n"
:
: "r"(0) // x0=0(退出码)
: "x8"
);
return 0;
}
编译出中间汇编代码
arduino
aarch64-linux-gnu-gcc -S -O0 -fverbose-asm -ffreestanding -nostdlib -o test.s test.c
编译出最后可执行文件
csharp
aarch64-linux-gnu-gcc -static -ffreestanding -nostdlib -e main -o test.out test.c
内核执行系统调用
kernel会在内核态实现该异常处理。
el0t_64_sync
kernel的用户态入口实现一般在 arch/arm64/kernel/entry.S
ini
entry_handler 0, t, 64, sync ; 启用 EL0 64位 同步异常(如SVC/Abort/Trap)处理入口
entry_handler 0, t, 64, irq ; 启用 EL0 64位 普通中断(IRQ)处理入口
entry_handler 0, t, 64, fiq ; 启用 EL0 64位 快速中断(FIQ)处理入口
entry_handler 0, t, 64, error ; 启用 EL0 64位 系统错误(SError)处理入口
其中entry_handler为汇编中的宏,具体定义如下:
csharp
; el:req, ht:req, regsize:req, label:req 代表4个参数
; el\el\ht\()_\regsize\()_\label 拼接出字符串,比如el0t_64_sync
.macro entry_handler el:req, ht:req, regsize:req, label:req
SYM_CODE_START_LOCAL(el\el\ht\()_\regsize\()_\label)
kernel_entry \el, \regsize
mov x0, sp
bl el\el\ht\()_\regsize\()_\label\()_handler
.if \el == 0
b ret_to_user
.else
b ret_to_kernel
.endif
SYM_CODE_END(el\el\ht\()_\regsize\()_\label)
.endm
<include/linux/linkage.h>
arduino
/* SYM_L_* -- linkage of symbols */
#define SYM_L_GLOBAL(name) .globl name
#define SYM_L_WEAK(name) .weak name
#define SYM_L_LOCAL(name) /* nothing */
/* SYM_A_* -- align the symbol? */
#define SYM_A_ALIGN ALIGN
#define SYM_A_NONE /* nothing */
#ifndef SYM_CODE_START_LOCAL
#define SYM_CODE_START_LOCAL(name) \
SYM_START(name, SYM_L_LOCAL, SYM_A_ALIGN)
#endif
// ASM_NL 汇编换行符
// 此处生成参数name的段
/* SYM_ENTRY -- use only if you have to for non-paired symbols */
#ifndef SYM_ENTRY
#define SYM_ENTRY(name, linkage, align...) \
linkage(name) ASM_NL \
align ASM_NL \
name:
#endif
/* SYM_START -- use only if you have to */
#ifndef SYM_START
#define SYM_START(name, linkage, align...) \
SYM_ENTRY(name, linkage, align)
#endif
所以会在entry.s中生成el0t_64_sync函数段,该函数非常简单,先执行el0t_64_sync_handler,最后调用ret_to_user返回用户态。
el0t_64_sync_handler
<arch/arm64/kernel/entry-common.c>
arduino
asmlinkage void noinstr el0t_64_sync_handler(struct pt_regs *regs)
{
// 从esr寄存器读取陷入异常的原因
unsigned long esr = read_sysreg(esr_el1);
switch (ESR_ELx_EC(esr)) {
case ESR_ELx_EC_SVC64: // 64 位用户态 SVC 系统调用
el0_svc(regs);
break;
case ESR_ELx_EC_DABT_LOW:
el0_da(regs, esr);
...
}
el0_svc -> do_el0_svc -> el0_svc_common(arch/arm64/kernel/syscall.c文件中) -> invoke_syscall
invoke_syscall
<arch/arm64/kernel/syscall.c>
scss
void do_el0_svc(struct pt_regs *regs)
{
// sys_call_table 系统调用表
// regs->regs[8]中获取到系统调用号
el0_svc_common(regs, regs->regs[8], __NR_syscalls, sys_call_table);
}
static void el0_svc_common(struct pt_regs *regs, int scno, int sc_nr,
const syscall_fn_t syscall_table[])
{
...
invoke_syscall(regs, scno, sc_nr, syscall_table);
...
}
static long __invoke_syscall(struct pt_regs *regs, syscall_fn_t syscall_fn)
{
return syscall_fn(regs);
}
static void invoke_syscall(struct pt_regs *regs, unsigned int scno,
unsigned int sc_nr,
const syscall_fn_t syscall_table[])
{
long ret;
add_random_kstack_offset();
if (scno < sc_nr) {
syscall_fn_t syscall_fn;
// 根据方法号找到系统调用实现
syscall_fn = syscall_table[array_index_nospec(scno, sc_nr)];
// 调用系统调用实现
ret = __invoke_syscall(regs, syscall_fn);
} else {
ret = do_ni_syscall(regs, scno);
}
syscall_set_return_value(current, regs, 0, ret);
/*
* This value will get limited by KSTACK_OFFSET_MAX(), which is 10
* bits. The actual entropy will be further reduced by the compiler
* when applying stack alignment constraints: the AAPCS mandates a
* 16-byte aligned SP at function boundaries, which will remove the
* 4 low bits from any entropy chosen here.
*
* The resulting 6 bits of entropy is seen in SP[9:4].
*/
choose_random_kstack_offset(get_random_u16());
}
内核实现系统调用
系统调用表
arduino
...
/*
* Wrappers to pass the pt_regs argument.
*/
#define __arm64_sys_personality __arm64_sys_arm64_personality
// 通过宏声明系统调用的实现函数
// 拼接的函数名,比如 __arm64_mmap
#undef __SYSCALL
#define __SYSCALL(nr, sym) asmlinkage long __arm64_##sym(const struct pt_regs *);
#include <asm/unistd.h>
// 定义嵌套宏
#undef __SYSCALL
#define __SYSCALL(nr, sym) [nr] = __arm64_##sym,
// 通过宏生成系统调用表
const syscall_fn_t sys_call_table[__NR_syscalls] = {
[0 ... __NR_syscalls - 1] = __arm64_sys_ni_syscall,
#include <asm/unistd.h>
};
unistd.h头文件嵌套: arch/arm64/include/asm/unistd.h -> arch/arm64/include/uapi/asm/unistd.h -> include/uapi/asm-generic/unistd.h
<include/uapi/asm-generic/unistd.h> 中罗列了系统调用列表。
arduino
...
#define __NR_setxattr 5
__SYSCALL(__NR_setxattr, sys_setxattr)
#define __NR_lsetxattr 6
__SYSCALL(__NR_lsetxattr, sys_lsetxattr)
#define __NR_fsetxattr 7
__SYSCALL(__NR_fsetxattr, sys_fsetxattr)
#define __NR_getxattr 8
__SYSCALL(__NR_getxattr, sys_getxattr)
...
定义系统调用
系统调用表中的函数,是通过SYSCALL_DEFINE宏定义的。 比如mmap的系统调用实现如下:
<arch/arm64/kernel/sys.c>
arduino
#include <linux/compiler.h>
#include <linux/errno.h>
#include <linux/fs.h>
#include <linux/mm.h>
#include <linux/export.h>
#include <linux/sched.h>
#include <linux/slab.h>
// 会嵌套调用 #include <asm/syscall_wrapper.h>
#include <linux/syscalls.h>
#include <asm/cpufeature.h>
#include <asm/syscall.h>
...
SYSCALL_DEFINE6(mmap, unsigned long, addr, unsigned long, len,
unsigned long, prot, unsigned long, flags,
unsigned long, fd, unsigned long, off)
{
if (offset_in_page(off) != 0)
return -EINVAL;
return ksys_mmap_pgoff(addr, len, prot, flags, fd, off >> PAGE_SHIFT);
}
<include/linux/syscalls.h>
arduino
#include <linux/signal.h>
#include <linux/list.h>
#include <linux/bug.h>
#include <linux/sem.h>
#include <asm/siginfo.h>
#include <linux/unistd.h>
#include <linux/quota.h>
#include <linux/key.h>
#include <linux/personality.h>
#include <trace/syscall.h>
// 如果打开该宏,引入syscall_wrapper头文件
// arm64上使用该头文件的 SYSCALL_DEFINE 宏定义
#ifdef CONFIG_ARCH_HAS_SYSCALL_WRAPPER
#include <asm/syscall_wrapper.h>
#endif /
...
<arch/arm64/include/asm/syscall_wrapper.h>
scss
#define __SYSCALL_DEFINEx(x, name, ...) \
asmlinkage long __arm64_sys##name(const struct pt_regs *regs); \
ALLOW_ERROR_INJECTION(__arm64_sys##name, ERRNO); \
static long __se_sys##name(__MAP(x,__SC_LONG,__VA_ARGS__)); \
static inline long __do_sys##name(__MAP(x,__SC_DECL,__VA_ARGS__)); \
asmlinkage long __arm64_sys##name(const struct pt_regs *regs); \
asmlinkage long __arm64_sys##name(const struct pt_regs *regs) \
{ \
return __se_sys##name(SC_ARM64_REGS_TO_ARGS(x,__VA_ARGS__)); \
} \
static long __se_sys##name(__MAP(x,__SC_LONG,__VA_ARGS__)) \
{ \
long ret = __do_sys##name(__MAP(x,__SC_CAST,__VA_ARGS__)); \
__MAP(x,__SC_TEST,__VA_ARGS__); \
__PROTECT(x, ret,__MAP(x,__SC_ARGS,__VA_ARGS__)); \
return ret; \
} \
static inline long __do_sys##name(__MAP(x,__SC_DECL,__VA_ARGS__))
#define SYSCALL_DEFINE0(sname) \
SYSCALL_METADATA(_##sname, 0); \
asmlinkage long __arm64_sys_##sname(const struct pt_regs *__unused); \
ALLOW_ERROR_INJECTION(__arm64_sys_##sname, ERRNO); \
asmlinkage long __arm64_sys_##sname(const struct pt_regs *__unused)
#define SYSCALL_DEFINEx(x, sname, ...) \
SYSCALL_METADATA(sname, x, __VA_ARGS__) \
__SYSCALL_DEFINEx(x, sname, __VA_ARGS__)
ARM64上默认开启了ARCH_HAS_SYSCALL_WRAPPER宏。
<arch/arm64/Kconfig>
lua
config ARM64
def_bool y
...
select ARCH_HAS_SYSCALL_WRAPPER
所以 SYSCALL_DEFINE6 mmap 会定义 __arm64_sys_mmap 函数,即为系统调用表中mmap系统调用的实现。
CPU如何找到异常处理函数
执行svc指令,为什么会调到el0t_64_sync函数呢? 其实是通过查询启动时设置进ARM规定的寄存器内异常向量表,找到异常处理函数后进行跳转的。
生成异常向量表
异常向量表也是通过汇编宏实现的:
<arch/arm64/kernel/entry.S>
1. 定义汇编宏
less
.macro kernel_ventry, el:req, ht:req, regsize:req, label:req
.align 7
; 第一步,如果是从el0进el1,备份和清理tpidrro_el0
.Lventry_start\@:
.if \el == 0
/*
* This must be the first instruction of the EL0 vector entries. It is
* skipped by the trampoline vectors, to trigger the cleanup.
*/
b .Lskip_tramp_vectors_cleanup\@
.if \regsize == 64
mrs x30, tpidrro_el0
msr tpidrro_el0, xzr
.else
mov x30, xzr
.endif
.Lskip_tramp_vectors_cleanup\@:
.endif
; 第二步,在stack上构建异常栈帧
sub sp, sp, #PT_REGS_SIZE
; 第三步,跳转到异常处理函数段,比如el0t_64_sync
b el\el\ht\()_\regsize\()_\label
.org .Lventry_start\@ + 128 // Did we overflow the ventry slot?
.endm
2. 拼接构建异常向量表
go
/*
* Exception vectors.
*/
.pushsection ".entry.text", "ax"
.align 11
SYM_CODE_START(vectors)
kernel_ventry 1, t, 64, sync // Synchronous EL1t
kernel_ventry 1, t, 64, irq // IRQ EL1t
kernel_ventry 1, t, 64, fiq // FIQ EL1t
kernel_ventry 1, t, 64, error // Error EL1t
kernel_ventry 1, h, 64, sync // Synchronous EL1h
kernel_ventry 1, h, 64, irq // IRQ EL1h
kernel_ventry 1, h, 64, fiq // FIQ EL1h
kernel_ventry 1, h, 64, error // Error EL1h
kernel_ventry 0, t, 64, sync // Synchronous 64-bit EL0
kernel_ventry 0, t, 64, irq // IRQ 64-bit EL0
kernel_ventry 0, t, 64, fiq // FIQ 64-bit EL0
kernel_ventry 0, t, 64, error // Error 64-bit EL0
kernel_ventry 0, t, 32, sync // Synchronous 32-bit EL0
kernel_ventry 0, t, 32, irq // IRQ 32-bit EL0
kernel_ventry 0, t, 32, fiq // FIQ 32-bit EL0
kernel_ventry 0, t, 32, error // Error 32-bit EL0
SYM_CODE_END(vectors)
异常向量表的布局,需要严格遵守ARM的约定:
《Learn the architecture - AArch64 Exception Model》
中文解释如下:
| 相对基址偏移 | 异常入口类型 | 异常等级/模式 | 触发场景(举例) | 对应内核代码中的宏 |
|---|---|---|---|---|
| 0x000 | Synchronous | EL0 (AArch64) | 用户态系统调用(svc #0)、未定义指令 | tramp_ventry (EL0,64) |
| 0x080 | IRQ | EL0 (AArch64) | 用户态外设中断(如键盘中断) | tramp_ventry (EL0,64) |
| 0x100 | FIQ | EL0 (AArch64) | 用户态快速中断(极少使用) | tramp_ventry (EL0,64) |
| 0x180 | Error | EL0 (AArch64) | 用户态数据访问错误(如非法地址) | tramp_ventry (EL0,64) |
| 0x200 | Synchronous | EL1t (AArch64) | 内核态同步异常(如空指针访问) | kernel_ventry(1,t,64,sync) |
| 0x280 | IRQ | EL1t (AArch64) | 内核态外设中断(如网卡中断) | kernel_ventry(1,t,64,irq) |
| 0x300 | FIQ | EL1t (AArch64) | 内核态快速中断(极少使用) | kernel_ventry(1,t,64,fiq) |
| 0x380 | Error | EL1t (AArch64) | 内核态数据访问错误(如页表错误) | kernel_ventry(1,t,64,error) |
| 0x400 | Synchronous | EL1h (AArch64) | 虚拟化模式内核同步异常 | kernel_ventry(1,h,64,sync) |
| 0x480 | IRQ | EL1h (AArch64) | 虚拟化模式内核IRQ中断 | kernel_ventry(1,h,64,irq) |
| 0x500 | FIQ | EL1h (AArch64) | 虚拟化模式内核FIQ中断 | kernel_ventry(1,h,64,fiq) |
| 0x580 | Error | EL1h (AArch64) | 虚拟化模式内核错误异常 | kernel_ventry(1,h,64,error) |
| 0x600 | Synchronous | EL0 (AArch32) | 32位用户态系统调用 | tramp_ventry (EL0,32) |
| 0x680 | IRQ | EL0 (AArch32) | 32位用户态IRQ中断 | tramp_ventry (EL0,32) |
| 0x700 | FIQ | EL0 (AArch32) | 32位用户态FIQ中断 | tramp_ventry (EL0,32) |
| 0x780 | Error | EL0 (AArch32) | 32位用户态错误异常 | tramp_ventry (EL0,32) |
备份清理tpidrro_el0
tpidrro_el0:ARM64 架构中EL0(用户态)只读的线程局部存储(TLS)寄存器,核心作用是为用户态线程提供 "线程私有数据的快速访问入口"。
ini
.if \regsize == 64
; 将tpidrro_el0备份到x30寄存器
mrs x30, tpidrro_el0
; 用零值寄存器xzr清理tpidrro_el0
msr tpidrro_el0, xzr
为什么要保存tpidrro_el0寄存器
- 防止用户态数据泄露到内核态,引起安全性问题
- 调试等操作可能污染tpidrro_el0寄存器数据
- tpidrro_el0寄存器不会自动保存,此处不保存的话,如果切换任务后,tpidrro_el0会被其他用户态进程干扰污染
- tpidrro_el0是用户态只读寄存器,因此依赖内核做备份和清理操作
为什么使用x30寄存器
x30寄存器,在arm64中为Link Register (LR)链接寄存器,存储函数调用的返回地址(bl 指令会自动将返回地址写入 x30);若无函数调用时,可作为普通通用寄存器使用。
- 陷入异常时,并未发生函数调用,因此x30可以复用。而x0-x29等通用寄存器需要保留,后续会用于保存栈帧。
- 异常切换时,CPU只会自动保存/恢复通用寄存器(x0-x30),所以会用x30作为备份寄存器。
设置异常向量表
<arch/arm64/kernel/head.S>
scss
SYM_FUNC_START_LOCAL(__primary_switched)
adr_l x4, init_task
init_cpu_task x4, x5, x6
// 加载异常向量表(vectors)的虚拟地址到x8
adr_l x8, vectors // load VBAR_EL1 with virtual
// 将向量表地址写入VBAR_EL1寄存器
msr vbar_el1, x8 // vector table address
// 指令同步屏障,确保VBAR_EL1修改立即生效
isb
...
调用顺序:primary_entry -> __primary_switch -> __primary_switched,而 primary_entry 是 ARM64 主 CPU 核上电后执行的第一个汇编入口。
VBAR_EL1 是 ARM64 架构中EL1(内核态)的向量基地址寄存器(Vector Base Address Register),核心作用是存储异常向量表的虚拟基地址。
因此发生系统调用时,cpu通过vbar_el1查询到异常处理函数地址,进而跳转到异常处理函数。