Linux 系统调用的实现(x86

阿里云国内75折 回扣 微信号:monov8
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6

目录

1、系统调用的定义

    1.1  SYSCALL_METADATA宏

 1.2 __SYSCALL_DEFINEx定义

2、系统调用表-sys_call_table数组的定义

3、用户态系统调用流程


kernel 5.10 

1、系统调用的定义

        系统调用的定义我们其实都不陌生类似这样的函数SYSCALL_DEFINE0 SYSCALL_DEFINE1,SYSCALL_DEFINEx我们可以看看他们的定义

arch/x86/include/asm/syscall_wrapper.h

#ifndef SYSCALL_DEFINE0
#define SYSCALL_DEFINE0(sname)					\
	SYSCALL_METADATA(_##sname, 0);				\
	asmlinkage long sys_##sname(void);			\
	ALLOW_ERROR_INJECTION(sys_##sname, ERRNO);		\
	asmlinkage long sys_##sname(void)
#endif /* SYSCALL_DEFINE0 */

#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_DEFINE_MAXARGS	6

#define SYSCALL_DEFINEx(x, sname, ...)				\
	SYSCALL_METADATA(sname, x, __VA_ARGS__)			\
	__SYSCALL_DEFINEx(x, sname, __VA_ARGS__)

        最终发现都是SYSCALL_DEFINEx的一层封装其中x表示的是该系统调用的参数个数。SYSCALL_DEFINEx最终会调用SYSCALL_METADATA(sname, x, __VA_ARGS__)   和 __SYSCALL_DEFINEx(x, sname, __VA_ARGS__)我们拿open系统调用来举例这个定义告诉我们open系统调用有3个参数后面跟的是类型+参数名对

fs/open.c

SYSCALL_DEFINE3(open, const char __user *, filename, int, flags, umode_t, mode)
{
        if (force_o_largefile())
                flags |= O_LARGEFILE;
        return do_sys_open(AT_FDCWD, filename, flags, mode);
}

    1.1  SYSCALL_METADATA宏

        在看SYSCALL_METADATA函数之前我们先关注下前提知识点

      1##表示替换并拼接#表示替换成字符串这里面的sys_##sname 实际上替换的就是sname

       2) __MAP宏他的第一个参数表示的参数的类型-变量名称对的个数也是参数的个数他的第二个参数表示的是函数名称后面的参数就是参数类型参数名成对出现的参数列表根据以下定义可以看出他的作用就把各个参数类型和参数名单独展开__MAPn(n,m,t1,a1,t2,a2,...,tn,an) 展开成m(t1,a1),m(t2,a2),...,m(tn,an);

include/linux/syscalls.h

/*
 * __MAP - apply a macro to syscall arguments
 * __MAP(n, m, t1, a1, t2, a2, ..., tn, an) will expand to
 *    m(t1, a1), m(t2, a2), ..., m(tn, an)
 * The first argument must be equal to the amount of type/name
 * pairs given.  Note that this list of pairs (i.e. the arguments
 * of __MAP starting at the third one) is in the same format as
 * for SYSCALL_DEFINE<n>/COMPAT_SYSCALL_DEFINE<n>
 */
#define __MAP0(m,...)
#define __MAP1(m,t,a,...) m(t,a)
#define __MAP2(m,t,a,...) m(t,a), __MAP1(m,__VA_ARGS__)
#define __MAP3(m,t,a,...) m(t,a), __MAP2(m,__VA_ARGS__)
#define __MAP4(m,t,a,...) m(t,a), __MAP3(m,__VA_ARGS__)
#define __MAP5(m,t,a,...) m(t,a), __MAP4(m,__VA_ARGS__)
#define __MAP6(m,t,a,...) m(t,a), __MAP5(m,__VA_ARGS__)
#define __MAP(n,...) __MAP##n(__VA_ARGS__)

         接下来我们再看SYSCALL_METADATA宏意思就明了  __SC_STR_TDECL 、__SC_STR_ADECL根据定义可以知道这二个宏分别是取m(t,a)中的t和a最终types_##sname[]字符数组存放的就是类型字符串args##sname[]字符数组存放的则是变量字符串。最终他们会被赋值给结构体syscall_meta_##sname并存放在存储在section(“__syscalls_metadata”)当中该结构中存放系统调用的名称、相应参数信息还有系统调用进入和退出时的trace_event_call

     SYSCALL_TRACE_ENTER_EVENT(sname)与  SYSCALL_TRACE_EXIT_EVENT(sname)二个宏分别对应两个trace_event_call指针变量被创建 event_enter_##sname和event_exit_##sname这二个trace_event_call的指针包含上文提到的系统调用信息结构syscall_meta_##sname、系统调用入口和出口的回调函数、trace_event_class结构等信息他们最终会被被存储在section(“_ftrace_events”)当中但是这两个event并没有定义桩函数还没有调用入口  在ftrace syscall event的enable操作中会通过trace_event_call把syscall event的trace函数加入到数组中同时把自己的桩函数通过trace_event_call->trace_event_class->register_trace_sys_enter()、register_trace_sys_exit()函数注册到trace_sys_enter/trace_sys_exit插桩点的tracepoint。在syscall trace event初始化时会把meta数据存放到syscalls_metadata[]数组;

event enable的执行路径为

ftrace_enable_fops -> event_enable_write() -> 
ftrace_event_enable_disable() -> __ftrace_event_enable_disable() 
-> call->class->reg(call, TRACE_REG_UNREGISTER/TRACE_REG_REGISTER, file);

ftrace syscall trace event初始化流程 

start_kernel() -> trace_init() -> trace_event_init() -> init_ftrace_syscalls()
include/linux/syscalls.h

#define SYSCALL_TRACE_ENTER_EVENT(sname)				\
	static struct syscall_metadata __syscall_meta_##sname;		\
	static struct trace_event_call __used				\
	  event_enter_##sname = {					\
		.class			= &event_class_syscall_enter,	\ /*初始化
trace_event_class成员*/
		{							\
			.name                   = "sys_enter"#sname,	\
		},							\
		.event.funcs            = &enter_syscall_print_funcs,	\/*syscall enter的
回调函数*/
		.data			= (void *)&__syscall_meta_##sname,\ /*把__syscall_meta_##sname
赋值给trace_event_call的data成员*/
		.flags                  = TRACE_EVENT_FL_CAP_ANY,	\
	};								\
	static struct trace_event_call __used				\
	  __section("_ftrace_events")					\
	 *__event_enter_##sname = &event_enter_##sname;

#define SYSCALL_TRACE_EXIT_EVENT(sname)					\
....


#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 enter时的trace_event_call
 event_enter_##sname */
	SYSCALL_TRACE_EXIT_EVENT(sname);			\ /*定义syscall exit时的trace_event_call
 event_exit_##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,		\ //syscall enter时的race_event_call
		.exit_event	= &event_exit_##sname,		\ //初始化syscall exit的trace_event_call
         //链表成员初始化
		.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;

        上面一系列操作SYSCALL_METADATA宏实际上就做了二件事1、格式系统调用信息结构体syscall_metadata的初始化另一个就是初始化trace_event_call为ftrace 跟踪syscall event做准备。

 1.2 __SYSCALL_DEFINEx定义

#ifdef CONFIG_ARCH_HAS_SYSCALL_WRAPPER
/*
 * It may be useful for an architecture to override the definitions of the
 * SYSCALL_DEFINE0() and __SYSCALL_DEFINEx() macros, in particular to use a
 * different calling convention for syscalls. To allow for that, the prototypes
 * for the sys_*() functions below will *not* be included if
 * CONFIG_ARCH_HAS_SYSCALL_WRAPPER is enabled.
 */
#include <asm/syscall_wrapper.h>
#endif /* CONFIG_ARCH_HAS_SYSCALL_WRAPPER */

__SYSCALL_DEFINEx的定义取决于CONFIG_ARCH_HAS_SYSCALL_WRAPPER编译选项我们默认打开的所以__SYSCALL_DEFINEx会使用asm/syscall_wrapper.h文件中的定义

asm/syscall_wrapper.h

#define __SYSCALL_DEFINEx(x, name, ...)					\
	static long __se_sys##name(__MAP(x,__SC_LONG,__VA_ARGS__));	\
	static inline long __do_sys##name(__MAP(x,__SC_DECL,__VA_ARGS__));\
	__X64_SYS_STUBx(x, name, __VA_ARGS__)				\
	__IA32_SYS_STUBx(x, name, __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__))

        通过上面的铺垫 __SYSCALL_DEFINEx宏就是完成几个函数的申明、定义和调用还是以open系统调用为例其中展开后结果如下实际上是一行看着让人抓狂我拆开了

