冯诺依曼体系的计算机的工作原理就是:取指令、执行指令
下面以 x86 架构为例介绍操作系统的启动过程
x86架构下操作系统启动过程
对于 x86 架构 Linux 0.11 来说,操作系统启动主要执行了以下几段代码
BIOS → bootsect.s → setup.s → head.s → main.c
BIOS部分
x86 PC 刚开机时 CPU 处于实模式,寻址方式为计算逻辑地址 CS : IP 得到物理地址 CS << 4 + IP
开机时硬件设置 CS = 0xFFFF,IP = 0x0000,即寻址0xFFFF0 (ROM BIOS 映射区,Basic I/O System) ,
即跳转到BIOS代码开始的地方,接下来执行BIOS代码,
检查 RAM,键盘,显示器,硬盘、主板等硬件,一些系统检测数据如硬盘的参数放在 0x000000 处
将磁盘 0磁道 0扇区(操作系统引导扇区,512B/扇区)读入内存 0x7c00 处,
这 512B 包括 MBR(Master Boot Record),即主引导记录,磁盘分区表,结束标志 2 BYTE 0xAA、0x55(用于 BIOS 校验)
此外 BIOS 初始化时会在物理内存的开始处设置一个大小为 0x400 字节的中断向量表,提供 BIOS 中断支持
然后设置 CS = 0x07C0,IP = 0x0000,即跳转到引导程序执行
BIOS 执行后的 1MB 内存内的使用情况
bootsect.s 引导程序
主引导记录(引导程序),bootsect.s,看一下它的主要工作
上面这段代码将 bootsect 程序由当前所在位置 BOOTSEG(0x07C00) 复制到 INITSEG(0x90000)处,
然后使用 jmpi 跳转到 CS = INITSEG,IP = go 处,即是跳转到刚复制的地方继续执行下一条指令,
将 DS、ES 赋值为 CS,以及设置栈
接下来 bootsect.s 继续从磁盘读入四个扇区,
具体方式是用 INT 0x13 调用 BIOS 02H号 中断读取扇区到内存,
其中 AH 传递功能号,AL 表示读取扇区数量 (SETUPLEN,4),CH 传递柱面号,CL 传递开始扇区
DH 是磁头号,DL 是驱动器号,ES:BX 表示读入到的内存地址,这里是(0x90200)
该中断的执行结果如下
因此 CF 为零时,跳转到 ok_load_setup
否则调用 INT 13/00H 重置磁盘,重新执行 load_setup
到 ok_load_setup 后,使用中断 读取硬盘参数、读取光标、打印启动logo(#msg1)
然后调用 read_it 函数读入 system 模块,以及调用 kill_motor,暂且不表,
当 ES 小于 ENDSEG 时,继续读入,ENDSEG = SYSSEG + SYSSIZE
其中 SYSSIZE 是根据编译出系统镜像大小确定的
做完这些工作后,jmpi 跳转到 SETUPSEG:0 (0x90200)处
执行之前读入的 setup.s 的代码
bootsect.s is loaded at 0x7c00 by the bios-startup routines, and moves
itself out of the way to address 0x90000, and jumps there.
It then loads ‘setup’ directly after itself (0x90200), and the system
at 0x10000, using BIOS interrupts.
总结一下 bootsect 在被 BIOS 加载后的工作:
bootsect 把自己从0x07c00 移动到 0x90000,跳转到新的位置继续执行,通过 BIOS 中断在 0x90200 处读入四个扇区(2KB)的 setup.s,并继续读入后面的system模块到 0x10000 处,最后 jmpi 到 setup 执行
setup.s
setup.s 完成 OS 启动前的设置
取出光标位置,以及将各种硬件参数保存在内存中(覆盖掉了 bootsect),
然后关闭中断,将 system 从 0x10000~0x8FFFF 复制到 0x00000 处,
移动结束后,使用 LIDT 与 LGDT 指令加载 GDTR(全局描述符表寄存器,存放GDT入口地址)
与 IDTR(中断描述符表/中断向量表寄存器,存放 IDT 入口地址)
接下来通过键盘控制器8042开启 A20 地址线,初始化 8259,重设中断控制
接下来是关键的一步,设置 CR0 机器状态寄存器,将CPU切换到保护模式
这里使用 LMSW 指令,指令中源操作数的低四位,加载到 CR0 的 PE、MP、EM 及 TS 位
其中 PE(Protection Enable) 标志位为 0 的时候,CPU处于实模式,为 1 时,启动保护模式(通过切换电路)
============================================================================
保护模式下的地址翻译和中断处理
PE = 1 即保护模式时,CS 意为 select,前13位为在 GDT 中进行查找的索引号,
此时物理地址 = 通过 CS 查表 + IP,从而使CPU 能进行 4GB 的寻址,地址翻译由硬件完成(因为速度)
保护模式的中断向量表入口地址由 LIDT 指令载入,INT n 触发中断,n 即为 IDT 中的表项索引
进一步说明:GDT 与 IDT
============================================================================
继续,因此 LMSW 0x0001 执行以后,jmpi 经过 CS = 8 查表,GDT第8个字节开始的项如上所示
得到 Base Address 0x0,即 system 所在位置,这里放的是OS的第一部分代码 head.s
setup.s is responsible for getting the system data from the BIOS,
and putting them into the appropriate places in system memory.
both setup.s and system has been loaded by the bootblock.
This code asks the bios for memory/disk/other parameters, and
puts them in a “safe” place: 0x90000-0x901FF, ie where the
boot-block used to be. It is then up to the protected mode
system to read them from there before the area is overwritten
for buffer-blocks.
setup 首先从 BIOS 获取系统数据并放到 0x90000~0x901FF 覆盖 bootsect 程序,并进入保护模式
在 0x90000~0x901FF 这部分内存被用作缓冲块前,OS会从这里获取系统参数
进入保护模式后,jmpi 到 CS = 8(0000 0000 0000 1000),在 GDT 中查找得到地址 0x0,即转入 head.s执行
setup 执行完以后,内存情况如下图
head.s
head.s 重新初始化了 GDT 与 IDT
将 ds、es、fs 和 gs 都置为 0x10,设置系统栈
初始化 IDT,一共256项,每一项都指向一个默认的报错程序 ignore_int
初始化GDT,检查A20、x87浮点运算单元等,然后跳转到 after_page_tables
after_page_tables 中,将参数压栈后,将 L6 压栈作为 main 函数的返回地址
然后将 main 函数地址(_start)压栈,转去 set_paging 设置页表,
set_paging 中 ret 指令返回到 main 函数(_start)
而且可以看出,Linux 0.11 中 main 如果返回就会进入死循环
简单总结一下 head.s 的工作:
重新初始化 GDT、IDT、设置页表,跳到 main 函数执行
main.c
main.c 完成了操作系统的初始化,包括内存、中断、设备、始终、CPU等等
实际上 head.s 压入的三个参数 0、0、0 即 envp、argv、argc,start 函数并没有使用
以上是 main.c 中 start 函数进行的一部分初始化
简单看一下 mem_init()
这里就是以 4K 为大小初始化了页表 mem_map
好 这里我也看不懂,留作以后分解
至此,操作系统的启动过程如下
几个需要注意的点
- CR0 寄存器 PE 位控制的寻址方式,即CPU从实模式到保护模式的转换
- Linux 0.11中,OS 被读入到 0x10000 处,0x90000 处开始是 bootsect,即只为 OS 预留了 512 KB
- 还有什么,想起来再补充
最后再总结一下:
bootsect → setup → head → main
做了两件事
- 把操作系统读入内存
- 初始化
2019/12/15