系统调用
前置知识:
arm svc指令:基本和riscv ecall, x86 syscall没什么区别
对于syscall的一个概述,可以参考https://hackmd.io/@RinHizakura/S1wfy6nQO
系统调用是系统为用户程序提供的高特权操作接口。在本实验中,用户程序通过
svc
指令进入内核模式。在内核模式下,首先操作系统代码和硬件将保存用户程序的状态。操作系统根据系统调用号码执行相应的系统调用处理代码,完成系统调用的实际功能,并保存返回值。最后,操作系统和硬件将恢复用户程序的状态,将系统调用的返回值返回给用户程序,继续用户程序的执行。通过异常进入到内核后,需要保存当前线程的各 个寄存器值,以便从内核态返回用户态时进行恢复。保存工作在
exception_enter
中进行,恢复工作则由exception_exit
完成。可以参考kernel/include/arch/aarch64/arch/machine/register.h
中的寄存器结构,保存时在栈中应准备ARCH_EXEC_CONT_SIZE
大小的空间。
到此为止是上一节的内容
完成保存后,需要进行内核栈切换,首先从
TPIDR_EL1
寄存器中读取到当前核的per_cpu_info
(参考kernel/include/arch/aarch64/arch/machine/smp.h
),从而拿到其中的cpu_stack
地址。
TPIDR_EL1
(Thread Process ID Register for EL1)是ARM架构中一个特殊的寄存器,用于存储当前执行线程或进程的上下文信息。在操作系统内核中,这个寄存器经常被用来存储指向per_cpu_data
结构的指针,该结构包含了特定于CPU的数据,比如CPU的局部变量和栈指针。
实质上这是个“保留寄存器”,硬件上没有对其的直接操作,留给操作系统实现者自行使用,具体的初始化和设置在smp
之中,chcore将其设置为指向cpu_info结构体的指针,并且在之后不再变化
struct per_cpu_info {
/* The execution context of current thread */
u64 cur_exec_ctx;
/* Per-CPU stack */
char *cpu_stack;
/* struct thread *fpu_owner */
void *fpu_owner;
u32 fpu_disable;
char pad[pad_to_cache_line(sizeof(u64) +
sizeof(char *) +
sizeof(void *) +
sizeof(u32))];
} __attribute__((packed, aligned(64)));
extern struct per_cpu_info cpu_info[PLAT_CPU_NUM];
extern volatile char cpu_status[PLAT_CPU_NUM];
extern u64 ctr_el0;
void init_per_cpu_info(u32 cpuid)
{
struct per_cpu_info *info;
if (cpuid == 0)
ctr_el0 = read_ctr();
info = &cpu_info[cpuid];
info->cur_exec_ctx = 0;
info->cpu_stack = (char *)(KSTACKx_ADDR(cpuid) + CPU_STACK_SIZE);
info->fpu_owner = NULL;
info->fpu_disable = 0;
// 在这里初始化tpidr
asm volatile("msr tpidr_el1, %0"::"r" (info));
}
阅读了这些代码之后,最后的内核栈切换也可以理解了,内核栈的切换其实就是在汇编之中保存/加载不同的寄 存器在某个地址,而通过将这个地址存在某个保留寄存器之中,我们就能在保存好寄存器之后,直接赋值sp来顺利完成栈的切换
.macro switch_to_cpu_stack
mrs x24, TPIDR_EL1
add x24, x24, #OFFSET_LOCAL_CPU_STACK
ldr x24, [x24]
mov sp, x24
.endm
.macro switch_to_thread_ctx
mrs x24, TPIDR_EL1
add x24, x24, #OFFSET_CURRENT_EXEC_CTX
ldr x24, [x24]
mov sp, x24
.endm
即使不去翻 #OFFSET_XXX
的定义,也能想到是用类似container_of的hack得到
u64 cur_exec_ctx;
/* Per-CPU stack */
char *cpu_stack;
在cpu_info之中的偏移量
最后的定义也不出所料
#define OFFSET_CURRENT_EXEC_CTX 0
#define OFFSET_LOCAL_CPU_STACK 8
#define OFFSET_CURRENT_FPU_OWNER 16
#define OFFSET_FPU_DISABLE 24
以下有一个很长的调用链条,最终图在 https://www.notion.so/164bb0b856f780199160ce212c64356c?pvs=4#173bb0b856f78062906dc753069d407a
接下来我们尝试分析printf这个用户态函数,文档已经给出了他在musl-libc之中的调用链,而跟踪这个调用链,我们就可以一窥API和ABI的边界
// user/system-services/chcore-libc/musl-libc/src/stdio/__stdio_write.c
size_t __stdio_write(FILE *f, const unsigned char *buf, size_t len)
{
struct iovec iovs[2] = {
{ .iov_base = f->wbase, .iov_len = f->wpos-f->wbase },
{ .iov_base = (void *)buf, .iov_len = len }
};
struct iovec *iov = iovs;
size_t rem = iov[0].iov_len + iov[1].iov_len;
int iovcnt = 2;
ssize_t cnt;
for (;;) {
// HERE!!!
cnt = syscall(SYS_writev, f->fd, iov, iovcnt);
可以看到,最后回归到了一个SYS_writev的syscall
继续跟踪,我们似乎来到了奇怪的位置
// /usr/include/x86_64-linux-gnu/bits/syscall.h
#ifdef __NR_writev
# define SYS_writev __NR_writev
#endif
// /usr/include/x86_64-linux-gnu/asm/unistd_64.h
#define __NR_writev 20
// /usr/include/asm-generic/unistd.h
#define __NR_writev 66
__SC_COMP(__NR_writev, sys_writev, compat_sys_writev)
#ifdef __SYSCALL_COMPAT
#define __SC_COMP(_nr, _sys, _comp) __SYSCALL(_nr, _comp)
#define __SC_COMP_3264(_nr, _32, _64, _comp) __SYSCALL(_nr, _comp)
#else
#define __SC_COMP(_nr, _sys, _comp) __SYSCALL(_nr, _sys)
#define __SC_COMP_3264(_nr, _32, _64, _comp) __SC_3264(_nr, _32, _64)
#endif
此处, __SYSCALL_COMPAT
是兼容性的宏,不需要太过深入探究,而在正常情况下,用户态的 SYS_writev
就绑定到了 __NR_writev
,并继续绑定到了内核的 sys_writev
在 Linux 中,__NR_XXX
和 SYS_XXX
是用于系统调用编号的宏定义。它们在用户态和内核态之间提供了一种通过系统调用接口进行交互的机制。
__NR_XXX
和 SYS_XXX
的含义
__NR_XXX
:这是内核中定义的系统调用编号。每个系统调用都有一个唯一的编号,用于标识不同的系统调用。SYS_XXX
:这是用户态(如 glibc)使用的宏,通常直接定义为__NR_XXX
。它们用于在用户态代码中引用系统调用编号。