linux系统调用以及vsyscall、vdso

系统调用

现代操作系统的进程空间分为用户空间(user space)与内核空间(kernel space)。通常程序运行在用户空间中,当涉及一些敏感指令执行的时候,比如与硬件交互的操作,需要切换到内核空间,相关指令执行完毕后再返回用户空间继续执行。

系统调用(syscall)在此过程中作为沟通用户空间与内核空间的桥梁存在。

x86 架构,CPU有特权等级(privilege level, or rings)的概念,它分为 0-3 4 个等级。Linux 下用户空间代码运行在 ring 3,内核空间代码运行在 ring 0,ring 3 与 ring 0 的互相切换便是通过系统调用进行的

涉及到的 CPU 指令,x86 下面有三对: 

传统系统调用(int 0x80) 通过中断/异常实现,在执行 int 指令时,发生 trap。硬件找到在中断描述符表中的表项,在自动切换到内核栈 (tss.ss0 : tss.esp0) 后根据中断描述符的 segment selector 在 GDT / LDT 中找到对应的段描述符,从段描述符拿到段的基址,加载到 cs ,将 offset 加载到 eip。最后硬件将 ss / sp / eflags / cs / ip / error code 依次压到内核栈。返回时,iret 将先前压栈的 ss / sp / eflags / cs / ip 弹出,恢复用户态调用时的寄存器上下文。

sysenter 和 syscall 是为了加速系统调用所引入的新指令,通过引入新的 MSR寄存器来存放内核态的代码和栈的段号和偏移量,从而实现快速跳转:  

在调用 sysenter 时将 SYSENTER_CS_MSR 加载到 cs,将 SYSENTER_CS_MSR + 8 加载到 ss,将 IA32_SYSENTER_EIP 加载到 eip ,将 IA32_SYSENTER_ESP 加载到 esp ,整套切换到内核态。返回时,sysexit 将 IA32_SYSENTER_CS + 16 加载到 cs ,将 IA32_SYSENTER_CS + 24 加载到 cs ,而 eip 和 esp 分别从 edx 和 ecx 中加载,因此返回前应该将压栈的用户态 eip(计算出来的) 和 esp(调用前用户态保存到 ebp 进行传递) 设置到这两个寄存器中。 

在调用 syscall 时,会自动将 rip 保存到 rcx ,然后将 IA32_LSTAR 加载到 rip 。同时将 IA32_STAR[47:32] 加载到 cs ,IA32_STAR[47:32] + 8 加载到 ss 。栈顶指针的切换会延迟到内核态系统调用入口点 entry_SYSCALL_64 后进行处理,将用户态栈偏移 rsp 存到 per-cpu 变量 rsp_scratch 中,然后将 per-cpu 变量 cpu_current_top_of_stack ,即内核态的栈偏移加载到 rsp。返回时,sysret 将 IA32_STAR[63:48] 加载到 cs ,IA32_STAR[63:48] + 8 加载到 ss ,而 rip 从 rcx 中加载,因此返回前应该将压栈的用户态 rip 设置到 rcx 中。对于 rsp ,返回前根据先前压栈内容先设置为用户态 rsp。


总结

int 0x80要走复杂的中断流程,所以开销更大,快速系统调用通过MSR寄存器寄存器跳转进入调用例程,开销省了很多



vsyscall与vdso

vdso的出现是为了给外部比如glibc一个统一的系统调用接口,自动判断该使用 int 80还是比如sysenter、syscall 等,同时也为了加速部分系统调用比如time、getcpu等,直接在用户空间完成

当我们查看一个程序的/proc/pid/maps

[root@dldl ccc]# cat /proc/11327/maps
00400000-00401000 r-xp 00000000 08:02 201782296                          /backup/elf_write/tmp/a.out
00600000-00601000 r--p 00000000 08:02 201782296                          /backup/elf_write/tmp/a.out
00601000-00602000 rw-p 00001000 08:02 201782296                          /backup/elf_write/tmp/a.out
......
7f9d86443000-7f9d865fd000 r-xp 00000000 08:02 123814                     /usr/lib64/libc-2.28.so
7f9d865fd000-7f9d867fd000 ---p 001ba000 08:02 123814                     /usr/lib64/libc-2.28.so
.....
7ffd2bec4000-7ffd2bec7000 r--p 00000000 00:00 0                          [vvar]
7ffd2bec7000-7ffd2bec9000 r-xp 00000000 00:00 0                          [vdso]
ffffffffff600000-ffffffffff601000 r-xp 00000000 00:00 0                  [vsyscall]

