- 学习Traps, interrupts, and drivers
1.保护性控制转移
异常和中断都是保护性控制转移,让处理器从用户模式切换到内核模式(CPL=0),这样用户代码不会对内核或者其他环境造成影响。中断是由外部异步事件导致的处理器保护性控制转移,比如外设I/O的通知信号。异常是正在运行的代码同步事件导致的控制转移,比如除零或者访问无效内存。
为了确保控制转移真的被保护,x86平台上有两种机制:
-
中断描述符表。处理器确保中断和异常引起内核进入特定的入口点。
- x86允许最多256个不同的中断和异常入口点,每个都有一个独特的中断向量。向量是0到255的数字,中断向量由中断源决定:不同设备,错误条件以及请求内核产生中断的不同向量。CPU使用向量作为索引访问处理器中断描述符表(IDT),这个是内核在内核私有内存设置的。处理器从合适的入口处加载:
- 加载到EIP寄存器的值,这是个指向处理这种类型异常的内核代码的指针
- 加载到CS寄存器的值,包含特权级0-1
- x86允许最多256个不同的中断和异常入口点,每个都有一个独特的中断向量。向量是0到255的数字,中断向量由中断源决定:不同设备,错误条件以及请求内核产生中断的不同向量。CPU使用向量作为索引访问处理器中断描述符表(IDT),这个是内核在内核私有内存设置的。处理器从合适的入口处加载:
-
任务状态段。处理器需要存放中断异常发生之前的旧的处理器状态,比如原始EIP值和CS值,这样可以之后还原到之前的状态。保存这个的位置必须要受保护不能随意修改。
因此,x86处理器处理中断时会导致特权级由用户转为内核,也会将堆栈切换到内核内存中。任务状态段具体指明段选择子和堆栈的地址。处理器将SS, ESP, EFLAGS, CS, EIP和可选错误码压入堆栈中,之后从中断描述符中加载CS和EIP,设置ESP和SS指向新的堆栈。
1.1.Interrupts
- External (hardware generated) interrupts.
- External interrupts are received through pins on the processor.
- Software-generated interrupts.
- The INT n instruction permits interrupts to be generated from within software by supplying an interrupt vector number as an operand.
1.2.Exceptions
- Processor-detected program-error exceptions.
- Software-generated exceptions.
- Machine-check exceptions
1.2.1.Program-Error Exceptions
The processor generates one or more exceptions when it detects program errors during the execution in an application program or the operating system or executive. Intel 64 and IA-32 architectures define a vector number for
each processor-detectable exception. Exceptions are classified as faults, traps, and aborts.
- Faults — A fault is an exception that can generally be corrected and that, once corrected, allows the program to be restarted with no loss of continuity. When a fault is reported, the processor restores the machine state to the state prior to the beginning of execution of the faulting instruction. The return address (saved contents of the CS and EIP registers) for the fault handler points to the faulting instruction, rather than to the instruction following the faulting instruction.
- Traps — A trap is an exception that is reported immediately following the execution of the trapping instruction.Traps allow execution of a program or task to be continued without loss of program continuity. The return address for the trap handler points to the instruction to be executed after the trapping instruction.
- Aborts — An abort is an exception that does not always report the precise location of the instruction causing the exception and does not allow a restart of the program or task that caused the exception. Aborts are used to report severe errors, such as hardware errors and inconsistent or illegal values in system tables.
1.2.2.Software-Generated Exceptions
The INTO, INT1, INT3, and BOUND instructions permit exceptions to be generated in software. These instructions allow checks for exception conditions to be performed at points in the instruction stream. For example, INT3 causes a breakpoint exception to be generated.
1.2.3.Machine-Check Exceptions
The P6 family and Pentium processors provide both internal and external machine-check mechanisms for checking the operation of the internal chip hardware and bus transactions.
2.xv6 trap/interrupts
trap(实现系统调用)和中断主要区别:
- trap是由当前进程触发的,中断是由设备产生的,可能和当前进程没有关系。
- 中断门和trap门只有一位不相同,具体是中断门清除 EFLAGS 寄存器的 IF 位。所以运行trap处理程序时,还是会响应其它中断。
2.1.system call
When a process needs to invoke a kernel service, it invokes a procedure call in the operating system interface. Such a procedure is called a system call. The system call enters the kernel; the kernel performs the service and returns. Thus a process alternates between executing in user space and kernel space.
The kernel uses the CPU’s hardware protection mechanisms to ensure that each process executing in user space can access only its own memory. The kernel executes with the hardware privileges required to implement these protections; user programs execute without those privileges. When a user program invokes a system call, the hardware raises the privilege level and starts executing a pre-arranged function in the kernel.
Each process’s address space maps the kernel’s instructions and data as well as the user program’s memory. When a process invokes a system call, the system call executes in the kernel mappings of the process’s address space. This arrangement exists so that the kernel’s system call code can directly refer to user memory. In order to leave room for user memory to grow, xv6’s address spaces map the kernel at high addresses, starting at 0x80100000.
系统调用流程图:
2.2.Interrupts
回答以上问题:
Q1: how to find interrupt handler?
1.Hardware maps interrupt type to interrupt number in the traps.h(xv6).
x86 Interrupt numbers:
1 // x86 trap and interrupt constants.
2 // Processor-defined:
4 #define T_DIVIDE 0 // divide error
5 #define T_DEBUG 1 // debug exception
6 #define T_NMI 2 // non-maskable interrupt
7 #define T_BRKPT 3 // breakpoint
8 #define T_OFLOW 4 // overflow
9 #define T_BOUND 5 // bounds check
10 #define T_ILLOP 6 // illegal opcode
11 #define T_DEVICE 7 // device not available
12 #define T_DBLFLT 8 // double fault
13 // #define T_COPROC 9 // reserved (not used since 486)
14 #define T_TSS 10 // invalid task switch segment
15 #define T_SEGNP 11 // segment not present
16 #define T_STACK 12 // stack exception
17 #define T_GPFLT 13 // general protection fault
18 #define T_PGFLT 14 // page fault
19 // #define T_RES 15 // reserved
20 #define T_FPERR 16 // floating point error
21 #define T_ALIGN 17 // aligment check
22 #define T_MCHK 18 // machine check
23 #define T_SIMDERR 19 // SIMD floating point error
24
25 // These are arbitrarily chosen, but with care not to overlap
26 // processor defined exceptions or interrupt vectors.
27 #define T_SYSCALL 64 // system call
28 #define T_DEFAULT 500 // catchall
29
30 #define T_IRQ0 32 // IRQ 0 corresponds to int T_IRQ
31
32 #define IRQ_TIMER 0
33 #define IRQ_KBD 1
34 #define IRQ_COM1 4
35 #define IRQ_IDE 14
36 #define IRQ_ERROR 19
37 #define IRQ_SPURIOUS 31
2.OS sets up Interrupt Descriptor Table ( also called interrupt vector) at boot.
- IDT is in memory
- Each entry is an interrupt handler
- OS lets hardware know IDT base
- Defines all kernel entry points
- Hardware finds interrupt handler using interrupt number as index into IDT
- handler = IDT[intr_number]
流程图:
2.1.Setting Up the IDT
Set up the IDT to handle interrupt vectors 0-31 (the processor exceptions). The header files inc/trap.h and kern/trap.h contain important definitions related to interrupts and exceptions that you will need to become familiar with. The file kern/trap.h contains definitions that are strictly private to the kernel, while inc/trap.h contains definitions that may also be useful to user-level programs and libraries.
2.2.The overall flow of control
IDT trapentry.S trap.c
+----------------+
| &handler1 |---------> handler1: trap (struct Trapframe *tf)
| | // do stuff {
| | call trap // handle the exception/interrupt
| | // ... }
+----------------+
| &handler2 |--------> handler2:
| | // do stuff
| | call trap
| | // ...
+----------------+
.
.
.
+----------------+
| &handlerX |--------> handlerX:
| | // do stuff
| | call trap
| | // ...
+----------------+
2.3.代码实现
每个异常或中断都应该在 trapentry.S 有自己的处理函数handler,trap_init() 用来初始化IDT。每个异常处理是一个保存在堆栈上的 struct Trapframe ,调用 trap() 指向这个 Trapframe。trap() 之后处理异常中断。
在kern/trapentry.S 中定义有两个宏TRAPHANDLER/TRAPHANDLER_NOEC,如下所示:
#define TRAPHANDLER(name, num) \
.globl name; /* define global symbol for 'name' */ \
.type name, @function; /* symbol type is function */ \
.align 2; /* align function definition */ \
name: /* function starts here */ \
pushl $(num); \
jmp _alltraps
#define TRAPHANDLER_NOEC(name, num) \
.globl name; \
.type name, @function; \
.align 2; \
name: \
pushl $0; \
pushl $(num); \
jmp _alltraps
这两个宏的作用:
利用TRAPHANDLER和TRAPHANDLER_NOEC宏来实现interrupt handler。这两个宏所生成的代码的作用是,根据传入参数定义handler的“标签”,然后进行一些压栈操作。如果系统没有压入错误码,就压入一个0;然后压入中断号,接下来跳转到一个名为_alltrap的函数。
每个handler都要在栈上构造一个结构体 struct trapframe (见inc/trap.h),并调用 trap.c 中的 trap()。
78 _alltraps:
79 pushl %ds
80 pushl %es
81 pushal
82 movw $GD_KD,%ax
83 movw %ax,%ds
84 movw %ax,%es
85 pushl %esp
86 call trap
- 值压入堆栈构造一个stuct Trapframe
- 加载 GD_KD 到 %ds 和 %es
- pushl %esp 将指向 Trapframe 的指针作为 trap() 的参数
- 调用 trap function
当中断发生后,CPU会自动切换到一个新的栈,然后自动地将当前运行程序的EFLAGS寄存器压入栈中,然后压入CS和IP,然后对于一些特殊的异常CPU还会压入error code,然后才会跳转到中断服务程序的第一条指令 – 这第一条指令就是在handler中。
然后,handler就开始压栈了。为了保证Trapframe结构的一致性和完整性,对于CPU没有压入error code的情况,TRAPHANDLER_NOEC定义的handler首先压入一个0用于占位。 接着,TRAPHANDLER和TRAPHANDLER_NOEC都压入了trap number。
最后,两者都跳转到_alltraps,这是每个handler都一样的代码,它的作用依然是继续补充Trapframe结构,注意压栈顺序要和Trapframe结构中的顺序一致。经过上述一系列操作之后,一个完整的Trapframe结构诞生了,也就意味着保存现场的工作完成。
gdb 调试_alltraps:
设置断点:
+ symbol-file obj/kern/kernel
(gdb) b *0x7c00
Breakpoint 1 at 0x7c00
(gdb) c
(gdb) b *0x100025
Breakpoint 2 at 0x100025
(gdb) b *0xf0105fbb //查看obj/kern/kernel.asm: _alltraps
Breakpoint 3 at 0xf0105f4e: file kern/trapentry.S, line 57.
如上所示,入栈之前ds=es=0x23,此时栈地址为0xefffffe0和0xefffffdc,执行push %ds 和push %es之前和之后,地址数据:
//push %ds
(gdb) x/x 0xefffffe0
0xefffffe0: 0xf01d3000
(gdb) si
=> 0xf0105fbc <_alltraps+1>: push %es
_alltraps () at kern/trapentry.S:84
(gdb) x/x 0xefffffe0
0xefffffe0: 0x00000023
//push %es
(gdb) x/x 0xefffffdc
0xefffffdc: 0xf01000e5
(gdb) si
=> 0xf0105fbd <_alltraps+2>: pusha
_alltraps () at kern/trapentry.S:85
(gdb) x/x 0xefffffdc
0xefffffdc: 0x00000023
trap_init() 使用这些handler的地址来初始化IDT。
74 SETGATE(idt[T_DIVIDE], true, GD_KT,t_divide, 0);
75 SETGATE(idt[T_DEBUG], true, GD_KT,t_debug, 0);
76 SETGATE(idt[T_NMI], false, GD_KT,t_nmi, 0);
77 SETGATE(idt[T_BRKPT], true, GD_KT,t_brkpt, 3);
78 SETGATE(idt[T_OFLOW], true, GD_KT,t_oflow, 0);
...
Q2:why switch stack?
参考资料:
Documents/work/code/jos/lab3
https://linbo.github.io/2017/10/22/xv6-syscall_1