Linux 系统调用的实现(x86_64)

news2024/12/25 12:51:04

目录

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);
        //syscall(64)指令入口函数,设置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中的ss,sp字段,这代表的是用户栈指针)
        压入栈,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. 系统调用函数完成逻辑后,需要从内核空间回到用户空间,程序内核态转变为用户态,需要把之前保存的用户态现场进行恢复。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/164784.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

C语言常见错误汇总

1 数组遍历时使用sizeof(a) 任务&#xff1a;有个数组&#xff0c;找出第二大的数&#xff0c;并且打印出来&#xff08;使用*操作数组元素个数&#xff0c;不要使用[]&#xff09; #include<stdio.h> int main01() {int a[] { 100,100,100,234,123,500,32,68,41,99,1…

code.org免费的少儿编程入门平台

现在市面上的少儿编程课&#xff0c;都是先花9.9就能体验几节课&#xff0c;然后要花几千块才能继续学习后面的课程。这些钱大可不必花。 现在给大家推荐一个免费的网站&#xff0c;code.org&#xff0c;它是一个非营利组织创办的网站&#xff0c;目标是让每个学生都能像生物、…

高并发系统设计 --多级缓存

为了提高系统的性能&#xff0c;一般会引入“缓存机制”&#xff0c;将部分热点数据存入缓存中&#xff0c;用空间换取时间&#xff0c;以达到快速响应的目的。 我们对缓存的认知停留在redis&#xff0c;但其实缓存远远不是只有redis的&#xff0c;从客户端发起请求开始&#…

MySQL整体使用》导入数据、约束、多表查询、事务、变量类型、资源占用

我发的MySQL相关内容&#xff1a; C#基础知识体系框架图&#xff0c;及起对应我发过的博客 linux安装mysql8配置使用&#xff0c;并解决常见问题 MySQL常用命令&#xff08;DQL&#xff09; 执行脚本命令&#xff0c;本地生成SQL文件后在服务器执行 // 进入mysql命令控制 m…

svg绘(viewBox viewport preserveAspectRatio)代替png图片等

当我们的代码中需要一个小图标的时候没必要去iconfont进行下载图标使用 要是下载的png格式那么容量还很大 远不如svg 直接自己代码写 记住svg的坐标朝向和数学坐标轴不一样 实现下图添加的小图标 <svg width"20px" height"20px" style"border: …

2023java面试之Zookeeper基础

一、说说 Zookeeper 是什么&#xff1f;直译&#xff1a;从名字上直译就是动物管理员&#xff0c;动物指的是 Hadoop 一类的分布式软件&#xff0c;管理员三个字体现了 ZooKeeper 的特点&#xff1a;维护、协调、管理、监控。简述&#xff1a;有些软件你想做成集群或者分布式&a…

冯诺依曼体系结构

冯诺依曼体系结构 我们常见的计算机&#xff0c;如笔记本。我们不常见的计算机&#xff0c;如服务器&#xff0c;大部分都遵守冯诺依曼体系。 截至目前&#xff0c;我们所认识的计算机&#xff0c;都是有一个个的硬件组件组成&#xff1a; 输入单元&#xff1a;包括键盘, 鼠…

netbeans中配置maven

deploy-发布到远程maven库本节默认maven库为nexusnetbeans中按ctrl1&#xff0c;打开Project窗口&#xff1b;在Project窗口中找到相关的project或module,在项目名上点击鼠标右键&#xff1b;在弹出菜单中找到菜单“Run Maven”的子菜单“Goals”&#xff0c;并点击&#xff0c…

PCB封装创建(IC类+USB)

目录 一&#xff1a;IC类 封装原理图 规格参数选最大。创建过程 1.放置焊盘 2.我们需要八个上图焊盘&#xff0c;可以用特殊粘贴 3.丝印层设置 封装向导 右击0805R&#xff0c;选择footprint 输入焊盘尺寸 二&#xff1a;USB封装 原理图 创建过程 1.放置焊盘&#x…

SSM 03_SpringMVC REST风格 Postman SSM整合 拦截器

01-SpringMVC简介SpringMVC是隶属于Spring框架的一部分&#xff0c;主要是用来进行Web开发&#xff0c;是对Servlet进行了封装。SpringMVC是处于Web层的框架&#xff0c;所以其主要的作用就是用来接收前端发过来的请求和数据然后经过处理并将处理的结果响应给前端&#xff0c;所…