long __x64_sys_open(const struct pt_regs *regs);

static struct error_injection_entry __attribute__((__used__)) __attribute__((__section__("_error_injection_whitelist"))) _eil_addr___x64_sys_open = {

.addr = (unsigned long)__x64_sys_open,

.etype = EI_ETYPE_ERRNO,

};;

static long __se_sys_open(__typeof(__builtin_choose_expr((__builtin_types_compatible_p(typeof(( const char *)0), typeof(0LL)) || __builtin_types_compatible_p(typeof(( const char *)0), typeof(0ULL))), 0LL, 0L)) filename, __typeof(__builtin_choose_expr((__builtin_types_compatible_p(typeof(( int)0), typeof(0LL)) || __builtin_types_compatible_p(typeof(( int)0), typeof(0ULL))), 0LL, 0L)) flags, __typeof(__builtin_choose_expr((__builtin_types_compatible_p(typeof(( umode_t)0), typeof(0LL)) || __builtin_types_compatible_p(typeof(( umode_t)0), typeof(0ULL))), 0LL, 0L)) mode);

static inline __attribute__((unused)) __attribute__((no_instrument_function)) long __do_sys_open(const char * filename, int flags, umode_t mode);

long __x64_sys_open(const struct pt_regs *regs) {

        return __se_sys_open(regs->di, regs->si, regs->dx);

}

