内核1
在前一篇文章提到了,我们将内核的代码放置在了内存的1MB处,其实只要留给loader足够大的空间,放在哪里都可以。(但是要考虑用户态代码的存放位置。)但是我们还有一个没有解决的问题就是,我们如何将启动信息作为参数传递给内核?
C语言函数调用的过程
在C语言中,栈为函数提供了调用所需的内存空间。每个函数都有属于自己的栈帧,其中包括函数的局部变量、参数、返回地址等信息。当一个函数调用另一个函数时,被调用的函数的栈帧被压入栈中,然后控制权转移到被调用函数。被调用函数执行完毕后,控制权返回给调用函数,调用函数的栈帧被弹出栈中,恢复到调用前的状态。在函数调用时,先将返回地址压入到栈中,然后函数参数从右至左压入栈中,接着对应的call指令自动压入返回地址到栈中调用函数。(第一个返回地址是函数的返回地址,第二个是call指令下一条指令的地址。)如果函数有参数,则通过esp寄存器的值加上对应的偏移量来取函数参数。不过我们跳转到kernel之后首先进入的是汇编环境,编译器不会帮助我们来完成自动的函数调用,所以我们要手动将传递给kernel的参数压入栈中。
好吧我知道这么说会很抽象,因为我写着写着就晕了,我们来看一下栈中的内容来更好的理解一下上面那段话。
通过观察栈中内存的内容我们可以发现只要取出esp+4处的内容压入栈中就可以得到参数供函数调用了。所以代码为
1 | push 4(%esp) |
(挂上一个参考视频,讲解了CSAPP书中的内容)
【CSAPP-深入理解计算机系统】3-7. 过程(函数调用)_哔哩哔哩_bilibili
GDT表
进入内核后,我们首先要来重新设置一下最核心、最重要的结构之一:GDT表。为什么它很重要呢?附上一张图片:
在这张图中我们可以看到GDT表位于最中间的位置,图中显示了系统调用、中断异常处理、进程等功能都与GDT表有关。表中的项叫做段描述符(sigment descriptor),指向了某一块内存区域。其结构如下:
段描述符的结构大致可以分为三类:基地址、段界限值、属性值。所以一个GDT表可以看作是段描述符这个结构体的数组。
1 |
|
然后在初始化cpu函数中对gdt表进行初始化即可。在gdt表中,选择子代表了表项的索引,即表项相对于gdt表的偏移量。代码中selector >> 3是将选择子(位)转化成选择子(字节)来找到对应的表项。这个道理跟
1 | int a[10]; |
是一样的。
保护模式平坦模型简介
在这个系统中我们使用了平坦模型作为内存的模型(所有段的段起始地址设为0,长度设为4G,分页大小4KB)。所以从逻辑地址到线性地址的转换为:
①从段寄存器中取出段选择子
②根据段选择子查找GDT表项获得基地址
③基地址+偏移量
段选择子的结构为
3~15位的Index为段选择子的索引,取出index后还要将其右移三位才是索引值,对应了上面的selector >> 3。
重新加载GDT表
介绍完了平坦模型,我们就要在GDT表中加入代码段、数据段的表项了。由于第零个表项是系统占用,所以我们将代码段的选择子设置为0x08,数据段的选择子设置为0x10。根据平坦模型的特点,base为0,limit是4GB,然后设置属性值,再使用lgdt指令将gdt表加载到gdtr寄存器中,将数据段选择子加载到段寄存器中。
关于属性值,详细信息请参考intel系统编程手册。数据段设置属性值:段存在、特权级0、普通段、数据段、可读写、32位。代码段设置属性值:段存在、特权级0、普通段、代码段、可读取、32位。
进入内核后第一件工作已经做完了。之后要做的就是操作系统的重要功能之一:中断与异常处理。