操作系统-- 二级引导器

抛转引玉

上节建造了我们自己的“计算机”,并且在上面安装了GRUB,GRUB不是已经把我们的操作系统加载到了内存中了吗?为什么还需要二级引导器这个东西呢?

  • 二级引导器作为操作系统的先驱,它需要收集机器信息,确定这个计算机能不能运行我们的操作系统,对CPU、内存、显卡进行一些初级配置,放置好内核相关的文件。
  • 二级引导器不是执行具体的加载任务,而是解析内核文件、收集机器环境信息

设计机器信息结构

既然二级引导器是收集机器环境信息的,那么就需要有地点存放,需要我们设计一个数据结构,信息放在这个数据结构中,这个结构放在内存1M的地方,方便以后传给我们的操作系统。
数据结构的关键代码:


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;

PS:__attribute__((packed))
attrubte ((packed)) 的作用就是告诉编译器取消结构在编译过程中的优化对齐,按照实际占用字节数进行对齐。是GCC特有的语法。这个功能是跟操作系统没关系,跟编译器有关,gcc编译器不是紧凑模式的
参考:从gcc的__attribute__((packed))聊到结构体大小的问题

规划二级引导器

先整体了解下二级引导器的功能模块:

它们经过编译后形成三个文件,编译过程如下:

最后三个文件用我们前面说的映像工具打包成映像文件,其指令如下:

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函数,实现如下:

#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_enty函数主要干两件事,即分别调用write_realintsvefile()、write_ldrkrlfile()函数,把映像文件中的initldrsve.bin文件和initldrkrl.bin文件写入特定的内存空间中

进入二级引导器

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

由于模块的改变,还需要写一小段汇编代码--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中断表的地址和长度

代码中1~4行是加载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语言代码才能恢复执行。

要完成上面的功能,必须要写一个汇编函数才能完成:


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中,0x18表示段描述索引,它正指向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_mode: 标号处进入实模式,然后根据传递进来(由 ax 寄存器传入)的函数号,到函数表中调用对应的函数,里面的函数执行完成后,再次进入保护模式,加载 EIP 和 ESP 寄存器从而回到 realadr_call_entry 函数中。GDT 还是 imghead.asm 汇编代码文件中的 GDT,这没有变,因为它是由 GDTR 寄存器指向的。

write_realintsvefile() 函数的功能与意义:它会把映像文件中的initldrsve.bin文件写入到特定的内存空间中,而initldrsve.bin正是上面的realintsve.asm文件编译出来的。

二级引导器主函数

先建立一个 C 文件 ldrkrlentry.c,在其中写上一个主函数:

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

ldrkrl_entry()函数是在initldr32.asm文件中被调用,从那条call ldrkrl_entry指令开始进入ldrkrl_entry()函数,在其中调用了init_bstartparm()函数,它负责处理开始参数的。

小结:
大致的启动流程:
1、grub启动后,选择对应的启动菜单项,grub会通过自带文件系统驱动,定位到对应的eki文件

2、grub会尝试加载eki文件【eki文件需要满足grub多协议引导头的格式要求】
这些是在imginithead.asm中实现的,所以要包括:
A、grub文件头,包括魔数、grub1和grub2支持等
B、定位的_start符号等

3、grub校验成功后,会调用_start,然跳转到_entry
A、_entry中:关闭中断
B、加载GDT
C、然后进入_32bits_mode,清理寄存器,设置栈顶
D、调用inithead_entry【C】

4、inithead_entry.c
A、从imginithead.asm进入后,首先进入函数调用inithead_entry
B、初始化光标,清屏
C、从eki文件内部,找到initldrsve.bin文件,并分别拷贝到内存的指定物理地址
D、从eki文件内部,找到initldrkrl.bin文件,并分别拷贝到内存的指定物理地址
E、返回imginithead.asm

5、imginithead.asm中继续执行
jmp 0x200000
而这个位置,就是initldrkrl.bin在内存的位置ILDRKRL_PHYADR
所以后面要执行initldrkrl.bin的内容

6、这样就到了ldrkrl32.asm的_entry
A、将GDT加载到GDTR寄存器【内存】
B、将IDT加载到IDTR寄存器【中断】
C、跳转到_32bits_mode
初始寄存器
初始化栈
调用ldrkrl_entry【C】

7、ldrkrlentry.c
A、初始化光标,清屏
B、收集机器参数init_bstartparm【C】

8、bstartparm.c
A、初始化machbstart_t
B、各类初始化函数,填充machbstart_t的内容
C、返回

9、ldrkrlentry.c
A、返回

10、ldrkrl32.asm
A、跳转到0x2000000地址继续执行

posted @ 2022-05-28 22:12  牛犁heart  阅读(467)  评论(0编辑  收藏  举报