内核映像文件

一个内核工程文件打包成的一个文件就叫做内核映像文件,其中包括二级引导器的模块、内核模块,图片和字库文件。能够被GRUB加载。

格式如下: GRUB通过映像文件的GRUB头来识别文件,并且根据映像文件头描述符和文件头描述符还可以解析其它文件。

映像文件头描述符和文件描述符是两个C结构体,如下:

//映像文件头描述符
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;

创建映像文件

这里使用qemu作为虚拟机来启动操作系统,前置知识在qemu的使用

  1. 创建磁盘镜像

这里大小为100M.

dd if=/dev/zero of=sparrow.img bs=512 count=204800
  1. 对磁盘进行分区
parted -s sparrow.img mklabel msdos 
parted -s sparrow.img mkpart primary 2048s 100% 
parted -s sparrow.img set 1 boot on
  1. 查找一个空闲的回环设备映射到镜像文件
losetup --find --show --partscan sparrow.img
  1. 格式化分区

如果上一步,镜像文件映射到回环设备/dev/loop0

mkfs.ext4 -q /dev/loop0p1
  1. 挂载分区,安装grub

注意挂载时使用的设备是分区/dev/loop0p1,而进行grub-install时使用的设备是镜像/dev/loop0。

挂载设备到文件,而设备又是映射到镜像文件的,那么对文件的修改就是对镜像的修改。

mkdir ISO 
mount /dev/loop0p1 ISO 
mkdir -p ISO/boot/grub 
grub-install --boot-directory=ISO/boot --force --allow-floppy --target=i386-pc /dev/loop0 
cp grub.cfg ISO/boot/grub
  1. 取消挂载和映射,删除手动创建的分区设备
umount ISO 
losetup -d /dev/loop0 
rm -f /dev/loop0p1
  1. 通过qemu模拟x86虚拟机
qemu-system-i386 -m 1024 -drive format=raw,file=sparrow.img

总之就是我们创建了一个虚拟磁盘,对其进行了分区和格式化。我们如果想要修改磁盘的内容,将其映射到一个回环设备上,然后将这个设备挂载到文件夹上,这样我们就能够通过对文件夹的修改从而修改虚拟磁盘文件。

wsl配置

上面的这些在正常的图形化系统上都没有问题,但是这里我使用的是wsl2的ubuntu系统,所以先配置所需的环境。

安装xfce4和VcXsrv软件,在wsl的配置文件如/etc/profile或者~/.bash上添加如下内容

export DISPLAY=$(awk '/nameserver / {print $2; exit}' /etc/resolv.conf 2>/dev/null):0  
export LIBGL_ALWAYS_INDIRECT=1

后续就可以通过执行startxfce4来启动图形化界面了。

二级引导器

GRUB负责的是操作系统加载进内存,而二级引导器负责的是检验计算机 能不能运行我们的操作系统,并且初始化好一些硬件(cpu,显卡,内存)的配置,把内核相关的文件放到正确的位置上。二者的工作使命是不一样的。

二级引导器不是执行具体的加载任务的,而是解析内核文件、收集机器环境信息。

设计机器信息结构

二级引导器收集的信息,这里设计一个数据结构来存储,并将这个数据结构存放在内存1MB的位置,方便以后操作系统使用。

 
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.asmGRUB头的汇编部分
inithead.cGRUB头的C语言部分,用于放置二级引导器到指定内存
realintsve.asm实现调用BIOS中断的功能
ldrkrl32.asm二级引导器核心入口汇编部分
ldrkrlentry.c二级引导器核心入口
bstartparm.c实现收集机器信息建立页面数据
chkcpmm.c实现检查CPU工作模式和内存视图
fs.c实现解析映像文件的功能
graph.c实现切换显卡图形模式
vgastr.c实现字符串输出

其实通过功能划分,我们也可以看出执行过程。通过GRUB来将二级引导器放入指定位置,然后通过其入口执行二级引导器。

二级引导器来调用BIOS中断检查硬件和获取机器信息并保存,

通过nasm和GCC来将这些文件编译成目标文件,然后用ld链接成为可执行文件,可执行文件再通过objcopy拷贝的同时进行格式转换为raw binary文件(去掉symbols和relocation信息)。

最后生成的三个文件用映像工具lmoskrlimg打包成映像文件。

lmoskrlimg -m k -lhf initldrimh.bin -o spawos.eki -f initldrkrl.bin initldrsve.bin

