接上一篇 BIOS 启动到实模式,这篇开始 loader
的编写。首先回顾一下那张磁盘镜像和内存分布图:html
目前只须要关注 1MB 一下的内存分布,主要是黄色 mbr
和蓝色 loader
部分。上一篇中已经将 mbr
加载到内存,而且程序流经过 mbr 最后一条指令 jmp LOADER_BASE_ADDR (0x8000)
已经执行到了 loader
的入口处,接下来就须要将 loader 实现。git
总的来讲, loader 的工做主要有如下几项:web
GDT(Global Descriptor Table)
,初始化内核代码和数据段寄存器(segment registers
),带领 CPU 进入保护模式(protection mode
);page directory
)和页表(page tables
),打开虚拟内存(virtual memory
),进入 paging
模式;kernel
镜像到内存,而后进入到 kernel 代码执行,至此系统的控制权转交到了 kernel ;能够看到 loader 的工做是比较多的,而且已经涉及到了x86 体系架构中的一些核心部分,所以为了读懂并实现 loader,你必须作好如下的知识准备:shell
elf
文件格式,由于 kernel 会被编译连接成该格式的文件;仍然和以前同样,先给出个人项目代码连接 src/boot/loader.S,供你参考。segmentfault
这个源代码已经比较多了,尤为是它仍是汇编写成的,并且代码里还包含了不少工具函数和打印相关的函数。为了不陷入混乱,这里抽取出几个最重要的关键节点(函数),分别表明了上面所述的 loader
须要作的几项工做:多线程
# 入口 loader_start # 初始化 GDT 并进入保护模式 setup_protection_mode protection_mode_entry # 初始化 kernel 页目录和页表 setup_page # 加载并进入 kernel init_kernel
接下来咱们一个一个实现这些功能。本篇咱们首先初始化 GDT,进入 32-bit 保护模式
。架构
在开始以前,咱们首先看 loader 的开始部分的代码,和 mbr 同样,这里仍然首先定义了 loader 编码的起始内存地址,为 0x8000
,这是由于咱们预先设计好了,mbr 会将 loader 从磁盘上加载到内存 0x8000 位置处并跳转过去,因此 loader 的编址必须从该地址开始。ide
; 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 的定义处。这里定义了 4 个 GDT entry
,每一个 entry 占了 8 个字节即 64 bits。关于 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。仍是那句话,这是一个枯燥、麻烦、细致可是绕不开的工做,没有什么难点,须要的是读文档手册的耐心。
为了照顾对汇编还不是很熟悉的同窗,有必要将 dd
伪指令的做用解释一遍。dd
的意思是 define double (4-bytes)
,与之相似的还有 db (byte)
,dw (word, 2-bytes)
,它们出如今汇编源代码里,就是指在编译后的二进制里,在该位置上写入后面所定义的数据内容。由此你能够再次体会一下汇编与编译后的二进制的关系,这几乎就是一种刻板的翻译而已。
设置完 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 的虚拟内存,留待下一篇。