2.8 Linux系统调用

1、概述

系统调用是操作系统为在用户态运行的进程与硬件设备(如打印机和磁盘)进行交互提供的一组接口。用户程序可以通过这组“特殊”接口来获得操作系统内核提供的服务,例如用户可以通过文件系统相关的调用请求系统打开文件和关闭文件等。

系统调用的主要功能是使用户可以使用操作系统提供的有关设备管理、文件系统、进程控制、进程通讯以及存储管理方面的功能,而不必要了解操作系统的内部结构和有关硬件的细节问题,从而减轻用户负担和保护系统以及提高资源利用率。

系统调用的执行让用户程序从用户态切换到内核态,该切换动作由一条软中断指令或一条类似的指令完成,在用户态执行完系统提供的服务程序后切换回用户态,继续执行用户态程序。

1

2、X86切换到内核态的两种指令

本文只讲x86架构系统调用的实现。在x86架构中,由用户态切换到内核态有两种方法:一是通过软中断指令(int 0x80)实现;而是使用最新专有的系统调用指令sysenter/sysexit来实现的。Sysenter相对int 0x80做了优化,性能上有了成倍的增加。

下面是一些有关 sysenter/sysexit 指令和 INT n/IRET 指令在 Intel Pentium CPU 上的性能对比:

2

  • sysenter/sysexit 指令优化的详细说明:

在 Linux 2.4 内核中,用户态 Ring3 代码请求内核态 Ring0 代码完成某些功能是通过系统调用完成的,而系统调用的是通过软中断指令(int 0x80)实现的。在 x86 保护模式中,处理 INT 中断指令时,CPU 首先从中断描述表 IDT 取出对应的门描述符,判断门描述符的种类,然后检查门描述符的级别 DPL 和 INT 指令调用者的级别 CPL,当 CPL<=DPL 也就是说 INT 调用者级别高于描述符指定级别时,才能成功调用,最后再根据描述符的内容,进行压栈、跳转、权限级别提升。内核代码执行完毕之后,调用 IRET 指令返回,IRET 指令恢复用户栈,并跳转会低级别的代码。

其实,在发生系统调用,由 Ring3 进入 Ring0 的这个过程浪费了不少的 CPU 周期,例如,系统调用必然需要由 Ring3 进入 Ring0(由内核调用 INT 指令的方式除外,这多半属于 Hacker 的内核模块所为),权限提升之前和之后的级别是固定的,CPL 肯定是 3,而 INT 80 的 DPL 肯定也是 3,这样 CPU 检查门描述符的 DPL 和调用者的 CPL 就是完全没必要。正是由于如此,Intel x86 CPU 从 PII 300(Family 6,Model 3,Stepping 3)之后,开始支持新的系统调用指令 sysenter/sysexit。sysenter 指令用于由 Ring3 进入 Ring0,SYSEXIT 指令用于由 Ring0 返回 Ring3。由于没有特权级别检查的处理,也没有压栈的操作,所以执行速度比 INT n/IRET 快了不少。

AMD 的 CPU 支持一套与之对应的指令 SYSCALL/SYSRET。在纯 32 位的 AMD CPU 上,还没有支持 sysenter 指令,而在 AMD 推出的 AMD64 系列 CPU 上,处于某些模式的情况下,CPU 能够支持 sysenter/sysexit 指令。在 Linux 内核针对 AMD64 架构的代码中,采用的还是 SYSCALL/SYSRET 指令。至于这两种指令最终谁将成为标准,目前还无法得出结论。

值得一提的是,从 Windows XP 开始,Windows 的系统调用方式也从软中断 int 0x2e 转换到采用 sysenter 方式,由于完全不再支持 int 方式,因此 Windows XP 的对 CPU 的最低配置要求是 PentiumII 300MHz。

3、内核态的实现

操作系统提供的系统调用服务程序叫做system call service routine。一般你在用户态调用一个xxx()的系统服务,在内核态就有一个对应的sys_xxx()函数,该命名规则不是绝对的。内核态将所有的sys_xxx()函数指针组成一张表,然后会根据具体的系统调用号来查找使用这张表。
Linux内核中系统调用服务表叫做sys_call_table,定义在arch/i386/kernel/syscall_table.S中:

3

每一个系统调用服务函数,还有一个对应的系统调用号,和这个函数指针表一一对应,其在Include/asm-i386/unistd.h文件中定义:

4

3.1、Int 0x80模式

“Int 0x80”是软中断,该软中断的中断服务程序是system_call,定义在arch/i386/kernel/entry.S中:

5

既然是中断,还有中断向量的初始化,在arch/i386/kernel/traps.c的trap_init()中初始化Int 0x80的中断向量:

6

3.2、sysenter/sysexit模式

sysenter/sysexit模式的内核服务程序是sysenter_entry,定义在arch/i386/kernel/entry.S中:

7

4、用户态(glibc) 的实现

对用户态来说,一般不会使用“int 0x80”来直接调用内核态的系统调用,glibc函数把晦涩“int 0x80”指令包装起来,用户态程序调用的是glibc提供的api函数。
所以研究系统调用用户态的实现看glibc的实现即可。

4.1、只支持Int 0x80模式

