Linux内核源码分析 - 系统调用

本文以x86_64平台为例,分析linux下的系统调用是如何被执行的。

假设目标系统调用是write,其对应的内核源码为:

// fs/read_write.c
SYSCALL_DEFINE3(write, unsigned int, fd, const char __user *, buf,
                size_t, count)
{
        return ksys_write(fd, buf, count);
}

这里主要看下SYSCALL_DEFINE3这个宏定义:

// include/linux/syscalls.h
#define SYSCALL_DEFINE1(name, ...) SYSCALL_DEFINEx(1, _##name, __VA_ARGS__)
#define SYSCALL_DEFINE2(name, ...) SYSCALL_DEFINEx(2, _##name, __VA_ARGS__)
#define SYSCALL_DEFINE3(name, ...) SYSCALL_DEFINEx(3, _##name, __VA_ARGS__)
#define SYSCALL_DEFINE4(name, ...) SYSCALL_DEFINEx(4, _##name, __VA_ARGS__)
#define SYSCALL_DEFINE5(name, ...) SYSCALL_DEFINEx(5, _##name, __VA_ARGS__)
#define SYSCALL_DEFINE6(name, ...) SYSCALL_DEFINEx(6, _##name, __VA_ARGS__)
...
#define SYSCALL_DEFINEx(x, sname, ...)                          \
        ...
        __SYSCALL_DEFINEx(x, sname, __VA_ARGS__)

该宏又引用了__SYSCALL_DEFINEx,继续看下:

// arch/x86/include/asm/syscall_wrapper.h
#define __SYSCALL_DEFINEx(x, name, ...)                                 \
        asmlinkage long __x64_sys##name(const struct pt_regs *regs);    \
        ...                                                             \
        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 __x64_sys##name(const struct pt_regs *regs)     \
        {                                                               \
                return __se_sys##name(SC_X86_64_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__));\
                ...                                                     \
                return ret;                                             \
        }                                                               \
        static inline long __do_sys##name(__MAP(x,__SC_DECL,__VA_ARGS__))

该宏的参数中,x为3,name为_write,...代表的__VA_ARGS__为unsigned int, fd, const char __user *, buf, size_t, count。

接着,在宏的定义中,先声明了三个函数,分别为__x64_sys_write、_se_sys_write、__do_sys_write,紧接着,定义了__x64_sys_write和_se_sys_write的实现,__x64_sys_write内调用_se_sys_write,_se_sys_write内调用__do_sys_write。

__do_sys_write只是一个方法头,它和最开始的write系统调用的方法体构成完整的方法。

由上可以看到,三个方法中,只有__x64_sys_write方法没有static,即只有它是外部可调用的,所以我们看下哪里引用了__x64_sys_write。

// arch/x86/entry/syscalls/syscall_64.tbl
#
# 64-bit system call numbers and entry vectors
#
# The format is:
# <number> <abi> <name> <entry point>
#
# 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                    __x64_sys_read
1       common  write                   __x64_sys_write
...

我们会在一个非c文件中,找到了对__x64_sys_write方法的引用,但这个文件又是怎么被使用的呢?

根据arch/x86/entry/syscalls/Makefile我们可以知道,是有对应的shell脚本,根据上面的文件来生成c版的头文件,比如下面两个。

kernel内部使用的:

// arch/x86/include/generated/asm/syscalls_64.h
#ifdef CONFIG_X86
__SYSCALL_64(0, __x64_sys_read, )
#else /* CONFIG_UML */
__SYSCALL_64(0, sys_read, )
#endif
#ifdef CONFIG_X86
__SYSCALL_64(1, __x64_sys_write, )
#else /* CONFIG_UML */
__SYSCALL_64(1, sys_write, )
#endif
...

给用户使用的:

// arch/x86/include/generated/uapi/asm/unistd_64.h
#define __NR_read 0
#define __NR_write 1
...

那生成的这两个头文件又是给谁使用的呢?看下下面这个文件:

