From this part you may now see what you have been very familiar with.

But instead of looking from inside out, now outside in.

0x00 Partial Translation of Intel 80386 Manual (II)

Chapter 9 Exceptions and Interrupts

异常和中断是特殊类型的控制流转移。他们的主要区别在于中断是用于处理在处理器外部、与处理器异步发生的信息;而异常则是用于处理处理器在执行指令时产生的一些状况。

中断有两种,分别是:

  • 可阻断中断,这类中断由INTR引脚触发。
  • 不可阻断中断,这类中断由N(on)M(askable)I(nterrupts)引脚触发。

异常也有两种,分别是:

  • 处理器检测到的,又被继续细分为 故障(fault)、 陷阱(trap) 和 中止(abort) 。
  • 由软件按预期产生的,由指令 INTO, INT 3, INT nBOUND 触发。这些指令常被叫做“软件中断”,但是处理器实际上是按异常来处理它们的。

9.1 Identifying Interrupts

处理器会给不同的中断或异常赋予一个不同的标识符。

其中不可阻断中断是由处理器识别并赋予一个预先确定且不可更改的,范围在0~31内的标识符。

可阻断中断是由额外的中断控制器来处理的,可以被程序改变,范围在32~255内的标识符可以给可阻断中断和异常任意使用。

下表给出了预先定义的中断和异常。

Table 9-1. Interrupt and Exception ID Assignments

Identifier   Description

0            Divide error
1            Debug exceptions
2            Nonmaskable interrupt
3            Breakpoint (one-byte INT 3 instruction)
4            Overflow (INTO instruction)
5            Bounds check (BOUND instruction)
6            Invalid opcode
7            Coprocessor not available
8            Double fault
9            (reserved)
10           Invalid TSS
11           Segment not present
12           Stack exception
13           General protection
14           Page fault
15           (reserved)
16           Coprecessor error
17-31        (reserved)
32-255       Available for external interrupts via INTR pin

接下来详细说明 故障、陷阱和中止的区别。它们之间主要的区别在于它们什么时候被报告、以及相应的指令能否重新执行。

  • 故障:故障是在触发故障的指令执行之前被触发的。在触发故障后,机器会被重置到一个可以让这个指令重新执行的状态。
  • 陷阱:陷阱将在触发故障的指令执行完毕之后触发。
  • 中止:中止是一种既无法精确确定何时触发(之前、之中、之后),亦无法恢复到可以重新开始的状态的异常。中止用于报告严重的错误,例如硬件级的错误,或者系统数据结构中存在无效值。

9.2 Enabling & Disabling Interrupts

9.2.1 NMI Masks Further NMIs

当一个NMI中断的处理例程正在执行的时候,处理器会无视在这个过程中触发的其他NMI,直到下一个 IRET 指令为止。

9.2.2 IF Masks INTR

eflags寄存器中的 IF 位会对处理器处理可阻断中断的行为产生影响。

  • IF = 0 时,处理器无视可阻断中断。
  • IF = 1 时,处理器启用可阻断中断。

除了处理器的reset信号、CLISTI指令外,以下的情形也可能对IF产生影响:

  • PUSHF POPFIRET
  • 由中断门触发的中断会自动清空IF,关闭中断。
9.2.3 RF Masks Debug Faults
9.2.4 MOV or POP to SS Masks

距离:以下两条指令

mov ss, ax
mov esp, StackTop

第一条指令执行完毕后,第二条指令执行时触发中断或异常。

在这种情况下,为了保证ss和esp的值正确,所有NMI INTR debug 异常 single-step 陷阱都会延迟到修改ss的指令的下一条指令执行完毕后触发。

注意上面列举的被延迟的中断和异常并不包括所有的异常。在这种情况下,请使用LSS指令。

9.3 Priority Among Simultaneous Interrupts and Exceptions

当一条指令执行完毕后触发了多个异常和中断,处理器会按照下表的优先级和以下规则进行处理:

  • 一次一个,直到所有待处理的中断或异常处理完毕。
  • 由高优先级到低优先级。
  • 低优先级的异常会被丢弃;将在待处理完毕的中断和异常处理完毕后重新开始处理。