static long __se_sys_open(__typeof(__builtin_choose_expr((__builtin_types_compatible_p(typeof(( const char *)0), typeof(0LL)) || __builtin_types_compatible_p(typeof(( const char *)0), typeof(0ULL))), 0LL, 0L)) filename, __typeof(__builtin_choose_expr((__builtin_types_compatible_p(typeof(( int)0), typeof(0LL)) || __builtin_types_compatible_p(typeof(( int)0), typeof(0ULL))), 0LL, 0L)) flags, __typeof(__builtin_choose_expr((__builtin_types_compatible_p(typeof(( umode_t)0), typeof(0LL)) || __builtin_types_compatible_p(typeof(( umode_t)0), typeof(0ULL))), 0LL, 0L)) mode)  {

long ret = __do_sys_open(( const char *) filename, ( int) flags, ( umode_t) mode);

(void)(sizeof(struct { int:(-!!(!(__builtin_types_compatible_p(typeof(( const char *)0), typeof(0LL)) || __builtin_types_compatible_p(typeof(( const char *)0), typeof(0ULL))) && sizeof(const char *) > sizeof(long))); })), (void)(sizeof(struct { int:(-!!(!(__builtin_types_compatible_p(typeof(( int)0), typeof(0LL)) || __builtin_types_compatible_p(typeof(( int)0), typeof(0ULL))) && sizeof(int) > sizeof(long))); })), (void)(sizeof(struct { int:(-!!(!(__builtin_types_compatible_p(typeof(( umode_t)0), typeof(0LL)) || __builtin_types_compatible_p(typeof(( umode_t)0), typeof(0ULL))) && sizeof(umode_t) > sizeof(long))); })); do { } while (0);

return ret;

}

static inline __attribute__((unused)) __attribute__((no_instrument_function)) long __do_sys_open(const char * filename, int flags, umode_t mode)

最后这个__do_sys_open函数就和open.c里面的函数体进行了合并