元宇宙时代业务扩张,专精特新小巨人找到了增长“神器”

进入2023年&#xff0c;元宇宙时代正扑面而来。自从脸书公司更名为Meta以来&#xff0c;元宇宙就在全球迅速走红。《福布斯》认为&#xff0c;2030年全球元宇宙的市场规模有望高达5万亿美元。更为重要的是&#xff0c;元宇宙正在成为数实融合的新界面、未来商业的新型基础设施。…

如何在浏览器中安装使用Vue开发者工具?Vue开发者工具的安装使用?可直接提取插件安装使用

一个混迹于Github、Stack Overflow、开源中国、CSDN、博客园、稀土掘金、51CTO等 的野生程序员。 目标&#xff1a;分享更多的知识&#xff0c;充实自己&#xff0c;帮助他人 GitHub公共仓库&#xff1a;https://github.com/zhengyuzh 以github为主&#xff1a; 1、分享前端后端…

【阶段四】Python深度学习08篇:深度学习项目实战:循环神经网络SimpleRNN、LSTM进行淘宝商品评论文本情感分析

本篇的思维导图: 项目背景 随着信息化社会的发展,互联网成为方便、快捷的信息获取渠道之一。在电子商务和社会网站中,大量非结构化的评论文本作为最直观的用户体验数据被保存下来。如何利用这些文字信息归纳出用户对某一事、物的观点态度成为自然语言(NLP)领域一项…

RNN从理论到实战【实战篇】

来源&#xff1a;投稿 作者&#xff1a;175 编辑&#xff1a;学姐 昨天的文章中&#xff0c;我们学习了RNN的理论部分&#xff0c;本文来看如何实现它&#xff0c;包括堆叠RNN和双向RNN。从而理解它们的原理。最后看一个应用到词性标注任务的实战。 RNNCell 首先实现单时间步…

iMX6ULL —按键输入捕获与GPIO输入配置与高低电平读取

硬件介绍1.1 板子上按键原理图先来看原理图&#xff0c;我板子上有4个按键sw1~sw4:1.1.1 SW1SW1是板子的系统复位按键&#xff0c;不可编程使用1.1.2 SW2、SW3SW2&#xff1a;SNVS_TAMPER1&#xff0c;GPIO5_1平时是低电平&#xff0c;按下去是高电平。SW3&#xff1a;ONOFF它也…

2023年java面试题之zookeeper基础2

一、请描述一下 Zookeeper 的通知机制是什么&#xff1f;Zookeeper 允许客户端向服务端的某个 znode 注册一个 Watcher 监听&#xff0c;当服务端的一些指定事件触发了这个 Watcher &#xff0c;服务端会向指定客户端发送一个事件通知来实现分布式的通知功能&#xff0c;然后客…

echarts基本用法

目录 tooltip:{ // 设置提示框信息 图表的提示框组件 legend:{ // 图例组件 toolbox : { //工具箱组件 可以另存为图片等功能 grid{ //网格配置 grid可以控制线型图 柱状图 图表大小 xAxs: { // 设置x轴的相关配置 y轴同理 series:[ // 系列图表 它决定着显示那种…

Spring MVC 详解 (Spring Boot)

Spring MVC 详解 - Spring Boot一、什么是 Spring MVC1.1 MVC 定义1.2 MVC 和 Spring MVC 的关系1.3 学习目的二、Spring MVC 创建和连接2.1 创建 Spring MVC 项目2.2 相关注解三、获取参数3.1 使用 Servlet API3.2 通过方法参数直接拿到3.2.1 传递单个参数3.2.2 传递多个参数3…

【Acwing 周赛复盘】第86场周赛复盘(2023.1.14)

【Acwing 周赛复盘】第86场周赛复盘 周赛复盘 ✍️ 本周个人排名&#xff1a;678/2358 AC情况&#xff1a;2/3 这是博主参加的第一次周赛&#xff0c;深刻体会到了世界的参差 &#x1f602; 看到排名 TOP3 的大佬都是不到 5 分钟内就 AK 了&#xff0c;真是恐怖如斯&#xff0…

29.动态内存申请

1.动态内存分配的概念 在数组一章中&#xff0c;介绍过数组的长度是预先定义好的&#xff0c;在整个程序中固定不变&#xff0c;但是在实际的编程中&#xff0c;往往所需的内存空间取决于实际输入的数据&#xff0c;而无法预先确定。为了解决上述问题&#xff0c;C语言提供了一…