Table 9-2. Priority Among Simultaneous Interrupts and Exceptions

Priority   Class of Interrupt or Exception

HIGHEST    Faults except debug faults
Trap instructions INTO, INT n, INT 3
Debug traps for this instruction
Debug faults for next instruction
NMI interrupt
LOWEST     INTR interrupt

9.4 Interrupt Descriptor Table

中断描述符表将每个中断或异常标识符与一个指向对应处理例程的描述符关联起来。和全局描述符表、局部描述符表一样,中断描述符表也是一个由8字节大小的描述符组成的数组。不过与全局描述符表不同的是,中断描述符表的第一个元素可以是一个有效的描述符。

将中断或异常标识符转换为中断描述符表中对应的例程的方法是,简单地将它乘以8.但是描述符表的长度并不必须是固定的:只有出现的异常或中断的对应描述符会被访问。

中断描述符表可以位于内存的任何位置:处理器使用IDTR寄存器来存储它的位置。指令LIDTSIDT 可以用于操作IDTR寄存器。

这两个指令的操作数都是一个线性地址指针,指向一块大小为6-byte的内存空间。这个空间的内容如下:

Format of LIDT/SIDT operand points to

这个空间里的内容会被拷贝到IDTR当中。注意LIDT 仅能当CPL = 0 时执行;但是SIDT可以以任何权限等级执行。

9.5 Interrupt Descriptors

可以是三种描述符:

  • 任务门描述符
  • 中断门描述符
  • 陷阱门描述符

IDT Gate Descriptors

9.6 Interrupt Tasks and Interrupt Procedures

CALL指令可以指向一个过程或者任务一样,一个中断或异常同样可以以任务或过程的方式来启动中断服务例程。如果中断或异常对应的描述符表中的描述符是一个中断门或者陷阱门,那么它就会像CALL目标是调用门一样启动中断服务例程;如果对应的描述符是一个任务门,那么它就像CALL目标是一个任务门一样进行任务切换。

9.6.1 Interrupt Procedures

一个中断门或陷阱门间接地指向了一段将在当前任务的上下文中执行的过程。整个转换过程如下图所示:

Interrupt Vectoring for Procedures

其中:描述符中的选择子部分需要指向一个描述符表中的可执行段;偏移量部分则和对应段描述符结合形成最终的线性地址。

接下来介绍中断或异常跳转过程和CALL方式进行控制转移的不同之处。

首先是控制权转移的时候,栈结构的维护。

Stack Layout after Interrupt

需要注意的是当IRET的时候,EFLAGS会被从栈中弹出,并恢复到eflags寄存器。此时:

  • IOPL部分仅当CPL=0的时候会被恢复。
  • IF位仅当CPL <= IOPL 的时候会被恢复。

在中断触发过程中,一些标志位也被使用:

  • 中断门和陷阱门触发的时候,将eflags保存后tf标志位将被清空,以防止单步调试影响中断过程。
  • 中断门触发的时候,将eflags保存后将清空if标志位,以防止中断互相打断。陷阱门则不会清空if标志位。

在中断触发过程中应用的保护条例和过程调用过程中应用的相似:处理器不允许将控制权转移给一个有着更低权限(数字上权限等级更大)的过程。如果违反了这个条例,那么就会触发一个通用保护异常。

那么为了保证永远不会出现这种情况,一般来说有两种策略:

  • 将处理例程放置在一个一致的(conforming)段内。
  • 将处理例程放置在一个权限级为0的段内。
9.6.2 Interrupt Tasks

当一个中断对应的描述符是一个任务门时,任务切换会被触发。

任务切换进行的过程和一般任务切换大体相同,不过需要注意以下区别:

  • 使用 IRET 指令返回原来触发中断的任务。
  • 如果有 error code 的话,它会被处理器自动压到对应优先级的栈中。

9.7 Error Code