{

 if ((64 != 32))
  flags |= 00100000;

 return do_sys_open(-100, filename, flags, mode)

}

        通过上面宏展开可以知道open系统调用最终生成了一个__x64_sys_open、__se_sys_open、__do_sys_open函数的声明和定义并且存在__x64_sys_open->__se_sys_open->__do_sys_open的调用关系从这里可以看出我们定义的open系统调用实际上就是__x64_sys_open函数的调用。

        小技巧分享看宏展开实际是有技巧的linux的make很强大可以支持单文件编译所以我们要看宏展开的结果只需要进行改文件的预编译既可以我们使用make fs/open.i就可以得到open.c预编译后的文件了。

2、系统调用表-sys_call_table数组的定义

        从上面的分析流程知道系统调用的定义实际上就是定义了一个__x64_sys_xx的函数所以我们需要关注下__x64_sys_xx哪里调用的这里就可以联系上系统调用地方的入口了。还是拿__x64_sys_open为例发现并没有搜到于是我们得找下系统调用的入口看看具体是怎么发起具体系统调用的。

        这里我们就要看系统调用的入口点了arch/x86/entry/entry_64.S 下的entry_SYSCALL_64就是64位系统系统调用的入口点这个入口点的进入我们后面再细讲先看他是如何发起具体系统调用了看了汇编arch/x86/entry/entry_64.S的代码会通过call    do_syscall_64指令调用do_syscall_64函数来发起具体的系统调用函数看下该函数的定义

#ifdef CONFIG_X86_64
__visible noinstr void do_syscall_64(unsigned long nr, struct pt_regs *regs)
{
        nr = syscall_enter_from_user_mode(regs, nr);

        instrumentation_begin();
        if (likely(nr < NR_syscalls)) {
                nr = array_index_nospec(nr, NR_syscalls);
                regs->ax = sys_call_table[nr](regs); //系统调用
#ifdef CONFIG_X86_X32_ABI
        } else if (likely((nr & __X32_SYSCALL_BIT) &&
                          (nr & ~__X32_SYSCALL_BIT) < X32_NR_syscalls)) {
                nr = array_index_nospec(nr & ~__X32_SYSCALL_BIT,
                                        X32_NR_syscalls);
                regs->ax = x32_sys_call_table[nr](regs);
#endif
        }
        instrumentation_end();
        syscall_exit_to_user_mode(regs);
}
#endif

        显然该函数会通过入参regs确定系统调用号nr然后通过sys_call_table[nr]数组成员函数来发起具体的系统调用这个数组实际就是系统调用函数数组他的下标实际就是系统调用号他的数组成员实际就是系统调用定义的函数就是我们上文提到的__x86_sys_xx函数我们进一步验证这一点

        代码全局搜索sys_call_table会发现定义如下

#define __SYSCALL_X32(nr, sym)
#define __SYSCALL_COMMON(nr, sym) __SYSCALL_64(nr, sym)

#define __SYSCALL_64(nr, sym) extern long __x64_##sym(const struct pt_regs *);
#include <asm/syscalls_64.h>
#undef __SYSCALL_64

#define __SYSCALL_64(nr, sym) [nr] = __x64_##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] = &__x64_sys_ni_syscall,
#include <asm/syscalls_64.h>
};

        这个定义有点简洁啥也没看出来认真分析下发现第一个__SYSCALL_64宏实际上是对所有外部定义系统调用函数的声明extern long __x64_##sym(const struct pt_regs *)然后包含了个asm/syscalls_64.h的头文件表示对所有的外部系统调用进行声明第二个__SYSCALL_64覆盖了第一个宏的定义变成了一个给数组成员赋值的操作然后还是包含了个asm/syscalls_64.h头文件从这几行代码我们可以推断实际上第一个宏会被第一次包含的头文件asm/syscalls_64.h拿来调用目的是作为系统调用函数的外部引用申明而第二个宏定义是为了被asm/syscalls_64.h头文件调用用来给数组成员函数赋值。我们发现源码里面并没有syscalls_64.h这个头文件我们搜索他发现他在Makefile中原来他是编译的时候生成的,编译脚本内容如下

