Part A:用户环境和异常处理

用户环境创建

见上一篇:MIT-JOS系列5:用户环境(一)

处理中断和异常

基础知识

受保护的控制转移

异常(exceptions)和中断(interrupts)都是受保护的控制转移(protected control transfers),它们将处理器模式从用户态切换到内核态,不给用户模式干扰到其他环境或内核功能的机会。在Intel的术语里,中断一般是指由处理器外部的异步事件引发的受保护的处理器控制权转移,例如外部I/O设备发出的活动信号;异常则是由当前执行的代码同步地引起的控制权转移,例如除零异常或非法存储器访问

SRE实战 互联网时代守护先锋,助力企业售后服务体系运筹帷幄!一键直达领取阿里云限量特价优惠。

为了确保这些控制转移被切实地受到了保护,处理器的中断/异常机制被设计为:当中断或异常发生时,当前执行的代码无法选择进入内核的位置或方式。处理器确保只能在严格受控的情况下才能进入内核态。在x86下,两种机制配合工作以提供这种保护:

  1. 中断描述符表IDT(中断向量表):处理器保证中断和异常只能引起代码进入到内核的一些特定的、已被明确定义的入口点。这些入口点由内核决定,而非中断或异常引发时正在执行的代码决定

    x86允许内核有256种不同的中断或异常入口,每个入口的值由整数0~255表示,称为中断向量。一个中断向量的值由引发中断的源决定,不同的设备、错误条件以及应用对内核的请求将引发不通的中断。CPU利用中断向量作为中断描述符表IDT的索引查找中断处理程序的入口地址(中断门,gate),IDT表被设置在内核空间。从这个表中的相应条目中,处理器可以读取到:(事实上就是个虚拟地址,修改cs:eip进行跳转)

    • 需要加载到寄存器eip中的值:它指出内核中处理该中断的代码的入口地址
    • 需要加载到寄存器cs中的值:它指出运行中断处理程序的运行特权级(即将当前进程切换到内核态)
  2. 任务状态段TSS:当中断或异常发生,切换到内核态运行中断处理程序之前,处理器需要一个地方保存当前处理器的状态,例如寄存器EIP和CS的值以便在中断处理程序结束后能恢复到中断发生的地方,继续执行原来的代码。这个区域也需要受到保护,避免被用户态的程序访问以破坏内核。

    因此当处理器处理一个中断、从用户态切换到内核态时,也要将它的堆栈切换到内核态中以保存处理器的状态。数据结构任务状态段(TSS)就是用来指出内核堆栈所在的段选择子和地址。处理器向内核栈中顺序压入SS, ESP, EFLAGS, CS, EIP和error code(可选),然后它从中断向量加载CS和EIP的值,并将ESP和SS的值设置为内核栈

    尽管TSS非常大并且还有很多其他的功能,但JOS仅把它用作定义从用户态切换到内核态时内核堆栈的位置。在JOS的定义中,内核态指特权级为0,因此在TSS数据结构中使用EPS0SS0来定义内核堆栈的位置。JOS不使用TSS的其他域

中断和异常的类型

所有由x86处理器内部同步地产生的异常的中断号都在0~31之间,例如页面错误引起的异常对应的中断向量是14

大于31的终端号都用作软件中断(software interrupts)或硬件中断(hardware interrupts),软件中断由int指令生成,硬件中断由外部设备在需要时异步地生成

在本节中我们扩展JOS以处理x86处理器内部生成的0~31号中断向量,在下一节中我们令JOS能够处理48号中断(用作系统调用)。在lab4中继续扩展JOS使它能够处理外部硬件中断,例如时钟中断

处理器在用户态和内核态都可以引发异常,但若引发异常时处理器已经在内核态,就不需要切换运行状态和堆栈位置,也不需要保存SS, ESP的值只需要将EFLAGS, CS, EIP压栈。通过这种方式,内核可以处理嵌套中断


设置中断描述符表IDT

目前仅处理处理器内部异常(中断号0~31)

头文件inc/trap.hkern/trap.h中包含了一些关于中断和异常的非常重要的定义

  • kern/trap.h包含的定义仅内核态可见
  • inc/trap.h包含的定义对用户态也可见

0~31中的部分中断向量被Intel保留,它们永远不会被处理器生成,因此不用处理他们

系统预留的中断类型如下,可以通过这张表得知该中断的类型和需不需要错误码:

MIT-JOS系列6:用户环境(二) 随笔 第1张

最后实现的控制关系应当如下:

MIT-JOS系列6:用户环境(二) 随笔 第2张

每个中断或异常都应该在trapentry.S中有它相对应的处理程序,并通过trap_init()用这些处理程序的地址初始化IDT表。每个中断处理程序应该在栈中有一个Trapframe结构体并调用trap()指向这个结构体,然后trap()调用特定的程序处理中断或异常

