共计 2882 个字符,预计需要花费 8 分钟才能阅读完成。
系列目录
- 序篇
- 筹备工作
- BIOS 启动到实模式
- GDT 与保护模式
- 加载并进入 Kernel
- 显示与打印
- GDT 和 IDT,中断解决
- 关上虚拟内存
- 实现堆和 malloc
- 创立第一个内核线程
- 多线程运行与切换
- 锁与多线程同步
- 过程的实现
- 进入用户态
- 一个简略的文件系统
- 加载可执行程序
- 零碎调用的实现
- 键盘驱动
- 运行 shell
从 mbr 到 loader
接上一篇 BIOS 启动到实模式,这篇开始 loader
的编写。首先回顾一下那张磁盘镜像和内存分布图:
目前只须要关注 1MB 一下的内存散布,次要是黄色 mbr
和蓝色 loader
局部。上一篇中曾经将 mbr
加载到内存,并且程序流通过 mbr 最初一条指令 jmp LOADER_BASE_ADDR (0x8000)
曾经执行到了 loader
的入口处,接下来就须要将 loader 实现。
loader 的工作
总的来说,loader 的工作次要有以下几项:
- 建设
GDT(Global Descriptor Table)
,初始化内核代码和数据段寄存器(segment registers
),率领 CPU 进入保护模式(protection mode
); - 建设 kernel 页目录(
page directory
)和页表(page tables
),关上虚拟内存(virtual memory
),进入paging
模式; - 加载
kernel
镜像到内存,而后进入到 kernel 代码执行,至此零碎的控制权转交到了 kernel;
能够看到 loader 的工作是比拟多的,并且曾经波及到了 x86 体系架构中的一些外围局部,因而为了读懂并实现 loader,你必须做好以下的常识筹备:
- GDT,段内存寻址,段寄存器,保护模式;
- 虚拟内存,页目录,页表;
elf
文件格式,因为 kernel 会被编译链接成该格局的文件;
loader 实现
依然和之前一样,先给出我的我的项目代码链接 src/boot/loader.S,供你参考。
这个源代码曾经比拟多了,尤其是它还是汇编写成的,而且代码里还蕴含了很多工具函数和打印相干的函数。为了防止陷入凌乱,这里抽取出几个最重要的要害节点(函数),别离代表了下面所述的 loader
须要做的几项工作:
# 入口
loader_start
# 初始化 GDT 并进入保护模式
setup_protection_mode
protection_mode_entry
# 初始化 kernel 页目录和页表
setup_page
# 加载并进入 kernel
init_kernel
接下来咱们一个一个实现这些性能。本篇咱们首先初始化 GDT,进入 32-bit 保护模式
。
进入 loader
在开始之前,咱们首先看 loader 的开始局部的代码,和 mbr 一样,这里依然首先定义了 loader 编码的起始内存地址,为 0x8000
,这是因为咱们事后设计好了,mbr 会将 loader 从磁盘上加载到内存 0x8000 地位处并跳转过来,所以 loader 的编址必须从该地址开始。
; LOADER_BASE_ADDR = 0x8000
SECTION loader vstart=LOADER_BASE_ADDR
接下来正式进入 loader 的第一条代码 jmp loader_start
,它是一个简略的跳转,咱们跳到了 loader_start
开始真正执行 loader 的工作:
loader_entry:
jmp loader_start
; 全局数据
; ...
loader_start:
call clear_screen
call setup_protection_mode
如果你对这种汇编编码的形式不相熟,可能会感觉奇怪,为什么要 jmp
一下,两头跳过的局部是什么?答案是,两头是咱们要定义的数据局部,相似于 .c
文件里定义的全局变量。那里定义了一堆用来打印的字符串,以及至关重要的 GDT
。
你可能曾经意识到了,汇编源代码里的指令和数据局部是能够自在混淆排布的,而且最终编译进去的二进制里它们排布程序齐全遵循源代码的排布。所以你能够任意安顿你的指令和数据所处的地位,只有指令流能顺利地流转和执行上来,不至于跑飞就行。当然,整个 loader
的起始地位,即 0x8000
处必须是入口代码,因为这是和 mbr
约定好的跳转地址。至于前面全副能够自由发挥和排布。
初始化 GDT 表
来到下面说的全局数据的定义局部,你能够跳过我退出的一些打印字符串信息,间接来到 GDT 的定义处。这里定义了 4 个 GDT entry
,每个 entry 占了 8 个字节即 64 bytes。对于 GDT 的含意和字段格局,能够参考这里,也能够参考我之前举荐的 JamesM’s kernel development tutorials。这些都是 x86 体系架构的历史包袱,我不想节约笔墨再解释一遍,然而咱们的代码必须实现并听从它的法令。
GDT 第一个 entry 是保留项不做应用;第四个为显示器 video
内存段描述符,这个其实并不是必须的,你能够忽视它;所以咱们只须要关注第二和第三项即可,它们是:
- 内核代码段(
kernel code
)描述符; - 内核数据段(
kernel data
)描述符;
咱们用 dd
伪指令定义这两个段描述符(segment descriptor
):
CODE_DESC:
dd DESC_CODE_LOW_32
dd DESC_CODE_HIGH_32
DATA_DESC:
dd DESC_DATA_LOW_32
dd DESC_DATA_HIGH_32
DESC_CODE_LOW_32
,DESC_CODE_HIGH_32
,DESC_DATA_LOW_32
,DESC_DATA_HIGH_32
都定义在了 src/boot/boot.inc 中,你能够对照下面给出的手册文档验证每一个 bit。还是那句话,这是一个干燥、麻烦、粗疏然而绕不开的工作,没有什么难点,须要的是读文档手册的急躁。
进入保护模式
设置完 GDT 后,咱们就能够进入保护模式:
; enable A20
in al, 0x92
or al, 0000_0010b
out 0x92, al
; load GDT
lgdt [gdt_ptr]
; open protection mode - set cr0 bit 0
mov eax, cr0
or eax, 0x00000001
mov cr0, eax
; refresh pipeline
jmp dword SELECTOR_CODE:protection_mode_entry
留神这里应用了 lgdt
指令加载 GDT
,并且关上了 cr0
寄存器的保护模式的 bit 位,正式进入保护模式。前面通过一个 far jump
,将 cs
段寄存器初始化为 kernel code
段。留神 cs
寄存器的值不能间接通过 mov
指令设置,而是必须通过跳转语句隐式地被设置。
跳转后,接下来程序来到 protection_mode_entry
的执行,这里初始化了几个 kernel data
段寄存器:
protection_mode_entry:
; set data segments
mov ax, SELECTOR_DATA
mov ds, ax
mov es, ax
mov ss, ax
; set video segment
mov ax, SELECTOR_VIDEO
mov gs, ax
而后就来到了 loader 的重点局部 setup_page
函数,开始建设 kernel 的虚拟内存,留待下一篇。