Error Code Format

如果异常与某个特定的段有关,处理器会将一个错误码压栈。这个错误码的格式如图所示。其中:

  • EX 当为1时,代表是由于一个程序外部的事件触发了异常。
  • I(DT) 为1时,代表selector index部分指示了一个IDT中的门描述符。
  • 如果I不为1,则TI = 0 => GDT; = 1 => LDT
  • 剩下的14位是段选择子的高14位。

9.8 Exception Conditions

这部分主要描述每个异常到底是什么东西。

需要注意的是,对于故障、陷阱和中止,它们压栈的 CS:EIP 值并不尽相同。具体的区别如下:

  • 故障(faults):指向触发故障的指令。
  • 陷阱(traps):指向触发故障的指令执行完毕后将执行的下一条指令。
  • 中止(aborts):中止无法特定 CS:EIP 值(参看之前对三种异常的解释)。
9.8.1 Interrupt 0: Divide Error(fault)
  • 条件: DIVIDIV 指令除数是 0.
9.8.2 Interrupt 1: Debug Exceptions(fault/trap)
  • 作为 fault: 设置的指令断点 以及通常发现的其他错误。
  • 作为 trap: 设置的数据断点 单步 设置的任务切换断点

这个异常不会产生错误码。处理例程可以通过观察除错寄存器来获取必要的信息。

9.8.3 Interrupt 3: Breakpoint(trap)
  • 条件:INT 3指令直接触发。
  • 应用方式:系统软件或除错软件直接替换可执行段中的指令序列来触发中断。
  • 注意:由于压栈的 CS:EIP 是下一条指令,所以在返回继续执行前,需要先将栈上的CS:EIP减一(INT 3指令仅一字节),并恢复正确的指令序列。
9.8.4 Interrupt 4: Overflow(trap)
  • 条件:INTO 指令执行时 OF 标志位为1.
9.8.5 Interrupt 5: Bounds Check(fault)
  • 条件:BOUND 指令的操作数越界。
9.8.6 Interrrupt 6: Invalid Opcode(fault)
  • 条件:试图执行无效的机器码或指令提供了无效的操作数。
  • 没有错误码。
9.8.7 Interrupt 7: Coprocessor Not Available
  • 条件:执行ESC指令时,CR0EM 位不为0.
  • 条件:执行WAITESC指令时,CR0MPTS位都被设置。
9.8.8 Interrupt 8: Double Fault
  • 条件:当试图执行一个异常的处理例程时,出现了另外一个异常,并且无法按顺序完成处理。

更加具体的条件是:请看表

Table 9-3. Double-Fault Detection Classes

Class           ID          Description

                 1          Debug exceptions
                 2          NMI
                 3          Breakpoint
Benign           4          Overflow
Exceptions       5          Bounds check
                 6          Invalid opcode
                 7          Coprocessor not available
                16          Coprocessor error

                 0          Divide error
                 9          Coprocessor Segment Overrun
Contributory    10          Invalid TSS
Exceptions      11          Segment not present
                12          Stack exception
                13          General protection

Page Faults     14          Page fault
----------------------------------------------------------
 Table 9-4. Double-Fault Definition

                                  SECOND EXCEPTION

                           Benign       Contributory    Page
                           Exception    Exception       Fault

           Benign          OK           OK              OK
           Exception

FIRST      Contributory    OK           DOUBLE          OK
EXCEPTION  Exception

           Page
           Fault           OK           DOUBLE          DOUBLE

double fault会产生错误码,但是它总是0. 产生错误的指令可能不会重新执行。当处理双重故障的时候再次产生异常,处理器会立即崩溃。

9.8.9 Interrupt 9 – Coprocessor Segment Overrun
  • 条件:在将协处理器操作数的中间部分转移到NPX的时候检测到段或页保护机制被违反。
9.8.10 Interrupt 10 – Invalid TSS(fault)
  • 条件:在一次任务切换中,目标TSS无效。

此时错误码可以用于分辨TSS无效的原因(见表)。

