建造二级引导器

二级引导器的作用

二级引导器作为操作系统的先驱,它需要收集机器信息,确定这个计算机能不能运行我们的操作系统,对 CPU、内存、显卡进行一些初级的配置,放置好内核相关的文件。

二级引导器的设计

3169e9db4549ab036c2de269788a281e.webp

bd55f67d02edff4415f06c914403bc40.webp

运行流程

GRUB启动后,选择对应的启动菜单,GRUB会通过自带的文件驱动,定位到对应的eki文件(即我们的操作系统)

GRUB通过识别GRUB头部(imginithead.asm),获取二级引导器的信息并加载(inithead_entry.c将二级引导器的代码(这些代码将对机器进行初始设置),加载到内存里)。

代码讲解

实现GRUB头

我们的 GRUB 头有两个文件组成,一个 imginithead.asm 汇编文件,它有两个功能,既能让 GRUB 识别,又能设置 C 语言运行环境,用于调用 C 函数;第二就是 inithead.c 文件,它的主要功能是查找二级引导器的核心文件——initldrkrl.bin,然后把它放置到特定的内存地址上。

多重引导的前置知识

能够被 GRUB 引导的内核有两个条件:

  • 需要有一个 Multiboot Header ,这个 Multiboot Header 必须在内核镜像的前 8192 个字节内,并且是首地址是 4 字节对其的。
  • 内核的加载地址在 1MB 以上的内存中,这个要求是 GRUB 附加的,并非多重引导规范的规定。

Multiboot Header的分布必须如下所示:

偏移量 类型 域名 备注
0 u32 magic 必需,为0x1BADB002
4 u32 flags 必需
8 u32 checksum 必需
12 u32 header_addr 如果flags[16]被置位
16 u32 load_addr 如果flags[16]被置位
20 u32 load_end_addr 如果flags[16]被置位
24 u32 bss_end_addr 如果flags[16]被置位
28 u32 entry_addr 如果flags[16]被置位
32 u32 mode_type 如果flags[2]被置位
36 u32 width 如果flags[2]被置位
40 u32 height 如果flags[2]被置位
44 u32 depth 如果flags[2]被置位

flags:flags域指出OS映像需要引导程序提供或支持的特性。0-15 位指出需求:如果引导程序发现某些值被设置但出于某种原因不理解或不能不能满足相应的需求,它必须告知用户并宣告引导失败。16-31位指出可选的特性:如果引导程序不能支持某些位,它可以简单的忽略它们并正常引导。自然,所有 flags 字中尚未定义的位必须被置为 0。这样,flags 域既可以用于版本控制也可以用于简单的特性选择。

checksum: 域 checksum 是一个 32 位的无符号值,当与其他的 magic 域(也就是 magic 和 flags)相加时,结果必须是 32 位的无符号值 0(即magic + flags + checksum = 0)

header_addr: header的地址,这里往后的 32 个字节不是必须的,并且对于内核为 ELF 格式时是不需要的。

由此我们定义结构体映像文件头描述符s_mlosrddsc和文件头描述符:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
//映像文件头描述符
typedef struct s_mlosrddsc
{
u64_t mdc_mgic; //映像文件标识
u64_t mdc_sfsum;//未使用
u64_t mdc_sfsoff;//未使用
u64_t mdc_sfeoff;//未使用
u64_t mdc_sfrlsz;//未使用
u64_t mdc_ldrbk_s;//映像文件中二级引导器的开始偏移
u64_t mdc_ldrbk_e;//映像文件中二级引导器的结束偏移
u64_t mdc_ldrbk_rsz;//映像文件中二级引导器的实际大小
u64_t mdc_ldrbk_sum;//映像文件中二级引导器的校验和
u64_t mdc_fhdbk_s;//映像文件中文件头描述的开始偏移
u64_t mdc_fhdbk_e;//映像文件中文件头描述的结束偏移
u64_t mdc_fhdbk_rsz;//映像文件中文件头描述的实际大小
u64_t mdc_fhdbk_sum;//映像文件中文件头描述的校验和
u64_t mdc_filbk_s;//映像文件中文件数据的开始偏移
u64_t mdc_filbk_e;//映像文件中文件数据的结束偏移
u64_t mdc_filbk_rsz;//映像文件中文件数据的实际大小
u64_t mdc_filbk_sum;//映像文件中文件数据的校验和
u64_t mdc_ldrcodenr;//映像文件中二级引导器的文件头描述符的索引号
u64_t mdc_fhdnr;//映像文件中文件头描述符有多少个
u64_t mdc_filnr;//映像文件中文件头有多少个
u64_t mdc_endgic;//映像文件结束标识
u64_t mdc_rv;//映像文件版本
}mlosrddsc_t;

