Part A:用户环境和异常处理
注:根据MIT-JOS的lab指导手册,以下不明确区分“环境”和“进程”
用户环境创建
见上一篇:MIT-JOS系列5:用户环境(一)
处理中断和异常
基础知识
受保护的控制转移
异常(exceptions)和中断(interrupts)都是受保护的控制转移(protected control transfers),它们将处理器模式从用户态切换到内核态,不给用户模式干扰到其他环境或内核功能的机会。在Intel的术语里,中断一般是指由处理器外部的异步事件引发的受保护的处理器控制权转移,例如外部I/O设备发出的活动信号;异常则是由当前执行的代码同步地引起的控制权转移,例如除零异常或非法存储器访问
为了确保这些控制转移被切实地受到了保护,处理器的中断/异常机制被设计为:当中断或异常发生时,当前执行的代码无法选择进入内核的位置或方式。处理器确保只能在严格受控的情况下才能进入内核态。在x86下,两种机制配合工作以提供这种保护:
-
中断描述符表IDT(中断向量表):处理器保证中断和异常只能引起代码进入到内核的一些特定的、已被明确定义的入口点。这些入口点由内核决定,而非中断或异常引发时正在执行的代码决定
x86允许内核有256种不同的中断或异常入口,每个入口的值由整数0~255表示,称为中断向量。一个中断向量的值由引发中断的源决定,不同的设备、错误条件以及应用对内核的请求将引发不通的中断。CPU利用中断向量作为中断描述符表IDT的索引查找中断处理程序的入口地址(中断门,gate),IDT表被设置在内核空间。从这个表中的相应条目中,处理器可以读取到:(事实上就是个虚拟地址,修改cs:eip进行跳转)
- 需要加载到寄存器eip中的值:它指出内核中处理该中断的代码的入口地址
- 需要加载到寄存器cs中的值:它指出运行中断处理程序的运行特权级(即将当前进程切换到内核态)
-
任务状态段TSS:当中断或异常发生,切换到内核态运行中断处理程序之前,处理器需要一个地方保存当前处理器的状态,例如寄存器EIP和CS的值以便在中断处理程序结束后能恢复到中断发生的地方,继续执行原来的代码。这个区域也需要受到保护,避免被用户态的程序访问以破坏内核。
因此当处理器处理一个中断、从用户态切换到内核态时,也要将它的堆栈切换到内核态中以保存处理器的状态。数据结构任务状态段(TSS)就是用来指出内核堆栈所在的段选择子和地址。处理器向内核栈中顺序压入
SS, ESP, EFLAGS, CS, EIP和error code(可选)
,然后它从中断向量加载CS和EIP的值,并将ESP和SS的值设置为内核栈尽管TSS非常大并且还有很多其他的功能,但JOS仅把它用作定义从用户态切换到内核态时内核堆栈的位置。在JOS的定义中,内核态指特权级为0,因此在TSS数据结构中使用
EPS0
和SS0
来定义内核堆栈的位置。JOS不使用TSS的其他域
中断和异常的类型
对中断和异常的梳理,可以参考MIT-JOS系列:问题汇总(中断和异常概念整理),下面的话是我第一次做这个实验时写的,我也不知道两者有没有冲突,如果有,以问题汇总里的说法为准。。
所有由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.h
和kern/trap.h
中包含了一些关于中断和异常的非常重要的定义
kern/trap.h
包含的定义仅内核态可见inc/trap.h
包含的定义对用户态也可见
0~31中的部分中断向量被Intel保留,它们永远不会被处理器生成,因此不用处理他们
系统预留的中断类型如下,可以通过这张表得知该中断的类型和需不需要错误码:
最后实现的控制关系应当如下:
每个中断或异常都应该在trapentry.S
中有它相对应的处理程序,并通过trap_init()
用这些处理程序的地址初始化IDT表。每个中断处理程序应该在栈中有一个Trapframe
结构体并调用trap()
指向这个结构体,然后trap()
调用特定的程序处理中断或异常
实现
llab3 exercise4需要编辑trapentry.S
和trap.c
实现上述功能。在trapentry.S
中有两个宏用于处理中断和异常:
TRAPHANDLER
:将中断号压栈,然后跳转到_alltraps
。用来处理有错误码的异常(CPU自动压入错误码)- TRAPHANDLER_NOEC:用来处理没有错误码的异常(压入0代替错误码)
这两个宏都接受两个参数(name, num)
,其中name
是中断处理程序的函数名,num
是相应中断号
在这个实验中,我们需要:
- 利用这两个宏在
trapentry.S
中为定义在inc/trap.h
中的每个异常编写入口点 - 为这两个宏编写
_alltraps
- 利用
SETGATE
宏修改trap_init()
为这些入口点初始化中断向量表IDT
_alltraps
需要能够:
- 以
Trapframe
结构体的格式把值压栈(err, trapno
由宏已压入,SS, ESP, EFLAGS, CS, EIP
压到内核栈中,之后从内核栈恢复,这里不用再压一遍) - 把
GD_KD
加载到ds和es
- 将
esp
压入,为trap()
传参 - 调用
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], 0, 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();
}
Q:为什么istrap
参数都设置为0?有些中断的类型不是trap
吗?
A:x86系统预留中断类型表格中给出的type是异常的type,与这个中断描述符是中断门还是陷阱门无关
异常包括:
- 故障(fault):中断返回后重新执行引起故障的指令(例如页面错误,经过修复后重新访问该页面)
- 陷阱(trap):中断返回后执行下一条指令(一般用于用户自发地陷入内核态)
- 中止(abort):严重的系统错误,程序中止
在设置中断向量表中的中断门时,包括两个概念:
- 中断门(Interrupt gate):进入中断后寄存器
eflags
的IF
位自动清零以屏蔽中断,返回用户态恢复寄存器时恢复IF
位 - 陷阱门(Trap gate):进入中断后不改变
IF
位
需要注意的是,这里的中断门和陷阱门与中断类型里的”陷阱“是两个概念,是在设置中断向量表时自定义的两个行为。若想内核的逻辑比较简单,就可以禁止中断嵌套,把所有的中断描述符都设置为”中断门“;若设计的内核比较复杂,就可以允许部分中断的中断嵌套,设置它为”陷阱门“
Q:_alltraps
为什么压栈esp
作为参数?
A:回顾MIT-JOS系列3:启动内核中关于栈的说明,函数call
向栈中压入函数的参数,call
时压栈eip
以便函数返回时恢复执行接下来的代码,call
后压栈ebp
,当前esp
赋值给ebp
,然后做一些奇奇怪怪的寄存器操作,比如esi,edi
,看不懂,它们在这个问题里不重要(。
进入函数后栈大概长这样:
高地址:
参数n
...
参数1
eip
ebp(旧)
...
低地址
进入函数后,当前ebp
值为旧ebp+4
(因为push
了一个ebp
),因此
ebp[0]
:旧ebp
ebp[1]
:eip
ebp[2]
:参数1
...
函数进入后会根据函数头找参数的个数,trap
函数只有一个参数,需要一个指向trapframe
结构体的指针,即一个trapframe
结构体的首地址,这个地址就是push
进去的esp
的值,然后tf
赋值为这个地址,就能用tf->xxx
取到其他值。注意这里要的是地址而不是直接的值,所以push
进去了esp
(esp
指向栈最后一个push
进入的参数,最后的指令为pushal
,因此esp
指向tf.tf_regs.reg_edi
)
自动化改进
其实我没改进代码。。只是在这边讲一下原理。。
trapentry.S
里的一堆宏是没办法压缩的,但能通过一些方法压缩trap_init()
的代码,用循环自动填充
首先在宏调用之前:
.data
.global funs
funs:
然后宏里面:
.data
.long name
所有.data
节会合并。每次宏展开时,.data
段里的.long name
会合并到funs
里作为函数数组,因此在trap_init
里用extern char funs[]
然后用funs[i]
作为每个函数地址访问即可。对于几个空缺的中断号,以及结尾,可以插入.long -1
或.long 0
之类的进行辨别,然后在循环中根据funs[i]
的值特殊处理
中断处理小结
-
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
并导出到全局变量,即定义了一个全局可见的函数t_divide
。结合我们写的_alltraps
的代码,在中断触发、进入中断处理程序前发生:- 从tss找到内核栈的地址,临时保存旧栈的
ss, esp
,修改当前ss, esp
指向内核栈 - 向内核栈压入
旧ss, 旧esp, eflag, cs, eip
,若中断有错误码,自动压入错误码 cs, eip
指向中断处理程序入口,准备执行中断处理程序
从标号
t_divide
开始执行将发生:- 入栈中断错误码和中断号
num
(对于不需要错误码的中断TRAPHANDLER_NOEC
入栈0,对于需要错误码的中断,错误码已经由CPU自动入栈,因此TRAPHANDLER
只pushl
一个num
) - 调用
_alltraps
:- 将各寄存器的值按
Trapframe
的格式压栈(这里,在后续调用trap()
后,会从栈中顺序出栈数据赋值给参数的tf
结构体 - 利用
GD_KD
设置ds, es
,指向内核数据段 - 压栈
esp
作为参数 - 调用
trap()
处理中断,根据中断处理结果销毁原环境或继续执行
- 将各寄存器的值按
到这里,完成对每一个发生的中断的统一处理,之后在
trap()
中再根据中断号(由参数tf
中的trapno
读出)对不同中断分别处理 - 从tss找到内核栈的地址,临时保存旧栈的
-
trapentry.S
与trap_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
-
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),指出该中断出现时是否需要关中断防止中断嵌套;设置执行权限(用户可调用/仅内核可调用),确保用户态无法进行权限以外的操作。
-
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), butsoftint
's code saysint $14
. Why should this produce interrupt vector 13? What happens if the kernel actually allowssoftint
'sint $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
指令,可能会使用户干扰到内核的页面管理