这个异常可能在原任务或者目标任务的上下文中被处理。在处理器完成目标TSS存在性检查之前,检测到的所有异常将在原任务上下文中处理。一旦完成检查,则视为任务切换完成。在这种情况下,所有之后产生的异常将在新任务上下文中处理。

为了保证执行上下文的正确性,这个异常的处理例程必须是通过任务门触发的任务。

Table 9-5. Conditions That Invalidate the TSS

Error Code              Condition

TSS id + EXT            The limit in the TSS descriptor is less than 103
LTD id + EXT            Invalid LDT selector or LDT not present
SS id + EXT             Stack segment selector is outside table limit
SS id + EXT             Stack segment is not a writable segment
SS id + EXT             Stack segment DPL does not match new CPL
SS id + EXT             Stack segment selector RPL < >  CPL
CS id + EXT             Code segment selector is outside table limit
CS id + EXT             Code segment selector does not refer to code
                        segment
CS id + EXT             DPL of non-conforming code segment < > new CPL
CS id + EXT             DPL of conforming code segment > new CPL
DS/ES/FS/GS id + EXT    DS, ES, FS, or GS segment selector is outside
                        table limits
DS/ES/FS/GS id + EXT    DS, ES, FS, or GS is not readable segment
9.8.11 Interrupt 11: Segment not Present(fault)
  • 条件:检测到一个将要使用的段描述符的Present位为0.
  • 注意:当这个异常在任务切换时触发,之后的段寄存器值将不会被检查。处理例程必须检查所有的段寄存器值是否正确。
  • 有错误码。此时错误码的I位指示是否是由于一个中断描述符表项触发的。
9.8.12 Interrupt 12: Stack Exception(fault)
  • 条件:和栈寄存器ss有关的越界问题。
  • 条件:用一个不是present但实际有效的描述符加载栈寄存器。
  • 错误码:当这个错误是与一个不present的栈段或者在跨权限等级的CALL指令执行时新栈出现了溢出,错误码将包含一个指向出问题的栈段的选择子。否则,错误码为0.
  • 在所有情况下,引起错误的指令都可以被重新执行。仅当在任务切换是出现栈异常时,压栈的返回指针指向新任务的第一条指令;其他情况下,指向引发异常的指令。
  • 当任务切换时出现栈异常,此时CS SS DS ES FS GS都是不可依赖的。异常处理例程必须负责检查这些寄存器,并在恢复执行前将它们置为正确的值。否则,可能在未来引发通用保护异常,而这会使得真正的原因难以定位。
9.8.13 Interrupt 13: General Protection Exception(fault)

所有违反保护机制而不包含在其他异常里的错误会引发通用保护异常。这些情况包括但不限于:

  • 使用 CS DS ES FS GS 是越界
  • 使用描述符表时越界
  • 控制流转移到不可执行段
  • 试图写不可写段或代码段
  • 读仅执行段
  • 以仅读描述符加载段寄存器SS
  • 以系统段加载SS DS ES FS GS
  • 以仅执行段加载DS ES FS GS
  • 以可执行段加载SS
  • 通过空选择子访问内存
  • 切换到一个正忙的任务
  • 违反优先级条例
  • 以PG=1且PE=0加载CR0
  • 通过陷阱门或中断门从V86模式转换到非0的优先级
  • 指令长度超过15字节

错误码:当加载一个描述符触发了异常是,错误码包含指向它的选择子。否则,错误码为0.

9.8.14 Interrupt 14: Page Fault(fault)
  • 前提:PG = 1
  • 条件:目标页的初级页表或次级页表对应项不是present
  • 条件:当前过程没有足够的权限访问目标页
  • 错误码:与其他的异常的错误码不同;缺页故障的错误码有着不同的结构。如下图所示:

Page Fault Error Code Format

其中:

  • U/S 指示发生异常时处理器的执行状态
  • W/R 指示试图访问的指令的行为
  • P 指示是否由不present触发——0表示是。

并且,CR2将被设置为触发缺页中断的线性地址。

