此文章首发于我的博客:我的博客,博客上的排版更好一些…
首先,先放一张我画的Bootasm阶段(调用bootmain之前)的内存分布图
注:这个内存分布图只标识了大概位置,并没有考虑对齐
在此基础上,再来分析ucore的启动代码
1.启动地址
首先我们要知道第一条指令的地址在哪,当然,第一条指令指的时操作系统执行的指令。根据上图的描述,第一条指令的地址是0x7c00.
在makefile中,这一段程序指定了bootblock的入口函数为 start,其实地址为 0x7c00.使用gdb 调试可以很容易的验证我们的猜想。
这个0x7c00不是随意指定的,BIOS会磁盘的第一个扇区,也就是主引导扇区加载到0x7c00处,然后再将pc指向0x7c00.所以如果我们想要运行程序,就一定要先放在0x7c00处。
关于主引导扇区,可以看一下我之前的文章
2.执行指令
前两条指令是 cli 和 cld
cli 表示禁止中断,关于cld,我查到的资料是
cld使DF 复位,即是让DF=0,std使DF置位,即DF=1.这两个指令用于串操作指令中。通过执行cld或std指令可以控制方向标志DF,决定内存地址是增大(DF=0,向高地址增加)还是减小(DF=1,向地地址减小)。
然后就是 清零 ax 寄存器,并把ds,es,ss段寄存器赋值为0
此时我们还是在16位模式下的
这一段的作用是使能A20门.(标号是 set a20 我看了好久都没看出来…)关于A20门的作用,在ucore的文档上就有,大致是需要使能A20门才可以访问高于1MB的内存
3.A20门
我们具体来看一下是怎么使能A20门的,这部分的内容需要一个前置知识,就是CPU的端口.
这段代码里使用的0x60和0x64端口叫做 “8042” PS/2 Controller 是键盘的端口
文档在这里
0x64端口在读写时有着不同的意义,读时是状态寄存器,写时是命令寄存器
在向端口写入之前,我们要先确定端口是否可用。从0x64端口读出数据到al寄存器,如果第二个bit不是0,对应上图 Input buffer 的状态为 full ,就循环等待直到其为0。确认端口可用后,就向端口写入0xd1。
像0x64端口写入0xD1意味着我们要往0x60端口写数据。而且在写之前必须确认端口是空的。
最后,我们向0x60端口写入0xDF(第一个bit为1),开启A20门
4.加载全局描述符表寄存器GDTR
GDTR是一个48位的寄存器,其中高32位为基地址,低16位为段界限。这里把从gdtesc开始的6Byte载入GDTR中
gdt的描述在代码的最后,其内存位置(第一张图)也在高位。gdt里包含了三个段描述符,第一个是空描述符,第二个是代码段,第三个是数据段。
gdtesc中的低16位的十进制为23,代表gdt长度为(3*8 -1) = 23Byte,高32为是gdt的基地址。
在使用GDB调试的时候可以验证我们的猜想,光标部分就是三个段描述符,后面的 0x 7c540017 和 0x89550000是小段序,对应着 0x 0017 和 0x007c54. 前者对应段描述符的长度,后者对应起始地址。
加载完gdt后,将cr0的PE位置为1,进入保护模式
5.初始化保护模式
这里ljmp展开为 ljmp 0x8, protcseg
0x8 就是我们刚刚设置的 代码段 的段描述符,基址为0。因此会跳转到 protcseg标号处。
再接着就是把除了cs的所有段寄存器置为 0x10 , 即数据段的段描述符
6. Go to bootmain
最后一步,置ebp = 0 -> 栈底从0x7c00位置开始
置esp = start(0x7c00)-> 保存返回地址(当然不因该返回)
调用bootmain
由此也可以看出我们的栈内存空间是 0x0 到 0x7c00 的一段大概31KB的空间
完成了上述的几步之后,程序就跳转到了bootmain
7.结语:
零碎的看了好多次ucore,这次要从头开始认真做上面的文档有些看的不是很详细,有错误欢迎大家指出