终于快完了,倒数第二章,后面几章有些问题分析的不是非常清楚,欢迎知道的大神给小弟指点一二。
逐级向下研究运行库,就到了用户层面与内核层面的界限了,也就是常说的系统调用(System call)。系统调用是应用程序(运行库也是应用程序的一部分)与操作系统内核之间的接口,它决定了应用程序如何与内核打交道。
系统调用主要具有以下两方面的作用:
- 由于系统有限的资源有可能被多个不同的应用程序同时访问,因此如果不加以保护,那么各个应用程序难免产生冲突。所以现代操作系统都将可能产生冲突的系统资源给保护起来,阻止应用程序直接访问。通过以上的描述,可以发现系统调用的第一个作用就是对系统资源进行包括,应用程序想对这些资源进行访问,必须通过内核,由内核实现这部功能,并将结果返回给应用程序。
- 某些行为,应用程序不借助操作系统是无法办到或不能有效地办到。书中给出了让程序等待一段时间的例子。
Linux下系统调用的C语言形式被定义在/usr/include/unistd.h。
现代CPU可在多种不同的特权级下执行指令,在Linux系统中,分别使用了其特权级3与特权级0,分别对应用户模式(user mode)和内核模式(kernel mode),也即用户应用程序运行在特权级3下,内核程序运行在特权级0下。这两种模式也被分别称为用户态与内核态。由于有多种特权模式的存在,操作系统就可以让不同的代码运行在不同的模式上,以限制其权利,提高稳定性和安全性。
运行在高特权级的代码将自己降至低特权级是允许的,但反过来低特权级的代码将自己提升至高特权级则是不能轻易就进行的。因此在将低特权级的环境转为高特权级时,须要使用一种较为安全和受控的形式,以防止低特权级模式的代码破坏高特权模式代码的执行。
系统调用是运行在内核态的,用户应用程序基本都是运行在用户态的。操作系统主要通过中断的方式从用户态跳转至内核态。中断一般具有两个属性,分别是中断号与中断处理程序,不同的中断具有不同的中断号,而同时一个中断处理程序一一对应一个中断号。
通常意义上,中断有两种类型,一种称为硬件中断,这种中断来自于硬件的异常或其他事件的发生,如电源掉电、键盘被按下等。另一种称为软件中断,软件中断通常是一条指令(i386下是int),带有一个参数记录中断号,使用这条指令用户可以手动触发某个中断并执行其中断处理程序。由于中断号是很有限的,所以操作系统无法用每个中断号来对应一个系统调用,在linux下就是使用int 0x80中断来触发所有的系统调用。系统调用通过不同的系统调用号来区别不同的系统调用,进而调用不同的系统调用函数。
好了,以上就是原理的简单分析,接下来以具体的源码为例,为大家分析实现与原理之间的关系。
使用与书中相同的例子:fork()函数,程序源码如下:
#include <unistd.h>
int main()
{
fork();
return 0;
}
这里给大家分享的一点就是 <unistd.h>文件中定义了所有的系统调用函数。
接下来使用gdb跟踪fork的执行过程,启动gdb,运行至fork函数处,step进入,此时得到
__libc_fork () at ../sysdeps/nptl/fork.c:59
可以发现fork实际上调用的是__libc_fork (),这个函数的位置也比较明确——sysdeps/nptl/fork.c。这里要给大家分享的一点是如果通过静态分析源码的方法来找到fork函数的实现是非常困难的,甚至可能根本就找不到,因为这些函数都是在编译时通过编译脚本与不同的编译模板产生的,具体的分析请见以下这篇blog: http://blog.csdn.net/nancygreen/article/details/7852021
好了,可以继续跟踪执行,fork函数的返回值为pid,源码中对pid进行修改的语句是
pid = ARCH_FORK ();
ARCH_FORK ()是一个宏定义,其实际定义位于/glibc-2.21/sysdeps/unix/sysv/linux/x86_64/arch-fork.h中,其具体定义如下:
#include <sched.h>
#include <sysdep.h>
#include <tls.h>
#define ARCH_FORK() \
INLINE_SYSCALL (clone, 4, \
CLONE_CHILD_SETTID | CLONE_CHILD_CLEARTID | SIGCHLD, 0, \
NULL, &THREAD_SELF->tid)
看来我们找的路线没有错,好了现在来看看INLINE_SYSCALL的具体实现了,现在的线索也比较明确,就是这几个头文件,这里其实应该编译一下,因为以上三个头文件在很多地方都有,这里比较凑巧,我在相同的文件夹下,恰好找到了sysdep.h文件,其中就定义了INLINE_SYSCALL,所以我估计系统调用的入口应该就在此处。这里还要插一句,通过fork的实现可以明确的看到fork调用的是clone系统调用,这里给出的四个参数分别是“CLONE_CHILD_SETTID | CLONE_CHILD_CLEARTID | SIGCHLD”、“0”、“NULL”、“&THREAD_SELF->tid”,大家要注意这几个参数,因为接下来还要用到这几个参数。
INLINE_SYSCALL的具体内容如下:
# undef INLINE_SYSCALL
# define INLINE_SYSCALL(name, nr, args...) \
({ \
unsigned long int resultvar = INTERNAL_SYSCALL (name, , nr, args); \
if (__glibc_unlikely (INTERNAL_SYSCALL_ERROR_P (resultvar, ))) \
{ \
__set_errno (INTERNAL_SYSCALL_ERRNO (resultvar, )); \
resultvar = (unsigned long int) -1; \
} \
(long int) resultvar; })
其实很简单就是三句话
unsigned long int resultvar = INTERNAL_SYSCALL (name, , nr, args);
再使用一个宏定义“INTERNAL_SYSCALL”,获得结果值。
if (__glibc_unlikely (INTERNAL_SYSCALL_ERROR_P (resultvar, ))) \
{ \
__set_errno (INTERNAL_SYSCALL_ERRNO (resultvar, )); \
resultvar = (unsigned long int) -1; \
}
通过一个判断,对其结果进行检查,若结果存在问题,则设置错误码,同时将结果置为-1。
(long int) resultvar;
由于arch-fork实际上就是一个宏定义,并不是一个函数,所以不能使用return语句,因此arch-fork的返回值是通过括号内的语句直接返回的(这一点貌似在c程序设计语言中谈到过)。
好了接下来只看第一句就好了,因为第二句通过其宏定义就可以知道,这是一个不太可能发生的情况(__glic_unlikely)。
“INTERNAL_SYSCALL (name, , nr, args);”定义如下:
# undef INTERNAL_SYSCALL
# define INTERNAL_SYSCALL(name, err, nr, args...) \
INTERNAL_SYSCALL_NCS (__NR_##name, err, nr, ##args)
此处可以发现,“INTERNAL_SYSCALL” 又被定义成为 “INTERNAL_SYSCALL_NCS”,好了接着看:
# define INTERNAL_SYSCALL_NCS(name, err, nr, args...) \
({ \
unsigned long int resultvar; \
LOAD_ARGS_##nr (args) \
LOAD_REGS_##nr \
asm volatile ( \
"syscall\n\t" \
: "=a" (resultvar) \
: "0" (name) ASM_ARGS_##nr : "memory", REGISTERS_CLOBBERED_BY_SYSCALL); \
(long int) resultvar; })
又是一个宏定义,首先来看看“INTERNAL_SYSCALL_NCS”的几个参数,
“__NR_##name”,其中“##”是将##左右两边的标签组合在一起(token pasting or token concatenation)。被展开成为“__NR_fork”,此处“__NR_fork”也是一个宏,表示fork的系统调用号,通过“INTERNAL_SYSCALL_NCS”函数的第一个参数就把函数名与系统调用号联系了起来。关于系统调用号的定义可在“/arch/sh/include/uapi/asm/unistd_64.h”中查找。
然后是err,估计是错误码,在这个宏函数中我并没有找到具体使用的地方。
nr的值为4,代表参数的个数,由于系统调用需要陷入内核,而内核与用户进程使用不同的内存空间,所以不能用栈传递参数(不过这一点已经改变,x86_64下已经改为使用寄存器传递函数参数)。
##args就是具体的参数,此处“##”的作用就不太明确了。
好了接着回到系统调用的实现上来。
unsigned long int resultvar;
首先声明一个返回值变量。
LOAD_ARGS_##nr (args) \
LOAD_REGS_##nr
根据前面的分析,LOAD_ARGS_##nr(args)会被展开成为LOAD_ARGS_4(args),LOAD_REGS_##nr会被展开称为LOAD_REGS_4。
好了接下来看LOAD_ARGS_4(args)的实现:
# define LOAD_ARGS_4(a1, a2, a3, a4) \
LOAD_ARGS_TYPES_4 (long int, a1, long int, a2, long int, a3, \
long int, a4)
# define LOAD_ARGS_TYPES_4(t1, a1, t2, a2, t3, a3, t4, a4) \
t4 __arg4 = (t4) (a4); \
LOAD_ARGS_TYPES_3 (t1, a1, t2, a2, t3, a3)
# define LOAD_ARGS_TYPES_3(t1, a1, t2, a2, t3, a3) \
t3 __arg3 = (t3) (a3); \
LOAD_ARGS_TYPES_2 (t1, a1, t2, a2)
# define LOAD_ARGS_TYPES_2(t1, a1, t2, a2) \
t2 __arg2 = (t2) (a2); \
LOAD_ARGS_TYPES_1 (t1, a1)
# define LOAD_ARGS_TYPES_1(t1, a1) \
t1 __arg1 = (t1) (a1); \
LOAD_ARGS_0 ()
# define LOAD_ARGS_0()
此处可以看到LOAD_ARGS_4通过多个宏定义的调用,其作用其实就是声明4个long int类型的变量,在sysdep.h中仅定义:
# define LOAD_REGS_6 \
LOAD_REGS_TYPES_6 (long int, a1, long int, a2, long int, a3, \
long int, a4, long int, a5, long int, a6)
说明系统系统最多可传递六个参数,另一方面由于参数都是long int型的,所以在系统调用中不能使用浮点类型数据。
好了,接着看“LOAD_REGS_TYPES_4”
# define LOAD_REGS_4 \
LOAD_REGS_TYPES_4 (long int, a1, long int, a2, long int, a3, \
long int, a4)
# define LOAD_REGS_TYPES_4(t1, a1, t2, a2, t3, a3, t4, a4) \
register t4 _a4 asm ("r10") = __arg4;
LOAD_REGS_TYPES_3(t1, a2, t2, a2, t3, a3)
# define LOAD_REGS_TYPES_3(t1, a1, t2, a2, t3, a3) \
register t3 _a3 asm ("rdx") = __arg3; \
LOAD_REGS_TYPES_2(t1, a1, t2, a2)
# define LOAD_REGS_TYPES_2(t1, a1, t2, a2) \
register t2 _a2 asm ("rsi") = __arg2; \
LOAD_REGS_TYPES_1(t1, a1)
# define LOAD_REGS_TYPES_1(t1, a1) \
register t1 _a1 asm ("rdi") = __arg1; \
LOAD_REGS_0
# define LOAD_REGS_0
此处还是相同的嵌套宏定义,不过比较明确的是又申请了4个long int类型的变量,分别是“_a1”、“_a2”、“_a3”、“_a4”,同时将__arg1赋给了rdi寄存器,__arg2赋给了rsi寄存器,__arg3赋给了rdx寄存器,__arg4赋给了r10寄存器。
继续向下走:
asm volatile ( \
"syscall\n\t" \
: "=a" (resultvar) \
: "0" (name) ASM_ARGS_##nr : "memory", REGISTERS_CLOBBERED_BY_SYSCALL); \
此处可以看到在x86_64体系结构下,进行系统调用的方式再一次改变,不再是int 0x80或是sysenter,而是采用了新的指令——syscall指令,对于syscall指令的详细描述请见:
http://www.mouseos.com/arch/syscall_sysret.html
"=a" (resultvar)表示用eax输出返回结果并存储在resultvar中。
"0" (name) ASM_ARGS_##nr,这一句展开后变为:
“0”(name) "r" (_a1) "r" (_a2) "r" (_a3) "r" (_a4)
一点一点的看,“0”(name)表示name作为输入,“0”指示由编译器选择和输出相同的寄存器(即eax)来传递参数,“r”表示需要将“_a1”与某个通用寄存器相关联,先将操作数的值读入寄存器,然后在指令中使用相应寄存器,而不是“_a1”本身,当然指令执行完后需要将寄存器中的值存入变量“_a1”,从表面上看好像是指令直接对“result”进行操作,实际上GCC做了隐式处理,这样我们可以少写一些指令。此处表示_a1、_a2、_a3、_a4可使用任意的寄存器。
"memory", REGISTERS_CLOBBERED_BY_SYSCALL的内容不是非常重要,在此也就不详细分析了。
好了,现在问题的核心就在syscall指令上了。
接下来看看syscall的执行流程,与int指令不同的是,syscall不再依靠中断向量表查找相应的中断处理程序,而是采用将特殊用途寄存器中的内容拷贝到相应的寄存器中的方式启动系统调用处理函数,并根据需要保存寄存器。但syscall指令与int 0x80虽然在过程上可能不同,但最终都是调用sys_call_table函数。
好,先从原理上看看syscall指令的作用流程:
- 设定rip寄存器
- 设定cs寄存器
- 设定ss寄存器
通过cs/rip寄存器可以定位到执行哪段代码,而ss寄存器描述了栈段寄存器,但此处没有设定esp寄存器,也就意味着没有设定栈顶指针(关于这个问题,我以后会进行进一步分析)
关于syscall/sysret指令的详细描述请见:http://www.mouseos.com/arch/syscall_sysret.html
好了,回到我们的主题上来,还是跟踪系统调用的执行过程。其实跟踪到此处我们的线索其实已经断了,因为不知道cs寄存器中的内容,所以不知道接下来会跳转到何处继续执行,不过此处比较幸运,在网上浏览相关信息的时候,发现了一个/arch/x86/kernel/的文件夹,在其中有一个entry_64.S的文件,这个文件中恰好有一个system_call函数,而这个函数恰好是系统调用处理函数(此处我无法证明这个说法的正确性,这个文件中的注释也没有特别明确的说明)。
这个system_call函数的内容如下:
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, rax_enosys=1
movq_cfi rax,(ORIG_RAX-ARGOFFSET)
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 ret_from_sys_call /* and return regs->ax */
movq %r10,%rcx
call *sys_call_table(,%rax,8) # XXX: rip relative
movq %rax,RAX-ARGOFFSET(%rsp)
其中比较有价值的是:
cmpl $__NR_syscall_max,%eax 比较系统调用号是否合法,
call *sys_call_table(,%rax,8) 调用sys_call_table中的函数
现在就是的重点就是:“sys_call_table”、__NR_syscall_max,这里直接搜索是搜索不出来答案的,这个数字的生成是由内核在编译阶段由arch/x86/entry/syscalls/syscall_64.tbl 文件生成的,其中还涉及一个脚本文件:arch/x86/entry/syscalls/syscalltbl.sh。有机会要编译配置以下内核。具体内容请见: https://0xax.gitbooks.io/linux-insides/content/SysCall/syscall-2.html第一小节
sys_call_table的定义位于“/arch/sh/kernel/syscall_64.S”,其中每一项都是系统调用函数的地址,*sys_call_table(,%rax,8)指的就是sys_call_table上偏移量为0+%rax*8地址所指向的函数。由于是64位环境,所以此处使用rax*8而不是eax*4。回到我们的例子中,对于clone的系统调用经过一系列跳转,最终会执行内核中sys_clone函数。sys_clone执行完成后会将结果保存到eax中,并通过sysret指令返回用户栈。至此由库函数陷入内核的过程也就大致走了一遍,其中还有许多细节不是很清楚,特别是syscall指令执行前对于寄存器的操作,以及sysret指令执行后如何由内核栈恢复到用户栈。
此处要谈一点sys_clone的实现,在内核源码中实际上是不存在sys_clone的实现,其声明位于include/linux/syscalls.h:
#ifdef CONFIG_CLONE_BACKWARDS
asmlinkage long sys_clone(unsigned long, unsigned long, int __user *, int,
int __user *);
#else
#ifdef CONFIG_CLONE_BACKWARDS3
asmlinkage long sys_clone(unsigned long, unsigned long, int, int __user *,
int __user *, int);
#else
asmlinkage long sys_clone(unsigned long, unsigned long, int __user *,
int __user *, int);
#endif
#endif
通过对比可以发现,sys_clone使用5个或6个参数,但此处sys_clone使用4个参数(关于这个问题暂且留下,如果有了答案我会第一时间进行补充)。
以第一个函数声明为例,进行研究,在文件的开头部分有如下定义:
#define SYSCALL_DEFINE5(name, ...) SYSCALL_DEFINEx(5, _##name, __VA_ARGS__)
#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 \
__attribute__((section("__syscalls_metadata"))) \
*__p_syscall_meta_##sname = &__syscall_meta_##sname;
#else
#define SYSCALL_METADATA(sname, nb, ...)
#endif
#define __SYSCALL_DEFINEx(x, name, ...) \
asmlinkage long sys##name(__MAP(x,__SC_DECL,__VA_ARGS__)) \
__attribute__((alias(__stringify(SyS##name)))); \
static inline long SYSC##name(__MAP(x,__SC_DECL,__VA_ARGS__)); \
asmlinkage long SyS##name(__MAP(x,__SC_LONG,__VA_ARGS__)); \
asmlinkage long SyS##name(__MAP(x,__SC_LONG,__VA_ARGS__)) \
{ \
long ret = SYSC##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 SYSC##name(__MAP(x,__SC_DECL,__VA_ARGS__))
通过以上宏的层层展开,我们最后得到sys_clone的定义如下:
asmlinkage long sys_clone(unsigned long, unsigned long, int __user *, int, int __user *);
但在内核源码中不是直接通过sys_clone定义的,是以如下形式定义的:
#ifdef __ARCH_WANT_SYS_CLONE
#ifdef CONFIG_CLONE_BACKWARDS
SYSCALL_DEFINE5(clone, unsigned long, clone_flags, unsigned long, newsp,
int __user *, parent_tidptr,
int, tls_val,
int __user *, child_tidptr)
#elif defined(CONFIG_CLONE_BACKWARDS2)
SYSCALL_DEFINE5(clone, unsigned long, newsp, unsigned long, clone_flags,
int __user *, parent_tidptr,
int __user *, child_tidptr,
int, tls_val)
#elif defined(CONFIG_CLONE_BACKWARDS3)
SYSCALL_DEFINE6(clone, unsigned long, clone_flags, unsigned long, newsp,
int, stack_size,
int __user *, parent_tidptr,
int __user *, child_tidptr,
int, tls_val)
#else
SYSCALL_DEFINE5(clone, unsigned long, clone_flags, unsigned long, newsp,
int __user *, parent_tidptr,
int __user *, child_tidptr,
int, tls_val)
#endif
{
return do_fork(clone_flags, newsp, 0, parent_tidptr, child_tidptr);
}
#endif
通过以上函数定义,可以发现无论采用哪种定义形式,sys_clone最后都会调用do_fork函数。该函数定义位于“/kernel/fork.c”。
好了说了这么多,该把上述系统调用的过程进行一个简单的总结,系统调用的大致过程是:
触发系统调用(现已知道三种方法:int 0x80、sysenter、syscall),这一步还要传递系统调用号与参数,系统调用号一般都是eax寄存器,但参数使用的寄存器可能就各不相同。
切换堆栈。上述三种方法切换堆栈的方法不同,但目的相同,都是要把用户内存空间下的ss、esp、eflags、cs、eip等寄存器保存,切换为内核堆栈。在int 0x80下,上述ss、esp、eflags、cs、eip寄存器被控制单元直接保存到堆栈中;syscall指令执行后,用户内存空间下的rip被保存到rcx中,rflags被保存到r11,user_cs/kernel_cs在syscall_init函数执行时已经被写入到MSR_STAR寄存器中,rsp寄存器被保存到old_rsp,ss寄存器由于在全局描述符表中与cs相邻,所以得到ss寄存器相当于也已经被保存,这个过程与int 0x80的压栈过程相对应。再来看恢复过程,iret执行时依次从栈中恢复寄存器的值;sysret指令执行后其效果相当于依次从rcx中恢复rip的值,从r11中恢复rflags的值,从MSR_STAR中恢复cs与ss的值,从old_rsp中恢复rsp的值。
返回系统调用。分别对应指令iret、sysexit、sysret指令,这一步主要是要把内核堆栈切换回用户堆栈,并从系统调用处继续执行。
以上就是系统调用过程的简单分析,对于其中不清楚的地方欢迎大家给我补充。
补充:上文中有许多不清晰的地方,所以这几日我又发了些帖子,查了查资料,现把其中不完整的地方,给大家做一个补充:
首先是“syscall指令执行后,跳转到何处运行”。
关于这个问题,我参考以下这篇blog:https://0xax.gitbooks.io/linux-insides/content/SysCall/syscall-1.html并结合我机器的实际情况,给出答案。
通过之前的一些资料我们已经了解到,syscall是根据三个特殊寄存器的值保存的上下文,直接跳转到某个程序处开始执行,这里既然要根据寄存器的值进行跳转,那么事先就要将相应的值放入这些寄存器中,这里执行这个功能的函数就是syscall_init,程序源码如下(arch/x86/kernel/cpu/common.c):
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);
#ifdef CONFIG_IA32_EMULATION
syscall32_cpu_init();
#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);
}
通过简单分析程序,就能对它的功能有一个简单的了解,此处MSR_STAR应该是用于保存代码段起始地址,而MSR_LSTAR保存的应当是rip,cs:rip标明了将要执行的程序的地址,而MSR_CSTAR保存的应当是SS段的地址(这个比较不确定)。通过函数名就可以比较明确的知道,这是一个系统调用初始化函数,而rip寄存器在syscall指令执行后也会跳转到system_call执行。通过int 0x80、sysenter、syscall三种系统调用触发方式的对比可以发现,系统调用的实际执行函数都是system_call,而后一句非常关键的语句就是“call *sys_call_table(,%rax,8)”。
这里提到sys_call_table,就要引出的第二个问题,在32位模式下对应的语句是“call *sys_call_table(,%eax,4)”,之所以会产生如此差异,是由于在64位模式下,指针长为8个字节,所以是*8。
关于syscall指令没有设定rsp指针的问题,我现在也有了答案。解答就在这篇blog中:http://www.mouseos.com/arch/freebsd_swapgs.html
感谢这位大神在邮件中回复我。
系统调用程序的运行不可能不需要运行栈的支持,在syscall指令中没有设定rsp寄存器,而是采用了一种“迂回”的方式,通过swapgs指令与syscall/sysret指令相配合,共同设定rsp的值,swapgs 指令目的是通过 syscall 切入到 kernel 系统服务后,通过交换 IA32_KERNEL_GS_BASE 与 IA32_GS_BASE 值,从而得到 kernel 数据结构块!其中:
- IA32_KERNEL_GS_BASE 寄存器:这是一个 MSR 寄存器,用来保存 kernel 级别的数据结构指针!
- 最后一个是 IA32_GS_BASE 寄存器:但这个寄存器并不是因为 swapgs 指令而存在的,是由于 x64 体系的设计!(以上内容引自这篇blog)
SWAPGS_UNSAFE_STACK
而上述内容的定义位于(linux-source-3.19.0/arch/x86/include/asm/irqflags.h):
#define SWAPGS_UNSAFE_STACK swapgs
但仅靠swapgs指令还不够,通过这条指令仅能设置栈的基地址,还需要设置栈的偏移量,指令如下:
movq %rsp,PER_CPU_VAR(old_rsp)
movq PER_CPU_VAR(kernel_stack),%rsp
至此内核运行的上下文就被建立了起来。
好,深入研究实现机制这么久,来看看上层实现是如何利用系统调用的,我们已知通过库函数可以调用系统调用,除此以外还有一种方法直接调用系统。可通过如下函数:
extern long int syscall (long int __sysno, ...) __THROW;
该函数定义为unistd.h中,其中__sysno就是系统调用号。举个栗子:
#include <unistd.h>
#include <sys/syscall.h>
#include <sys/types.h>
#include <stdio.h>
int main(int argc, char *argv[])
{
pid_t tid;
tid = syscall(__NR_gettid);
printf("tid = %d\n",tid);
return 0;
}
其中__NR_gettid还可以改为SYS_gettid,<sys/syscall.h> 定义了相关内容。关于__NR_gettid的定义,直接通过预处理文件获得,命令如下:
gcc -E -o test_syscall_function.i test_syscall_function.c
其中的一部分。。。。
# 2 "test_syscall_function.c" 2
# 1 "/usr/include/x86_64-linux-gnu/sys/syscall.h" 1 3 4
# 24 "/usr/include/x86_64-linux-gnu/sys/syscall.h" 3 4
# 1 "/usr/include/x86_64-linux-gnu/asm/unistd.h" 1 3 4
# 12 "/usr/include/x86_64-linux-gnu/asm/unistd.h" 3 4
# 1 "/usr/include/x86_64-linux-gnu/asm/unistd_64.h" 1 3 4
# 13 "/usr/include/x86_64-linux-gnu/asm/unistd.h" 2 3 4
# 25 "/usr/include/x86_64-linux-gnu/sys/syscall.h" 2 3 4
unistd_64.h定义了当前机器中的系统调用号。