操作系统导论--受限制的直接执行

Posted 捕获一只小肚皮

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了操作系统导论--受限制的直接执行相关的知识,希望对你有一定的参考价值。

受限直接执行

为了使程序尽可能快地运行,操作系统开发人员想出了一种技术——我们称之为受限的直接执行。

这个概念的“直接执行”部分很简单:只需直接在CPU上运行程序即可。因此,当OS希望启动程序运行时,它会在进程列表中为其创建一个进程条目,为其分配一些内存,将程序代码(从磁盘)加载到内存中,找到入口点(main()函数或类似的),跳转到那里,并开始运行用户的代码。

听起来很简单,不是吗?但是,这种方法在我们虚拟化CPU时产生了一些问题。

  • 第一个问题:如果我们只运行一个程序,操作系统怎么能确保程序只做我们允许它做的事情,同时仍然高效地运行它?

  • 第二个问题:当我们运行一个进程时,操作系统如何让它停下来并切换到另一个进程,从而实现虚拟化CPU所需的时分共享?

第一个问题

直接执行的明显优势是快速,让程序直接在硬件CPU上运行,因此执行速度与预期的一样快, 但这样就相当于让所有进程做所有它想做的事情,导致无法用程序构建一些系统;

例如: 如果我们希望构建一个具有相关读写访问权限的文件系统,就不能简单地让任何用户进程向磁盘发出I/O(即直接让该程序在CPU上运行),如果这样做,一个进程就可以任意读取或写入整个磁盘,这样所有的保护(权限)都会失效。

因此,我们采用的方法是引入一种新的处理器模式,称为用户模式(user mode)。在用户模式下运行的代码会受到限制。例如,在用户模式下运行时,进程不能发出I/O请求。这样做会导致处理器引发异常,操作系统可能会终止进程。

与用户模式不同的内核模式(kernel mode),操作系统(或内核)就以这种模式运行。在此模式下,运行的代码可以做任何它喜欢的事,包括特权操作,如发出I/O请求和执行所有类型的受限指令。

但是,如果用户希望执行某种特权操作(如从磁盘读取),应该怎么做?为了实现这一点,几乎所有的现代硬件都提供了用户程序执行系统调用的能力。它允许内核小心地向用户程序暴露某些关键功能,例如访问文件系统、创建和销毁进程、与其他进程通信,以及分配更多内存。

要执行系统调用,程序必须执行特殊的陷阱(trap)指令,该指令同时跳入内核并将特权级别提升到内核模式。一旦进入内核,系统就可以执行任何需要的特权操作(如果允许),从而为调用进程执行所需的工作。完成后,操作系统调用一个特殊的从陷阱返回(return-from-trap)指令,如你期望的那样,该指令返回到发起调用的用户程序中,同时将特权级别降低,回到用户模式。

为什么系统调用看起来像过程调用?

​ 你可能想知道,为什么对系统调用的调用(如open()或read())看起来完全就像C中的典型过程调用。也就是说,如果它看起来像一个过程调用,系统如何知道这是一个系统调用,并做所有正确的事情?原因很简单:它的确是一个过程调用,但隐藏在过程调用内部的是著名的陷阱指令。更具体地说,当你调用open()(举个例子)时,你正在执行对C库的过程调用。其中,无论是对于open()还是提供的其他系统调用,库都使用与内核一致的调用约定来将参数放在众所周知的位置(例如,在栈中或特定的寄存器中),将系统调用号也放入一个众所周知的位置(同样,放在栈或寄存器中),然后执行上述的陷阱指令。库中陷阱之后的代码准备好返回值,并将控制权返回给发出系统调用的程序。因此,C库中进行系统调用的部分是用汇编手工编码的,因为它们需要仔细遵循约定,以便正确处理参数和返回值,以及执行硬件特定的陷阱指令。现在你知道为什么你自己不必写汇编代码来陷入操作系统了,因为有人已经为你写了这些汇编。

还有一个重要的细节没讨论:陷阱如何知道在OS内运行哪些代码?很显然用户程序是不知道的,如果其知道的话,狡猾的用户便可以肆无忌惮的操作系统,不受权限控制了;

内核通过在启动时(处于内核模式)设置陷阱表来实现。当机器启动时,操作系统做的第一件事,就是告诉硬件在发生某些异常事件(如程序进行系统调用)时要运行哪些代码,因此当发送系统调用时候,陷阱便知道在哪里执行了