除了libc库和ld等,还出现了 vvar vdso vsyscall 等,以下是对这些的总结

vsyscall(virtual system call)

提供了一种在用户空间下快速执行系统调用的方法,加速原理是对特定的系统调用使用函数调用代替

map 的起始地址固定(0xffffffffff600000),地址固定不变,有安全风险

vdso

vdso是用来代替 vsyscall 的,vsyscall之所以还存在,是为了兼容性考虑

vdso其实是一个动态库,它由内核提供,映射到每个进程的地址空间

vdso利用 ASLR(address space layout randomization)增强安全性,也就是每次程序运行,vdso的虚拟地址都会变化,有时候为了调试,可以关闭随机地址

#关闭地址随机
echo 0 > /proc/sys/kernel/randomize_va_space

我们通过gdb把一个运行程序的vdso内存内容导出到文件,其实就是一个动态链接文件,然后通过readelf -s 查看的

[root@dldl lib_test]# gdb -q ls
(gdb) b _start
Breakpoint 1 at 0x5e00
(gdb) r
(gdb) info program
    Using the running image of child Thread 0x7ffff7fde640 (LWP 11219).
Program stopped at 0x555555559e00.
It stopped at breakpoint 1.
Type "info stack" or "info registers" for more information.
(gdb) shell cat /proc/11219/maps | grep vdso
7ffff7ffa000-7ffff7ffc000 r-xp 00000000 00:00 0                          [vdso]
(gdb) dump memory /tmp/vdso.so 0x7ffff7ffa000 0x7ffff7ffc000
[root@dldl lib_test]# readelf -s /tmp/vdso.so 

Symbol table '.dynsym' contains 10 entries:
   Num:    Value          Size Type    Bind   Vis      Ndx Name
     0: 0000000000000000     0 NOTYPE  LOCAL  DEFAULT  UND 
     1: 00000000000009f0   801 FUNC    WEAK   DEFAULT   12 clock_gettime@@LINUX_2.6
     2: 0000000000000d20   421 FUNC    GLOBAL DEFAULT   12 __vdso_gettimeofday@@LINUX_2.6
     3: 0000000000000d20   421 FUNC    WEAK   DEFAULT   12 gettimeofday@@LINUX_2.6
     4: 0000000000000ed0    16 FUNC    GLOBAL DEFAULT   12 __vdso_time@@LINUX_2.6
     5: 0000000000000ed0    16 FUNC    WEAK   DEFAULT   12 time@@LINUX_2.6
     6: 00000000000009f0   801 FUNC    GLOBAL DEFAULT   12 __vdso_clock_gettime@@LINUX_2.6
     7: 0000000000000000     0 OBJECT  GLOBAL DEFAULT  ABS LINUX_2.6
     8: 0000000000000ee0    42 FUNC    GLOBAL DEFAULT   12 __vdso_getcpu@@LINUX_2.6
     9: 0000000000000ee0    42 FUNC    WEAK   DEFAULT   12 getcpu@@LINUX_2.6

从上面输出可以看出,获取当前时间,cpuid等都不必调用系统调用切换到内核态,直接在用户空间调用vdso的函数就可以获取

实现的原理是当前时间和cpuid都存放在内核中,而vdso会把这些数据映射到用户空间(即vvar),然后设置权限为只读,那么用户就可以随意读取这些内核空间的数据,只是不能修改,这样大大提高了性能

总结:


参考:

x86 架构下 Linux 的系统调用与 vsyscall, vDSO

Linux系统调用过程分析

[译] Linux 系统调用权威指南(2016)

System calls in the Linux kernel. Part 1


上一篇: 让动态库可以执行
下一篇: 无
作者邮箱: 203328517@qq.com