实现

llab3 exercise4需要编辑trapentry.Strap.c实现上述功能。在trapentry.S中有两个宏用于处理中断和异常:

  • TRAPHANDLER:将中断号压栈,然后跳转到_alltraps。用来处理有错误码的异常(CPU自动压入错误码)
  • TRAPHANDLER_NOEC:用来处理没有错误码的异常(压入0代替错误码)

这两个宏都接受两个参数(name, num),其中name是中断处理程序的函数名,num是相应中断号

在这个实验中,我们需要:

  • 利用这两个宏在trapentry.S中为定义在inc/trap.h中的每个异常编写入口点
  • 为这两个宏编写_alltraps
  • 利用SETGATE宏修改trap_init()为这些入口点初始化中断向量表IDT

_alltraps需要能够:

  1. Trapframe结构体的格式把值压栈(err, trapno由宏已压入,SS, ESP, EFLAGS, CS, EIP压到内核栈中,之后从内核栈恢复,这里不用再压一遍)
  2. GD_KD加载到ds和es
  3. esp压入,为trap()传参
  4. 调用trap()

代码实现如下:

trapentry.S

/*
 * Lab 3: Your code here for generating entry points for the different traps.
 */

TRAPHANDLER_NOEC(t_divide, T_DIVIDE)
TRAPHANDLER_NOEC(t_debug, T_DEBUG)
TRAPHANDLER_NOEC(t_nmi, T_NMI)
TRAPHANDLER_NOEC(t_brkpt, T_BRKPT)
TRAPHANDLER_NOEC(t_oflow, T_OFLOW)
TRAPHANDLER_NOEC(t_bound, T_BOUND)
TRAPHANDLER_NOEC(t_illop, T_ILLOP)
TRAPHANDLER_NOEC(t_device, T_DEVICE)
TRAPHANDLER(t_dblflt, T_DBLFLT)
TRAPHANDLER(t_tss, T_TSS)
TRAPHANDLER(t_segnp, T_SEGNP)
TRAPHANDLER(t_stack, T_STACK)
TRAPHANDLER(t_gpflt, T_GPFLT)
TRAPHANDLER(t_pgflt, T_PGFLT)
TRAPHANDLER_NOEC(t_fperr, T_FPERR)
TRAPHANDLER(t_align, T_ALIGN)
TRAPHANDLER_NOEC(t_mchk, T_MCHK)
TRAPHANDLER_NOEC(t_simderr, T_SIMDERR)


/*
 * Lab 3: Your code here for _alltraps
 */

_alltraps:
    pushl %ds
    pushl %es
    pushal
    movw $GD_KD, %ax
    movw %ax, %ds
    movw %ax, %es
    pushl %esp
    call trap

trap.c

void
trap_init(void)
{
    extern struct Segdesc gdt[];
    void t_divide();
    void t_debug();
    void t_nmi();
    void t_brkpt();
    void t_oflow();
    void t_bound();
    void t_illop();
    void t_device();
    void t_dblflt();
    void t_tss();
    void t_segnp();
    void t_stack();
    void t_gpflt();
    void t_pgflt();
    void t_fperr();
    void t_align();
    void t_mchk();
    void t_simderr();

    // LAB 3: Your code here.
    SETGATE(idt[T_DIVIDE], 0, GD_KT, t_divide, 0);
    SETGATE(idt[T_DEBUG], 1, GD_KT, t_debug, 0);
    SETGATE(idt[T_NMI], 0, GD_KT, t_nmi, 0);
    SETGATE(idt[T_BRKPT], 1, GD_KT, t_brkpt, 0);
    SETGATE(idt[T_OFLOW], 1, GD_KT, t_oflow, 0);
    SETGATE(idt[T_BOUND], 0, GD_KT, t_bound, 0);
    SETGATE(idt[T_ILLOP], 0, GD_KT, t_illop, 0);
    SETGATE(idt[T_DEVICE], 0, GD_KT, t_device, 0);
    SETGATE(idt[T_DBLFLT], 0, GD_KT, t_dblflt, 0);
    SETGATE(idt[T_TSS], 0, GD_KT, t_tss, 0);
    SETGATE(idt[T_SEGNP], 0, GD_KT, t_segnp, 0);
    SETGATE(idt[T_STACK], 0, GD_KT, t_stack, 0);
    SETGATE(idt[T_GPFLT], 0, GD_KT, t_gpflt, 0);
    SETGATE(idt[T_PGFLT], 0, GD_KT, t_pgflt, 0);
    SETGATE(idt[T_FPERR], 0, GD_KT, t_fperr, 0);
    SETGATE(idt[T_ALIGN], 0, GD_KT, t_align, 0);
    SETGATE(idt[T_MCHK], 0, GD_KT, t_mchk, 0);
    SETGATE(idt[T_SIMDERR], 0, GD_KT, t_simderr, 0);

    // Per-CPU setup 
    trap_init_percpu();
}

