kernel v0.1.1¶
机器上电启动,BIOS完成硬件自检、中断初始化等,按照BIOS配置读取第一个设备、第一柱面、第0扇区的前 512B 数据到内存中,然后检测这512B数据的最后两个字节是否是0xAA55,是的话就说明是可引导的操作系统,于是开始执行这段代码,这段代码默认被读到内存的 0x07C0的位置。
boot/bootsect.s 就是生成这段代码的,其大小一定小于512B且最后两个字节是 0xAA55
1. boot/bootsect.s¶
在内存中的起始地址是: 0x07C0
把自己(bootsect.s编译后的二进制)从 0x7C00 复制到 0x9000
然后跳转到 0x9000 开始执行,此处需要注意,跳转后使用了
jmpi go,INITSEG并不是从 0x9000 处开始执行,而是以 0x9000 位基址调到相对的 go 位置,也就是继续执行代码调整寄存器:DS、ES、SS、SP
读取磁盘接下来的4个扇区(1个扇区512B,4个是2KB),读取到 0x90200 开始的位置,也就是 setup.s 编译后的二进制
输出字符串 “Loading system …”,继续开始读取操作系统内核剩余部分(head.s编译后的二进制加内核部分)
开始读取操作系统 ES = 0x10000 (这块代码量比较大)
2. boot/setup.s¶
跳转到 setup.s 处开始执行。
读取硬件信息
关中断
将 system 移动到 0x00000 处(原来位于0x10000位置,此操作将BIOS在0x00000位置保存的中断向量表以及BIOS数据区完全覆盖)
加载IDT
加载GDT
设置8259A中断控制器
开启保护模式(设置PE位)
跳转到head.s 中
3.boot/head.s+内核¶
在header.s 中将寄存器从实模式转为保护模式(设置DS、ES、FS、GS的GDT表中的某一个全局描述符,这个全局描述符里保存着段的基址、段长、特权级等信息)
设置IDT
设置GDT
重新加载所有段寄存器
设置SS段选择符、ESP
确定A20是否打开
检测协处理器是否存在(存在则进行设置)
把要传递的参数进行压栈、把main函数的地址进行压栈
打开分页机制(设置成功并返回后开始执行 main 函数),将页目录表和4个页表放在内存的起始位置(这是操作系统能够掌控全局、掌控进程在内存中安全运行的基石),将CR3指向页目录表,设置CR0的PG位,启动分页寻址模式
开始执行内核里的main函数
4. init/main.c¶
规划内存布局
内存初始化,每个内存页都置为 空闲状态
中断初始化,给每个中断设置中断处理程序
块设备初始化,块设备请求队列(32个),struct request 用于表示块设备的输入/输出请求,它包含了描述和跟踪设备操作的相关信息。将请求按顺序链接成队列,内核可以有效处理多个并发的设备操作。
字符设备初始化
tty 初始化,串口设置、显示器接口设置
时间初始化,读取 CMOS 时间
进程控制初始化,在GDT中挂接 TSS 和 IDT,设置定时器每隔1/100S产生一次0x80中断,执行 system_call,reschedule、ret_from_sys_call
缓存区初始化(主要是块设备缓存区)
硬盘初始化,初始化硬盘中断,将与硬盘的请求交互工作由 do_hd_request 函数来处理(struct request 和 一个回调函数)
软盘初始化,同硬盘初始化流程一样,只是交互函数不一样
打开中断
切换到用户模式
执行 fork(),并在子进程中执行 init(),父进程等待子进程退出
init(), 打开 /dev/tty0,输出内存适用范围,再次fork,父进程等待子进程退出,子进程关机 stdin,打开 /etc/rc 执行 /bin/sh,退出子进程,父进程继续执行,再次 fork,父进程关闭标准输入、标准输出、标准错误,执行setsid,打开 /dev/tty0,执行 /bin/sh argv envp,父进程一致等待子进程退出。此时子进程打开了一个 shell,等待与用户交互。