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这个宏。

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

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值