#define FHDSC_NMAX 192 //文件名长度
//文件头描述符
typedef struct s_fhdsc
{
u64_t fhd_type;//文件类型
u64_t fhd_subtype;//文件子类型
u64_t fhd_stuts;//文件状态
u64_t fhd_id;//文件id
u64_t fhd_intsfsoff;//文件在映像文件位置开始偏移
u64_t fhd_intsfend;//文件在映像文件的结束偏移
u64_t fhd_frealsz;//文件实际大小
u64_t fhd_fsum;//文件校验和
char fhd_name[FHDSC_NMAX];//文件名
}fhdsc_t;

imginithead.asm

主要工作为初始化CPU的寄存器,加载GDT,切换到 CPU 的保护模式

先写一些宏定义等

1
2
3
4
5
6
7
MBT_HDR_FLAGS  EQU 0x00010003	;EQU可以理解为宏定义,相当于#define
MBT_HDR_MAGIC EQU 0x1BADB002 ;宏定义魔数
MBT2_MAGIC EQU 0xe85250d6 ;第二版多引导协议头魔数
global _start ;导入_start符号
extern inithead_entry ;外部的inithead_entry.c
[section .text]
[bits 32] ;汇编成32位代码

然后根据上面的表格结构写GRUB和GRUB2的结构代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
_start:
jmp _entry

;下面是GRUB的头
align 4
mbt_hdr:
dd MBT_HDR_MAGIC ;汇编dd伪指令定义32bit数字,即uint32
dd MBT_HDR_FLAGS
dd -(MBT_HDR_MAGIC+MBT_HDR_FLAGS)
dd mbt_hdr
dd _start
dd 0
dd 0
dd _entry

;下面是GRUB2的头
;包含两个头是为了同时兼容GRUB,GRUB2
ALIGN 8
mbhdr:
DD 0xE85250D6
DD 0
DD mhdrend - mbhdr
DD -(0xE85250D6 + 0 + (mhdrend - mbhdr))
DW 2, 0
DD 24
DD mbhdr
DD _start
DD 0
DD 0
DW 3, 0
DD 12
DD _entry
DD 0
DW 0, 0
DD 8
mhdrend:

接下来关中断,这是因为,实地址模式情况下,如果计算机突然有请求,比如你按键盘输入,产生中断,让CPU处理你的请求,这会导致操作系统引导停止,即使不停止,也会造成其他程序的中断,从而导致操作系统加载失败,那这就麻烦了,因此需要关闭中断功能,才能避免意外。

1
2
3
4
5
6
7
8
9
_entry:
cli ;关中断

in al, 0x70 ;从0x70(索引端口,用来指定内存单元)读取数据到AL
or al, 0x80 ;将AL最高位置为1
out 0x70,al ;将AL输出到0x70端口,关掉不可屏蔽中断

lgdt [GDT_PTR] ;加载GDT地址到GDTR寄存器
jmp dword 0x8 :_32bits_mode ;长跳转刷新CS影子寄存器

汇编基本知识:

  • 汇编指令IN与OUT用来操作端口(因为端口是独立编址的,所以不能使用MOV指令)
  • 端口有0-65535个,前0-255属于1字节端口(使用al),其他属于2字节端口(使用ax)
  • 对于1字节端口,可以使用立即数进行操作
  • IN表示从端口读,OUT使用正好相反

然后加载全局段描述符表GDT:

1
2
3
4
5
6
7
8
9
10
11
;GDT全局段描述符表
GDT_START:
knull_dsc: dq 0
kcode_dsc: dq 0x00cf9e000000ffff
kdata_dsc: dq 0x00cf92000000ffff
k16cd_dsc: dq 0x00009e000000ffff ;16位代码段描述符
k16da_dsc: dq 0x000092000000ffff ;16位数据段描述符
GDT_END:
GDT_PTR:
GDTLEN dw GDT_END-GDT_START-1 ;GDT界限
GDTBASE dd GDT_START

最后初始化寄存器,通用寄存器,栈寄存器,为调用inithead_entry.c做准备

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
_32bits_mode:
mov ax, 0x10
mov ds, ax
mov ss, ax
mov es, ax
mov fs, ax
mov gs, ax
xor eax,eax
xor ebx,ebx
xor ecx,ecx
xor edx,edx
xor edi,edi
xor esi,esi
xor ebp,ebp
xor esp,esp
mov esp,0x7c00 ;设置栈顶为0x7c00
call inithead_entry ;调用inithead_entry函数在inithead.c中实现
jmp 0x200000 ;跳转到0x200000地址

inithead_entry.c

我们前面已经提到,这个函数主要用来将二级引导器的代码放到内存中,但在移动文件之前,我们先进行一步

进入二级引导器