Grub内核映像格式
Grub工作的时候,需要一个内核映像文件,其中包括了二级引导器模块、内核模块、图片和字库等。这些不同的文件都被放到了一个映像文件中,为了Grub能够正常加载,需要一个预先定义好的格式,以便Grub能解析。
上图中,Grub头为4KB大小,Grub会用这一小段代码来识别映像文件。根据映像文件头描述符和文件描述符的信息,这一小段代码还可以解析映像文件中的其它文件。
映像文件头描述符和文件头描述符的结构如下:
//映像文件头描述符
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;
知道了映像文件格式,我们还需要有工具能够打包出映像文件才行。这个工具可以到下面这个链接中去拉取:
极客时间-操作系统实战45讲: 极客时间-操作系统实战45讲课程已经上线,欢迎订阅 - Gitee.comhttps://gitee.com/lmos/cosmos/tree/master/tools/lmoskrlimg 将代码拉下来后在Linux下编译出这个工具即可。工具的使用方法如下:
lmoskrlimg -m k -lhf GRUB头文件 -o 映像文件 -f 输入的文件列表
-m 表示模式 只能是k内核模式
-lhf 表示后面跟上GRUB头文件
-o 表示输出的映像文件名
-f 表示输入文件列表
例如:lmoskrlimg -m k -lhf grubhead.bin -o kernel.img -f file1.bin file2.bin file3.bin file4.bin
二级引导器的工作有哪些
我们要做的二级引导器,主要作为操作系统的先驱,它需要收集机器信息,确定这个计算机能不能运行我们的操作系统,对 CPU、内存、显卡进行一些初级的配置,放置好内核相关的文件。对于要搜集的信息,在后面的笔记中会记录。
设计机器信息数据结构
二级引导器所搜集的信息,需要按照一定的数据结构来存储。我们规定在内存1MB的地方来存放这个数据结构。
下面是这个数据结构的一些关键信息(完整的结构,请查看Cosmos/initldr/include/ldrtype.h,代码仓库极客时间-操作系统实战45讲: 极客时间-操作系统实战45讲课程已经上线,欢迎订阅 - Gitee.comhttps://gitee.com/lmos/cosmos/tree/master/lesson10~11/Cosmos)。
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;
二级引导器规划
我们的二级引导器模块划分如下表所示:
文件 | 功能描述 |
imginithead.asm | GRUB头汇编 |
inithead.c | GRUB头的C语言实现部分,用于将二级引导器放到指定内存中 |
realintsve.asm | 实现调用BIOS中断功能 |
ldrkrl32.asm | 二级引导器核心入口汇编 |
ldrkrlentry.c | 二级引导器核心入口 |
bstartpram.c | 收集机器信息,创建页面数据 |
chkcpmm.c | 检查CPU工作模式和内存视图 |
fs.c | 解析映像文件 |
graph.c | 切换显卡图形模式 |
vgastr.c | 字符串输出显示 |
这些文件,完整的代码,在下面这个链接中可以找到(lesson10~11/Cosmos/initldr/ldrkrl):
极客时间-操作系统实战45讲: 极客时间-操作系统实战45讲课程已经上线,欢迎订阅 - Gitee.comhttps://gitee.com/lmos/cosmos/tree/master/lesson10~11 这个工程编译后,会生成三个bin文件: initldrmh.bin,initldrsve.bin和initldrkrl.bin。我们使用前面所说的lmoskrlimg打包工具制作出映像文件即可。打包命令如下:
lmoskrlimg -m k -lhf initldrimh.bin -o Cosmos.eki -f initldrkrl.bin initldrsve.bin
实现GRUB头
GRUB头由两个文件组成:
1. imginithead.asm汇编,它被GRUB所识别,也设置C语言运行环境(用于后续调用C函数)。
2. inithead.c,它主要查找二级引导器的核心文件 -- initldrkrl.bin,然后将其放到内存指定地址上去。
imginithead.asm
我们先来实现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:
如果想要深入理解multiboot头格式,可以去网上搜索一下Grub multiboot/multiboot2规范,这里给一个multiboot2规范的下载地址仅供参考:
https://download.csdn.net/download/vivo01/85626166https://download.csdn.net/download/vivo01/85626166 接下来,关闭中断,加载GDT:
_entry:
cli ;关中断
in al, 0x70
or al, 0x80
out 0x70,al ;关掉不可屏蔽中断
lgdt [GDT_PTR] ;加载GDT地址到GDTR寄存器
jmp dword 0x8 :_32bits_mode ;
;………………
;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
上面的in,out指令是读和写0x70端口的指令,关于x86端口的描述,之前的笔记中有给过参考链接,也可以自行去google。
lgdt指令是修改全局描述符表GDT寄存器的指令,此处表示加载GDT_PTR地址处开始的6个字节,在GDT_PTR处,前两个字节表示GDT的大小,后四个字节表示GDT的起始地址。
如果需要了解GDT,请回看之前的笔记:
OS实战笔记(3) -- X86 CPU三种工作模式(实模式,保护模式,长模式)_x86保护模式_亦枫Leonlew的博客-CSDN博客X86 CPU三种工作模式(实模式,保护模式,长模式)https://blog.csdn.net/vivo01/article/details/125752362 jmp dword 0x8 : _32bits_mode,这里的0x8表示选择了GDT里(TI(bit2) = 0)索引为1的段描述符所确定的基地址(参考下图),加上_32bits_mode所指示的偏移得到最终地址。
下标为1的段 描述符值为0x00cf9e000000ffff,可以参考下图进行理解。
可以看到这个段的基地址是0x0,jmp指令实际可以理解为跳转到_32bits_mode处。
_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地址
_32bits_mode处代码最核心的功能是设置esp,有了栈后就可以调用C语言的函数了。后面call inithead_entry函数主要功能就是加载对应的映像文件中的 initldrsve.bin 文件和 initldrkrl.bin 文件写入到特定的内存地址空间中去。
#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后,最后的jmp 0x200000是跳转到二级引导器的主模块(initldrkrl.bin)。
进入二级引导器主模块
前面加载了initldrkrl.bin到内存0x200000并跳转到了二级引导器的主模块,由于模块发生了变化,我们首先会重新去设置一下GDT,IDT,栈指针,相关寄存器。这些工作在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中断表的地址和长度
大部分内容基本和前面一致,这里需要注意最后jmp的地址是0x2000000( IMGKRNL_PHYADR, 内核映像地址),不是initldrkrl.bin加载到内存中的地址。
ldrkrl_entry函数是二级引导器的C语言主函数,暂时还未实现。
调用BIOS中断
前面说过,二级引导器主要功能中最重要的一点是收集机器信息,比如内存布局,显卡图形模式等。这些信息要通过BIOS提供的中断服务才能实现。
BIOS中断服务工作在16位实模式,而目前我们所用的二级引导器工作在32位保护模式下。因此在二级引导器中无法直接调用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]处的数据装入 CS:EIP,也就是把 0x18:0x1000 装入到 CS:EIP 中。根据之前的描述,我们知道所使用的段的index是0x3(0x18的bits[15:3],根据段选择子的定义, 低3位是RPL和TI, 其他位才是索引)。 之前在汇编中定义的二级引导器的GDT表中,下标为3的段描述符对应的是16位代码段。0x1000 代表段内的偏移地址(由于段描述符指定的基地址是0x0,因此实际物理地址就是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模式
这个文件会单独编译成initldrsve.bin,在前面write_realintsvefile()函数作用就是将这个文件的内容放到对应的内存地址0x1000上。
二级引导器主函数
二级引导器主函数放在ldrkrlentry.c中,目前仅调用一个空函数init_bstartparm()用来收集机器信息,在后面实现。
void ldrkrl_entry()
{
init_bstartparm();
return;
}