操作系统实验Ucore:Kernel_init(四)
本文首发于我的博客
上一节进行到了kernel_init的printf_kernelinfo,继续往下分析
1.pmm_init
这个函数,顾名思义是用来初始化物理内存的函数,这个函数只会调用gdt_init()

这里我稍微修改了一下代码框架,原本的gdt_pd的定义为注释部分

这段代码看上去没问题,但是实际跑的时候就会发现gdt_pd的两个字段都是0。

我写了一个程序来模拟这种情况,看上去好像没有问题,但这个程序是无法通过编译的。

这个错误告诉我们编译器无法在执行前算出a1的地址。所以不可以这样初始化变量。
至于为什么会这样,改天再研究吧。现在先关注到实验上来。总之,这样改完之后,程序就可以正确运行了。
关于TSS的部分现在先不用管,然后就到了lgdt函数的位置了

我们要执行pmm_init的意义就在于我们需要重新为内核建立段描述符。第一次建立段描述符还是在bootasm中,那时候我们刚从实模式进入保护模式,那时候建立的段描述符有3个
-
空描述符
-
代码段描述符
-
数据段描述符
现在内核需要建立六个描述符

对于我们的操作系统来说,所有的段描述符都是可以访问4G的内存的,唯一的区别在于权限的不同,我们实际上并没有使用分段机制,这叫做 平坦内存模型(Flat memory model)。
在使用lgdt指令加载完gdt之后,刷新各个段寄存器。最后还会执行一条ljmp指令
关于这个ljmp,在实模式进入保护模式时也有涉及,但当时没有注意,现在看到书上的描述,才知道这样做的意义。
-
使用ljmp指令可以重新加载CS段寄存器,并刷新其对应的缓存器,使其指向正确的地址
-
在实模式进入保护模式时,这样做的另一个目的是清空流水线,在进入把保护模式时,ljmp之后的指令已经进入了流水线,而且完成了译码阶段,所以要清空流水线,重新按照32位模式加载指令。(理解这部分内容需要了解CPU流水线)
至此,内存的段描述符就建立完成了。gdt_init函数也全部执行完了
2.pic_init
这段程序没有深入的研究,其和操作系统的内核也没有太大的关系,主要是和初始化外设(另一个原因是我也不会…)。暂时先了解:
-
这个东西是 8259A芯片,pic的全称是(Programmable Interrupt Controller)可编程的中断控制器
-
所有的外部中断都不直接于CPU进行通讯,而是由8259A统一收集中断请求后再交给CPU
-
8259A芯片可以编程屏蔽部分外界中断,对于一个中断,其能被处理的先决条件是没有被8259A芯片屏蔽且CPU没有屏蔽外界中断(Eflags 寄存器的 IF 位)
-
一个8259A只有八个中断引脚,一般使用两个8259A组成级联(Cascade)关系。这样一共支持15个外部中断,我们可以编程来给中断引脚分配中断号
-
outb(IO_PIC1 + 1, IRQ_OFFSET);
outb(IO_PIC2 + 1, IRQ_OFFSET + 8);
这两句话比较关键,告诉我们主片的中断号从32开始,从片的从40开始
至于为什么从这里开始,是因为Intel将32以下的终端号保留使用,用户正常情况下可以使用32-255号中断
3.idt_init
初始化完8259之后,就要初始化中断的处理了。
这个部分比较复杂,需要多看几次才能理解,我也尽量讲清楚。

在idt_init中,需要初始化中断描述符表,这个表的位置在数据段中。一共有255个表项,对应着可以接收的255个中断号。由于CPU在接收到中断的时候会根据此项来确定中断处理函数的地址,所以一个idt表项中应当含有中断处理函数的CS:IP值,在这里就是CS段选择子和中断处理程序的偏移量。
我们使用kernel的代码段作为段选择子,偏移由__vectors数组指定。注意,现在所有的段选择子的基址都是0,也即是说,偏移的地址就是实际的物理地址。
__vectors数组存放在数据段中,依据255 * 4 的方式组织,每一项是一个指向对应异常处理函数地址的指针。
异常处理函数有255个,由vector开头加上异常号,这些函数连续的放在代码段中。

