x86体系
在x86,使用int 0x80来实现系统调用。现在使用syscall和sysentre指令。一般使用sysenter指令,不过实现差不多,内核使用了vdso来兼容所有指令,接下来详细分析如何实现vdso层,以及glibc库调用vdso层的接口。
(VDSOs (Virtual Dynamically-linked Shared Objects) are a way to export kernel space routines to user space applications, using standard mechanisms for linking and loading (i.e. standard ELF format).详见wikipedia
看glibc的代码,/sysdeps/unix/sysv/linux/i386/syscall.S.
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. 系统调用,在分析wget时,也见到这个东东了,就是进入内核。*/ */
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)
通过vdso调用内核对应接口,达到执行系统调用的作用。
下面通过宏定义i386_USE_SYSENTER来决定是否使用快速系统调用,他通过makefile中参数进行控制。
#ifdef I386_USE_SYSENTER /*这里在wget分析19中见到,如果没有定义,就使用int 0x80进入内核,
如果通过快速系统调用通过宏定义决定使用哪种方式来获得vdso的页地址(内核实现系统调用的页),shared打开是
很常用的情况。
# 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是在/nppt/sysdeps/i386/tcb-offsets.sym里面定义的:
SYSINFO_OFFSET offset(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;
uintptr_t sysinfo; 偏移值就是0x10,就是tcbhead_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;
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函数作用:
Call the OS-dependent function to set up file 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 the unwind our frame and run the user entry point on the same stack we entered on
翻译:调用系统依赖的函数来初始化文件所以我们可以像文件访问一样做事。它会调用dl_main来作动态调用
函数所作的实际工作,放松框架和在我们运行点和进入时在相同的堆栈。
它根据函数的类型来进行不同的操作。这里的sysinfo类型是AT_SYSINFO。这里一般说取的是ELF auxiliary
vectors的值,就是说把相关信息放在ELF auxiliary vectors中。
************** 见博客ELF 程序翻译。********************
#define AT_SYSINFO 32
#ifdef NEED_DL_SYSINFO
case AT_SYSINFO: new_sysinfo = auxv->a_un.a_val; /*这个是辅助向量中的那个u32val值。这里由于类型是系统调用,所以使用的是它对应的偏移地址*/
break;
#endif ..................................................
#if defined NEED_DL_SYSINFO /* Only set the sysinfo value if we also have the vsyscall DSO. */ if (GLRO(dl_sysinfo_dso) != new_sysinfo)
GLRO(dl_sysinfo) = new_sysinfo;
#endif
接下来是内核了。
我们先来了解下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将它作为动态库来处理,此时用户空间就很容易来执行这块内存的代码。
在内核初始化的时候,内核判断是否支持快速系统系统调用,如果支持的话则会将快速中断相关代码
拷贝到mmap的内存中,否则就拷贝软中断指令。
接下来是arch/x86/vdso/vdso32-setup.c的sysenter_setup函数
函数判断支持哪些指令,然后做不同处理,最优处理的是syscall,然后是sysenter,最后是int 0x80.
这里主要看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; /*将地址赋给vsycall,拷贝在后面*/
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); //这里是将是将整个vdso32-sysenter.so连接文件的文件拷贝到系统调用页面*/
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映射到用户空间,因此我们来看rch_setup_additional_pages这个函数,这个函数很简单就是映射上面copy的页的内容到用户空间。
/*完成用户空间的映射应该是最终调用mmap()函数*/
这里有个需要注意的就是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)
{
ret = install_special_mapping(mm, addr, PAGE_SIZE, VM_READ|VM_EXEC| M_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。
/*下面就是初始化这个auxv的type和val值,val是系统调用的地址(在用户空间的)*/
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来取得对应系统调用函数。
系统调用是运行在内核空间的,调用通过vdso这个环境。一开始linux装载和系统相关的调用代码(当然不是系统调用的这个函数).so文件装载到一个相应的物理页面,由内核分配的。然后将这个.so文件对映设置到辅助向量中,此辅助向量在线程堆栈中,这样线程可以通过访问辅助向量来访问这些调用函数(在别的虚拟地址空间)。就是这样一个过程。vdso就是提供了一个动态加载的环境,在内核中就是直接调用int 0x80这个调用函数也是在另一个空间也是通过辅助向量访问的。