luochicun Web安全 2019年9月7日发布
收藏
本文,我们将讨论在x86上实现用户到内核转换的许多方法,即系统调用。让我们首先快速回顾一下实际需要完成哪些系统调用。
在现代操作系统中,用户模式(执行普通应用程序代码)和内核模式(能够接触系统配置和设备)是有区别的。系统调用是应用程序从操作系统内核请求服务并弥合它们之间差距的一种方法。为了实现这一点,CPU需要为应用程序提供一种机制,使其能够安全地从用户模式转换到内核模式。
所以在这样的运行环境中,安全是最重要的,意味着应用程序不能跳转到任意的内核代码。否则,这将允许应用程序在系统上做它想做的任何事情,留下安全隐患。内核必须能够配置定义的入口点,而处理器的系统调用机制必须强制执行这些入口点。在处理系统调用之后,操作系统还需要知道应用程序的返回位置,因此系统调用机制也必须提供此信息。
所以,我提出了四种符合我以上描述的运行机制,这些机制都适用于64位环境,如果你想了解在32位环境下的运行机制,请点此单独了解。
1.程序使用int指令中断;
2. 调用门;
3.使用sysenter/sysexit进行快速系统调用;
4.使用syscall/sysret进行快速系统调用;
程序中断是最古老的机制,其关键思想是使用与硬件中断相同的方法进入内核。本质上,它仍然是1982年在286上以保护模式引入的机制,但是,即使是早期的cpu也已经有了这个机制。
因为中断向量0x80仍然可以用于在64位Linux上进行系统调用,所以我们将继续使用这个例子:
处理器通过从int指令中获取中断向量号并在中断描述符表(IDT)中查找相应的描述符来找到内核入口地址。这个描述符将是中断门(Interrupt gate)描述符和陷阱门(Trap gate)描述符,它包含指向内核中处理函数的指针。
使用中断门(Interrupt gate)描述符和陷阱门(Trap gate)描述符的不同权限级别之间的这些类型的转换也会导致处理器切换堆栈。内核权限级别的堆栈指针保存在任务状态段(TSS)中,TSS 全称task state segment,是指在操作系统进程管理的过程中,任务(进程)切换时的任务现场信息。
切换到新的堆栈之后,处理器将返回地址和用户的堆栈指针推送到内核堆栈上。然后,内核中的一个典型处理程序例程将继续推送堆栈上的通用寄存器,并保存它们。在此过程中,在堆栈上创建的数据结构称为中断帧。
要返回到用户空间,内核在恢复通用寄存器之后执行iret(中断返回)指令。iret恢复用户的堆栈,并在最初输入内核的int指令之后继续执行。尽管这里的描述很简短,但iret是x86指令集中最复杂的指令之一。
现在,说说我的第二种机制,调用门非常类似于程序中断机制。某种程度上,调用门在某种程度上是实现系统调用的正式方法,但是我知道除了恶意程序之外,调用门没有其他用途。
我在这里强调了程序中断流和调用门遍历之间的区别:
用户通过执行远端调用来启动系统调用,而不是int。远程调用来自x86分段内存模型的遗留部分,在这个模型中,调用指令不仅指定要去的指令指针,而且还引用指令指针相对于使用选择器的内存段(在本例中是0x18)。
处理器在全局描述符表中查找对应的段,在我们的例子中,找到的是调用门而不是普通段。与中断门一样,调用门指定内核中的指令指针。在本例中,处理器会忽略调用指令提供的指令指针。其余的工作类似于程序中断的情况,不过,由于硬件创建了不同的堆栈帧布局,内核必须使用不同的指令来返回路径。
正如你将在下面看到的,这两种内核入口方法都很慢。在对中断门和调用门遍历时,处理器会从GDT重新加载代码和堆栈段寄存器。可想而知,每个描述符的负载是多么的大,因为处理器必须先解码相当混乱的数据结构并执行许多检查。
在现代操作系统中,许多检查都是毫无意义的。比如,分段内存模型提供的特性及其支持的分层保护域均不需要使用。每个段都有一个零基数和最大大小,而不是不相交的内存段。通过分页实现保护,保护环仅用于实现内核和用户模式。保护环是一种用来在发生故障时保护数据和功能(提升容错度)和避免恶意操作 (提升计算机安全)的设计方式
这个问题的解决方案来自于Intel和AMD: sysenter/sysexit是在Intel上实现快速系统调用的指令对,他们在1997年推出了奔腾II。AMD在1998年推出了与K6-2类似但不兼容的指令对syscall/sysret。
这两个指令对的工作原理基本相同,不需要查询内存中的描述符表来决定做什么,大部分功能都是硬编码的,未使用的灵活性也会丢失:sysenter和syscall采用平面内存模型,并加载具有固定值的段描述符。另外,它们还与权限级别的任何非标准使用不兼容。
内核可访问的特定于模型的寄存器(MSR)指向内核的系统调用入口点,目前sysenter还以这种方式切换到内核堆栈。处理器不需要解释内存中的数据结构,返回地址保留在通用寄存器中,以便内核保存到它想保存的任何位置。
我已经测量了使用内核中的空系统调用处理程序进入和退出内核的系统消耗,具体的microbenchark代码请点此下载。我们只考虑硬件对系统调用路径的系统消耗。如下所示,性能差异惊人:
使用syscall/sysret或sysenter/sysexit执行系统调用比使用传统方法快很多,这两种现代方法每次从用户到内核来回的往返都要花费大约70个周期,该周期是一个小于64位的整数除法!