第二个问题

在进程之间切换应该很简单,操作系统应该决定停止一个进程并开始另一个进程,但实际上这有点棘手,因为当一个进程在CPU上运行时,就意味着操作系统没有运行,如果操作系统没有运行,它怎么能做事情?

所以我们碰到了关键问题:操作系统如何重新获取CPU的控制权;

历史上处理有两种方式:

  • 通过进程自己调用yield系统调用进行协作,归还CPU控制的控制权,没错,此系统调用仅仅只是用来归还控制权; 但这种方式很被动,例如,如果某个进程(无论是恶意的还是充满缺陷的)进入无限循环,并且从不进行系统调用,那么操作系统将永远也无法获得CPU控制权;
  • 利用时钟中断重新获得控制权 时钟设备可以编程为每隔几毫秒产生一次中断。产生中断时,当前正在运行的进程停止,操作系统中预先配置的中断处理程序(interrupt handler)会运行。此时,操作系统重新获得CPU的控制权,因此可以做它想做的事:停止当前进程,并启动另一个进程。

保存和恢复上下文

既然操作系统已经重新获得了控制权,无论是通过系统调用协作,还是通过时钟中断更强制执行,都必须决定:是继续运行当前正在运行的进程,还是切换到另一个进程;

如果决定进行切换,OS就会执行一些底层代码,即所谓的上下文切换(context switch)。

上下文切换在概念上很简单:操作系统要做的就是为当前正在执行的进程保存一些寄存器的值(保存到它的内核栈),并为即将执行的进程恢复一些寄存器的值(从它的内核栈取出)。

这样一来,操作系统就可以确保最后执行从陷阱返回指令时,不是返回到之前运行的进程,而是继续执行另一个进程。

为了保存当前正在运行的进程的上下文,操作系统会执行一些底层汇编代码,来保存通用寄存器、程序计数器,以及当前正在运行的进程的内核栈指针,然后切换内核栈,并恢复寄存器、程序计数器,供即将运行的进程使用。当操作系统最终执行从陷阱返回指令时,即将执行的进程变成了当前运行的进程。至此上下文切换完成

例如上图:

进程A正在运行,然后被中断时钟中断。硬件把它的进程数据保存到A的内存栈中,然互进入内核(切换到内核模式)。

在时钟中断处理程序中,操作系统决定从正在运行的进程A切换到进程B。此时,它调用switch()例程,该例程仔细保存当前寄存器的值(保存到A的进程结构),恢复进程B的寄存器(从它的进程结构),然后切换上下文(switch context),具体来说是通过改变栈指针来使用B的内核栈(而不是A的)。最后,操作系统从陷阱返回,开始运行进程B。

请注意,在此协议中,有两种类型的寄存器保存/恢复。

  • 第一种是发生时钟中断的时候(进程和OS之间的切换)。在这种情况下,运行进程的用户寄存器由硬件隐式保存,使用该进程的内核栈。

  • 第二种是当操作系统决定从A切换到B。在这种情况下,内核寄存器被软件(即OS)明确地保存,但这次被存储在该进程的进程结构的内存中。后一个操作让系统从好像刚刚由A陷入内核,变成好像刚刚由B陷入内核

小结

为了实现CPU虚拟化,我们描述了一些关键的底层机制,并将其统称为受限直接执行(limited direct execution)。

基本思路很简单:就让你想运行的程序在CPU上运行,但首先确保设置好硬件,以便在没有操作系统帮助的情况下限制进程可以执行的某些操作.

白话就是:

用户代码是程序,操作系统也是程序,但是后者可以做的事情更多,因此像涉及和硬件打交道的都交给OS处理,这个就是系统调用;

两个程序都需要CPU的运行,因此不可能一起运行,这就涉及当OS没有在CPU上运行时候,怎么确保它在一定时间内能继续控制整个计算机.

因此提供了两个方案: 给任何运行的用户程序一个yield调用,以及增加一个时钟硬件,到了一定时间就切换回来执行操作系统;

以上是关于操作系统导论--受限制的直接执行的主要内容,如果未能解决你的问题,请参考以下文章

《操作系统导论》第6章 | 受限直接执行

Linux内核工程导论——进程

通信导论-访问控制列表ACL原理

2019-2020-1 学期 2427 《网络空间安全导论》 第三周学习总结

CSRF与平行越权的区别

php代码审计5审计命令执行漏洞