基本的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
14
15
16
17
18
19
20
21
22
  
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
10
11
12
13
14
15
16
17
18
  
#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
2
  
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
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
  
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
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
  
\# 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
5
6
7
8
  
/* 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
13
14
15
16
17
18
19
20
21
22
23
24
  
#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
15
16
17
18
19
20
21
22
23
24
25
26
27
28
  
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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
  
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
3
4
  
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
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
  
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
14
15
16
17
18
19
20
21
22
23
24
25
26
	  
.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
5
6
7
8
  
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
12
13
14
15
16
17
18
19
20
  
#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
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
  
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
9
10
11
12
13
14
  
#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来取得对应系统调用函数。