arch/x86/entry/syscalls/Makefile

syscall64 := $(srctree)/$(src)/syscall_64.tbl
systbl := $(srctree)/$(src)/syscalltbl.sh

$(out)/syscalls_64.h: $(syscall64) $(systbl)
        $(call if_changed,systbl)

        从上面我们知道syscalls_64是脚本syscalltbl.sh 和 源文件 syscall_64.tbl共同生成的通过makefile语句知道:具体执行的命令为sh $(systbl) $(syscall64) $(out)/syscalls_64.h我们自定义输出到out(实际是syscalls_64.sh)中:sh arch/x86/entry/syscalls/syscalltbl.sh ./arch/x86/entry/syscalls/syscall_64.tbl out,最终生成的文件截取部分如下

         上面我们截取的open相关的从上面定义看__SYSCALL_COMMON实际上就是__SYSCALL_64的封装所以第一次包含的头文件件最终会装换成所有系统调用函数外部引用声明第二部分包含的头文件实际就是替换数组下标系统调用号对应系统调用定义的函数原本他们默认的都是__x64_sys_ni_syscall这个函数什么都不做直接返回-ENOSYS就是做一个占位的避免有的系统调用函数没有定义这样我们的系统调用向量数组就被定义好了在int 80中断响应时会根据系统调用号定位数组sys_call_table[__NR_syscall_max+1] 中具体对应的系统调用函数

3、用户态调用系统调用的流程

        系统调用的用户态陷入我们先举个例子看下通过汇编代码看下他的陷入内核的流程      

#include <stdio.h>
#include <string.h>
#include <unistd.h>

int main() {
    write(1, "hello world", strlen("hello world"));
    return 0;
}

       对上面的代码我们可以通过预处理hello.i-> 编译(hello.s)->汇编(hello.o)-链接(hello)等过程生成可执行文件当然gcc会帮我们处理这个流程

        gcc -o hello hello.c 

        得到二进制后我们通过反汇编看到他的汇编代码和机器码这里只附上核心流程

//通过 (PLT) 调用全局偏移量表GOT对应的函数指针
00000000004003f0 <.plt>:
  4003f0:       ff 35 12 0c 20 00       pushq  0x200c12(%rip) # 601008 <_GLOBAL_OFFSET_TABLE_+0x8>
  4003f6:       ff 25 14 0c 20 00       jmpq   *0x200c14(%rip)  # 601010 <_GLOBAL_OFFSET_TABLE_+0x10>
  4003fc:       0f 1f 40 00             nopl   0x0(%rax)


//write函数入口进入glibc,通过过 PLT过程链接表去查找 libc.so 动态库的对应接口
0000000000400400 <write@plt>:
  400400:       ff 25 12 0c 20 00       jmpq   *0x200c12(%rip)  # 601018 <write@GLIBC_2.2.5>
  400406:       68 00 00 00 00          pushq  $0x0
  40040b:       e9 e0 ff ff ff          jmpq   4003f0 <.plt>


//查看所有section的内容筛选hello: objdump  -s hello | grep hello
// 4005d0 是字符存放的的地址
//4005d0 68656c6c 6f20776f 726c6400           hello world.

//main函数入口
000000000040051d <main>:
  40051d:       55                      push   %rbp //rbp压栈
  40051e:       48 89 e5                mov    %rsp,%rbp //被调函数rbp = rsp
  400521:       ba 0b 00 00 00          mov    $0xb,%edx /* 给write传递的
第3个参数strlen("hello world")存放到edx中 */
  400526:       be d0 05 40 00          mov    $0x4005d0,%esi /* 给write
的第2个参数"hello world"存放到esi这里是吧地址存放到esi中*/
  40052b:       bf 01 00 00 00          mov    $0x1,%edi    /*给write的
第1个参数fd(1)存放到edi*/
  400530:       e8 cb fe ff ff          callq  400400 <write@plt>  //这里