中断处理小结

  1. trapentry.S中的TRAPHANDLER(name, num)宏和TRAPHANDLER_NOEC(name, num)宏:

    这两个宏为每一种中断设置中断处理程序的入口。例如调用TRAPHANDLER_NOEC(t_divide, T_DIVIDE)T_DIVIDE设置入口,宏展开后,相当于如下代码:

    .globl t_divide
    .type t_divide, @function
    .align 2
    t_divide:
    pushl $0
    pushl $T_DIVIDE
    jmp _alltraps

    它为中断T_DIVIDE在代码段中设置标号t_divide并导出到全局变量。结合我们写的_alltraps的代码,在中断触发、进入中断处理程序前发生

    • 从tss找到内核栈的地址,临时保存旧栈的ss, esp,修改当前ss, esp指向内核栈
    • 向内核栈压入旧ss, 旧esp, eflag, cs, eip,若中断有错误码,自动压入错误码
    • cs, eip指向中断处理程序入口,准备执行中断处理程序

    从标号t_divide开始执行将发生

    • 入栈中断错误码和中断号num(对于不需要错误码的中断TRAPHANDLER_NOEC入栈0,对于需要错误码的中断,错误码已经由CPU自动入栈,因此TRAPHANDLERpushl一个num
    • 调用_alltraps
      • 将各寄存器的值按Trapframe的格式压栈(这里,在后续调用trap()后,会从栈中顺序出栈数据赋值给参数的tf结构体
      • 利用GD_KD设置ds, es,指向内核数据段
      • 压栈esp指向参数(?
      • 调用trap()处理中断,根据中断处理结果销毁原环境或继续执行

    到这里,完成对每一个发生的中断的统一处理,之后在trap()中再根据中断号(由参数tf中的trapno读出)对不同中断分别处理

  2. trapentry.Strap_init()的关系:

    通过宏在trapentry.S中设置的标号(例如t_divide)是中断处理程序的入口,一个中断在触发后应该进入到对应的标号处开始执行。在trap_init()中则通过SETGATE()填写IDT表,为不通的中断向量设置同的中断处理程序:

    • 设置其类型(是trap还是interrupt:控制权通过陷阱门(trap gate)进入处理程序时维持IF标志位不变,即不关中断;而当控制权通过中断门进入中断处理程序时,处理器清IF标志,即关中断,以避免嵌套中断的发生。陷阱常被用于用户程序自发调用以陷入内核态,使用内核的一些功能,它的出现不一定意味着一个错误,例如断点)
    • 设置中断处理程序的入口(段选择子和偏移)。这个偏移即为trapentry.S中设置的标号的偏移地址。在我们之前写的代码中用void t_divide();声明了一个t_divide函数,因为trapentry.S对标号导出了全局变量,因此函数的定义就是汇编代码t_divide标号开始的内容;因此,在trap_init()中也可以把这些函数定义改成extern char t_divide[];得到标号t_divide的偏移地址,然后赋值给SETGATE(),两者效果是一样的

Question

  1. What is the purpose of having an individual handler function for each exception/interrupt? (i.e., if all exceptions/interrupts were delivered to the same handler, what feature that exists in the current implementation could not be provided?)

    首先中断和异常分为有错误码和没有错误码两种类型,对于有错误码的,cpu会自动向栈中压入一个错误码,因此为了保持数据结构一致,我们要设计handler区分这两种类型,为没有参数的中断压入一个0作为错误码;

    其次异常handler区分了不同中断的类型(trap/interrupt),指出该中断出现时是否需要关中断防止中断嵌套;设置执行权限(用户可调用/仅内核可调用),确保用户态无法进行权限以外的操作。

  2. Did you have to do anything to make the user/softint program behave correctly? The grade script expects it to produce a general protection fault (trap 13), but softint's code says int $14. Why should this produce interrupt vector 13? What happens if the kernel actually allows softint's int $14 instruction to invoke the kernel's page fault handler (which is interrupt vector 14)?

    int $14是缺页异常,我们将其调用权限设置为0(仅内核态可用),但当前程序处于用户态下,特权级为3,没有权限执行系统调用,因此使用int $14指令触发了protection fault(trap 13)。如果让用户程序softint能够执行int $14指令,可能会使用户干扰到内核的页面管理

扫码关注我们
微信号:SRE实战
拒绝背锅 运筹帷幄