首页 > kernel > linux下系统调用的实现

linux下系统调用的实现

原创文章,转载请注明: 转载自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这个宏。

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打开的情况,也就是最常用的情况。

#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里面定义:

SYSINFO_OFFSET		offsetof (tcbhead_t, sysinfo)

下面就是 tcbhead_t的结构:

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进行初始化的。

# 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的注释写的很清楚:

/* 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

#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一个可执行文件,下面是我的机器上的情况:

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的内容拷贝到对应的页。

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到对应的页。

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的地址重定向到这里。

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,然后供上面使用。

	.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表示调用系统调用。

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。

#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。

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。

#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来取得对应系统调用函数。

Share
分类: kernel 标签: , ,
  1. Fu Haiping
    2010年12月11日12:45 | #1

    好文章!讲的清晰明了

    [回复]

  1. 本文目前尚无任何 trackbacks 和 pingbacks.