系统调用解析第一部分【翻译】

翻译自 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。

现在我们看了在大部分平台上系统调用的标准实现,我们现在在一个比较好的位置来理解其它架构,那是第二篇文章的主题。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值