调用write
  400535:       b8 00 00 00 00          mov    $0x0,%eax
  40053a:       5d                      pop    %rbp
  40053b:       c3                      retq
  40053c:       0f 1f 40 00             nopl   0x0(%rax)

       上面的汇编可以看出内核通过edi,esi,edx(64位用相应寄存器)完成write系统调用参数的传递和存储然后通过libc调用了libc库中的函数来实现write系统调用

        通过gdb来查看write进入libc后的汇编代码从而进一步了解他是进入系统调用的细节;

        执行 gdb hello,

        设置断点b write

        运行 r

        经过以上操作后查看汇编代码

//write函数断点落到glibc库中
Breakpoint 2, write () at ../sysdeps/unix/syscall-template.S:81
81      T_PSEUDO (SYSCALL_SYMBOL, SYSCALL_NAME, SYSCALL_NARGS)
(gdb) disassemble
Dump of assembler code for function write:
=> 0x00007ffff7afcb90 <+0>:     cmpl   $0x0,0x2dd41d(%rip)        # 0x7ffff7dd9fb4 <__libc_multiple_threads>
   0x00007ffff7afcb97 <+7>:     jne    0x7ffff7afcba9 <write+25>
    /*这里把write的系统调用号填充到 %eax寄存器中64位是%rax寄存器*/
   0x00007ffff7afcb99 <+0>:     mov    $0x1,%eax  
    /*这里调用syscall指令进入内核*/
   0x00007ffff7afcb9e <+5>:     syscall
   0x00007ffff7afcba0 <+7>:     cmp    $0xfffffffffffff001,%rax
   0x00007ffff7afcba6 <+13>:    jae    0x7ffff7afcbd9 <write+73>
   0x00007ffff7afcba8 <+15>:    retq

        从上面可以看出libc库中调用系统调用前会指定系统调用号然后调用syscall指令进入内核发起系统调用, 进入内核后就可以通过内核中的sys_call_table系统调用表查找具体系统调用的处理函数了

        从上面的过程研究发现用户态到内核态发起系统调用需要做以下2件事

        1)  通过寄存器来存储和传递系统调用的参数系统调用号

        2调用syscall指令进入到内核空间

        通过上文的流程1我们清楚了但是syscall具体做了什么了下面继续看下

        上文中提过entry_SYSCALL_64就是系统调用的入口其实entry_SYSCALL_64 也是 64 位 syscall 指令入口函数早期的 x86 CPU 架构系统调用依靠软中断实现但是软中断要内存查表比较慢后来为了执行 快速的系统调用添加了一组 MSR 寄存器分别存储了执行系统调用后内核系统调用入口函数所需要的段寄存器、堆栈栈顶、函数地址。这样就不再需要内存查表了当 linux 内核启动时MSR 特殊模块寄存器会存储 syscall 指令的入口函数地址当 syscall 指令执行后系统从特殊模块寄存器中取出入口函数地址进行调用

        通过全局搜索函数entry_SYSCALL_64会发现在内核初始化的过程中会把MSR 特殊模块寄存器设置为entry_SYSCALL_64函数入口地址

        下面我们来说说系统调用入口函数是怎么设置的。X86_64对于64位的进程来说只有一个系统调用指令就是syscall它的入口函数在linux-src/arch/x86/entry/entry_64.S, 函数名叫entry_SYSCALL_64。对于32位的进程来说有三个系统调用指令 int 0x80、sysenter、syscall它们的入口函数都在 linux-src/arch/x86/entry/entry_64_compat.S函数名分别叫做entry_INT80_compat、entry_SYSENTER_compat、entry_SYSCALL_compat。设置它们的代码在两个地方syscall(64)、syscall(32)、sysenter 这三个设置在一个地方在文件linux-src/arch/x86/kernel/cpu/common.c中的函数 syscall_init

