Linuxer.
全部博文(199)
分类: LINUX
2014-04-30 15:16:26
Linux VDSO机制介绍
系统调用
当用户态的进程调用一个系统调用时,cpu从用户态切换到内核态开始执行一个内核函数。对于X86架构来说,有两种不同的方式调用系统调用:
(1)执行int $0x80(可编程异常)汇编语言指令。在Linux内核的老版本中,这是从用户态切换到内核态的唯一方式。执行iret汇编指令返回到用户态。
(2)执行sysenter汇编语言指令。在Intel Pentium II微处理器芯片中引入了这条指令,现在Linux 2.6内核支持这条指令。执行sysexit汇编指令返回到用户态。
通过int 0x80系统调用的实现方式:
在内核初始化期间,会调用中断初始化函数trap_init()对0x80设置中断描述符表项。即在此函数中设置系统的IDT表项,而0x80作为IDT中的一个特殊的可编程异常,必然需要进行设置中断描述符表项。
#define SYSCALL_VECTOR 0x80
void (void)
{
……
set_system_trap_gate(SYSCALL_VECTOR,&system_call);
……
}
因此,当用户态发出int $0x80指令时,cpu切换到内核态并开始从地址system_call处开始执行指令。
ENTRY(system_call)
RING0_INT_FRAME # can't unwind into user space anyway
pushl_cfi %eax //该条指令是对系统调用号留一个副本,以防需要的时候使用
SAVE_ALL //所有的cpu寄存器保存到栈中
//检查thread_info结构中的flag字段是否有调试跟踪的情况,如果有则跳到syscall_trace_entry处
GET_THREAD_INFO(%ebp)
testl $_TIF_WORK_SYSCALL_ENTRY,TI_flags(%ebp)
jnz syscall_trace_entry
//安全性检查,eax保存的系统调用号不能超过nr_syscalls(系统调用分派表中的表项数),否则出错
cmpl $(nr_syscalls), %eax
jae syscall_badsys
syscall_call:
//该语句的执行就是调用系统调用的服务例程,服务例程的地址所在的位置 = sys_call_table + eax * 4,从这个地址单元获取指向服务例程的指针。eax 中是系统调用号。每个系统调用号占用4个字节。
call *sys_call_table(,%eax,4)
//行完服务例程后返回的结果存放在eax中,这时候将返回的结果压栈到指定的位置。
movl %eax,PT_EAX(%esp)
//系统调用地址表
ENTRY(sys_call_table)
.long sys_restart_syscall
.long sys_exit
.long ptregs_fork
.long sys_read
.long sys_write
.long sys_open
.long sys_close
.long sys_waitpid
.long sys_creat
.long sys_link
……
通过sysenter系统调用的实现方式:
由于int 0x80还是属于系统异常的中断机制,每次触发都要走一边linux的中断路径,并且还要进行一些一致性和安全性的检查,所以导致系统调用很慢。而之后intel提供了一种叫做快速系统调用的sysenter指令。
1.汇编语言指令sysenter使用三种特殊的寄存器:
(1) SYSENTER_CS_MSP:内核代码段的段选择符
(2) SYSENTER_EIP_MSR:内核入口点的线性地址
(3) SYSENTER_ESP_MSR:内核堆栈指针
2.切换到内核态并开始执行内核入口点的第一条指令,即SYSENTER_EIP_MSR寄存器保存的地址。即转去处的汇编语句。执行内核初始化期间,cpu调用enable_sep_cpu()来初始化这三个特殊寄存器。
void (void)
{
int = ();
struct *tss = &(init_tss, );
//是否sysenter/sysexit支持
if (!()) {
();
return;
}
tss->x86_tss.ss1 = ;
tss->x86_tss.sp1 = sizeof(struct ) + (unsigned long) tss;
//为写MSR寄存器,设置MSR寄存器
(, , 0);
(, tss->x86_tss.sp1, 0);
(, (unsigned long) , 0);
();
}
3.只要cpu和Linux内核都支持sysenter指令,标准库libc中的封装函数就可以使用它。
在初始化阶段,sysenter_setup()函数建立一个称为vsyscall页的页框,并把它的物理地址与FIX_VSYSCALL固定映射的线性地址相关联。然后把预先定义好的一个或两个ELF共享对象拷贝到该页中。
int (void)
{
//分配一页空页,返回页框的线性地址
void *syscall_page = (void *)();
const void *vsyscall;
vsyscall_len;
//从一个内核虚地址得到该页的描述结构 struct page*
[0] = (syscall_page);
//如果cpu支持syscall,__kernel_vsyscall使用syscall进行系统调用;
//如果cpu不支持syscall,但支持sysenter,__kernel_vsyscall使用sysenter进行系统调用;
//如果cpu不支持syscall和sysenter,__kernel_vsyscall使用int 80进行系统调用。
if (()) {
vsyscall = &;
vsyscall_len = & - &;
} else if (()){//cpu支持sysenter指令
vsyscall = &;
vsyscall_len = & - &;
} else {//支持老的0x80
vsyscall = &;
vsyscall_len = & - &;
}
//填充该vsyscall页
(syscall_page, vsyscall, vsyscall_len);
//ELF文件重定位
(syscall_page);
return 0;
}
#define () (())
#define () (())
根据cpu支持哪种方式的系统调用,起始地址vsyscall被设为vdso32_syscall_start,vdso32_sysenter_start或vdso32_int80_start。
下面看看起始地址vdso32_syscall_start,vdso32_sysenter_start和vdso32_int80_start保存的是什么内容。
这3个起始地址保存的都是elf共享库,定义在arch\x86\vdso\vdso32.s。
.globl vdso32_int80_start, vdso32_int80_end
vdso32_int80_start:
.incbin "arch/x86/vdso/vdso32-int80.so"
vdso32_int80_end:
.globl vdso32_syscall_start, vdso32_syscall_end
vdso32_syscall_start:
#ifdef CONFIG_COMPAT
.incbin "arch/x86/vdso/vdso32-syscall.so"
#endif
vdso32_syscall_end:
.globl vdso32_sysenter_start, vdso32_sysenter_end
vdso32_sysenter_start:
.incbin "arch/x86/vdso/vdso32-sysenter.so"
vdso32_sysenter_end:
这样syscall_page页中保存的内容为vdso32-sysenter.so
源代码中并不存在这个文件,只是在编译内核时自动生成该库文件,将/////目录下的一些汇编文件编译成vdso32-sysenter.so。
vdso32-sysenter.so的源文件是arch\x86\vdso\vdso32\sysenter.s
__kernel_vsyscall:
push %ecx
push %edx
push %ebp
movl %esp,%ebp
sysenter
pop %ebp
pop %edx
pop %ecx
ret
4. sysenter指令步骤
当Glibc调用系统调用时,标准库中的封装例程把系统调用号装入eax寄存器,并调用__kernel_vsyscall()函数。函数__kernel_vsyscall()把ebx、edx和ecx的内容保存到用户态堆栈中,把用户栈指针拷贝到ebx中,然后执行sysenter指令。
执行sysenter指令时,cpu控制单元:
(1)把SYSENTER_CS_MSP的内容拷贝到cs。
(2)把SYSENTER_EIP_MSR的内容拷贝到eip
(3)把SYSENTER_ESP_MSR的内容拷贝到esp。
(4)把SYSENTER_CS_MSR加8的值装入ss。
(5)Cpu从用户态切换到内核态,内核开始执行函数(由SYSENTER_EIP_MSR寄存器指向)。
(5) ia32_sysenter_target ()汇编语言函数执行下述步骤:
//. /arch/x86/kernel/entry_32.S
ENTRY(ia32_sysenter_target)
……
pushl_cfi %eax
SAVE_ALL
……
sysenter_do_call:
//检查系统调用号
cmpl $(NR_syscalls), %eax
jae syscall_badsys
//该语句的执行就是调用系统调用的服务例程,服务例程的地址所在的位置 = sys_call_table + eax * 4,从这个地址单元获取指向服务例程的指针。eax 中是系统调用号。每个系统调用号占用4个字节。
call *sys_call_table(,%eax,4)
……
VDSO原理:
1.如果使用ldd来获取一个可执行文件的共享库的依赖情况,会发现:
ldd /bin/sh
linux-vdso.so.1 => (0x00007fffbb5ff000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007effb85f7000)
/lib64/ld-linux-x86-64.so.2 (0x00007effb89cf000)
我们可以看到linux-vdso.so.1,没有与任何实际的文件相对应。这个就是linux用于支持新型系统调用的“虚拟”共享库。linux-vdso.so.1并不存在实际的文件,它只是操作系统生成的一个虚拟动态共享库(Virtual Dynamic Shared Library,VDSO)。因为该共享库不是以一个文件的格式(属于elf文件格式)存在于文件系统中,而是由内核提供的,对用户进程而言是虚拟的。
2. 从第三部分可以看到Linux-vdso.so.1库中包含的内容,该页面中包含共享库的内容依赖于处理器是否支持快速系统调用指令,在处理器支持快速系统调用指令的情况下,共享库为vdso32-sysenter.so;否则,共享库为vdso32-int80.so。这样内核为用户空间提供了一个统一接口的共享库,虽然库函数的实现依赖于处理器,但提供的接口是一样的。用户空间可以通过统一的接口来访问系统调用,而不必关心系统调用是用软中断还是用快速系统调用指令实现。
3.然后在用户运行新的进程时,系统会将该共享库Linux-vdso.so.1映射到进程空间中,不同的进程映射到进程空间的线性地址是不同的,所以可以看到VDSO在不同的进程地址空间中有不同的线性地址。当有系统调用触发时就会调用VDSO页面中的__kernel_vsyscall处的汇编语句,进而出发sysenter指令进入系统调用。
4.在新进程执行的时候,VDSO映射到进程虚拟空间的映射过程:do_execve()?()?()
//
int (struct *bprm, int uses_interp)
{
struct *mm = current->mm;
unsigned long ;
int = 0;
;
if ( == VDSO_DISABLED)
return 0;
(&mm->mmap_sem);
/* Test compat mode once here, in case someone
changes it via sysctl */
= ( == VDSO_COMPAT);
();
//下面是把页vdso32_pages映射到进程的线性地址空间。
//有两种映射方式,一种是COMPAT方式,一种是非COMPAT方式。
(1)采用COMPAT方式,把页vdso32_pages固定映射到线性地址VDSO_HIGH_BASE (即0xfff01000)。 (2)采用非COMPAT方式,通过get_unmapped_area()找到尚未映射的线性地址,通过函数 install_special_mapping()把页vdso32_pages映射到该线性地址。
if ()
= ;
else {
= (, 0, , 0, 0);
if (()) {
= ;
goto up_fail;
}
}
//进程描述符的mm->context.vdso指向页vdso32_pages映射的线性地址。
current->mm->.vdso = (void *);
if ( || !) {
= (mm, , ,
|| ||,);
if ()
goto up_fail;
}
//将线程描述符的sysenter_return指向vsyscall页的标号VDSO32_SYSENTER_RETURN,这是用sysenter指令进入系统调用,从系统调用退出执行的起始地址。
current_thread_info()->sysenter_return =
(, SYSENTER_RETURN);
up_fail:
if ()
current->mm->.vdso = ;
(&mm->mmap_sem);
return ;
}