目录
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)
总结下该函数的主要任务就是:
- 保存用户态现场,载入内核态的信息,程序工作状态从用户态转变为内核态。
- 获取系统调用参数和系统调用号,然后调用do_syscall_64发起具体的系统调用
- 系统调用函数完成逻辑后,需要从内核空间回到用户空间,程序内核态转变为用户态,需要把之前保存的用户态现场进行恢复。