void syscall_init(void)
{
        wrmsr(MSR_STAR, 0, (__USER32_CS << 16) | __KERNEL_CS);
        //syscall64指令入口函数设置msr寄存器为entry_SYSCALL_64函数地址
        wrmsrl(MSR_LSTAR, (unsigned long)entry_SYSCALL_64);

#ifdef CONFIG_IA32_EMULATION
        //设置syscall(32)的入口函数
        wrmsrl(MSR_CSTAR, (unsigned long)entry_SYSCALL_compat);
        /*
         * This only works on Intel CPUs.
         * On AMD CPUs these MSRs are 32-bit, CPU truncates MSR_IA32_SYSENTER_EIP.
         * This does not cause SYSENTER to jump to the wrong location, because
         * AMD doesn't allow SYSENTER in long mode (either 32- or 64-bit).
         */
         /* MSR 寄存器还会存储存储内核系统调用入口函数后进入内核态所需要的段寄存器、
堆栈栈顶、函数地址。 */
        wrmsrl_safe(MSR_IA32_SYSENTER_CS, (u64)__KERNEL_CS);
        wrmsrl_safe(MSR_IA32_SYSENTER_ESP,
                    (unsigned long)(cpu_entry_stack(smp_processor_id()) + 1));
        //sysenter的入口函数
        wrmsrl_safe(MSR_IA32_SYSENTER_EIP, (u64)entry_SYSENTER_compat);
#else
        wrmsrl(MSR_CSTAR, (unsigned long)ignore_sysret);
        wrmsrl_safe(MSR_IA32_SYSENTER_CS, (u64)GDT_ENTRY_INVALID_SEG);
        wrmsrl_safe(MSR_IA32_SYSENTER_ESP, 0ULL);
        wrmsrl_safe(MSR_IA32_SYSENTER_EIP, 0ULL);
#endif

        /* Flags to clear on syscall */
        wrmsrl(MSR_SYSCALL_MASK,
               X86_EFLAGS_TF|X86_EFLAGS_DF|X86_EFLAGS_IF|
               X86_EFLAGS_IOPL|X86_EFLAGS_AC|X86_EFLAGS_NT);
}

        从代码中可以看出只有在64位的情况下才会设置syscall指令的入口函数只有在系统兼容32位进程(CONFIG_IA32_EMULATION)的情况下才会设置syscall(32)、sysenter的兼容入口函数。大部分linux发行版都支持32位进程兼容;
       接下来我们看看64位syscall 入口函数entry_SYSCALL_64的实现:

        为了保护用户空间的上下文需要借助一个关键的结构体pt_regs具体成员描述如下

/* arch/x86/include/asm/ptrace.h */
struct pt_regs {
/*
 * C ABI says these regs are callee-preserved. They aren't saved on kernel entry
 * unless syscall needs a complete, fully filled "struct pt_regs".
 */
    unsigned long r15;
    unsigned long r14;
    unsigned long r13;
    unsigned long r12;
    unsigned long rbp;
    unsigned long rbx;
/* These regs are callee-clobbered. Always saved on kernel entry. */
    unsigned long r11;
    unsigned long r10;  /* 程序传递到内核的第 4 个参数。 */
    unsigned long r9;   /* 程序传递到内核的第 6 个参数。 */
    unsigned long r8;   /* 程序传递到内核的第 5 个参数。 */
    unsigned long rax;   /* 程序传递到内核的系统调用号。 */
    unsigned long rcx;   /* 程序传递到内核的 syscall 的下一条指令地址。 */
    unsigned long rdx;   /* 程序传递到内核的第 3 个参数。 */
    unsigned long rsi;   /* 程序传递到内核的第 2 个参数。 */
    unsigned long rdi;   /* 程序传递到内核的第 1 个参数。 */
/*
 * On syscall entry, this is syscall#. On CPU exception, this is error code.
 * On hw interrupt, it's IRQ number:
 */
    unsigned long orig_rax; /* 系统调用号。 */
/* Return frame for iretq 
 * 内核态返回用户态需要恢复现场的数据。*/
    unsigned long ip;       /* 保存程序调用 syscall 的下一条指令地址。 */
    unsigned long cs;       /* 用户态代码起始段地址。 */
    unsigned long flags;    /* 用户态的 CPU 标志。 */
    unsigned long sp;       /* 用户态的栈顶地址栈内存是向下增长的。 */
    unsigned long ss;       /* 用户态的数据段地址。 */
/* top of stack page */
};

        entry_SYSCALL_64函数的具体实现如下