实现思路

  1. grub启动后,选择对应的启动菜单项,grub会通过自带文件系统驱动,定位到对应的eki文件
  2. grub会尝试加载eki文件【eki文件需要满足grub多协议引导头的格式要求】 这些是在imginithead.asm中实现的,所以要包括:
    1. grub文件头,包括魔数、grub1和grub2支持等
    2. 定位的_start符号等
  3. grub校验成功后,会调用_start,然跳转到_entry
    1. _entry中:关闭中断
    2. 加载GDT
    3. 然后进入_32bits_mode,清理寄存器,设置栈顶
    4. 调用inithead_entry【C】
  4. inithead_entry.c
    1. 从imginithead.asm进入后,首先进入函数调用inithead_entry
    2. 初始化光标,清屏
    3. 从eki文件内部,找到initldrsve.bin文件,并分别拷贝到内存的指定物理地址
    4. 从eki文件内部,找到initldrkrl.bin文件,并分别拷贝到内存的指定物理地址
    5. 返回imginithead.asm
  5. imginithead.asm中继续执行 jmp 0x200000 而这个位置,就是initldrkrl.bin在内存的位置ILDRKRL_PHYADR 所以后面要执行initldrkrl.bin的内容
  6. 这样就到了ldrkrl32.asm的_entry
    1. 将GDT加载到GDTR寄存器【内存】
    2. 将IDT加载到IDTR寄存器【中断】
    3. 跳转到_32bits_mode 初始寄存器 初始化栈 调用ldrkrl_entry【C】
  7. ldrkrlentry.c
    1. 初始化光标,清屏
    2. 收集机器参数init_bstartparm【C】
  8. bstartparm.c
    1. 初始化machbstart_t
    2. 各类初始化函数,填充machbstart_t的内容
    3. 返回
  9. ldrkrlentry.c
    1. 返回
  10. ldrkrl32.asm
    1. 跳转到0x2000000地址继续执行,注意这和imginithead.asm跳转的0x200000地址不一样

实现GRUB头

GRUB启动后,选择对应的启动项,grub会通过自带的文件系统驱动,定位到eki文件。也就是上一步我们打包的映像文件spawos.eki

而eki需要满足grub多协议引导头的格式要求。

grub头文件需要包括魔数、grub1和grub2支持等。grub校验成功后,会调用_start,然后跳转到_entry

这部分由两个文件组成:

  • imginithead.asm:让GRUB识别,设置C语言运行环境,调用C函数。
  • inithead.c:主要功能是查找二级引导器的核心文件initldrkrl.bin,并放置到特定的内存上。

imginithead.asm的实现

首先是头结构,支持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_PTR的地址到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

初始化段寄存器和通用寄存器、栈寄存器,为调用inithead.entry这个函数做准备

 
_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中

inithead.c的实现

从eki内部将找到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];
}

把映像文件中的 initldrsve.bin 文件和 initldrkrl.bin 文件写入到特定的内存地址空间中。

进入二级引导器

在 imghead.asm 汇编文件代码中,我们的最后一条指令是“jmp 0x200000”,即跳转到物理内存的 0x200000 地址处,这个地址正是在 inithead.c 中由 write_ldrkrlfile() 函数放置的 initldrkrl.bin 文件,这一跳就进入了二级引导器的主模块了。

ldrkrl32.asm的实现

由于代码模块的改变,要把GDT、IDT,寄存器等重新设置,再开始执行二级引导器的主函数。这就是initldr32.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中断表的地址和长度

由于ldrkrl_entry函数依赖BIOS中断来获取内存布局信息等功能,所以要能够让C函数调用BIOS中断。 因为C语言工作在32位保护模式下,BIOS工作在16位实模式下,所以C函数中不可能调用BIOS中断。要想实现这个功能,需要处理以下问题:

  1. 保存 C 语言环境下的 CPU 上下文 ,即保护模式下的所有通用寄存器、段寄存器、程序指针寄存器,栈寄存器,把它们都保存在内存中。
  2. 切换回实模式,调用BIOS中断,把调用结果保存在内存中
  3. 切换回保护模式,重新加载第一步保存的上下文,重新恢复执行。

通过汇编代码实现

先保存CPU上下文

 
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 $

save_eip_jmp最后进行了长跳转,表示把[cpmty_mode]处的数据装入 CS:EIP,也就是把 0x18:0x1000 装入到 CS:EIP 中。

realintsve.asm的实现

这是用来辅助实现C语言运行环境调用实模式BIOS的代码。实现在realintsve.asm中。实现了到实模式的切换-》调用C函数传递的函数表的汇编函数-》切换回保护模式 -》恢复CPU上下文。

 
[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模式

real_entry中根据传递的函数号(由ax寄存器传递),到函数表func_table中调用对应的函数,函数表中的函数正是通过调用BIOS实现的函数。

二级引导器主函数

进入二级引导器 做好了进入二级引导器的准备工作。接下来便是实现其主函数了。

imginithead.asm设置好了C语言执行环境,然后通过inithead.c安排好了内核文件在内存中的位置,切换到二级引导器执行。在执行前,通过ldrkrl32.asm重新设置GDT、IDT、寄存器等内容,调用ldrkrl_entry函数。

ldrkrlentry.c的实现

 
void ldrkrl_entry()
{
    init_bstartparm();
    return;
}

init_bstartparm() 函数负责管理检查 CPU 模式、收集内存信息,设置内核栈,设置内核字体、建立内核 MMU 页表数据