NEMU PA 4 实验报告
一、实验目的
在前面的PA123中,我们分别实现了基本的运算单元,实现了各种指令和程序的装载,实现了存储器的层次结构。而在PA4中,为了让NEMU可以处理异常情况以及和外设交互,我们要做的事情有以下:
- PA4-1:为NEMU添加异常和中断支持
- PA4-2:为NEMU添加外设与IO支持
二、实验步骤
PA4-1 异常和中断响应
我们上课时了解到,打断系统运行的特殊事件有两样:异常和中断。它们俩的分类如下:
- 内部异常:在执行一条指令时,由处理器在其内部检测到的,与正在执行的指令相关的同步事件
- 故障:缺页、非法操作码、除数为零……
- 陷阱:用户程序主动调用操作系统处理例程
- 终止:执行指令时发生严重错误,如内存校验错误
- 外部中断:典型地由I/O设备触发,与当前正在执行的指令无关的异步事件
那么我们操作系统是怎样响应它们的呢?
-
OS需要先初始化中断描述符表IDT。
在NEMU中就是kernel在
init_cond()
函数中调用了位于kernel/src/irq/idt.c
的init_idt()
,这个函数中将IDT初始化如下:vec
的那一列就是定义的一系列异常和中断响应程序及其入口,实现位于kernel/src/irq/do_irq.S
。 -
然后进程执行过程中CPU检测到异常或中断后,立刻保护当前程序执行状态。
-
再根据异常和中断号去查表得到处理程序的入口地址,转到OS提供的异常/中断处理程序继续执行。
-
处理完后恢复现场,返回原程序继续。
好,现在让我们来看看NEMU是怎么具体实现对中断和异常的处理的:
首先,无论是中断还是异常的处理,第一阶段都是保护程序状态。这个操作由NEMU模拟的硬件实现,依次将EFLAGS, CS, EIP寄存器的值压栈。然后我们可以分开两种响应处理:
对中断的处理:
因为中断是属于一个外部信号,我们无法预测它会在啥时候到来,所以采用的方法就是让cpu执行指令的过程中保持对外部中断信号的检测。在nemu/src/cpu/cpu.c/exec()
中有一个do_intr()
函数,cpu每执行完一条指令,都会调用这个函数来检查是否有外部中断的到来。
#ifdef IA32_INTR
void do_intr()
{
if (cpu.intr && cpu.eflags.IF)
{
// get interrupt number
uint8_t intr_no = i8259_query_intr_no();
assert(intr_no != I8259_NO_INTR);
// tell the PIC interrupt info received
i8259_ack_intr();
// raise interrupt
raise_intr(intr_no);
}
}
#endif
对异常的处理:
我们在这里主要处理的异常为trap
(自陷)。自陷操作是可控的,是我们自己发出的一系列指令,在后面进行系统调用方面会大有用途。 我们先来看看trap
的样例文件:
#include "trap.h"
const char str[] = "Hello, world!\n";
int main()
{
asm volatile( "movl $4, %eax;"
"movl $1, %ebx;"
"movl $str, %ecx;"
"movl $14, %edx;"
"int $0x80");
HIT_GOOD_TRAP;
return 0;
}
在这里面,int $0x80
就是向CPU发出trap
信号的指令。它在NEMU中会调用这个函数:
void raise_sw_intr(uint8_t intr_no)
{
// return address is the
// next instruction
cpu.eip += 2;
raise_intr(intr_no);
}
可以看出,我们两种响应最后都是要调用raise_intr()
函数。这个函数的实现也是我们PA4-1的关键,我们需要在这个函数里完成:
-
根据异常或中断号
intr_no
查询IDT,这个intr_no
分别由用户和i8259中断控制器提供。 -
查询得到处理程序的入口地址。中断门和陷阱门的门描述符结构如下,不同之处在TYPE字段。
-
清除IF位如果当前信号位中断信号;
-
将EIP设置为查询得到的处理程序入口。
接下来就靠处理程序操作,然后通过iret
指令返回程序断点处就可以了。
所以,我们在PA4-1做了:
- 在
include/config.h
定义宏#define IA32_INTR
并且make clean; - 参照i386手册在
nemu/include/cpu/reg.h
中定义IDTR
结构体,并在CPU_STATE
中添加idtr
寄存器和中断引脚(框架代码已经提供) - 实现了包括
lidt
、cli
、sti
、int
、pusha
、popa
、iret
等指令
PA4-2 外设与IO
在这一章,我们需要完成NEMU与外界的交互,让NEMU可以正常进行输入与输出。
我们先来看看NEMU中CPU完成与外设通信的几种方式:
- 方式1:端口映射I/O(port-mapped I/O)
- 串口(Serial)、键盘(Keyboard)、硬盘(IDE)
- 方式2:内存映射I/O (Memory Mapped I/O, mmio)
- 显卡(VGA)
- 其它只需要理解:
- 声卡(Audio)实验性质
- 时钟(Timer)只产生时钟中断
可以将CPU和外设的交互简要概括为:
将外设的数据、状态、控制寄存器称为I/O端口;对端口进行编号;CPU使用in与out指令同端口间通过按编号“打电话”的方式通信。
设备制造商和OS可以约定占用的端口数和端口参数的设置,并且为OS提供相应的驱动程序。OS安装了相应的驱动后,驱动程序熟知这些约定,便可通过in和out指令完成对设备的控制和数据读写(直接控制法)。
我们在PA4-2里完成了对串口、硬盘、键盘的端口映射模拟,完成了对显卡的内存映射模拟。
三、思考题
本章没有明确的思考题,有几个要点:
PA4-1.跳转前决定是否允许中断嵌套?
-
当处理外部中断时,清除EFLAGS寄存器中的IF位,实现关中断,不允许嵌套
-
当处理内部异常时,不清除EFLAGS寄存器中的IF位,不关闭中断,允许嵌套
PA4-1.在函数irq_handle中,结合kernel/src/irq/do_irq.S,理解tf怎么传进来的?tf里面有什么?
我们可以观察到,tf
是TrapFrame
结构的一个指针,我们在上面将程序状态压栈以及进行int $0x80
前对寄存器压栈时就是按照这个结构的顺序来压栈的;而在call irq_handle
前,我们有一个这样的操作:
而这个时候esp的值就是tf
的首地址,我们结合TrapFrame
的结构来看:
可以明显看出只要有了首地址,后面的元素就可以通过指针直接访问。