本文接前文—— 《自己动手写操作系统》——制作一个简单的操作系统-CSDN博客内容,回答为什么机器开机后会执行这段汇编代码,这段代码有什么特殊之处?
背景知识
8086CPU
- 处理器是16位,地址总线是20位,访存范围是1MB。
CS和IP寄存器
参考CS和IP寄存器的作用及执行分析_cs寄存器和ip寄存器的作用-CSDN博客得到这几点关键结论:
- 任何时刻,8086CPU都会将CS:IP指向的指令作为下一条需要取出的执行指令
- 8086CPU中的计算公式为 (CS << 4)|IP, 即CS左移4位,然后再加上IP
BIOS
- CPU加电之后,硬件会把一个ROM芯片映射到地址空间
0xF0000~0xFFFFF
上。同时,会把CS寄存器的值改为0xFFFF,其余寄存器的值全部清零,因此此时CS:IP=0xFFFF:0x0000=0xFFFF0
,即下一条即将执行的指令位置。而0xFFFF0~0xFFFFF
之间其实只有16B的空间,这里只是一条跳转指令,跳转到BIOS真正的入口地址:
jmp far f000:e05b #- 这条指令会将CS设为0xf000,将IP设为0xe05b,对应的指令地址是0xfe05b
- CPU随后开支执行BIOS上的代码,主要是是硬件输入输出设备相关的检查,以及建立一个最初的中断向量表(位于
0x00000~0x003FF
之间); - BIOS 代码最后阶段的工作,就是检查启动盘上的 mbr 分区,所谓 mbr 分区就是磁盘上的第一个 512B 内容,又叫引导分区;BIOS 会对这 512B 做一个检查:它的最后2个字节必须是两个 magic number:0x55 和 0xaa,否则它就不是一个合法的启动盘;
- 检查通过后,BIOS 将这 512B 加载到内存
0x07C00 ~0x07E00
之间,然后指令跳转到0x07C00
开始执行,至此 BIOS 退出舞台;
系统启动
启动image是由 bootsect、setup 和 system 三个顺序合并形成的;bootsect 是由 bootsect.s 汇编产生的;setup是由 setup.s 汇编产生的;system 是由进程模块、内存模块、设备驱动、初始化模块等部分组成的。
bootsect.s
这个汇编程序编译之后大小严格为512B,且满足最后两个字节是0x55 和 0xaa,它主要完成以下几件事情:
- 将引导磁盘上的第2~5这四个扇区构成的 setup 模块读入到了内存的0x90200 处;
- 将引导磁盘上的第6个扇区开始读取长度为 SYSSIZE 的操作系统 system 模块,并将其存放到内存的 0x10000 处。
由此可见,这段代码的作用是将setup和system模块加载到内存中。
setup.s
setup.s 的主要任务包括:(1)获取一些硬件参数,包括内存大小、磁盘信息、硬件信息等;(2)在实模式下先完成 GDT 表的形成以及初始化;(3)在实模式下将整个 system 模块拖动到 0x0 地址处;(4)进入保护模式。
进入保护模式之后,就允许访问1MB之后的扩展内存,供上层应用使用。而1MB就留给操作系统,其中:
- 0x00000~0x90000存放sysetem模块;
- 0x90000~0x100000存放重要参数。
head.s
head.s是在32保护模式下执行的代码,主要任务包括:
- 设置中断表 IDT
- 设置 GDT 表
- 设置页表
- 跳到操作系统的初始化代码
main.c
main.c的主要任务是读取各种参数完成初始化,并启动一个shell终端,并响应shell命令。至此,操作系统相当于启动完毕。
实模式和保护模式
实模式
早期CPU时期,地址总线是20位,8个16位通用寄存器和4个16位段寄存器,通过物理地址 = 段基址<<4 + 段内偏移
这种方式计算物理地址,访问范围为1MB。
保护模式
后来的CPU,地址总线是32位,寄存器也是32位,因此内存访问范围为4GB,但是依然采用段地址:段内偏移的方式表示地址。
- 首先从gdtr寄存器(48位寄存器,保存了GDT基址和GDT长度)拿到GDT的基址,从段寄存器中拿到索引(16位);
- 结合基址和索引,从GDT中拿到对应的描述符(每个描述符占据8个字节,存储了段基址、段界限和段属性);
- 从GDT条目中获取段基址base(32位);
- 结合寄存器中的偏移地址offset(32位),可以计算出线性地址
线形地址 = 段基址 + 段内偏移
- 通过MMU和分页机制,实现由线性地址转换为物理地址。