我正在参加「掘金·启航计划」
这篇文章是某蒟蒻的 读 Linux 源码 操作系统课笔记。
Linux 0.11 boot:main 醒之前
main 醒之前,即梦醒之前(郊区工地口音下的谐音梗),也就是最简单的部分(梦谁还不会做了):进入系统之前的引导、基础初始化工作。这篇笔记是读源码:
/boot目录下的三个汇编文件。
开机:操作系统 boot:
- bootsect 载入 kernel code
- setup:设中断,etc. 从 16b 实模式 -> 32b 保护模式
- head:kernel 的头
代码在 /boot。
开机
CPU 加电:
-
进入实模式:16 bit (最大寻址 1 MB)
-
设置第一行代码:
CS:IP = 0xF000:0xFFF0 => 0xFFFF0 => BIOS- CS:代码段寄存器,IP:指令寄存器(段内 offset)
BIOS 启动
BIOS 启动 -> BIOS 自检 -> BIOS 程序从南桥映射到 mem (启动块、中断向量、中断服务程序)-> 开始执行 BIOS 引导程序:
INT 0x19:加载第一扇区 -> 0x07C00
- 第一扇区:bootsect:Linux 引导程序
bootsect
bootsect:规划内存,加载第二、三批程序(setup、head + kernel,即整个 OS)
-
设常量:定一些位置:
*SEG -
复制 bootsect:
- 把自身程序(512B)从
BOOTSEG=0x07C00->INITSEG=0x90000:即是主(as code: running)又是客(as data: copied) - 跳到新的位置继续跑
- 这个操作脱 BIOS 支配:OS 按照自己意志安排内存
- 把自身程序(512B)从
-
设置栈:以后就可以用栈了,之前不可以
-
将 setup 程序加载到内存
SETUPSEG=0x90200(紧挨着 bootsect)INT 0x19:BIOS 的中断服务- setup 是
2~5扇(共4 * 512 B)
-
载入第三批程序:系统模块 ->
SYSSEG=0x10000- 还是
INT 0x19 - 有 240 扇:多、慢
- 显示
"Loading system ..."
- 还是
-
确定根设备(不重要)
-
根设备:根文件系统的设备:others FS 挂接其上
-
Linux 0.11 启动需要:系统内核镜像 + 根文件系统
root_dev: 主设备号 = 2 次设备号 = type * 4 + nr type { 2: 1.2 MB 7: 1.44 MB } nr { 0 A驱 1 B驱 2 C驱 3 D驱 }
-
-
jmpi 0, SETUPSEG:跳到 setup 执行
setup
setup:从 16 位实模式 -> 32 位保护模式
-
从 CMOS 拿机器系统数据: ^96f613
- 510B 数据 ->
[0x90000, 0x901FD]:- 覆盖 bootsect 程序区域(512B)
- 只差 2B 就到 setup (
0x90200) - 卸磨杀驴 + 精打细算
- CMOS 信息是 BIOS 自检写入的
- 完成数据读取用的还是 BIOS 的中断服务
- 510B 数据 ->
-
cli关中断- cli:设置
Reg.EFLAGS.IF = 0 - 后面很长时间内一直关:setup 关,head 也关,直到 main 末尾才开(
sti()) - 关中断:不是发不出中断,只是 CPU 不理会来的中断
- cli:设置
-
复制内核代码:
0x10000 -> 0x000000x10000:SYSSEG:内核 code:head + C 程序0x00000:覆盖 BIOS 的 INT 表、服务程序- 整体左(低)移 0x10000 (64KB)
-
做 IDT、GDT
- IDT:中断描述符表
- GDT:32 位下的全局(段)描述符表
- 描述段:基址、限长、特权级
- 这两个表硬编码在
setup.s里:- IDT:
idt_48此时是空的 - GDT:
gdt_48:有三项(NULL、内核代码段、内核数据段)
- IDT:
- 用 IDTR、GDTR 寄存器指向两个表
- 安全:user 不能改:用户做假表无用,CPU 不认
特权级基于段:CPU 执行式,检查段是否有特权
- 访问控制施加于线性地址
GDT 本身不是段,但 LDT (Local)是段:
- GDT:没人描述 GDT 的基址、限长
- LDT:GDT 中描述了 LDT 段
-
开 A20:实现 32 位寻址:寻址空间 1MB -> 4GB
-
A20 是第 21 根寻址线,开 A20 就开了寻址线 20 到 31:
- 20 根线(
0xFFFFF:1MB) -> 32 根线(0xFFFFFFFF:4GB)
- 20 根线(
-
32 位:最大地址
0x8个F:4GB -
但是 Linux 最大物理内存支持 16 MB:4GB 绰绰有余
-
(至此:32 位开)
-
改 8259A:重新映射中断
-
开保护模式:
Reg.CR0.PE = 1(PE 是 CR0[0])
(至此:保护模式开)
保护模式:分段保护机制
- 代码只能同级跳
- 数据可以高访低
-
jmpi 0, 8:跳到 head 执行-
0:offset -
8:seg selector:8 = 1 0 00: 1 : GDT[1]: 内核代码段 0 : (G or L)DT:0 为 GDT 00 : 特权级(RPL)
-
特权级:
- CPL:current:当前指令的
- DPL:descriptor:描述符里的
- code seg 跑起来时 CPL <- DPL
- RPL:requested:
RPL == DPL才能跳
Reference: Intel64-IA32-SDM Vol3 5.5 (P vol.3A 5-7,PDF 2979/4834)
head
head:做分页
-
标号
pg_dir指向0x00000:- 预留:之后把页表目录(data)放这里,覆盖后面的部分 head 程序(code)
-
设置 DS、ES、FS、GS 为 0x10:
0x10 = 0001, 0 0 00:选择子(同jmpi 0, 8的 8):selector(GDT[2],GDT,0特权)- 最终指向内核数据段
-
设置系统“用户”栈:
-
设 esp 指向 C 语言里定义的
user_stack数组
-
-
设 IDT:
-
开 256 项,全指向
ignore_int(就在 head.s 程序里): -
实际工作:
printk("Unknown interrupt") -
中断向量:16 位实模式 v.s. 32 位保护模式:
-
- printk:kernel:在内存中用的
- printf:format or file:FS 启动后才能用
现在关中断了,在这里急于设置 IDT:
- 一方面,可以做测试:在后面临时加一行 sli,测试时候中断相应是否正确
- 另一方面,所有在后面没设置的中断都会走这里设的
ignore_int(这个 handler 代码刚好没被覆盖,永久留下来了)
-
设 GDT:
- NULL
- 内核代码段:0x08,16MB
- 内核数据段:0x10,16MB
- NULL
- 252 项预留:
.fill repeat, size, value伪指令:原地重复创建 repeat 个 size 大小的空间:每个都置值为 value
-
再设 DS、ES、FS、GS 为 0x10:GDT 改了之后这些要刷新,重置可以强制刷新
-
检验 A20 时候打开:
- 用 19 位的最大值 + 1:看是否溢出回滚,即:
Loop: 往 0x000000 放值 if 0x100000 == 0x000000: jmp Loop # neq 就再测:没开 A20 就死循环 A20 正确设置 ✅ -
check_x87:检查是否有数学协处理器(486 就开始内置了)
-
标号(data):
pg0、pg1、pg2、pg3:放 4 个内核页表- 一个表 4 KB:刚好是 1 页
- 一个表放 1024 项
-
main 压入栈:模拟 call (先设好放着,之后再用)
- 压参数:3 个 0
- 压返回地址:L6: main 不该返回的,返回就去 L6 死循环
- 压 main 函数地址
-
setup_paging:开分页
- 内存清零:
pg_dir+ 4 个页表(pg\d) - 4 个页表放到
pg_dir里:mov $pg0 + 7, _pg_dir$pg0 + 7是地址 + 信息:- 页地址只用 20b (32-12:4KB 对齐)
- 空出来 12 b 放(控制)属性:
7 = 111存在且用户可读写- 2(U/S):0 超级用户、1 用户
- 为何置 1:后面
move_to_user_modeiret 翻到 PL 3 可以跑!!important!!
- 为何置 1:后面
- 1(W/R):0 只读、1 可读写
- 0(P):1 为页存在
- 控制加在线性地址上
- 2(U/S):0 超级用户、1 用户
- 填写 4 个页表中的内容:线性 <-> 物理:恒等映射
- 设
Reg.CR3(页目录基址寄存器):指向pg_dir的物理地址(少有的物理地址,如果这里再用线性,寻址就套娃了,单程递归) - 设
Reg.CR0.PG:启用分页- IA32 开了 PE 才能开 PG
- 内存清零:
一个 pg_dir 对应一个线性空间
- 换 pg_dir 就换线形空间:但是 32 位系统,一个就够了
- ret:main 出栈运行
ret:
pop addr
jmp addr
call:
push ret_addr # ret_addr 即 call 的下一行代码
jmp func
Why ret, not call:
- call 暗示还有回来
- ret 暗示不回来:boot 真心为 kernel:直接禅让,不僭越
但是好像其实用 jmp 也行。
至此,head 结束,汇编的 boot 结束,转到 C 程序运行了。
此时内存如下: