翻译自 Anatomy of a system call, part 1 [LWN.net]
系统调用是用户空间和内核空间交互的主要机制。因为他们很重要,不意外的是我们发现内核包含大量各种各样的机制来保证系统调用可以跨平台的通用的实现。并切可以以一种有效和一致的方式呈现给用户空间。
我一直在做移植FreeBSD的Capsicum安全框架到Linux的工作,因为这个涉及添加几个新的系统调用(包括一个特别的execveat()系统调用),我会研究它实现的细节。作为结果,本文是2篇探索内核系统调用实现细节的一篇。在这篇文章中,我们专注主流的情况:一个普通系统调用(read)的实现原理,以及x86_64用户代码调用它的机制。第二篇文章会关注一些非主流的例子,以及其它系统调用机制。
用SYSCALL_DEFINEn()定义一个系统调用
read()系统调用作为探索内核系统调用机制的一个例子。它实现在fs/read_write.c当中。它是一个很短的函数,它会调用vfs_read()完成具体的工作。从调用角度看,最有趣的事这个函数的定义方式,它用SYSCALL_DEFINE3()这个宏来定义。事实上从代码上看,这个函数到底调用了什么函数不是很清楚。
SYSCALL_DEFINE3(read, unsigned int fd, char __user *, buf, size_t, count)
{
struct fd f = fdget_pos(fd);
ssize_t ret = -EBADF;
/* ... */
最新的代码如下:
ssize_t ksys_read(unsigned int fd, char __user *buf, size_t count)
{
struct fd f = fdget_pos(fd);
ssize_t ret = -EBADF;
if (f.file) {
loff_t pos, *ppos = file_ppos(f.file);
if (ppos) {
pos = *ppos;
ppos = &pos;
}
ret = vfs_read(f.file, buf, count, ppos);
if (ret >= 0 && ppos)
f.file->f_pos = pos;
fdput_pos(f);
}
return ret;
}
SYSCALL_DEFINE3(read, unsigned int, fd, char __user *, buf, size_t, count)
{
return ksys_read(fd, buf, count);
}
这些SYSCALL_DEFINEn()宏是内核代码定义系统调用的标准方式。这里尾部的n代表参数的个数。这些宏定义在 include/linux/syscalls.h,对于每个系统调用都给出了2个不同的输出。
SYSCALL_METADATA(_read, 3, unsigned int, fd, char __user *, buf, size_t, count)
__SYSCALL_DEFINEx(3, _read, unsigned int, fd, char __user *, buf, size_t, count)
{
struct fd f = fdget_pos(fd);
ssize_t ret = -EBADF;
/* ... */
#define SYSCALL_DEFINEx(x, sname, ...) \
SYSCALL_METADATA(sname, x, __VA_ARGS__) \
__SYSCALL_DEFINEx(x, sname, __VA_ARGS__)
#define SYSCALL_METADATA(sname, nb, ...) \
static const char *types_##sname[] = { \
__MAP(nb,__SC_STR_TDECL,__VA_ARGS__) \
}; \
static const char *args_##sname[] = { \
__MAP(nb,__SC_STR_ADECL,__VA_ARGS__) \
}; \
SYSCALL_TRACE_ENTER_EVENT(sname); \
SYSCALL_TRACE_EXIT_EVENT(sname); \
static struct syscall_metadata __used \
__syscall_meta_##sname = { \
.name = "sys"#sname, \
.syscall_nr = -1, /* Filled in at boot */ \
.nb_args = nb, \
.types = nb ? types_##sname : NULL, \
.args = nb ? args_##sname : NULL, \
.enter_event = &event_enter_##sname, \
.exit_event = &event_exit_##sname, \
.enter_fields = LIST_HEAD_INIT(__syscall_meta_##sname.enter_fields), \
}; \
static struct syscall_metadata __used \
__section("__syscalls_metadata") \
*__p_syscall_meta_##sname = &__syscall_meta_##sname;
#define __SYSCALL_DEFINEx(x, name, ...) \
__diag_push(); \
__diag_ignore(GCC, 8, "-Wattribute-alias", \
"Type aliasing is used to sanitize syscall arguments");\
asmlinkage long sys##name(__MAP(x,__SC_DECL,__VA_ARGS__)) \
__attribute__((alias(__stringify(__se_sys##name)))); \
ALLOW_ERROR_INJECTION(sys##name, ERRNO); \
static inline long __do_sys##name(__MAP(x,__SC_DECL,__VA_ARGS__));\
asmlinkage long __se_sys##name(__MAP(x,__SC_LONG,__VA_ARGS__)); \
asmlinkage 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; \
} \
__diag_pop(); \
static inline long __do_sys##name(__MAP(x,__SC_DECL,__VA_ARGS__))
第一个宏,SYSCALL_METADATA()对这个系统调用构建了一组元数据。它只在内核构建时定义了CONFIG_FTRACE_SYSCALLS的时候扩展,它的扩展给出了数据的样板定义(用来描述系统调用和它的参数)。Anatomy of a system call, additional content [LWN.net]这篇文章详细的解析了这些宏。
__SYSCALL_DEFINEx()部分会更加有趣,它包含了系统调用的实现。一旦各种层次的宏和GCC类型拓展被展开了,最后的代码包含一些有趣的特性。
asmlinkage long sys_read(unsigned int fd, char __user * buf, size_t count)
__attribute__((alias(__stringify(SyS_read))));
static inline long SYSC_read(unsigned int fd, char __user * buf, size_t count);
asmlinkage long SyS_read(long int fd, long int buf, long int count);
asmlinkage long SyS_read(long int fd, long int buf, long int count)
{
long ret = SYSC_read((unsigned int) fd, (char __user *) buf, (size_t) count);
asmlinkage_protect(3, ret, fd, buf, count);
return ret;
}
static inline long SYSC_read(unsigned int fd, char __user * buf, size_t count)
{
struct fd f = fdget_pos(fd);
ssize_t ret = -EBADF;
/* ... */
首先,我们注意到系统调用实现事实上名字叫SYSC_read(),但是是static的,所以不能在模块外面访问。有一个wrapper函数,SyS_read()和它的别名函数sys_read(),可以在外部访问。近距离的看一下这些别名,我们注意到他们参数类型的不同,sys_read()需要准确的类型(比如第二个参数是char __user *buf),但是SyS_read()只需要一组long整数。我们查看历史,可以发现long版本确保32-bit值可以在64位内核平台上正确的有符号拓展,从而避免了漏洞。
最后我们看到SyS_read()里面用到的asmlinkage指令和asmlinkage_protect()调用。FAQ/asmlinkage - Linux Kernel Newbies 解释了asmlinkage意味着这个函数应该在stack上找它的参数而不是寄存器。asmlinkage_protect()解释了它是被用来防止编译器重用stack上的这些区域。出了sys_read()的定义,在include/linux/syscalls.h里面也有一个声明,这个让内核代码可以直接调用这个系统调用的实现。一般来说不鼓励在内核的其它地方直接调用系统调用。并且在内核代码中不常见。
系统调用表
寻找sys_read()的调用者,也为我们指向了用户空间怎么调用这个函数。对于一般的架构,它并不重写他们自己的版本。include/uapi/asm-generic/unistd.h包含一个引用sys_read()的记录。
#define __NR_read 63
__SYSCALL(__NR_read, sys_read)
这个为read()定义了通用系统调用号__NR_read (63),并且用__SYSCALL()来关联数字和sys_read(),这种关联是架构相关的。比如对于arm64,用 asm-generic/unistd.h头文件来填写一个表格,这个表格映射了数字和函数实现指针。
但是我们关注x86_64架构,这个架构并不使用这个通用的表格。x86_64在./arch/x86/entry/syscalls/syscall_64.tbl定义它自己的映射,其中有一个sys_read()的记录。
# SPDX-License-Identifier: GPL-2.0 WITH Linux-syscall-note
#
# 64-bit system call numbers and entry vectors
#
# The format is:
# <number> <abi> <name> <entry point> [<compat entry point> [noreturn]]
#
# The __x64_sys_*() stubs are created on-the-fly for sys_*() system calls
#
# The abi is "common", "64" or "x32" for this file.
#
0 common read sys_read
1 common write sys_write
2 common open sys_open
3 common close sys_close
这表明,read()在x86_64上面有系统调用号码0(而不是63),并且对于2个x86_64 ABIs 有一个common的实现,也就是sys_read()。不同的ABIs会在这个系列的第二部分讨论。syscalltbl.sh脚本会从syscall_64.tbl表格当中生成arch/x86/include/generated/asm/syscalls_64.h。具体的就是为sys_read()生成一个__SYSCALL_COMMON()宏。这个宏是被用来实现系统调用表格,sys_call_table。它是一个关键的数据结构来映射syscall和sys_name()函数。
x86_64系统调用的调用
现在我们看一下用户空间程序怎么调用一个系统调用。这个本质上是架构相关的,所以文章剩下的部分主要关注x86_64架构(其它架构会在第二篇文章当中深入)。调用的过程涉及几个步骤,所以一个可以点击的图,可能会方便研究。
在之前的部分,我们发现了一个系统调用函数指针的表格,这个表格,使用数组初始化的GCC拓展确保任何没有初始化的记录只想sys_ni_syscall:
asmlinkage const sys_call_ptr_t sys_call_table[__NR_syscall_max+1] = {
[0 ... __NR_syscall_max] = &sys_ni_syscall,
[0] = sys_read,
[1] = sys_write,
/*... */
};
对于64比特的代码,这个表格是通过arch/x86/kernel/entry_64.S进行访问的,从system_call汇编如果点看,它使用了RAX寄存器来从数组中抓取相应的记录,然后调用它。
/*
* System call entry. Up to 6 arguments in registers are supported.
*
* SYSCALL does not save anything on the stack and does not change the
* stack pointer. However, it does mask the flags register for us, so
* CLD and CLAC are not needed.
*/
/*
* Register setup:
* rax system call number
* rdi arg0
* rcx return address for syscall/sysret, C arg3
* rsi arg1
* rdx arg2
* r10 arg3 (--> moved to rcx for C)
* r8 arg4
* r9 arg5
* r11 eflags for syscall/sysret, temporary for C
* r12-r15,rbp,rbx saved by C code, not touched.
*
* Interrupts are off on entry.
* Only called from user space.
*
* XXX if we had a free scratch register we could save the RSP into the stack frame
* and report it properly in ps. Unfortunately we haven't.
*
* When user can change the frames always force IRET. That is because
* it deals with uncanonical addresses better. SYSRET has trouble
* with them due to bugs in both AMD and Intel CPUs.
*/
ENTRY(system_call)
CFI_STARTPROC simple
CFI_SIGNAL_FRAME
CFI_DEF_CFA rsp,KERNEL_STACK_OFFSET
CFI_REGISTER rip,rcx
/*CFI_REGISTER rflags,r11*/
SWAPGS_UNSAFE_STACK
/*
* A hypervisor implementation might want to use a label
* after the swapgs, so that it can do the swapgs
* for the guest and jump here on syscall.
*/
GLOBAL(system_call_after_swapgs)
movq %rsp,PER_CPU_VAR(old_rsp)
movq PER_CPU_VAR(kernel_stack),%rsp
/*
* No need to follow this irqs off/on section - it's straight
* and short:
*/
ENABLE_INTERRUPTS(CLBR_NONE)
SAVE_ARGS 8,0
movq %rax,ORIG_RAX-ARGOFFSET(%rsp)
movq %rcx,RIP-ARGOFFSET(%rsp)
CFI_REL_OFFSET rip,RIP-ARGOFFSET
testl $_TIF_WORK_SYSCALL_ENTRY,TI_flags+THREAD_INFO(%rsp,RIP-ARGOFFSET)
jnz tracesys
system_call_fastpath:
#if __SYSCALL_MASK == ~0
cmpq $__NR_syscall_max,%rax
#else
andl $__SYSCALL_MASK,%eax
cmpl $__NR_syscall_max,%eax
#endif
ja badsys
在这个功能之前,SAVE_ARGS宏把各种寄存器放到stack上,这个匹配了我们之前看到的asmlinkage指令。我们再往外看,system_call入口点也被syscall_init()函数引用,这个函数在内核启动序列当中。
void syscall_init(void)
{
/*
* LSTAR and STAR live in a bit strange symbiosis.
* They both write to the same internal register. STAR allows to
* set CS/DS but only a 32bit target. LSTAR sets the 64bit rip.
*/
wrmsrl(MSR_STAR, ((u64)__USER32_CS)<<48 | ((u64)__KERNEL_CS)<<32);
wrmsrl(MSR_LSTAR, system_call);
wrmsrl(MSR_CSTAR, ignore_sysret);
/* ... */
wrmsrl指令把一个值写到一个model-specific寄存器;在这个例子中,system_call的地址(系统调用的处理函数)被写到了MSR_LSTAR(0xc0000082),这个寄存器就是x86_64用来处理SYSCALL指令的model-specific register。
好了,现在我们可以把用户空间到内核代码联系其哎。x86_64用户程序怎么调用一个系统调用的标准ABI就是,把系统调用号码(0 for read)写到RAX寄存器,其它的参数写到特定的寄存器(RDI,RSI,RDX是前三个参数),然后发起SYSCALL指令。这个指令会让处理器转换到ring 0并且调用在MSR_LSTAR当中指向的代码,也就是system_call()。system_call代码把寄存器中的内容push到内核stack,然后调用sys_call_table表当中RAX指定的记录当中的函数指针,这个函数是一个asmlinkage修饰的SYSC_read()的wrapper。
现在我们看了在大部分平台上系统调用的标准实现,我们现在在一个比较好的位置来理解其它架构,那是第二篇文章的主题。