2-setup_gdt.md (#22)
authorFFreestanding <62629010+FFreestanding@users.noreply.github.com>
Mon, 22 Jan 2024 14:00:43 +0000 (22:00 +0800)
committerGitHub <noreply@github.com>
Mon, 22 Jan 2024 14:00:43 +0000 (14:00 +0000)
docs/tutorial/2-setup_gdt.md [new file with mode: 0644]
docs/tutorial/3-interrupts.md [new file with mode: 0644]

diff --git a/docs/tutorial/2-setup_gdt.md b/docs/tutorial/2-setup_gdt.md
new file mode 100644 (file)
index 0000000..3571b78
--- /dev/null
@@ -0,0 +1,121 @@
+## 准备工作
+
+```sh
+git checkout fedfd71f5492177a7c7d7fd2bd1529a832106395
+```
+
+观看相应视频
+
+## 代码分析
+
+这个commit实现了分段。
+
+libs/libc/string下面是一些字符串操作函数,比较基础,所以略过。
+
+主要分析boot.S和gdt.c中的代码
+
+**为了分段,我们需要准备GDT,还要使用GDTR存放GDT的入口地址。**
+
+`gdt.c`中负责构造好GDT。
+
+```c
+#define GDT_ENTRY 5
+
+uint64_t _gdt[GDT_ENTRY];
+uint16_t _gdt_limit = sizeof(_gdt);
+```
+
+GDT实际就是数组,一个单位(段描述符)是64bits。所以数组类型是`uint64_t`。_gdt_limit是GDT的大小。
+
+```c
+void _set_gdt_entry(uint32_t index, uint32_t base, uint32_t limit, uint32_t flags) {
+    _gdt[index] = SEG_BASE_H(base) | flags | SEG_LIM_H(limit) | SEG_BASE_M(base);
+    _gdt[index] <<= 32;
+    _gdt[index] |= SEG_BASE_L(base) | SEG_LIM_L(limit);
+}
+```
+
+`_set_gdt_entry`函数负责自动地构造GDT的一个entry(段描述符)。根据资料,参数base要分成两端,保存到56-63bits(`SEG_BASE_H(base)`)和16-39bits(`SEG_BASE_M(base)`、`SEG_BASE_L(base)`)的位置[1]。limit保存到48-51bits(`SEG_LIM_L(limit)`)的位置。index表示设置第几号描述符。参数flags稍后分析。
+
+```c
+void
+_init_gdt() {
+    _set_gdt_entry(0, 0, 0, 0);
+    _set_gdt_entry(1, 0, 0xfffff, SEG_R0_CODE);
+```
+
+第0号entry默认为空,第1号的base为0、limit为0xfffff。这种分段模式叫做平坦模式。一个指令需要访问一个数据,地址设为x。需要先通过ds得到一个段描述符(base为0、limit为0xfffff)。因为标志位G为1,所以要在limit的20位的后面补充`fff`得到真实的地址范围上限,即x取值范围是`0x0`-`0xffffffff`。再通过0+x得到最后的地址。可以发现这个范围会映射到32位的所有内存地址。
+
+flags为`SEG_R0_CODE`(权限为ring0的代码段)。
+
+```c
+#define SEG_R0_CODE         SD_TYPE(SEG_CODE_EXRD) | SD_CODE_DATA(1) | SD_DPL(0) | \
+                            SD_PRESENT(1) | SD_AVL(0) | SD_64BITS(0) | SD_32BITS(1) | \
+                            SD_4K_GRAN(1)
+```
+
+这些标志位都可以查看资料了解,`SD_DPL(0)`表示该段权限为ring0。
+
+接下来使用GDTR存放GDT的入口地址
+
+```assembly
+.section .text
+    .global start_
+    .type start_, @function     /* Optional, this just give the 
+                                 * linker more knowledge about the label 
+                                 */
+    start_:
+        movl $stack_top, %esp
+        /* 
+            TODO: kernel init
+                1. Load GDT
+                2. Load IDT
+                3. Enable paging
+        */
+        call _kernel_init
+        
+        subl $0x6, %esp
+        movl $_gdt, 2(%esp)
+        movw _gdt_limit, %ax
+        movw %ax, (%esp)
+        lgdt (%esp)
+        addl $0x6, %esp
+```
+
+使用lgdt来设置GDTR的值
+
+`lgdt`指令的操作数为6字节,2个低位字节为GDT的大小减1,4个高位字节为GDT的32位地址。
+
+在`_kernel_init`调用后,`subl $0x6, %esp`抬高了栈顶,得到6字节的位置。然后把`$_gdt`、`_gdt_limit`保存到栈上。
+
+```assembly
+        movw $0x10, %cx
+        movw %cx, %es
+        movw %cx, %ds
+        movw %cx, %fs
+        movw %cx, %gs
+        movw %cx, %ss
+```
+
+es等寄存器指向2号描述符(ring0数据段)
+
+利用retf把0x8写入cs寄存器,让cs指向ring0代码段
+
+```assembly
+        pushw $0x08
+        pushl $_after_gdt
+        retf
+
+    _after_gdt:
+        pushl %ebx
+        call _kernel_main
+
+        cli
+    j_:
+        hlt
+        jmp j_
+```
+
+## 参考
+
+[1]https://wiki.osdev.org/Global_Descriptor_Table
diff --git a/docs/tutorial/3-interrupts.md b/docs/tutorial/3-interrupts.md
new file mode 100644 (file)
index 0000000..2039221
--- /dev/null
@@ -0,0 +1,160 @@
+## 准备工作
+
+```sh
+git checkout fedfd71f5492177a7c7d7fd2bd1529a832106395
+```
+
+观看相应视频
+
+## 代码分析
+
+软件或者硬件中断发生后,CPU会通过一个表去寻找一个函数指针,并调用。那么我们需要弄清楚怎么找怎么调用。
+
+比如我们使用`int $0`(软件中断)会触发0号异常,CPU会根据IDTR来寻找IDT。最后根据IDT的第0项(不同于0号GDT为空)并把相关中断信息保存到栈上,最后调用指定函数。
+
+IDT也是由8字节,也是通过IDTR来定位。boot.S新增了下面这段来初始化IDTR。和GDTR差不多,略过。
+
+```assembly
+        movl $_idt, 2(%esp)
+        movw _idt_limit, %ax
+        movw %ax, (%esp)
+        lidt (%esp)
+```
+
+接下来看看如何设置IDT的entry。
+
+```c
+void _set_idt_entry(uint32_t vector, uint16_t seg_selector, void (*isr)(), uint8_t dpl) {
+    uintptr_t offset = (uintptr_t)isr;
+    _idt[vector] = (offset & 0xffff0000) | IDT_ATTR(dpl);
+    _idt[vector] <<= 32;
+    _idt[vector] |= (seg_selector << 16) | (offset & 0x0000ffff);
+}
+```
+
+把段选择子(段寄存器存储的值)保存到16-31bits的位置,因为我们存的相当于函数指针,所以段选择子的值是CS段寄存器保存的值(指向代码段)。分段中设置了CS值为0x8。函数指针分成两个部分保存,48-63bits位置保存高16位,0-15bits保存低16位。根据Abort、Trap、Fault类型来设置8-11位。最后设置一下剩余的标志位即可。
+
+下面是已经安装了一个0号异常处理函数
+
+```c
+void
+_init_idt() {
+    _set_idt_entry(FAULT_DIVISION_ERROR, 0x08, _asm_isr0, 0);
+}
+```
+
+`_asm_isr0`实现在`arch/x86/interrupt.S`中下面代码
+
+```assembly
+.section .text
+    isr_template 0
+```
+
+这里用到了一个伪指令`.macro`,起到宏的作用。
+
+```assembly
+.macro isr_template vector, no_error_code=1
+    .global _asm_isr\vector
+    .type _asm_isr\vector, @function
+    _asm_isr\vector:
+        .if \no_error_code
+            pushl $0x0
+        .endif
+        pushl $\vector
+        jmp interrupt_wrapper
+.endm
+```
+
+vector这里是0,那么`_asm_isr\vector`就是`_asm_isr0`。`no_error_code=1`表示默认值为1。
+
+最后可以转换成下面代码。
+
+```assembly
+    .global _asm_isr0
+    .type _asm_isr0, @function
+    _asm_isr0:
+        .if 1
+            pushl $0x0
+        .endif
+        pushl 0
+        jmp interrupt_wrapper
+```
+
+这里是处理异常的部分,执行到了这里,CPU会把EFLAGS、CS、EIP保持在栈上给我们处理。也可能多push一个error code。这段宏的作用是根据是否需要手动push error code来定义函数,从而我们可以规范化这个结构,统一使用结构体`isr_param`来描述。`#pragma pack(push, 1)`和`#pragma pack(pop)`表示不对该结构体进行对齐。因为CPU给我们提供的数据也没有进行对齐处理。
+
+```c
+#pragma pack(push, 1)
+typedef struct {
+    unsigned int vector;
+    unsigned int err_code;
+    unsigned int eip;
+    unsigned short cs;
+    unsigned int eflags;
+} isr_param;
+#pragma pack(pop)
+```
+
+因为栈没有对齐,而且调用函数前要先对齐栈,所以先要向上对齐,`movl %eax, (%esp)`把旧栈顶保存起来,调用`interrupt_handler`。
+
+```assembly
+    interrupt_wrapper:
+
+        movl %esp, %eax
+        andl $0xfffffff0, %esp
+        subl $16, %esp
+        movl %eax, (%esp)
+
+        call interrupt_handler
+        pop %eax
+        movl %eax, %esp
+        addl $8, %esp
+
+        iret
+```
+
+调用完后,恢复旧栈顶,再恢复成未对齐的状态,执行iret(需要error code在栈上)。所以push error code是必要的。
+
+因为栈顶保存的是旧栈顶的值,所以参数param指向旧栈顶。旧栈顶的值正好和param的字段一一对应。根据vector来分配调用指定函数。
+
+```c
+void 
+interrupt_handler(isr_param* param) {
+    switch (param->vector)
+    {
+        case 0:
+            isr0(param);
+            break;
+    }
+}
+```
+
+isr0的功能就是清屏并打印信息。
+
+```c
+void isr0 (isr_param* param) {
+    tty_clear();
+    tty_put_str("!!PANIC!!");
+}
+```
+
+## FAQ
+
+**1.为什么本应该push error code的情况却没有push?**
+
+int指令主动触发的中断不会push error code。
+
+> 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]
+
+下面这种被动触发会push error code。
+
+```c
+int a = 1/0;
+```
+
+**2.为什么有时候无法追踪异常来源?**
+
+触发了Abort。之后我们会写一个打印内核调用栈的功能,触发Abort后,如果试图打印内核函数调用栈来搜索问题代码,最后会得到不可信的结果(大概率会访问未映射的内存,又触发General Protection)。
+
+## 参考
+
+[1]Intel手册,Volume 3,6.4.2 Software-Generated Exceptions