XV6系统调用实现

X86的保护机制

x86 有四个特权级,从 0(特权最高)编号到 3(特权最低)。在实际使用中,大多数的操作系统都使用两个特权级,0 和 3,他们被称为内核模式和用户模式。当前执行指令的特权级存在于 %cs 寄存器中的 CPL 域中。

在 x86 中,中断处理程序的入口在中断描述符表(IDT)中被定义。这个表有256个表项,每一个都提供了相应的 %cs 和 %eip。

int指令的硬件动作

一个程序要在 x86 上进行一个系统调用,它需要调用 int n 指令,这里 n 就是 IDT 的索引。int 指令进行下面一些步骤(硬件自动完成):

  • 从 IDT 中获得第 n 个描述符,n 就是 int 的参数。
  • 检查 %cs 的域 CPL <= DPL,DPL 是描述符中记录的特权级。
  • 如果DPL < CPL,就在 CPU 内部的寄存器中保存 %esp 和 %ss 的值(即不会发生特权级切换,所以不用将程序低特权级别下的%esp 和 %ss保存到高特权级栈中)。
  • 从一个任务段(TSS)描述符中加载 %ss 和 %esp(高特权级栈对应的%ss 和 %esp)。
  • 将 %ss 压栈(低特权级的%ss压入高特权级栈 )。
  • 将 %esp 压栈(低特权级的%esp压入高特权级栈)。
  • 将 %eflags 压栈。
  • 将 %cs 压栈。
  • 将 %eip 压栈。
  • 清除 %eflags 的一些位。
  • 设置 %cs 和 %eip 为描述符中的值。

int 指令是一个非常复杂的指令,可能有人会问是不是所有的这些操作都是必要的。检查 CPL <= DPL 使得内核可以禁止一些特权级系统调用。例如,如果用户成功执行了 int 指令,那么 DPL 必须是 3。如果用户程序没有合适的特权级,那么 int 指令就会触发 int 13,这是一个通用保护错误。再举一个例子,int 指令不能使用用户栈来保存值,因为用户可能还没有建立一个合适的栈,因此硬件会使用任务段(TSS)中指定的栈(这个栈在内核模式中建立)。

当内陷发生时,处理器会做下面一些事。如果处理器在用户模式下运行,它会从TSS中加载 %esp 和 %ss,把老的 %ss 和 %esp 压入新的栈中。如果处理器在内核模式下运行,上面的事件就不会发生。处理器接下来会把 %eflags,%cs,%eip 压栈。对于某些内陷来说,处理器会压入一个错误字。而后,处理器从相应 IDT 表项中加载新的 %eip 和 %cs。

XV6系统调用实现_第1张图片

图 3-1 展示了一个 int 指令之后的栈的情况,注意这是发生了特权级转换(即描述符中的特权级DPL比 CPL中的特权级低的时候)栈的情况。如果这条指令没有导致特权级转换,x86 就不会保存 %ss 和 %esp。在任何一种情况下,%eip 都指向中断描述符表中指定的地址,这个地址的第一条指令就是将要执行的下一条指令,也是 int n 的中断处理程序的第一条指令。操作系统应该实现这些中断处理程序。

操作系统可以使用 iret 指令来从一个 int 指令中返回。它从栈中弹出 int 指令保存的值,然后通过恢复保存的 %eip 的值来继续用户程序的执行。

特权级切换与栈切换

当特权级从用户模式向内核模式转换时,内核不能使用用户的栈,因为它可能不是有效的。用户进程可能是恶意的或者包含了一些错误,使得用户的 %esp 指向一个不是用户内存的地方。栈切换的方法是让硬件从一个TSS中读出新的%ss和新的 %esp 的值(内核态栈的%ss个%esp,函数 switchuvm会把用户进程的内核栈顶地址存入TSS中)。

XV6系统调用之特权级切换与栈切换

xv6 使用一个 perl 脚本来产生IDT表项指向的中断处理函数入口点。每一个入口都会压入一个错误码(如果 CPU 没有压入的话),压入中断号,然后跳转到 alltraps

Alltraps继续保存处理器的寄存器:它压入 %ds, %es, %fs, %gs, 以及通用寄存器EAX/EBX/ECX/EDX/ESP/EBP/ESI/EDI。这么做使得内核栈上压入一个 trapframe(中断帧)结构体,这个结构体包含了中断发生时处理器的寄存器状态(参见图3-2)。处理器负责压入 %ss,%esp,%eflags,%cs 和 %eip。处理器或者中断入口会压入一个错误码(如果某些中断,处理器不压入错误码,那么软件中断入口会压入一个错误码,实际上目的就是为了保持所有中断的trapframe一致),而alltraps负责压入剩余的。中断帧包含了所有处理器从当前进程的内核态恢复到用户态需要的信息,所以处理器可以恰如中断开始时那样继续执行。特别的,userinit通过手动建立中断帧来达到这个目标。

XV6系统调用实现_第2张图片

 Firgure 3-2的布局刚好和struct trapframe定义对应起来。

被保存的 %eip 是 int 指令下一条指令的地址。%cs 是用户代码段选择符。%eflags 是执行 int 指令时的 eflags 寄存器,alltraps 同时也保存 %eax,它存有系统调用号,内核在之后会使用到它。

现在用户态的寄存器都保存了,alltraps 可以完成对处理器的设置并开始执行内核的 C 代码。处理器在进入中断处理程序之前设置选择符 %cs 和 %ss(硬件自动完成)alltraps 设置 %ds 和 %es。

一旦段设置好了,alltraps 就可以调用 C 中断处理程序 trap 了。它压入 %esp 作为 trap 的参数,%esp 指向刚在栈上建立好的中断帧。然后它调用 trap。trap 返回后,alltraps 弹出栈上的参数然后执行标号为 trapret 处的代码。我们在第二章阐述第一个用户进程的时候跟踪分析了这段代码,在那里第一个用户进程通过执行 trapret 处的代码来退出到用户空间。同样地事情在这里也发生:弹出中断帧会恢复用户模式下的寄存器,然后执行 iret 会跳回到用户空间。

现在我们讨论的是发生在用户模式下的中断,但是中断也可能发生在内核模式下。在那种情况下硬件不需要进行栈转换,也不需要保存栈指针或栈的段选择符;除此之外的别的步骤都和发生在用户模式下的中断一样,执行的 xv6 中断处理程序的代码也是一样的。而 iret 会恢复了一个内核模式下的 %cs,处理器也会继续在内核模式下执行。

你可能感兴趣的:(开发OS,XV6,系统调用)