在linux 2.4内核只采用int 0x80的系统调用方式,所以对应的glibc库也支持采用int 0x80的方式。这种情况下,glibc中定义了_syscall0 ~ _syscall6一共7个不同参数个数的宏以供定义用户态的系统调用函数使用。
在Include/asm-i386/unistd.h文件中,可以见到这些宏的定义:

8

_syscall0 ~ _syscall6宏,使用了gcc嵌入式汇编的方式,在c语言中调用汇编语句“int 0x80”进行系统调用。嵌入式汇编的一般形式是:asm volatile (“” : output : input : modify);具体的gcc嵌入式汇编的格式可以参见参考资料。

举例,Glibc库中用户态的系统函数调用可以封装成:

Int xxx(int arg1,int arg2)
{
   Int ret =0 ;

  Ret = _syscall2(int, xxx, int, arg1, int, arg2) ;

      Return (ret) ;
}

同时要保证内核中有相应的服务程序sys_xxx(),Include/asm-i386/unistd.h文件中有对应__NR_xxx系统调用号的定义。

4.2、兼容Int 0x80和sysenter/sysexit模式

在linux 2.6内核中加入对sysenter/sysexit模式的支持,同时支持Int 0x80和sysenter/sysexit模式的系统调用。

虽然sysenter/sysexit模式的效率高,但是并不是所有的CPU都支持该特性,老的CPU没有此特性。为了实现用户代码接口的统一,设计了一种新的机制:内核态提供给用户态一段通用代码,内核启动时根据 CPU 类型,决定这段代码采取哪种系统调用方式,对于用户态glibc 来说,无需考虑系统调用方式,直接调用这段入口代码,即可完成系统调用。这种方式可以叫做vsyscall模式。

前面说到内核态需要提供给用户态一段通用代码,这段代码的大小为1页内存,地址在0xffffe000开始到0xffffefff,称为vsyscall页。页中的内容是内核在启动时根据cpu的sysenter/sysexit能力,选择把int 80h模式的vsyscall代码或者sysenter模式的vsyscall代码复制到0xffffe000页中。

地址0xffffe000是属于内核虚地址空间,但是这部分的内核地址用户态是可以访问的。而且在使用ldd查看用户可执行程序的库依赖关系时,这段vsyscall代码被看成一个虚拟的库linux-gate.so.1。

9

int 80h模式的vsyscall代码,在arch/i386/kernel/ vsyscall_int80.S中:

10

sysenter模式的vsyscall代码,在arch/i386/kernel/vsyscall_sysenter.S中:

11

arch/i386/kernel/vsyscall-sysenter.S 和arch/i386/kernel/vsyscall-int80.S入口名都是 __kernel_vsyscall,这两个文件编译出的二进制代码由arch/i386/kernel/vsyscall.S所包含,并导出起始地址和 结束地址:

12

内核在启动时,调用sysenter_setup 函数设置vsyscall页,定义在arch/i386/kernel /sysenter.c:

13

配合vsyscall模式,新版本glibc中系统调用的宏是INLINE_SYSCALL,在sysdeps/unix/sysv/linux/i386/sysdep.h中定义:

14

举例,Glibc库中用户态的系统函数fork的实现:

15

4.3、内核和glibc的新旧版本兼容关系

在支持系统调用的模式上:

  • Cpu:分为支持的和不支持sysenter指令的;
  • 内核 :分为原始模式的和 支持vsyscall模式的。两种模式对int 0x80指令都支持;vsysvall模式的内核需要配合vsyscall模式的库自动适配cpu的sysenter能力;
  • Glibc库:分为int 80模式的和使用vsyscall模式。int 80模式的库在两种内核下都可以执行;vsyscall模式的库需要配合vsyscall模式的内核才能正常使用sysenter,否则使用int 80方法。

5、32位程序在64位内核的系统调用

16

64位内核的系统调用关系图
由上图可见,在32位应用程序运行在64位的内核上,其调用路径为左边黄色色箭头部分,而64位应用程序的系统调用走的是右边绿色箭头。
所以在32位应用程序的系统调用就必须考虑在 Ia32entry.S中是否有对应的系统调用入口,从目前的代码分析看,只有ioctl接口有差别,其他的接口需要再深入排查。
可以查看arch/x86_64/kernel/entry.S和arch/x86_64/ia32/ia32entry.S中的实际代码。感兴趣自己再研究一下64位系统下的系统调用机制。

6、举例:增加一条自己的系统调用

  • 1、在内核代码中增加自己函数,并编译进内核:
asmlinkage int sys_mysyscall(void)
{
        current->uid = current->euid = current->suid = current->fsuid = 0;
        return 0;
}  
  • 2、在arch/i386/kernel/syscall_table.S中增加自己调用入口:
.long sys_mysyscall
  • 3、在Include/asm-i386/unistd.h文件中增加自己的系统调用号,并修改NR_syscalls:
#define __NR_ mysyscall     xxx

#define NR_syscalls (xxx+1)
  • 4、重新编译内核

  • 5、编写用户态程序,调用自己新增的系统调用:

#include <linux/unistd.h>
_syscall0(int,mysyscall)    /* 注意这里没有分号 */

int main()
{
mysyscall();
printf(“em…, this is my uid: %d. \n”, getuid());
}

7、参考资料

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值