每一个异常处理函数的工作都是类似的,先push异常号,然后跳转,至于为什么这样,后文再说。
现在来总结一下一共用到了哪些变量
- idt[256] 位置:数据段 作用:用来根据异常号选择异常处理程序。
- __vectors 位置:数据段 作用:用来初始化idt,与中断处理无关。
- vectorX函数 位置:代码段 作用:实际的异常处理函数。
4.中断
之后就是初始化时钟,使能中断,我们主要来看看中断时如何处理 的。
关于中断的处理,需要分成两个部分,一个是CPU硬件的部分,一个是操作系统的部分。
首先说硬件,ucore的指导书上写的很详细,我这里总结一下,不过先阶段我并没有考虑权限的问题,因为第一个用户进程还没有起来,我们现在一直都在内核态。
cpu的工作为:
-
再执行每一条指令之后,都检查有没有中断产生
-
如果有中断,则根据 IDTR 找到 IDT,在根据中断编号找到对应的IDT表项
-
压入 eflags ,cs ,eip , errorcode
-
跳转到异常处理函数(根据idt的cs和eip)
cpu执行完之后,栈中就有了eflags,cs,ip和errcode。然后再由操作系统接手。
操作系统的工作:
首先调用的时 vectorX 函数,此函数push 异常号,调用 __alltraps 函数。

__alltraps 压入 ds ,es , fs ,gs再压入所有的通用寄存器,然后将段寄存器换成内核代码段,最后压入 esp,call trap 函数。
trap函数使用C语言写的,需要一个类型为 trapframe 的参数。真个过程的关键,就在于这个参数是怎么传递的。
5. 中断的参数传递
首先我们看一下从中断开始到进入trap函数,栈里究竟多了些什么。
CPU压入了eflags,cs,eip,errorcode。
vectorX压入了中断号
__alltraps压入了ds,es,fs,gs + 通用寄存器 + esp

画出图来就是这个样子。一般来说,函数的调用会由编译器来组织参数的传递,但是我们这次时从汇编语言到C语言只能自己来手动传递参数,更具上一篇文章的经验,我们知道,返回地址上面的一个数据就是函数的最左参数,这里是esp,也就是说,我们将esp当作第一个参数传入了trap函数
当我们压入esp时,esp指向的是我们压入的倒数第二个数据,也就是通用寄存器的最后一个。
再看esp上面的栈结构,比对一下trapframe结构体,会发现竟然是完全一模一样的!

传进去的参数就是一个指向trapeframe的指针,这里需要根据实际的内存来构造trapframe结构体,需要注意的就是,通用寄存器的地址最低(最后入栈),所以在结构体中的第一个,还有就是由于push的都是32位的数据,要对16位的段寄存器进行填充。
这样构造完成之后,在trap函数中就可以快乐的访问trapframe的内容了
当然到现在还只是猜测,我门需要验证一下,打开gdb,扫描一下内存,就会发现:

从高地址往低地址,依次是
0x00000216:eflags
0x00000008:cs (内核数据段,第一个段描述符,偏移8)
0x0010007a: eip(对应lab1里的while(1)代码)
0x00000000:errorcode(这个不太清楚,可能没有)
0x00000020:32,中断号
0x00000010:内核数据段(ds,第二个段描述符,偏移16)
。。。
0x00007b8c:作为参数的esp
0x00102bf1:__alltraps函数的返回地址
事实证明,内存的分布确实如此
6.code
第二个编程任务,要求我们每100个ticks在终端输出100ticks
有了前面的基础,这个就非常简单了,只要在trap_dispath里加几句就行。

效果就是每秒输出一个“100 ticks”,同理,其他的异常也在这个函数里实现。
7.总结
到这里,Lab1就算做完了,整体上入门实验还是比较困难的,要求的前置知识有很多。我也是尽量的弄明白一些部分,还有些不太懂得就先跳过了…
下一篇来回答Lab1提出的的几个问题
如有错误,欢迎指出~