首页 > 技术文章 > 设置工作模式与环境(中):建造二级引导器

YXBLOGXYY 2022-03-30 17:08 原文

1)上节课安装的GRUB不是已经 把我们的操作 系统加载到内存中了吗?为什么还要二级引导器?

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

2)二级引导器先去收集了计算机的硬件信息,那么这个硬件信息以何种方式,在哪个位置让我的操作系统知道呢?

  • 信息的话我们设计 一个数据结构来存放,地址的话放在内存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;

3)二级引导器的功能文件我们怎样设计的?

 

 

4)上面的文件我们需要编译才能使用,编译过程是怎样的?

 

 

把三个文件打包成映像文件

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

5)现在二级引导器文件虽然有了,那GRUB如何找到他呢?

  • 写一个GRUB头,这里分两个子文件

    • imginithead.asm 汇编文件,让 GRUB 识别,同时设置 C 语言运行环境,用于调用 C 函数

    • inithead.c 文件,找到二级引导器的核心文件——initldrkrl.bin,并且把它放置到特定的内存地址上。

6)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地址到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地址

7)上述代码的最后调用了 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 文件写入到特定的内存地址空间中,具体地址在上面代码中的宏有详细定义。

8)前面我们的imghead.asm 汇编文件代码中,我们的最后一条指令是“jmp 0x200000”,那么它是跳到哪里去了 ?

  • 其实就是 我们下面写 的C文件中把二级引导器核心文件放置的位置。这一跳就直接 进入二级引导器的主模块了。

9)现在我们跳到二级引导器的主模块了,那对应的CPU啊 ,寄存器啊这类的环境应该也要做相应的改变,代码应该是怎样的?

 
 _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中断表的地址和长度

10)接下来我们要获取内存布局信息,还要设置显卡的工作模式,这些都是需要BIOS提供的中断服务才能去完成的,可是我们在C函数中要想直接调用中断是不可能的,因为C的CPU工作环境是32位保护模式,中断的工作环境是16位的实模式。这该如何调用呢?

  • 我们的C正在工作,然后被中断了,所以我们要把C语言环境下的CPU上下文,也就是保护模式下的一些寄存器啥的保存到我们的内存中。

  • 现在我们可以切换到实模式了,我们调用BIOS中断,设置好我们的显卡,内存啊相关的配置,然后把他们保存到内存中。

  • 最后我们切换回保护模式,重新把我们寄存器的信息恢复,那我们的环境就是C环境了。

11)上面的三个流程用代码是怎样实现的?

 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 $

12)现在还差二级引导器的主函数,长什么样?

 
 void ldrkrl_entry()
 {
     init_bstartparm();
     return;
 }
  • init_bstartparm()就是收集计算机信息的函数,下节内容我们再去编写他。

  •  

推荐阅读