// arch/x86/entry/syscall_64.c
#define __SYSCALL_64(nr, sym, qual) [nr] = sym,

asmlinkage const sys_call_ptr_t sys_call_table[__NR_syscall_max+1] = {
        /*
         * Smells like a compiler bug -- it doesn't work
         * when the & below is removed.
         */
        [0 ... __NR_syscall_max] = &sys_ni_syscall,
#include <asm/syscalls_64.h>
};

该文件中定义了一个const的数组变量sys_call_table,数组下标为系统调用的编号,值为该编号对应的系统调用方法。

最开始整个数组都初始化为sys_ni_syscall,该方法内会返回错误码ENOSYS,表示对应的方法未实现。

接着用#include <asm/syscalls_64.h>的方式再初始化存在的系统调用。

该include的文件就是上面生成的arch/x86/include/generated/asm/syscalls_64.h,syscalls_64.h文件里调用__SYSCALL_64,为对应的系统下标赋值。

最后,sys_call_table[1] = __x64_sys_write。

到这里,我们基本可以猜测,肯定有个地方是根据系统调用的编号,到数组sys_call_table中找到对应方法,然后调用。

让我们来看下这段代码在哪里

// arch/x86/entry/common.c
__visible void do_syscall_64(unsigned long nr, struct pt_regs *regs)
{
        ...
        if (likely(nr < NR_syscalls)) {
                nr = array_index_nospec(nr, NR_syscalls);
                regs->ax = sys_call_table[nr](regs);
        }
        ...
}

上面的方法就是我们要找的方法。

我们再看下这个方法是在哪里被调用的。

// arch/x86/entry/entry_64.S
ENTRY(entry_SYSCALL_64)
        ...
        call    do_syscall_64           /* returns with IRQs disabled */
        ...

上面的就是对应的汇编代码了,这里为了简单,省略掉了该汇编方法的其他部分。

那这段汇编代码又是在哪里调用的呢?

// arch/x86/kernel/cpu/common.c
void syscall_init(void)
{
        ...
        wrmsrl(MSR_LSTAR, (unsigned long)entry_SYSCALL_64);
        ...
}

在上面的方法中,我们可以看到,汇编代码entry_SYSCALL_64被写到了MSR_LSTAR表示的寄存器中。

该寄存器的作用就是,当我们执行syscall机器指令时,MSR_LSTAR寄存器中存放的对应方法就会被执行,即在user space,我们只要执行syscall机器指令,给它对应的系统调用编号和参数,kernel space里对应的系统调用就会被执行了。

有兴趣的可以分析并执行下下面的汇编代码,好好体会下整个系统调用的流程。

# ----------------------------------------------------------------------------------------
# Writes "Hello, World" to the console using only system calls. Runs on 64-bit Linux only.
# To assemble and run:
#
#     gcc -c hello.s && ld hello.o && ./a.out
#
# or
#
#     gcc -nostdlib hello.s && ./a.out
# ----------------------------------------------------------------------------------------

        .global _start

        .text
_start:
        # write(1, message, 13)
        mov     $1, %rax                # system call 1 is write
        mov     $1, %rdi                # file handle 1 is stdout
        mov     $message, %rsi          # address of string to output
        mov     $13, %rdx               # number of bytes
        syscall                         # invoke operating system to do the write

        # exit(0)
        mov     $60, %rax               # system call 60 is exit
        xor     %rdi, %rdi              # we want return code 0
        syscall                         # invoke operating system to exit
message:
        .ascii  "Hello, world\n"

到这里,系统调用对应的kernel space部分就已经分析完毕了,下篇文章我们结合对应的c源码,看下user space的部分是如何实现的。

完。

原文发布于微信公众号 - Linux内核及JVM底层相关技术研究(ytcode)

原文发表时间:2019-05-17

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

编辑于

我来说两句

0 条评论
登录 后参与评论

扫码关注云+社区

领取腾讯云代金券