You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

445 lines
17 KiB
Markdown

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

# 11 | 设置工作模式与环境(中):建造二级引导器
你好我是LMOS。
上节课我们建造了属于我们的“计算机”并且在上面安装好了GRUB。这节课我会带你一起实现二级引导器这个关键组件。
看到这儿你可能会问GRUB不是已经把我们的操作系统加载到内存中了吗我们有了GRUB我们为什么还要实现二级引导器呢
这里我要给你说说我的观点,二级引导器作为操作系统的先驱,它需要**收集机器信息**确定这个计算机能不能运行我们的操作系统对CPU、内存、显卡进行一些初级的配置放置好内核相关的文件。
因为我们二级引导器不是执行具体的加载任务的,而是解析内核文件、收集机器环境信息,它具体收集哪些信息,我会在下节课详细展开。
## 设计机器信息结构
二级引导器收集的信息需要地点存放我们需要设计一个数据结构。信息放在这个数据结构中这个结构放在内存1MB的地方方便以后传给我们的操作系统。
为了让你抓住重点,我选取了这个数据结构的**关键代码**这里并没有列出该结构的所有字段Cosmos/initldr/include/ldrtype.h这个结构如下所示。
```
typedef struct s_MACHBSTART
{
u64_t mb_krlinitstack;//内核栈地址
u64_t mb_krlitstacksz;//内核栈大小
u64_t mb_imgpadr;//操作系统映像
u64_t mb_imgsz;//操作系统映像大小
u64_t mb_bfontpadr;//操作系统字体地址
u64_t mb_bfontsz;//操作系统字体大小
u64_t mb_fvrmphyadr;//机器显存地址
u64_t mb_fvrmsz;//机器显存大小
u64_t mb_cpumode;//机器CPU工作模式
u64_t mb_memsz;//机器内存大小
u64_t mb_e820padr;//机器e820数组地址
u64_t mb_e820nr;//机器e820数组元素个数
u64_t mb_e820sz;//机器e820数组大小
//……
u64_t mb_pml4padr;//机器页表数据地址
u64_t mb_subpageslen;//机器页表个数
u64_t mb_kpmapphymemsz;//操作系统映射空间大小
//……
graph_t mb_ghparm;//图形信息
}__attribute__((packed)) machbstart_t;
```
## 规划二级引导器
在开始写代码之前,我们先来从整体划分一下二级引导器的功能模块,从全局了解下功能应该怎么划分,这里我特意为你梳理了一个表格。
![](https://static001.geekbang.org/resource/image/31/1e/3169e9db4549ab036c2de269788a281e.jpg?wh=1636*846 "二级引导器功能划分表")
前面表格里的这些文件,我都放在了课程配套源码中了,你可以从[这里](https://gitee.com/lmos/cosmos/tree/master/lesson10~11)下载。
上述这些文件都在lesson1011/Cosmos/initldr/ldrkrl目录中它们在编译之后会形成三个文件编译脚本我已经写好了下面我们用一幅图来展示这个编译过程。
![](https://static001.geekbang.org/resource/image/bd/40/bd55f67d02edff4415f06c914403bc40.jpg?wh=5005*3110 "二级引导器编译过程示意图")
这最后三个文件用我们前面说的映像工具打包成映像文件,其指令如下。
```
lmoskrlimg -m k -lhf initldrimh.bin -o Cosmos.eki -f initldrkrl.bin initldrsve.bin
```
## 实现GRUB头
我们的GRUB头有两个文件组成**一个imginithead.asm汇编文件**它有两个功能既能让GRUB识别又能设置C语言运行环境用于调用C函数**第二就是inithead.c文件**它的主要功能是查找二级引导器的核心文件——initldrkrl.bin然后把它放置到特定的内存地址上。
我们先来实现imginithead.asm它主要工作是初始化CPU的寄存器加载GDT切换到CPU的保护模式我们一步一步来实现。
首先是GRUB1和GRUB2需要的两个头结构代码如下。
```
MBT_HDR_FLAGS EQU 0x00010003
MBT_HDR_MAGIC EQU 0x1BADB002
MBT2_MAGIC EQU 0xe85250d6
global _start
extern inithead_entry
[section .text]
[bits 32]
_start:
jmp _entry
align 4
mbt_hdr:
dd MBT_HDR_MAGIC
dd MBT_HDR_FLAGS
dd -(MBT_HDR_MAGIC+MBT_HDR_FLAGS)
dd mbt_hdr
dd _start
dd 0
dd 0
dd _entry
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:
```
然后是关中断并加载GDT代码如下所示。
```
_entry:
cli ;关中断
in al, 0x70
or al, 0x80
out 0x70,al ;关掉不可屏蔽中断
lgdt [GDT_PTR] 加载GDT地址到GDTR寄存器
jmp dword 0x8 :_32bits_mode 长跳转刷新CS影子寄存器
;………………
;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函数做准备代码如下所示。
```
_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函数这个函数我们需要另外在inithead.c中实现我们这就来实现它如下所示。
```
#define MDC_ENDGIC 0xaaffaaffaaffaaff
#define MDC_RVGIC 0xffaaffaaffaaffaa
#define REALDRV_PHYADR 0x1000
#define IMGFILE_PHYADR 0x4000000
#define IMGKRNL_PHYADR 0x2000000
#define LDRFILEADR IMGFILE_PHYADR
#define MLOSDSC_OFF (0x1000)
#define MRDDSC_ADR (mlosrddsc_t*)(LDRFILEADR+0x1000)
void inithead_entry()
{
write_realintsvefile();
write_ldrkrlfile();
return;
}
//写initldrsve.bin文件到特定的内存中
void write_realintsvefile()
{
fhdsc_t *fhdscstart = find_file("initldrsve.bin");
if (fhdscstart == NULL)
{
error("not file initldrsve.bin");
}
m2mcopy((void *)((u32_t)(fhdscstart->fhd_intsfsoff) + LDRFILEADR),
(void *)REALDRV_PHYADR, (sint_t)fhdscstart->fhd_frealsz);
return;
}
//写initldrkrl.bin文件到特定的内存中
void write_ldrkrlfile()
{
fhdsc_t *fhdscstart = find_file("initldrkrl.bin");
if (fhdscstart == NULL)
{
error("not file initldrkrl.bin");
}
m2mcopy((void *)((u32_t)(fhdscstart->fhd_intsfsoff) + LDRFILEADR),
(void *)ILDRKRL_PHYADR, (sint_t)fhdscstart->fhd_frealsz);
return;
}
//在映像文件中查找对应的文件
fhdsc_t *find_file(char_t *fname)
{
mlosrddsc_t *mrddadrs = MRDDSC_ADR;
if (mrddadrs->mdc_endgic != MDC_ENDGIC ||
mrddadrs->mdc_rv != MDC_RVGIC ||
mrddadrs->mdc_fhdnr < 2 ||
mrddadrs->mdc_filnr < 2)
{
error("no mrddsc");
}
s64_t rethn = -1;
fhdsc_t *fhdscstart = (fhdsc_t *)((u32_t)(mrddadrs->mdc_fhdbk_s) + LDRFILEADR);
for (u64_t i = 0; i < mrddadrs->mdc_fhdnr; i++)
{
if (strcmpl(fname, fhdscstart[i].fhd_name) == 0)
{
rethn = (s64_t)i;
goto ok_l;
}
}
rethn = -1;
ok_l:
if (rethn < 0)
{
error("not find file");
}
return &fhdscstart[rethn];
}
```
我们实现了inithead\_entry函数它主要干了两件事即分别调用write\_realintsvefile();、write\_ldrkrlfile()函数把映像文件中的initldrsve.bin文件和initldrkrl.bin文件写入到特定的内存地址空间中具体地址在上面代码中的宏有详细定义。
这两个函数分别依赖于find\_file和m2mcopy函数。
正如其名find\_file函数负责扫描映像文件中的文件头描述符对比其中的文件名然后返回对应的文件头描述符的地址这样就可以得到文件在映像文件中的位置和大小了。
find\_file函数的接力队友就是m2mcopy函数因为查找对比之后最后就是m2mcopy函数负责把映像文件复制到具体的内存空间里。
代码中的其它函数我就不展开了,感兴趣的同学请自行研究,或者自己改写。
## 进入二级引导器
你应该还有印象刚才说的实现GRUB头这个部分在imghead.asm汇编文件代码中我们的最后一条指令是“**jmp 0x200000**”即跳转到物理内存的0x200000地址处。
请你注意这时地址还是物理地址这个地址正是在inithead.c中由write\_ldrkrlfile()函数放置的initldrkrl.bin文件这一跳就进入了二级引导器的主模块了。
由于模块的改变我们还需要写一小段汇编代码建立下面这个initldr32.asm配套代码库中对应ldrkrl32.asm文件并写上如下代码。
```
_entry:
cli
lgdt [GDT_PTR]加载GDT地址到GDTR寄存器
lidt [IDT_PTR]加载IDT地址到IDTR寄存器
jmp dword 0x8 :_32bits_mode长跳转刷新CS影子寄存器
_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,0x90000 使得栈底指向了0x90000
call ldrkrl_entry 调用ldrkrl_entry函数
xor ebx,ebx
jmp 0x2000000 跳转到0x2000000的内存地址
jmp $
GDT_START:
knull_dsc: dq 0
kcode_dsc: dq 0x00cf9a000000ffff ;a-e
kdata_dsc: dq 0x00cf92000000ffff
k16cd_dsc: dq 0x00009a000000ffff 16位代码段描述符
k16da_dsc: dq 0x000092000000ffff 16位数据段描述符
GDT_END:
GDT_PTR:
GDTLEN dw GDT_END-GDT_START-1 ;GDT界限
GDTBASE dd GDT_START
IDT_PTR:
IDTLEN dw 0x3ff
IDTBAS dd 0 这是BIOS中断表的地址和长度
```
我来给你做个解读代码的14行是在加载GDTR和IDTR寄存器然后初始化CPU相关的寄存器。
和先前一样因为代码模块的改变所以我们要把GDT、IDT寄存器这些东西重新初始化最后再去调用二级引导器的主函数ldrkrl\_entry。
## 巧妙调用BIOS中断
我们不要急着去写ldrkrl\_entry函数因为在后面我们要获得内存布局信息要设置显卡图形模式而这些功能依赖于BIOS提供中断服务。
可是要在C函数中调用BIOS中断是不可能的因为C语言代码工作在32位保护模式下BIOS中断工作在16位的实模式。
所以C语言环境下调用BIOS中断需要处理的问题如下
1.保存C语言环境下的CPU上下文 ,即保护模式下的所有通用寄存器、段寄存器、程序指针寄存器,栈寄存器,把它们都保存在内存中。
2.切换回实模式调用BIOS中断把BIOS中断返回的相关结果保存在内存中。
3.切换回保护模式重新加载第1步中保存的寄存器。这样C语言代码才能重新恢复执行。
要完成上面的功能必须要写一个汇编函数才能完成我们就把它写在ldrkrl32.asm文件中如下所示 。
```
realadr_call_entry:
pushad ;保存通用寄存器
push ds
push es
push fs ;保存4个段寄存器
push gs
call save_eip_jmp 调用save_eip_jmp
pop gs
pop fs
pop es ;恢复4个段寄存器
pop ds
popad ;恢复通用寄存器
ret
save_eip_jmp:
pop esi 弹出call save_eip_jmp时保存的eip到esi寄存器中
mov [PM32_EIP_OFF],esi 把eip保存到特定的内存空间中
mov [PM32_ESP_OFF],esp 把esp保存到特定的内存空间中
jmp dword far [cpmty_mode]长跳转这里表示把cpmty_mode处的第一个4字节装入eip把其后的2字节装入cs
cpmty_mode:
dd 0x1000
dw 0x18
jmp $
```
上面的代码我列了详细注释,你一看就能明白。不过这里唯一不好懂的是**jmp dword far \[cpmty\_mode\]指令**,别担心,听我给你解读一下。
其实这个指令是一个**长跳转**,表示把\[cpmty\_mode\]处的数据装入CSEIP也就是把0x180x1000装入到CSEIP中。
这个0x18就是段描述索引这个知识点不熟悉的话你可以回看我们[第五节课](https://time.geekbang.org/column/article/375278)它正是指向GDT中的16位代码段描述符0x1000代表段内的偏移地址所以在这个地址上我们必须放一段代码指令不然CPU跳转到这里将没指令可以执行那样就会发生错误。
因为这是一个16位代码所以我们需要新建立一个文件realintsve.asm如下所示。
```
[bits 16]
_start:
_16_mode:
mov bp,0x20 ;0x20是指向GDT中的16位数据段描述符
mov ds, bp
mov es, bp
mov ss, bp
mov ebp, cr0
and ebp, 0xfffffffe
mov cr0, ebp CR0.P=0 关闭保护模式
jmp 0:real_entry 刷新CS影子寄存器真正进入实模式
real_entry:
mov bp, cs
mov ds, bp
mov es, bp
mov ss, bp ;重新设置实模式下的段寄存器 都是CS中值即为0
mov sp, 08000h ;设置栈
mov bp,func_table
add bp,ax
call [bp] 调用函数表中的汇编函数ax是C函数中传递进来的
cli
call disable_nmi
mov ebp, cr0
or ebp, 1
mov cr0, ebp CR0.P=1 开启保护模式
jmp dword 0x8 :_32bits_mode
[BITS 32]
_32bits_mode:
mov bp, 0x10
mov ds, bp
mov ss, bp重新设置保护模式下的段寄存器0x10是32位数据段描述符的索引
mov esi,[PM32_EIP_OFF]加载先前保存的EIP
mov esp,[PM32_ESP_OFF]加载先前保存的ESP
jmp esi eip=esi 回到了realadr_call_entry函数中
func_table: ;函数表
dw _getmmap ;获取内存布局视图的函数
dw _read ;读取硬盘的函数
dw _getvbemode 获取显卡VBE模式
dw _getvbeonemodeinfo 获取显卡VBE模式的数据
dw _setvbemode 设置显卡VBE模式
```
上面的代码我们只要将它编译成16位的二进制的文件并把它放在0x1000开始的内存空间中就可以了。这样在realadr\_call\_entry函数的最后就运行到这段代码中来了。
上述的代码的流程是这样的:首先从 \_16\_mode:标号处进入实模式然后根据传递进来由ax寄存器传入的函数号到函数表中调用对应的函数里面的函数执行完成后再次进入保护模式加载EIP和ESP寄存器从而回到realadr\_call\_entry函数中。GDT还是imghead.asm汇编代码文件中的GDT这没有变因为它是由GDTR寄存器指向的。
说到这里相信你会立刻明白之前write\_realintsvefile()函数的功能与意义了。它会把**映像文件中的initldrsve.bin文件写入到特定的内存地址空间中**而initldrsve.bin正是由上面的realintsve.asm文件编译而成的。
## 二级引导器主函数
现在我们准备得差不多了从二级引导器的主函数开始这个函数我们要用C来写估计你也感受到了写汇编语言的压力所以不能老是写汇编。
我们先建立一个C文件ldrkrlentry.c在其中写上一个主函数代码如下。
```
void ldrkrl_entry()
{
init_bstartparm();
return;
}
```
上述代码中的 ldrkrl\_entry()函数在initldr32.asm文件配套代码库中对应ldrkrl32.asm中被调用从那条call ldrkrl\_entry 指令开始进入了ldrkrl\_entry()函数,在其中调用了**init\_bstartparm()函数**,这个函数我们还没有实现,但通过名字我们不难推测,它是负责处理开始参数的。
你还记不记得,我们建造二级引导器的目的,就是要收集机器环境信息。我们要把这些信息形成一个有结构的参数,传递给我们的操作系统内核以备后续使用。
由此,我们能够确定,**init\_bstartparm()函数成了收集机器环境信息的主函数**,下节课我们就会去实现它。
## 重点回顾
今天我们开始实现二级引导器了,但是我们还没有完全实现,我们下一节课再接着继续这项工作。
现在,我们来梳理一下这节课的内容,回顾一下我们今天的成果。
1.我们设计了机器信息结构,用于存放后面二级引导器收集到的机器信息。
2.对二级引导器代码模块进行了规划,确定各模块的主要功能。
3.实现了GRUB规定的GRUB头以便被GRUB识别在GRUB头中初始化了CPU寄存器并且跳转到物理内存的0x200000地址处真正进入到二级引导器中开始运行。
4.为了二级引导器能够调用BIOS中断服务程序我们实现了专门用来完成调用BIOS中断服务程序的realintsve.asm模块。
5.最后,我们实现了二级引导器的主函数,由它调用完成其它功能的函数。
这里我还想聊聊,为什么我们要花这么多功夫,去设计二级引导器这个组件呢?
我们把这些处理操作系统运行环境的工作独立出来,交给二级引导器来做,这会**大大降低后面开发操作系统的难度,也能增加操作系统的通用性。**而且,针对不同的硬件平台,我们只要开发不同的二级引导器就好了。
## 思考题
请问GRUB头中为什么需要\_entry标号和\_start标号的地址
欢迎你在留言区跟我交流活动。如果你身边的同事、朋友,对二级引导器的建立有兴趣,也欢迎你把这节课分享给他。
我是LMOS我们下节课见