缺页故障可以在很多种情况下被触发。下面将介绍这些情况:

  • 任务切换时产生缺页故障

处理器在进行任务切换时,可能会访问以下四个段:

  • 将原任务的状态写回它的TSS
  • 读取GDT以特定新的TSS
  • 读取TSS并检查新任务的段描述符
  • 可能读取LDT以检查新任务的段描述符

前两个出现异常时,将在原任务的上下文中处理;后两个则是新任务的上下文中。CS:EIP 指向新任务的第一条指令。在这种情况下,缺页中断处理例程必须是一个任务,且由任务门调用。

  • 栈顶寄存器变更时发生缺页故障

考虑以下连续指令:

  mov ss, ax
  mov sp, StackTop

当第二条指令访问内存产生缺页中断时,栈顶 SS:ESP 将处于错误的状态。如果使用任务切换实现中断处理,则不会出现问题,但是如果使用一般的中断门或陷阱门,则会CPU将使用无效的SS:ESP值。

因此,建议使用 LSS 指令。

9.8.15 Interrupt 16: Coprocessor Error
  • 条件:在ESC指令的开始或WAIT指令执行时且EM位为0时,检测到一个来自80287或80387的信号传递到ERROR#输入引脚。

0x01 PART A: User Envir & Exception Handling

What’s in the Env structure?

struct Env {
	struct Trapframe env_tf;	// Saved registers
	struct Env *env_link;		// Next free Env
	envid_t env_id;			// Unique environment identifier
	envid_t env_parent_id;		// env_id of this env's parent
	enum EnvType env_type;		// Indicates special system environments
	unsigned env_status;		// Status of the environment
	uint32_t env_runs;		// Number of times environment has run

	// Address space
	pde_t *env_pgdir;		// Kernel virtual address of page dir
};

Env 结构体是内核用于维护用户进程的环境的结构。其中:

  • env_tfinc/trap.h 中定义,用于在这个环境(注:环境与进程类似,下同)不在运行时保存它的寄存器值。内核将在用户态和内核态之间切换时保存这些内容。
  • env_link 链表
  • env_id 环境的uid。
  • env_parent_id 它的父环境的id。
  • env_type 环境的类型。大多数情况下都是 ENV_TYPE_USER
  • env_status 环境的状态。它是以下值之一:
    • ENV_FREE 这个结构体处于非活动状态,从而位于env_free_list中。
    • ENV_RUNNABLE 这个结构体所代表的环境正在等待被CPU执行。
    • ENV_RUNNING 这个结构体所代表的环境正在运行。
    • ENV_NOT_RUNNABLE 这个结构体所代表的环境处于活动状态,但是并不应该被执行:例:正在等待一个进程间通信。
    • ENV_DYING 这个结构体所代表的环境是一个僵尸环境。僵尸环境将在它下一次陷入内核时被释放。
  • env_pgdir 这个环境的初级页表的内核虚拟地址。

与Unix进程类似,JOS 环境将进程与地址空间一一配对——进程由保存的寄存器定义,地址用间由env_pgdir定义。当一个环境将被执行时,内核必须正确地为处理器设置寄存器值和地址空间。

由于JOS仅允许一个环境同时运行,所以每个进程没有额外的内核栈。

Allocate Memory for Envs

需要修改mem_init(),以为环境数组分配空间。

Simply write as previous pages mappings. No difficulty.

Creating and Running Environments

env.c 中提供了几个用于管理环境的函数:

  • env_init 初始化所有 Env 结构体、envs 数组以及将它们加入 env_free_list ,之后调用 env_init_cpu 设置分段的权限。
  • env_setup_vm 为新的环境分配初级页表,并且将内核部分空间映射到目标环境的地址空间中。
  • region_alloc 负责新的环境的物理内存空间分配和映射。
  • load_icode 负责将硬写进内核可执行的用户环境代码载入内存空间。
  • env_create 负责创建一个环境和将代码载入的整个流程。
  • env_run 运行一个已经存在的环境。