//arch/x86/entry/entry_64.S

SYM_CODE_START(entry_SYSCALL_64)
        UNWIND_HINT_EMPTY
        /*swapgs指令切换gs寄存器从用户态到内核态
        其实就是修改了运行级别使其可以访问内核。*/
        swapgs
        /*在发生中断、异常时前程序运行在用户态
        RSP指向的是Interrupted Procedure's Stack即用户栈*/
        /* tss.sp2 is scratch space. */
        movq    %rsp, PER_CPU_VAR(cpu_tss_rw + TSS_sp2)//保存用户堆栈指针
        SWITCH_TO_KERNEL_CR3 scratch_reg=%rsp //SWITCH_TO_KERNEL_CR3 切换到内核堆栈空间。
        movq    PER_CPU_VAR(cpu_current_top_of_stack), %rsp //切换到该线程对应的内核堆栈

SYM_INNER_LABEL(entry_SYSCALL_64_safe_stack, SYM_L_GLOBAL)
        //保存通用目的寄存器到内核堆栈空间(pt_regs)。
        /* Construct struct pt_regs on stack */
        /*cpu控制单元将用户堆栈指针TSS中的sssp字段这代表的是用户栈指针
        压入栈RSP已经指向内核栈所以入栈指的的是入内核栈。*/
        /* 保存数据段起始地址。 */
        pushq   $__USER_DS                              /* pt_regs->ss */
        /* 保存函数栈栈顶地址。 */
        pushq   PER_CPU_VAR(cpu_tss_rw + TSS_sp2)       /* pt_regs->sp */
        /* 保存 CPU 标识。 */
        pushq   %r11                                    /* pt_regs->flags */
        /* 保存代码段起始地址。 */
        pushq   $__USER_CS                              /* pt_regs->cs */
        /* 保存 syscall 的下一条指令指令寄存器。 */
        pushq   %rcx                                    /* pt_regs->ip */
SYM_INNER_LABEL(entry_SYSCALL_64_after_hwframe, SYM_L_GLOBAL)
        /* 保存系统调用号。 */
        pushq   %rax                                    /* pt_regs->orig_ax */

        /* 将部分寄存器数据填充到 struct pt_regs 数据结构的其它成员。 */
        PUSH_AND_CLEAR_REGS rax=$-ENOSYS

        /* IRQs are off. */
        movq    %rax, %rdi //rax是系统调用编号作为第1个参数保存在寄存器pt_regs->rdi
        movq    %rsp, %rsi /*rsp内核栈地址其实就是pt_regs的地址,作为第2个参数保存
在寄存器pt_regs->rdi */
        
        //调用do_syscall_64函数,执行具体的系统调用函数
        call    do_syscall_64           /*returns with IRQs disabled */ 
           ...

         */
        STACKLEAK_ERASE_NOCLOBBER

        SWITCH_TO_USER_CR3_STACK scratch_reg=%rdi //将cr3恢复为用户PGD地址。

        popq    %rdi
        popq    %rsp  //恢复rsp为用户态栈顶
        USERGS_SYSRET64 /*USERGS_SYSRET64宏其扩展调用 swapgs 指令交换用户
        GS 和内核GS sysret 指令执行从系统调用处理退出。*/
SYM_CODE_END(entry_SYSCALL_64)

        总结下该函数的主要任务就是

  1. 保存用户态现场载入内核态的信息程序工作状态从用户态转变为内核态。
  2. 获取系统调用参数和系统调用号然后调用do_syscall_64发起具体的系统调用
  3. 系统调用函数完成逻辑后需要从内核空间回到用户空间程序内核态转变为用户态需要把之前保存的用户态现场进行恢复。
阿里云国内75折 回扣 微信号:monov8
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6
标签: linux