2-setup_gdt.md (#22)
[lunaix-os.git] / docs / tutorial / 3-interrupts.md
1 ## 准备工作
2
3 ```sh
4 git checkout fedfd71f5492177a7c7d7fd2bd1529a832106395
5 ```
6
7 观看相应视频
8
9 ## 代码分析
10
11 软件或者硬件中断发生后,CPU会通过一个表去寻找一个函数指针,并调用。那么我们需要弄清楚怎么找怎么调用。
12
13 比如我们使用`int $0`(软件中断)会触发0号异常,CPU会根据IDTR来寻找IDT。最后根据IDT的第0项(不同于0号GDT为空)并把相关中断信息保存到栈上,最后调用指定函数。
14
15 IDT也是由8字节,也是通过IDTR来定位。boot.S新增了下面这段来初始化IDTR。和GDTR差不多,略过。
16
17 ```assembly
18         movl $_idt, 2(%esp)
19         movw _idt_limit, %ax
20         movw %ax, (%esp)
21         lidt (%esp)
22 ```
23
24 接下来看看如何设置IDT的entry。
25
26 ```c
27 void _set_idt_entry(uint32_t vector, uint16_t seg_selector, void (*isr)(), uint8_t dpl) {
28     uintptr_t offset = (uintptr_t)isr;
29     _idt[vector] = (offset & 0xffff0000) | IDT_ATTR(dpl);
30     _idt[vector] <<= 32;
31     _idt[vector] |= (seg_selector << 16) | (offset & 0x0000ffff);
32 }
33 ```
34
35 把段选择子(段寄存器存储的值)保存到16-31bits的位置,因为我们存的相当于函数指针,所以段选择子的值是CS段寄存器保存的值(指向代码段)。分段中设置了CS值为0x8。函数指针分成两个部分保存,48-63bits位置保存高16位,0-15bits保存低16位。根据Abort、Trap、Fault类型来设置8-11位。最后设置一下剩余的标志位即可。
36
37 下面是已经安装了一个0号异常处理函数
38
39 ```c
40 void
41 _init_idt() {
42     _set_idt_entry(FAULT_DIVISION_ERROR, 0x08, _asm_isr0, 0);
43 }
44 ```
45
46 `_asm_isr0`实现在`arch/x86/interrupt.S`中下面代码
47
48 ```assembly
49 .section .text
50     isr_template 0
51 ```
52
53 这里用到了一个伪指令`.macro`,起到宏的作用。
54
55 ```assembly
56 .macro isr_template vector, no_error_code=1
57     .global _asm_isr\vector
58     .type _asm_isr\vector, @function
59     _asm_isr\vector:
60         .if \no_error_code
61             pushl $0x0
62         .endif
63         pushl $\vector
64         jmp interrupt_wrapper
65 .endm
66 ```
67
68 vector这里是0,那么`_asm_isr\vector`就是`_asm_isr0`。`no_error_code=1`表示默认值为1。
69
70 最后可以转换成下面代码。
71
72 ```assembly
73     .global _asm_isr0
74     .type _asm_isr0, @function
75     _asm_isr0:
76         .if 1
77             pushl $0x0
78         .endif
79         pushl 0
80         jmp interrupt_wrapper
81 ```
82
83 这里是处理异常的部分,执行到了这里,CPU会把EFLAGS、CS、EIP保持在栈上给我们处理。也可能多push一个error code。这段宏的作用是根据是否需要手动push error code来定义函数,从而我们可以规范化这个结构,统一使用结构体`isr_param`来描述。`#pragma pack(push, 1)`和`#pragma pack(pop)`表示不对该结构体进行对齐。因为CPU给我们提供的数据也没有进行对齐处理。
84
85 ```c
86 #pragma pack(push, 1)
87 typedef struct {
88     unsigned int vector;
89     unsigned int err_code;
90     unsigned int eip;
91     unsigned short cs;
92     unsigned int eflags;
93 } isr_param;
94 #pragma pack(pop)
95 ```
96
97 因为栈没有对齐,而且调用函数前要先对齐栈,所以先要向上对齐,`movl %eax, (%esp)`把旧栈顶保存起来,调用`interrupt_handler`。
98
99 ```assembly
100     interrupt_wrapper:
101
102         movl %esp, %eax
103         andl $0xfffffff0, %esp
104         subl $16, %esp
105         movl %eax, (%esp)
106
107         call interrupt_handler
108         pop %eax
109         movl %eax, %esp
110         addl $8, %esp
111
112         iret
113 ```
114
115 调用完后,恢复旧栈顶,再恢复成未对齐的状态,执行iret(需要error code在栈上)。所以push error code是必要的。
116
117 因为栈顶保存的是旧栈顶的值,所以参数param指向旧栈顶。旧栈顶的值正好和param的字段一一对应。根据vector来分配调用指定函数。
118
119 ```c
120 void 
121 interrupt_handler(isr_param* param) {
122     switch (param->vector)
123     {
124         case 0:
125             isr0(param);
126             break;
127     }
128 }
129 ```
130
131 isr0的功能就是清屏并打印信息。
132
133 ```c
134 void isr0 (isr_param* param) {
135     tty_clear();
136     tty_put_str("!!PANIC!!");
137 }
138 ```
139
140 ## FAQ
141
142 **1.为什么本应该push error code的情况却没有push?**
143
144 int指令主动触发的中断不会push error code。
145
146 > If INT n provides a vector for one of the architecturally-defined exceptions, the processor generates an interrupt to the correct vector (to access the exception handler) but does not push an error code on the stack.[1]
147
148 下面这种被动触发会push error code。
149
150 ```c
151 int a = 1/0;
152 ```
153
154 **2.为什么有时候无法追踪异常来源?**
155
156 触发了Abort。之后我们会写一个打印内核调用栈的功能,触发Abort后,如果试图打印内核函数调用栈来搜索问题代码,最后会得到不可信的结果(大概率会访问未映射的内存,又触发General Protection)。
157
158 ## 参考
159
160 [1]Intel手册,Volume 3,6.4.2 Software-Generated Exceptions