这里简单说一下实现的思路:

env_init

之前已经将所有的环境使用 ENV_FREE 初始化了,所以真正的工作只有创建链表。

env_setup_vm

这个可以偷个懒,就是直接将内核的初级页表复制过来,这样就不用再繁琐地设置内核空间映射了。

之后增加引用次数以防被重新分配,并且设置 Env 结构体的 env_pgdir 项即可。

region_alloc

组合使用 page_lookuppage_allocpage_insert 即可,相对简单。

load_icode

这个仍然可以偷懒,具体来说就是将 bootloader 的代码给复制过来……

之后就很简单了,记住要给用户环境分配个栈空间。

env_create

组合调用 env_allocload_icode ,非常简单。

env_run

将当前正在运行的环境设置为可运行,将将被运行的环境(也就是那个参数)设置为正在运行,然后通过 lcr3env_pop_tf 退回用户态就好。

Setting Up the IDT

这里我们需要设置中断描述符表。这个表为CPU指明了发生中断时应该去哪里找到处理这个中断的服务例程。

首先修改 trapentry.S ,使用提供的两个宏创建一些简短的小程序,这些程序提供了中断产生后第一个跳转的目标:它会在栈上为那些没有错误码的中断压入错误码,然后跳转到下一步。

下一步的 _alltraps ,将在栈上初始化 struct Trapframe 的结构。这里由于已经有 pushal 一键将多数寄存器压栈,所以我们只需保存 dses,然后将它们置为内核的数据段。最后将 esp 压栈,跳转到内核的中断分发子程序即可。

至于 trap_init ,只需要简单地使用 SETGATE 设置对应的描述符即可。这里需要关注的是:哪些东西是可以在用户态 int 触发的,哪些是只能在内核态 int 触发的。提示:只有那几个无关紧要的中断是能在用户态触发的。

两个问题:

  1. 使用单独的简短小程序的功能就是规范错误码的存在,从而使得 struct Trapframe 结构固定。
  2. 因为14号中断不允许在用户态 int 触发,所以实际上触发了通用保护异常(No.13)。如果允许用户态 int 触发内核的14号中断缺页异常,那么会导致缺页处理出现严重混乱。

0x02 Part B: Page Faults, Breakpoints, and System Calls

Page Faults

修改中断分发函数,将缺页中断分发到缺页中断处理函数。这个很简单,就不说了。

Breakpoints

修改中断分发函数,当断点中断发生时,直接触发内核monitor。很简单,也不说了。

两个问题:

  1. 是取决于之前说的权限级。只要设置为用户态可以 int ,就不会触发通用保护中断。提示:断点中断是 No.3 ,通过单字节指令 int3 触发。
  2. 权限控制的终极要义是防止写爆了的用户态程序。在这个意义上,控制哪些中断可以被用户强制触发是非常重要的。

System Calls

增加一个标号为 T_SYSCALL 的中断描述符,分发到 syscall(),并实现 syscall(),将对应的系统调用分发到各个对应的处理函数。虽然码量不小,但是其实不难,就不细说了。

User-mode Startup

其实就是修改 libmain ,让它初始化 thisenv 指针。 envs 数组我们之前已经正确映射了,所以只需要简单使用 sys_getenvid 系统调用,配合使用 ENVX 宏,即可直接获得我们需要的指针。

Page Faults & Memory Protection

user_mem_check

检查一个环境是否由权限访问给定范围的内存。

其实挺简单的,只需要使用 page_lookup 就行了。这里面比较麻烦的是需要计算爆了的地址,小心计算就好,并不算复杂。

之后在缺页处理函数里首先检查发生中断的权限级,如果是内核级就直接kernel panic,然后过 user_mem_assert 就好。

然后修改 syscall.c ,对所有系统调用需要用到内存的执行一遍 user_mem_assert

最后修改 kdebug.c 防止在这些信息中填入非法地址导致内核爆炸,调用 user_mem_check 就好。

0x03 Summary

这次lab还是没有做challenge,因为感觉还是很无聊……