原创文章,转载请注明: 转载自pagefault
本文链接地址: linux下系统调用的实现
基本的x86体系下系统调用相关的指令可以看这篇文章。
x86下,最早是使用软中断指令int 0×80来做的,不过现在内核是使用syscall和sysenter指令,只有64位下才会使用syscall,而大部分情况都是使用sysenter,这里我们主要介绍sysenter指令,不过具体实现3者现在都差不多,这是因为内核使用了VDSO来兼容所有的指令,接下来我们就要来详细的分析内核是如何实现vdso层,以及glibc库(也就是用户空间)是如何来调用vdso层的接口,从而进入内核。
首先来看glibc的代码,下面这段代码就是syscall的实现,位置是在sysdeps/unix/sysv/linux/i386/syscall.S这个文件里面。这段汇编很简单,就是保存寄存器,然后讲参数,系统调用号入站,最后调用ENTER_KERNEL进入内核。所以这里最关键的就是ENTER_KERNEL这个宏。
1
2
3
4
5
6
7
8
9
10
11
12
13
|
ENTRY (syscall)
PUSHARGS_6
/* Save register contents. */
_DOARGS_6(44)
/* Load arguments. */
movl 20(%esp), %eax
/* Load syscall number into %eax. */
ENTER_KERNEL
/* Do the system call. */
POPARGS_6
/* Restore register contents. */
cmpl $-4095, %eax
/* Check %eax for error. */
jae SYSCALL_ERROR_LABEL
/* Jump to error handler if error. */
L(pseudo_end):
ret
/* Return to caller. */
PSEUDO_END (syscall)
|
接下来我们就来看ENTER_KERNEL这个宏的实现,这个宏主要就是用来进入内核,通过vdso调用内核对应的系统调用接口,从而达到执行系统调用的目的。
通过下面的代码我们可以看到通过宏I386_USE_SYSENTER来决定是否使用快速系统调用,这里这个宏就不详细分析了,只需要知道他主要是通过makefile中的参数进行控制的就可以了。
如果I386_USE_SYSENTER没有定义,则说明不使用快速系统调用,此时使用老的方法,也就是使用软中断指令int $0×80来进入内核,而如果使用快速系统调用则通过SHARED宏来决定使用那种方式来得到vdso的页地址(也就是内核实现的系统调用的页,这个后面会详细介绍).这里接下来会详细分析SHARED打开的情况,也就是最常用的情况。
1
2
3
4
5
6
7
8
9
|
#ifdef I386_USE_SYSENTER
# ifdef SHARED
# define ENTER_KERNEL call *%gs:SYSINFO_OFFSET
# else
# define ENTER_KERNEL call *_dl_sysinfo
# endif
#else
# define ENTER_KERNEL int $0x80
#endif
|
因此这里最关键就是call *%gs:SYSINFO_OFFSET这段汇编了,首先我们知道寄存器%gs里面保存的是TLS(Thread Local Storage),然后SYSINFO_OFFSET是在nptl/sysdeps/i386/tcb-offsets.sym里面定义:
1
|
SYSINFO_OFFSET offsetof (tcbhead_t, sysinfo)
|
下面就是 tcbhead_t的结构:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
typedef
struct
{
void
*tcb;
/* Pointer to the TCB. Not necessarily the
thread descriptor used by libpthread. */
dtv_t *dtv;
void
*self;
/* Pointer to the thread descriptor. */
int
multiple_threads;
//SYSINFO_OFFSET也就是他的偏移。
uintptr_t
sysinfo;
uintptr_t
stack_guard;
uintptr_t
pointer_guard;
int
gscope_flag;
#ifndef __ASSUME_PRIVATE_FUTEX
int
private_futex;
#else
int
__unused1;
#endif
/* Reservation of some values for the TM ABI. */
void
*__private_tm[5];
} tcbhead_t;
|
通过上面的计算我们能够得到SYSINFO_OFFSET的值就是0×10,这里也就是调用tcbhead_t的sysinfo的值,而tcbhead_t.sysinfo这个值是在那里赋值的呢,看下面的代码,nptl/sysdeps/i386/tls.h:
这里TLS_INIT_TP是用来初始化一个thread pointer,而其中就将tcb的头进行了初始化,而头的sysinfo域是通过INIT_SYSINFO进行初始化的。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
# define TLS_INIT_TP(thrdescr, secondcall) \
({
void
*_thrdescr = (thrdescr); \
tcbhead_t *_head = _thrdescr; \
union
user_desc_init _segdescr; \
int
_result; \
\
_head->tcb = _thrdescr; \
/* For now the thread descriptor is at the same address. */
\
_head->self = _thrdescr; \
/* New syscall handling support. */
\
............................................................................................
#if defined NEED_DL_SYSINFO
# define INIT_SYSINFO \
//可以看到它的值就是dl_sysinfo的地址
_head->sysinfo = GLRO(dl_sysinfo)
#else
# define INIT_SYSINFO
#endif
|
接下来就是dl_sysinfo的值了,它是在函数_dl_sysdep_start (elf/dl-sysdep.c)中被赋值的,而_dl_sysdep_start这个函数是干吗的呢,glibc的注释写的很清楚:
1
2
3
4
|
/* Call the OS-dependent function to set up life so we can do things like
file access. It will call `dl_main’ (below) to do all the real work
of the dynamic linker, and then unwind our frame and run the user
entry point on the same stack we entered on. */
|
我的理解就是得到一些依赖os的函数的地址(动态库),然后放到对应的段,以便与后面存取。
下面就是对应的代码片段。这里可以看到它是通过判断函数的类型来进行不同的操作,这里我们节选我们感兴趣的sysinfo部分,这里可以看到sysinfo的类型就是AT_SYSINFO。这里一般来说取的就是ELF auxiliary vectors的值,也就是说内核会把相关的信息放到ELF auxiliary vectors中。而什么是ELF auxiliary vectors,这里介绍的比较详细:
http://articles.manugarg.com/aboutelfauxiliaryvectors.html
1
2
3
4
5
6
7
8
9
10
11
12
|
#define AT_SYSINFO 32
#ifdef NEED_DL_SYSINFO
case
AT_SYSINFO:
new_sysinfo = av->a_un.a_val;
break
;
#endif
..................................................
#if defined NEED_DL_SYSINFO
/* Only set the sysinfo value if we also have the vsyscall DSO. */
if
(GLRO(dl_sysinfo_dso) != 0 && new_sysinfo)
GLRO(dl_sysinfo) = new_sysinfo;
#endif
|
接下来就该到内核了,也就是说AT_SYSINFO类型对应的到底是那里。
在看内核代码之前,我们先来了解下vdso的结构,首先我们随便ldd一个可执行文件,下面是我的机器上的情况:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
ldd nginx
linux-gate.so.1 => (0xb77d9000)
libcrypt.so.1 => /lib/libcrypt.so.1 (0xb778a000)
libpcre.so.0 => /lib/libpcre.so.0 (0xb7753000)
libcrypto.so.1.0.0 => /usr/lib/libcrypto.so.1.0.0 (0xb75d9000)
libz.so.1 => /usr/lib/libz.so.1 (0xb75c4000)
libperl.so => /usr/lib/perl5/core_perl/CORE/libperl.so (0xb746c000)
libnsl.so.1 => /lib/libnsl.so.1 (0xb7455000)
libdl.so.2 => /lib/libdl.so.2 (0xb7451000)
libm.so.6 => /lib/libm.so.6 (0xb742c000)
libutil.so.1 => /lib/libutil.so.1 (0xb7428000)
libpthread.so.0 => /lib/libpthread.so.0 (0xb740e000)
libc.so.6 => /lib/libc.so.6 (0xb72c2000)
/lib/ld-linux.so.2 (0xb77da000)
|
这里我们看到有一个linux-gate.so.1的动态库,这个库其实是不存在的,而它其实就是一块内存,其中包括了vdso生成的系统调用的代码,也就是说内核mmap这块内存(其实这快内存也就是完全遵循elf格式)到用户空间,然后ldd将它作为动态库来处理,此时用户空间就很容易来执行这块内存的代码。
有关vdso的部分这篇也是介绍的不错,可以看看。
在初始化的时候,内核会判断系统之不支持快速系统调用,如果支持的话则将快速系统调用相关的代码拷贝到将要mmap的内存,否则就拷贝软中断指令。来看代码,是在arch/x86/vdso/vdso32-setup.c的sysenter_setup函数。
这个函数就是判断支持那些指令,然后做不同的处理,可以看到最优先处理的就是syscall,然后是sysenter,最后是int80,这里我们主要来看sysenter,这里可以看到是将vdso32_sysenter_start的地址付给vsyscall ,然后将vsyscall的内容拷贝到对应的页。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
|
int
__init sysenter_setup(
void
)
{
void
*syscall_page = (
void
*)get_zeroed_page(GFP_ATOMIC);
const
void
*vsyscall;
size_t
vsyscall_len;
//得到对应的页
vdso32_pages[0] = virt_to_page(syscall_page);
#ifdef CONFIG_X86_32
gate_vma_init();
#endif
//开始决定使用那种方式
if
(vdso32_syscall()) {
vsyscall = &vdso32_syscall_start;
vsyscall_len = &vdso32_syscall_end - &vdso32_syscall_start;
}
else
if
(vdso32_sysenter()){
vsyscall = &vdso32_sysenter_start;
vsyscall_len = &vdso32_sysenter_end - &vdso32_sysenter_start;
}
else
{
vsyscall = &vdso32_int80_start;
vsyscall_len = &vdso32_int80_end - &vdso32_int80_start;
}
//拷贝到对应的页
memcpy
(syscall_page, vsyscall, vsyscall_len);
//重定向。
relocate_vdso(syscall_page);
return
0;
}
|
接下来就是来看vdso32_sysenter_start到底是什么东西,它的定义是在arch/x86/vdso/vdso32.S中的。可以看到这里vdso32_sysenter_start代表的内容也就是vdso32-sysenter.so,也就是说上面代码就是拷贝vdso32-sysenter.so到对应的页。
1
2
|
vdso32_sysenter_start:
.incbin "arch/x86/vdso/vdso32-sysenter.so"
|
然后就是在fs/binfmt_elf.c文件的load_elf_binary函数中加载对应的vdso32-sysenter.so文件到内存,然后调用arch_setup_additional_pages将vsdo映射到用户空间,因此我们来看arch_setup_additional_pages这个函数,这个函数很简单就是映射上面copy的页的内容到用户空间。
这里有个需要注意的就是VDSO_HIGH_BASE这个值,其实我们上面拷贝完so之后会有一个重定向(relocate_vdso),这个重定向会将vdso的地址重定向到这里。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
|
int
arch_setup_additional_pages(
struct
linux_binprm *bprm,
int
uses_interp)
{
struct
mm_struct *mm = current->mm;
unsigned
long
addr;
int
ret = 0;
bool
compat;
if
(vdso_enabled == VDSO_DISABLED)
return
0;
down_write(&mm->mmap_sem);
/* Test compat mode once here, in case someone
changes it via sysctl */
compat = (vdso_enabled == VDSO_COMPAT);
map_compat_vdso(compat);
if
(compat)
addr = VDSO_HIGH_BASE;
else
{
addr = get_unmapped_area(NULL, 0, PAGE_SIZE, 0, 0);
if
(IS_ERR_VALUE(addr)) {
ret = addr;
goto
up_fail;
}
}
//设置vdso的地址为addr也就是我们前面设置的VDSO_HIGH_BASE
current->mm->context.vdso = (
void
*)addr;
if
(compat_uses_vma || !compat) {
/*
* MAYWRITE to allow gdb to COW and set breakpoints
*
* Make sure the vDSO gets into every core dump.
* Dumping its contents makes post-mortem fully
* interpretable later without matching up the same
* kernel and hardware config to see what PC values
* meant.
*/
ret = install_special_mapping(mm, addr, PAGE_SIZE,
VM_READ|VM_EXEC|
VM_MAYREAD|VM_MAYWRITE|VM_MAYEXEC|
VM_ALWAYSDUMP,
vdso32_pages);
if
(ret)
goto
up_fail;
}
current_thread_info()->sysenter_return =
VDSO32_SYMBOL(addr, SYSENTER_RETURN);
up_fail:
if
(ret)
current->mm->context.vdso = NULL;
up_write(&mm->mmap_sem);
return
ret;
}
|
而最关键的部分就是系统调用的实现部分是在arch/x86/vdso/vdso32/sysenter.S中的,也就是__kernel_vsyscall,linux会编译(可以看vdso下面的Makefile)它为一个so,然后供上面使用。
1
2
3
4
5
6
7
8
9
10
11
12
13
|
.globl __kernel_vsyscall
.type __kernel_vsyscall,@function
ALIGN
__kernel_vsyscall:
.LSTART_vsyscall:
push %ecx
.Lpush_ecx:
push %edx
.Lpush_edx:
push %ebp
.Lenter_kernel:
movl %esp,%ebp
sysenter
|
然后是arch/x86/vdso/vdso32/vdso32.ld.S中的也就是定义上面的__kernel_vsyscall为VDSO32_vsyscall这个名字,这里其实就是个别名了,到后面这个别名会用到,也就是在动态库中使用的就是VDSO32_vsyscall表示调用系统调用。
1
2
3
4
|
VDSO32_PRELINK = VDSO_PRELINK;
VDSO32_vsyscall = __kernel_vsyscall;
VDSO32_sigreturn = __kernel_sigreturn;
VDSO32_rt_sigreturn = __kernel_rt_sigreturn;
|
然后我们就来看内核和glibc库如何关联起来,这里关键也就是类型AT_SYSINFO对应的内容是什么,因此我们搜索内核代码,发现了下面这部分,这个宏也就是设置类型为AT_SYSINFO的内容以便与用户空间存取。
这里的原理是这样的,内核在装载镜像的时候会将这快(系统调用相关的)拷贝到用户空间,然后将对应的地址拷贝到ELF auxiliary vectors以供用户空间使用。
内核会将所需要的信息比如sysinfo地址放到ELF auxiliary vectors(一般来说都是键值对),然后用户空间就可以很简单的取到所需要的函数的地址,而这里NEW_AUX_ENT就是将类型地址的键值对放到ELF auxiliary vectors。
1
2
3
4
5
6
7
8
9
10
11
|
#define ARCH_DLINFO_IA32(vdso_enabled) \
do
{ \
if
(vdso_enabled) { \
NEW_AUX_ENT(AT_SYSINFO, VDSO_ENTRY); \
NEW_AUX_ENT(AT_SYSINFO_EHDR, VDSO_CURRENT_BASE); \
} \
}
while
(0)
#ifdef CONFIG_X86_32
//x86_32调用ARCH_DLINFO_IA32。
#define ARCH_DLINFO ARCH_DLINFO_IA32(vdso_enabled)
|
然后来看NEW_AUX_ENT是干吗的,这个宏主要是将对应的信息按照elf的格式进行设置。而它的定义的地方和ARCH_DLINFO调用的地方一致,那就是create_elf_fdpic_tables中。
可以看到NEW_AUX_ENT很简单,就是拷贝对应的值到用户空间的ELF auxiliary vectors。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
|
static
int
create_elf_fdpic_tables(
struct
linux_binprm *bprm,
struct
mm_struct *mm,
struct
elf_fdpic_params *exec_params,
struct
elf_fdpic_params *interp_params)
{
#define NEW_AUX_ENT(id, val) \
do
{ \
struct
{ unsigned
long
_id, _val; } __user *ent; \
\
ent = (
void
__user *) csp; \
//拷贝对应的id和value到用户空间.
__put_user((id), &ent[nr]._id); \
__put_user((val), &ent[nr]._val); \
nr++; \
}
while
(0)
...........................................
NEW_AUX_ENT(AT_EGID, (elf_addr_t) cred->egid);
NEW_AUX_ENT(AT_SECURE, security_bprm_secureexec(bprm));
NEW_AUX_ENT(AT_EXECFN, bprm->exec);
#ifdef ARCH_DLINFO
nr = 0;
csp -= AT_VECTOR_SIZE_ARCH * 2 *
sizeof
(unsigned
long
);
/* ARCH_DLINFO must come last so platform specific code can enforce
* special alignment requirements on the AUXV if necessary (eg. PPC).
*/
//调用ARCH_DLINFO完成sysinfo的拷贝
ARCH_DLINFO;
#endif
.....................................................................
|
最后我们就来看拷贝的是什么东西。可以看到上面的参数是AT_SYSINFO,VDSO_ENTRY第一个是id,第二个是VDSO_ENTRY,第一个我们知道就是glibc中的type,而第二个呢,来看内核的代码,其实很简单VDSO_ENTRY就是表示VDSO32_vsyscall这个符号的地址,而这个符号我们知道就是__kernel_vsyscall,也就是系统调用的实现函数。这下完全清楚了,那就是上面的glibc的ENTER_KERNEL最终调用的就是内核的__kernel_vsyscall。
1
2
3
4
5
6
7
8
|
#define VDSO_ENTRY \
((unsigned
long
)VDSO32_SYMBOL(VDSO_CURRENT_BASE, vsyscall))
#define VDSO32_SYMBOL(base, name) \
({ \
extern
const
char
VDSO32_##name[]; \
(
void
*)(VDSO32_##name - VDSO32_PRELINK + (unsigned
long
)(base)); \
})
|
总结一下,大体的过程是这样子的,内核在运行的时候会动态加载一个so到物理页,然后会将这个物理页映射到用户空间,并且会将里面的函数根据类型设置到ELF Auxiliary Vectors,然后glibc调用的时候就可以通过ELF Auxiliary Vectors来取得对应系统调用函数。