系统调用是应用程序与操作系统间的接口。Linux 下使用 0x80 号中断作为系统调用入口,使用 eax寄存器指定系统调用号,ebx、ecx、edx、esi、edi 和 ebp 用于传递调用参数;Windows 下使用0x2E 号中断作为系统调用入口。
直接使用系统调用编程有以下弊端:1)系统调用接口过于原始,使用不方便;2)各操作系统间系统调用不兼容。因此,运行库作为操作系统与应用程序间的抽象层可实现源码级的可移植性。
0x01 Linux 经典系统调用
现代操作系统中有用户模式和内核模式两种特权模式。操作系统通过中断从用户态切换到内核态。不同中断具有不同的中断号,一个中断号对应一个中断处理程序。内核中使用中断向量表存放中断处理程序的指针。
操作系统使用一个中断号对应所有的系统调用,如 Linux 下的 0x80 为中断处理程序 system_call 的中断号。不同系统调用函数通过 eax 寄存器传递系统调用号指定。Linux经典系统调用实现如下:
1) 触发中断
使用 int 0x80 触发系统调用中断。
2) 切换堆栈
- 从用户态切换到内核态时程序的当前栈也要从用户栈切换到内核栈。具体过程为:
- 将用户态的寄存器 SS、ESP、EFLAGS、CS 和 EIP 压入内核栈;
- 将 SS、ESP 设置为内核栈的相应值。
当从内核态回到用户态时则进行相反的操作。
3) 中断处理程序
int 0x80 切换了栈之后进入中断处理程序 system_call 进行系统调用。
0x02 Linux 快速系统调用机制
vsyscall 和 vdso 是用于在 Linux 中加速某些系统调用的两种机制。vsyscall 是早期的加速方式,它将部分内核代码放在vsyscall 区域。使得用户态程序可以直接调用简单的系统调用,比如 gettimeofday() 。该方式的问题是 vsyscall 的地址在内存空间中是固定的,并不能被地址随机化。vdso 与 vsyscall 的功能相同,其区别在于 vdso 地址可以被 ASLR 随机化。
vdso 是将部分内核调用映射到用户态的地址空间中,使得调用开销更小。由于使用 sysenter/sysexit 没有特权级别检查的处理,也就没有压栈操作,所以执行速度比 int n/iret 快了不少。
Linux 2.5 之后的版本通过虚拟共享库(Virtual Dynamic Shared Object,vdso)支持 sysenter/sysexit。vsdo 不存在实际的文件,只存在于进程虚拟地址空间中。新版本的 vdso 为 linux-vdso.so.1,而在旧版本系统中为 linux-gate.so.1。 该虚拟库为用户程序以处理器可支持的最快的方式调用系统函数提供了必要的逻辑。vsdo 中导出了一系列函数,其中 __kernel_vsyscall
函数负责系统调用。该函数通过 sysenter 进行系统调用。
系统调用多被封装成库函数提供给应用程序调用,应用程序调用库函数后,由 glibc 库负责进入内核调用系统调用函数。在 2.4 内核加上旧版的 glibc 的情况下,库函数通过 int 指令来完成系统调用,而内核提供的系统调用接口很简单,只要在 IDT 中提供 int0x80 的入口,库就可以完成中断调用。
在 2.6 内核中,内核代码同时包含了对 int 0x80 中断方式和 sysenter 指令方式调用的支持,因此内核会给用户空间提供一段入口代码,内核启动时根据 CPU 类型,决定这段代码采取哪种系统调用方式。对于 glibc 来说,无需考虑系统调用方式,直接调用这段入口代码,即可完成系统调用。
系统调用会有两种方式,在静态链接(gcc -static)时,采用 call *_dl_sysinfo
指令;在动态链接时,采用 call *gs:0x10
指令。用以下示例程序说明这两种情况;
1. 静态链接
首先编译生成静态链接可执行文件,接着使用 gdb 加载,并反编译 main 函数。main 函数中调用 getuid。
反编译 getuid 函数,可看到它通过 eax 传入中断号 0xC7,并调用 ds:0x80ea9f0
。ds:0x80ea9f0
内存处的值指向 _dl_sysinfo
函数,并不是内核映射页面的代码。
运行程序,再次查看 ds:0x80ea9f0
的值,此时为内核函数__kernel_vsyscall
函数的地址,该函数中通过 sysenter 进行系统调用。
查看该进程的虚拟内存空间,可看到 __kernel_vsyscall
函数在 vdso 区域。
2. 动态链接
使用以下命令编译动态链接可执行文件,并使用 gdb 加载程序。
运行程序后查看 main 函数和 getuid 函数的指令如下,getuid 函数中使用 eax 传入系统调用号,并通过 gs: 010
进行系统调用。
References:
[1] Linux 2.6 对新型 CPU 快速系统调用的支持
[2] 《程序员的自我修养》
[3] linux下系统调用的实现