设置工作模式与环境

操作系统实战45讲01:运行HelloOS_麦兜的学习笔记的博客-CSDN博客_操作系统45讲

参考:HelloOS操作记录_牛客博客

以下操作环境是:Windows10+VirtualBox(新建虚拟机: Ubuntu-64-bit; HelloOS)

需要先了解:

几行汇编几行C实现一个最简单的内核_了不起的盖茨比。的博客-CSDN博客

在这里插入图片描述

        PC 机 BIOS 固件是固化在 PC 机主板上的 ROM 芯片中的,掉电也能保存,PC 机上电后的第一条指令就是 BIOS 固件中的,它负责检测和初始化 CPU、内存及主板平台,然后加载引导设备(大概率是硬盘)中的第一个扇区数据,到 0x7c00 地址开始的内存空间,再接着跳转到 0x7c00 处执行指令,在我们这里的情况下就是 GRUB 引导程序。

当然,更先进的UEFI BIOS则不同
 

安装VBox增强工具

(物理主机与虚拟机共享剪切板数据等)

注:在虚拟机里安装好Ubuntu操作系统后--启动,按提示安装组件增强功能等后,需要进入目录(/media/....)下执行 sudo ./VBoxLinuxAdditions.run ,之后在控制-重启,然后就可以与win10主机共享剪切板数据了。

 

虚拟机Ubuntu访问物理主机win10下的文件夹

 

         Ubuntu-终端-执行如下命令(在cd ~即家目录),新建一个共享路径/mnt/shared,与1中的共享文件夹cosmos关联起来,之后进入/mnt/shared目录下,就可以看到win10目录下cosmos文件夹中的内容了

(第一条建立目录执行一次就可以了,第二步在每次启动Ubuntu后需再执行一次,也有自动挂载不用每次执行的命令,但是没有尝试过)
sudo mkdir /mnt/shared

sudo mount -t vboxsf cosmos /mnt/shared

 设置工作模式与环境(上):建立计算机

Ubuntu20 安装virtualbox

sudo apt update
sudo apt install virtualbox virtualbox-ext-pack

步骤

1、操作系统需要放在硬盘中,所以需要生产一块硬盘

        其实大多数虚拟机都是用文件来模拟硬盘的,即主机系统(HOST OS 即你使用的物理机系统 )下特定格式的文件,虚拟机中操作系统的数据只是写入了这个文件中。

        其实虚拟机只是用特定格式的文件来模拟硬盘,所以生产虚拟硬盘就变成了生成对应格式的文件,这就容易多了。我们要建立 100MB 的硬盘,这意味着要生成 100MB 的大文件。下面我们用 Linux 下的 dd 命令(用指定大小的块拷贝一个文件,并在拷贝的同时进行指定的转换)生成 100MB 的纯二进制的文件(就是 1~100M 字节的文件里面填充为 0 ),如下所示。


dd bs=512 if=/dev/zero of=hd.img count=204800

;bs:表示块大小,这里是512字节
;if:表示输入文件,/dev/zero就是Linux下专门返回0数据的设备文件,读取它就返回0
;of:表示输出文件,即我们的硬盘文件。
;count:表示输出多少块

执行以上命令就可以生成 100MB 的文件。文件数据为全 0。由于我们不用转换数据,就是需要全 0 的文件,所以 dd 命令只需要这几个参数就行

2、本机系统(host)识别和访问硬盘,需要硬盘上有文件系统

        虚拟硬盘也需要格式化才能使用,所谓格式化就是在硬盘上建立文件系统。只有建立了文件系统,现有的成熟操作系统才能在其中存放数据。

        可是,问题来了。虚拟硬盘毕竟是个文件,如何让 Linux 在一个文件上建立文件系统呢?

        这个问题我们要分成三步来解决。

        第一步,把虚拟硬盘文件变成 Linux 下的回环设备,让 Linux 以为这是个设备。其实在 Linux 下文件可以是设备,设备可以是文件。下面我们用 losetup 命令,将 hd.img 变成 Linux 的回环设备,代码如下。

(1)lsblk查看当前的的设备:

 (2)上述loop0-loop9均已有挂载点,需使用一个没有用到的,与课程中不同,如:sudo losetup /dev/loop10 hd.img 将 hd.img 变成 Linux 的回环设备

        第二步,格式化回环设备,建立 EXT4 文件系统 

       回环设备可以把文件虚拟成 Linux 块设备,用来模拟整个文件系统,让用户可以将其看作硬盘、光驱或软驱等设备,并且可用 mount 命令挂载当作目录来使用。
        我们可以用 Linux 下的 mkfs.ext4 命令格式化这个 /dev/loop0 回环块设备,在里面建立 EXT4 文件系统。

sudo mkfs.ext4 -q /dev/loop10

           第三步,将 hd.img 文件当作块设备,把它挂载到事先建立的 hdisk 目录下,并在其中建立一个 boot
我们用 Linux 下的 mount 命令,将 hd.img 文件当作块设备,把它挂载到事先建立的 hdisk 目录下,并在其中建立一个 boot,这也是后面安装 GRUB 需要的。如果能建立成功,就说明前面的工作都正确完成了。
mount 命令只能识别在纯二进制文件上建立的文件系统,如果使用虚拟机自己生成的硬盘文件,mount 就无法识别我们的文件系统了。

先创建一个hdisk目录,随便在哪儿创建都行

sudo mkdir hdisk

然后挂载硬盘文件,相当于给我们创建的二进制文件变成回环设备之后,建立文件系统,然后与这个二进制文件做一个映射


sudo mount -o loop ./hd.img ./hdisk/ ;挂载硬盘文件
sudo mkdir ./hdisk/boot/ ;建立boot目录

此时使用df -h 命令:

 可以看到成功将loop10 挂载到hdisk目录

3、安装GRUB,主要把grub安装到新建的文件里,所以把文件设置为设备,再把设备挂载到本机(host)已存在的目录上,安装grub

        Linux 会把 GRUB 安装在我们的物理硬盘上,可是我们现在要把 GRUB 安装在我们的虚拟硬盘上,而且我们的操作系统还没有安装程序。所以,我们得利用一下手上 Linux(HOST OS),通过 GRUB 的安装程序,把 GRUB 安装到指定的设备上(虚拟硬盘)。

sudo grub-install --boot-directory=./hdisk/boot/ --force --allow-floppy /dev/loop10

--boot-directory 指向先前我们在虚拟硬盘中建立的boot目录
--force --allow-floppy :指向我们的虚拟硬盘设备文件/dev/loop10

如果遇到EFI的问题:
添加参数:

sudo grub-install --target=i386-pc --boot-directory=./hdisk/boot/ --force --allow-floppy /dev/loop10

       

         可以看到,现在 ./hdisk/boot/ 目录下多了一个 grub 目录,表示我们的 GRUB 安装成功。请注意,这里还要在 /hdisk/boot/grub/ 目录下建立一个 grub.cfg 的文件,grub正是通过这个文件的内容,查找到操作系统映像文件的。

menuentry 'HelloOS' {
insmod part_msdos
insmod ext2
set root='hd0' #只有一个硬盘,因此设置根目录为hd0(就是hd.vdi)-----修改了这里,改了之后成功了。(注:这里原来是'hd0,msdos1',暂时还没搞懂二者的区别,后面再说)
multiboot2 /boot/HelloOS.eki #加载boot目录下的HelloOS.eki文件
boot #引导启动
}
set timeout_style=menu
if [ "${timeout}" = 0 ]; then
  set timeout=10 #等待10秒钟自动启动
fi

4、现在硬盘里安装好了grub,需要使用虚拟机启动这个硬盘了,但是虚拟机不识别这个硬盘文件格式,所以要转成虚拟机能识别格式的硬盘文件格式

VBoxManage convertfromraw ./hd.img --format VDI ./hd.vdi
;convertfromraw 指向原始格式文件
;--format VDI  表示转换成虚拟需要的VDI格式

5、设置虚拟机使用这个硬盘 

首先将制作好的hd.vdi文件拷贝到windows 主机的共享目录下,然后在windows 下 virtualbox 新建:

注册我们制作的.VDI文件 

 

 点击启动

 显示:

 

         上面的错误显示,GRUB 没有找到 HelloOS.eki 文件,这是因为我们从来没有向虚拟硬盘中放入 HelloOS.eki 文件,所以才会失败。

        但这是我们最成功的失败,因为我们配置好了虚拟机,手动建造了硬盘,并在其上安装了 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;

规划二级引导器

        在开始写代码之前,我们先来从整体划分一下二级引导器的功能模块,从全局了解下功能应该怎么划分:

实现 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地址v

        上述代码的最后调用了 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 文件,代码如下:

_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 语言代码才能重新恢复执行。

要完成上面的功能,必须要写一个汇编函数才能完成,我们就把它写在 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 中。这个 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 位的二进制的文件,并把它放在 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 文件 ldrkrlentry.c,在其中写上一个主函数,代码如下:

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

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

        建造二级引导器的目的,就是要收集机器环境信息。我们要把这些信息形成一个有结构的参数,传递给我们的操作系统内核以备后续使用。由此,我们能够确定,init_bstartparm() 函数成了收集机器环境信息的主函数。

        在二级引导器中,我们要检查 CPU 是否支持 64 位的工作模式、收集内存布局信息,看看是不是合乎我们操作系统的最低运行要求,还要设置操作系统需要的 MMU 页表、设置显卡模式、释放中文字体文件。

检查与收集机器信息


        我们需要一个函数,来检查CPU的模式,收集内存信息,设置内核栈,设置内核字体,建立内核MMU页表数据


//初始化machbstart_t结构体,清0,并设置一个标志
void machbstart_t_init(machbstart_t* initp)
{
    memset(initp,0,sizeof(machbstart_t));
    initp->mb_migc=MBS_MIGC;
    return;
}
void init_bstartparm()
{
    machbstart_t* mbsp = MBSPADR;//1MB的内存地址
    machbstart_t_init(mbsp);
    return;
}

        这个函数一开始在内存1MB的地方初始化了一个我们上一讲说过的一个储存机器信息的数据结构。后续还会调用其他函数。

检查CPU


        我们首先得弄清楚CPU能执行怎样的代码,是否支持64位长模式。需要两个函数:
第一个函数检查CPU是否支持CPUID指令 。
第二个函数用CPUID来检查CPU是否支持64位长模式。
        如果有一条检查失败,我们就打印提示信息,并主动死机。
 


//通过改写Eflags寄存器的第21位,观察其位的变化判断是否支持CPUID
int chk_cpuid()
{
    int rets = 0;
    __asm__ __volatile__(
        "pushfl \n\t"
        "popl %%eax \n\t"
        "movl %%eax,%%ebx \n\t"
        "xorl $0x0200000,%%eax \n\t"
        "pushl %%eax \n\t"
        "popfl \n\t"
        "pushfl \n\t"
        "popl %%eax \n\t"
        "xorl %%ebx,%%eax \n\t"
        "jz 1f \n\t"
        "movl $1,%0 \n\t"
        "jmp 2f \n\t"
        "1: movl $0,%0 \n\t"
        "2: \n\t"
        : "=c"(rets)
        :
        :);
    return rets;
}
//检查CPU是否支持长模式
int chk_cpu_longmode()
{
    int rets = 0;
    __asm__ __volatile__(
        "movl $0x80000000,%%eax \n\t"
        "cpuid \n\t" //把eax中放入0x80000000调用CPUID指令
        "cmpl $0x80000001,%%eax \n\t"//看eax中返回结果
        "setnb %%al \n\t" //不为0x80000001,则不支持0x80000001号功能
        "jb 1f \n\t"
        "movl $0x80000001,%%eax \n\t"
        "cpuid \n\t"//把eax中放入0x800000001调用CPUID指令,检查edx中的返回数据
        "bt $29,%%edx  \n\t" //长模式 支持位  是否为1
        "setcb %%al \n\t"
        "1: \n\t"
        "movzx %%al,%%eax \n\t"
        : "=a"(rets)
        :
        :);
    return rets;
}
//检查CPU主函数
void init_chkcpu(machbstart_t *mbsp)
{
    if (!chk_cpuid())
    {
        kerror("Your CPU is not support CPUID sys is die!");
        CLI_HALT();
    }
    if (!chk_cpu_longmode())
    {
        kerror("Your CPU is not support 64bits mode sys is die!");
        CLI_HALT();
    }
    mbsp->mb_cpumode = 0x40;//如果成功则设置机器信息结构的cpu模式为64位
    return;
}

获取内存布局

        我们描述内存,主要看内存开始地址,内存大小,内存类型,所以我们获取内存布局信息就是获取这个e820map_t结构体的数组,然后检查一下内存大小是否合适(不能太小)
        描述一段内存有一个数据结构,如下所示。


#define RAM_USABLE 1 //可用内存
#define RAM_RESERV 2 //保留内存不可使用
#define RAM_ACPIREC 3 //ACPI表相关的
#define RAM_ACPINVS 4 //ACPI NVS空间
#define RAM_AREACON 5 //包含坏内存
typedef struct s_e820{
    u64_t saddr;    /* 内存开始地址 */
    u64_t lsize;    /* 内存大小 */
    u32_t type;    /* 内存类型 */
}e820map_t;

         我们说获取内存布局信息就是获取这个结构体的数组,然后检查一下内存大小是否合适,这个用一个函数来实现:


#define ETYBAK_ADR 0x2000
#define PM32_EIP_OFF (ETYBAK_ADR)
#define PM32_ESP_OFF (ETYBAK_ADR+4)
#define E80MAP_NR (ETYBAK_ADR+64)//保存e820map_t结构数组元素个数的地址
#define E80MAP_ADRADR (ETYBAK_ADR+68) //保存e820map_t结构数组的开始地址
void init_mem(machbstart_t *mbsp)
{
    e820map_t *retemp;
    u32_t retemnr = 0;
    mmap(&retemp, &retemnr);
    if (retemnr == 0)
    {
        kerror("no e820map\n");
    }
    //根据e820map_t结构数据检查内存大小
    if (chk_memsize(retemp, retemnr, 0x100000, 0x8000000) == NULL)
    {
        kerror("Your computer is low on memory, the memory cannot be less than 128MB!");
    }
    mbsp->mb_e820padr = (u64_t)((u32_t)(retemp));//把e820map_t结构数组的首地址传给mbsp->mb_e820padr 
    mbsp->mb_e820nr = (u64_t)retemnr;//把e820map_t结构数组元素个数传给mbsp->mb_e820nr 
    mbsp->mb_e820sz = retemnr * (sizeof(e820map_t));//把e820map_t结构数组大小传给mbsp->mb_e820sz 
    mbsp->mb_memsz = get_memsize(retemp, retemnr);//根据e820map_t结构数据计算内存大小。
    return;
}

        怎么获取e820map 结构数组?要调用了 BIOS 中断,就能获取 e820map 结构数组。

初始化内核栈


        c语言实现必需有栈,所以给即将运行的内核初始化一个栈。
        在机器信息结构 machbstart_t 中,记录一下栈地址和栈大小,供内核在启动时使用
注意:
        我们要判断某个地址和内存中存放的地址是否冲突,因为内存中已经有机器信息结构、内存视图结构数组、二级引导器、内核映像文件,所以在处理内存空间时不能和内存中已经存在的他们冲突,否则就要覆盖他们的数据。0x8f000~(0x8f000+0x1001),正是我们的内核栈空间,我们需要检测它是否和其它空间有冲突。


#define IKSTACK_PHYADR (0x90000-0x10)
#define IKSTACK_SIZE 0x1000
//初始化内核栈
void init_krlinitstack(machbstart_t *mbsp)
{
    if (1 > move_krlimg(mbsp, (u64_t)(0x8f000), 0x1001))
    {
        kerror("iks_moveimg err");
    }
    mbsp->mb_krlinitstack = IKSTACK_PHYADR;//栈顶地址
    mbsp->mb_krlitstacksz = IKSTACK_SIZE; //栈大小是4KB
    return;
}

放置内核文件与字库文件

        因为我们的内核已经编译成了一个独立的二进制程序,和其它文件一起被打包到映像文件中了。所以我们必须要从映像中把它解包出来,将其放在特定的物理内存空间中才可以,放置字库文件和放置内核文件的原理一样,所以我们来一起实现。


//放置内核文件
void init_krlfile(machbstart_t *mbsp)
{
//在映像中查找相应的文件,并复制到对应的地址,并返回文件的大小,这里是查找kernel.bin文件
    u64_t sz = r_file_to_padr(mbsp, IMGKRNL_PHYADR, "kernel.bin");
    if (0 == sz)
    {
        kerror("r_file_to_padr err");
    }
    //放置完成后更新机器信息结构中的数据
    mbsp->mb_krlimgpadr = IMGKRNL_PHYADR;
    mbsp->mb_krlsz = sz;
    //mbsp->mb_nextwtpadr始终要保持指向下一段空闲内存的首地址 
    mbsp->mb_nextwtpadr = P4K_ALIGN(mbsp->mb_krlimgpadr + mbsp->mb_krlsz);
    mbsp->mb_kalldendpadr = mbsp->mb_krlimgpadr + mbsp->mb_krlsz;
    return;
}
//放置字库文件
void init_defutfont(machbstart_t *mbsp)
{
    u64_t sz = 0;
    //获取下一段空闲内存空间的首地址 
    u32_t dfadr = (u32_t)mbsp->mb_nextwtpadr;
//在映像中查找相应的文件,并复制到对应的地址,并返回文件的大小,这里是查找font.fnt文件
    sz = r_file_to_padr(mbsp, dfadr, "font.fnt");
    if (0 == sz)
    {
        kerror("r_file_to_padr err");
    }
    //放置完成后更新机器信息结构中的数据
    mbsp->mb_bfontpadr = (u64_t)(dfadr);
    mbsp->mb_bfontsz = sz;
    //更新机器信息结构中下一段空闲内存的首地址  
    mbsp->mb_nextwtpadr = P4K_ALIGN((u32_t)(dfadr) + sz);
    mbsp->mb_kalldendpadr = mbsp->mb_bfontpadr + mbsp->mb_bfontsz;
    return;
}

建立MMU页表

        在二级引导器中建立 MMU 页表数据,目的就是要在内核加载运行之初开启长模式时,MMU 需要的页表数据已经准备好了

        由于我们的内核虚拟地址空间从 0xffff800000000000 开始,所以我们这个虚拟地址映射到从物理地址 0 开始,大小都是 0x400000000 即 16GB,也就是说我们要虚拟地址空间:0xffff800000000000~0xffff800400000000 映射到物理地址空间 0~0x400000000。

使用长模式下的 2MB 分页方式

下面我们用代码实现它
        核心逻辑由两重循环,外层循环控制页目录指针,有16项,每一项指向一个页目录,每个页目录有512个物理页。
        内层循环控制物理页,物理地址每次增加2MB。即每次外循环对应512个内循环即偏移地址。

这样做的原因:虚拟内存16GB = 16 * 2^10 * 2^10 * 2^10 = 16(项)*512(即2^9) * 2 ^20

        顶级页目录第0项得和某一项指向同一个页目录指针页,这样他们访问同一个物理地址,内核启动初期,虚拟地址和物理地址就是相同的。顶级页目录中第 0 项和第((KRNL_VIRTUAL_ADDRESS_START) >> KPML4_SHIFT) & 0x1ff 项,指向同一个页目录指针页,这样的话就能让虚拟地址:0xffff800000000000~0xffff800400000000 和虚拟地址:0~0x400000000,访问到同一个物理地址空间 0~0x400000000,这样做是有目的,内核在启动初期,虚拟地址和物理地址要保持相同。
分段和分页的理解:Linux 内存管理 | 地址映射:分段、分页、段页_凌桓丶的博客-CSDN博客_linux段式内存管理


#define KINITPAGE_PHYADR 0x1000000
void init_bstartpages(machbstart_t *mbsp)
{
    //顶级页目录
    u64_t *p = (u64_t *)(KINITPAGE_PHYADR);//16MB地址处
    //页目录指针
    u64_t *pdpte = (u64_t *)(KINITPAGE_PHYADR + 0x1000);
    //页目录
    u64_t *pde = (u64_t *)(KINITPAGE_PHYADR + 0x2000);
    //物理地址从0开始
    u64_t adr = 0;
    if (1 > move_krlimg(mbsp, (u64_t)(KINITPAGE_PHYADR), (0x1000 * 16 + 0x2000)))
    {
        kerror("move_krlimg err");
    }
    //将顶级页目录、页目录指针的空间清0
    for (uint_t mi = 0; mi < PGENTY_SIZE; mi++)
    {
        p[mi] = 0;
        pdpte[mi] = 0;
    }
    //映射
    for (uint_t pdei = 0; pdei < 16; pdei++)
    {
        pdpte[pdei] = (u64_t)((u32_t)pde | KPDPTE_RW | KPDPTE_P);
        for (uint_t pdeii = 0; pdeii < PGENTY_SIZE; pdeii++)
        {//大页KPDE_PS 2MB,可读写KPDE_RW,存在KPDE_P
            pde[pdeii] = 0 | adr | KPDE_PS | KPDE_RW | KPDE_P;
            adr += 0x200000;
        }
        pde = (u64_t *)((u32_t)pde + 0x1000);
    }
    //让顶级页目录中第0项和第((KRNL_VIRTUAL_ADDRESS_START) >> KPML4_SHIFT) & 0x1ff项,指向同一个页目录指针页  
    p[((KRNL_VIRTUAL_ADDRESS_START) >> KPML4_SHIFT) & 0x1ff] = (u64_t)((u32_t)pdpte | KPML4_RW | KPML4_P);
    p[0] = (u64_t)((u32_t)pdpte | KPML4_RW | KPML4_P);
    //把页表首地址保存在机器信息结构中
    mbsp->mb_pml4padr = (u64_t)(KINITPAGE_PHYADR);
    mbsp->mb_subpageslen = (u64_t)(0x1000 * 16 + 0x2000);
    mbsp->mb_kpmapphymemsz = (u64_t)(0x400000000);
    return;
}

设置图形模式

在计算机加电启动时,计算机上显卡会自动进入文本模式,文本模式只能显示 ASCII 字符,不能显示汉字和图形,所以我们要让通过BIOS中断使得显卡切换到图形模式


void init_graph(machbstart_t* mbsp)
{
    //初始化图形数据结构
    graph_t_init(&mbsp->mb_ghparm);
    //获取VBE模式,通过BIOS中断
    get_vbemode(mbsp);
    //获取一个具体VBE模式的信息,通过BIOS中断
    get_vbemodeinfo(mbsp);
    //设置VBE模式,通过BIOS中断
    set_vbemodeinfo();
    return;
}

串联

现在我们需要在 init_bstartparm() 函数中把它们串联起来,即按照事情的先后顺序,依次调用它们完成相应的工作,实现检查、收集机器信息,设置工作环境。


void init_bstartparm()
{
    machbstart_t *mbsp = MBSPADR;
    machbstart_t_init(mbsp);
    //检查CPU
    init_chkcpu(mbsp);
    //获取内存布局
    init_mem(mbsp);
    //初始化内核栈
    init_krlinitstack(mbsp);
    //放置内核文件
    init_krlfile(mbsp);
    //放置字库文件
    init_defutfont(mbsp);
    init_meme820(mbsp);
    //建立MMU页表
    init_bstartpages(mbsp);
    //设置图形模式
    init_graph(mbsp);
    return;
}

显示logo

logo 文件是个 24 位的位图文件,目前为了简单起见,我们只支持这种格式的图片文件。下面我们去调用这个函数。


void logo(machbstart_t* mbsp)
{
    u32_t retadr=0,sz=0;
    //在映像文件中获取logo.bmp文件
    get_file_rpadrandsz("logo.bmp",mbsp,&retadr,&sz);
    if(0==retadr)
    {
        kerror("logo getfilerpadrsz err");
    }
    //显示logo文件中的图像数据
    bmp_print((void*)retadr,mbsp);
    return;
}
void init_graph(machbstart_t* mbsp)
{    
    //……前面代码省略
    //显示
    logo(mbsp);
    return;
}

进入:

 执行

ok@ok-VirtualBox:~/lesson12/Cosmos$ make vboxtest

 产物在:/exckrnl/hd.vdi,拷贝到windows下新建虚拟机使用即可。下面的error不用理会

        到此为止我们的二级引导器已经建立起来了,
        成功从 GRUB 手中接过了权柄,开始了它自己的一系列工作,二级引导器完成的工作不算
少,我来帮你梳理一下,重点如下。
        1. 二级引导器彻底摆脱了 GRUB 的控制之后,就开始检查 CPU,获取内存布局信息,确认
是不是我们要求的 CPU 和内存大小,接着初始化内核栈、放置好内核文件和字库文件,建
立 MMU 页表数据和设置好图形模式,为后面运行内核做好准备。
        2. 当二级引导器完成了上述功能后,就会显示我们操作系统的 logo,这标志着二级引导器
所有的工作一切正常。
        3. 进入 Cosmos,我们的二级引导器通过跳转到 Cosmos 的入口,结束了自己光荣使命,
Cosmos 的入口是一小段汇编代码,主要是开启 CPU 的长模式,最后调用了 Cosmos 的
第一个 C 函数 hal_start。

第一个C函数,hal层初始化,初始化平台

切换CPU到长模式

        在调用第一个c函数之前,我们仍然要写一段汇编,切换CPU进入长模式,初始化CPU寄存器和C语言要用的栈。
        因为目前代码执行流在二级引导器中,进入到 Cosmos 中这样在二级引导器中初始过的东西都不能用了

        因为 CPU 进入了长模式,寄存器的位宽都变了,所以需要重新初始化。让我们一起来写这段汇编代码吧,我们先在 Cosmos/hal/x86/ 下建立一个 init_entry.asm 文件,写上后面这段代码

[section .start.text]
[BITS 32]
_start:
    cli
    mov ax,0x10
    mov ds,ax
    mov es,ax
    mov ss,ax
    mov fs,ax
    mov gs,ax
    lgdt [eGdtPtr]        
    ;开启 PAE
    mov eax, cr4
    bts eax, 5                      ; CR4.PAE = 1
    mov cr4, eax
    mov eax, PML4T_BADR             ;加载MMU顶级页目录
    mov cr3, eax  
    ;开启 64bits long-mode
    mov ecx, IA32_EFER
    rdmsr
    bts eax, 8                      ; IA32_EFER.LME =1
    wrmsr
    ;开启 PE 和 paging
    mov eax, cr0
    bts eax, 0                      ; CR0.PE =1
    bts eax, 31
    ;开启 CACHE       
    btr eax,29                    ; CR0.NW=0
    btr eax,30                    ; CR0.CD=0  CACHE
    mov cr0, eax                    ; IA32_EFER.LMA = 1
    jmp 08:entry64
[BITS 64]
entry64:
    mov ax,0x10
    mov ds,ax
    mov es,ax
    mov ss,ax
    mov fs,ax
    mov gs,ax
    xor rax,rax
    xor rbx,rbx
    xor rbp,rbp
    xor rcx,rcx
    xor rdx,rdx
    xor rdi,rdi
    xor rsi,rsi
    xor r8,r8
    xor r9,r9
    xor r10,r10
    xor r11,r11
    xor r12,r12
    xor r13,r13
    xor r14,r14
    xor r15,r15
    mov rbx,MBSP_ADR
    mov rax,KRLVIRADR
    mov rcx,[rbx+KINITSTACK_OFF]
    add rax,rcx
    xor rcx,rcx
    xor rbx,rbx
    mov rsp,rax
    push 0
    push 0x8
    mov rax,hal_start                 ;调用内核主函数
    push rax
    dw 0xcb48
    jmp $
[section .start.data]
[BITS 32]
x64_GDT:
enull_x64_dsc:  dq 0  
ekrnl_c64_dsc:  dq 0x0020980000000000   ; 64-bit 内核代码段
ekrnl_d64_dsc:  dq 0x0000920000000000   ; 64-bit 内核数据段
euser_c64_dsc:  dq 0x0020f80000000000   ; 64-bit 用户代码段
euser_d64_dsc:  dq 0x0000f20000000000   ; 64-bit 用户数据段
eGdtLen      equ  $ - enull_x64_dsc   ; GDT长度
eGdtPtr:    dw eGdtLen - 1      ; GDT界限
        dq ex64_GDT

        上述代码中,1~11 行表示加载 70~75 行的 GDT,13~17 行是设置 MMU 并加载在二级引导器中准备好的 MMU 页表,19~30 行是开启长模式并打开 Cache,34~54 行则是初始化长模式下的寄存器,55~61 行是读取二级引导器准备的机器信息结构中的栈地址,并用这个数据设置 RSP 寄存器。

        最关键的是 63~66 行,它开始把 8 和 hal_start 函数的地址压入栈中。dw 0xcb48 是直接写一条指令的机器码——0xcb48,这是一条返回指令。这个返回指令有点特殊,它会把栈中的数据分别弹出到 RIP,CS 寄存器,这正是为了调用我们 Cosmos 的第一个 C 函数 hal_start。

        由于这是第一个 C 函数,也是初始化函数,我们还是要为它单独建立一个文件,以显示对它的尊重,依然在 Cosmos/hal/x86/ 下建立一个 hal_start.c 文件。写上这样一个函数。


void hal_start()
{
    //第一步:初始化hal层
    //第二步:初始化内核层
    for(;;);
    return;
}

hal层(硬件抽象层)初始化


        平台初始化,hal 层的内存初始化,中断初始化,最后进入到内核层的初始化。
        为了分离硬件的特性,我们设计了 hal 层,把硬件相关的操作集中在这个层,并向上提供接口,目的是让内核上层不用关注硬件相关的细节,也能方便以后移植和扩展。
        下面我们在 Cosmos/hal/x86/ 下建立一个 halinit.c 文件,写出 hal 层的初始化函数

void init_hal()
{
    //初始化平台
    //初始化内存
    //初始化中断
    return;
}


        这个函数也是一个调用者,没怎么干活。不过根据代码的注释能看出,它调用的函数多一点,但主要是完成初始化平台、初始化内存、初始化中断的功能函数。

初始化平台


        我们先来写好平台初始化函数,因为它需要最先被调用。这个函数主要负责完成两个任务,
一是把二级引导器建立的机器信息结构复制到 hal 层中的一个全局变量中,方便内核中的其它代码使用里面的信息,之后二级引导器建立的数据所占用的内存都会被释放。
二是要初始化图形显示驱动,内核在运行过程要在屏幕上输出信息
下面我们在 Cosmos/hal/x86/ 下建立一个 halplatform.c 文件,写上如下代码。


void machbstart_t_init(machbstart_t *initp)
{
    //清零
    memset(initp, 0, sizeof(machbstart_t));
    return;
}

void init_machbstart()
{
    machbstart_t *kmbsp = &kmachbsp;
    machbstart_t *smbsp = MBSPADR;//物理地址1MB处
    machbstart_t_init(kmbsp);
    //复制,要把地址转换成虚拟地址
    memcopy((void *)phyadr_to_viradr((adr_t)smbsp), (void *)kmbsp, sizeof(machbstart_t));
    return;
}
//平台初始化函数
void init_halplaltform()
{
    //复制机器信息结构
    init_machbstart();
    //初始化图形显示驱动
    init_bdvideo();
    return;
}


        kmachbsp 你可能会有点奇怪,它是个结构体变量,结构体类型是 machbstart_t,这个结构和二级引导器所使用的一模一样。同时,它还是一个 hal 层的全局变量,我们想专门有个文件定义所有 hal 层的全局变量,于是我们在 Cosmos/hal/x86/ 下建立一个 halglobal.c 文件,写上如下代码。

//全局变量定义变量放在data段
#define HAL_DEFGLOB_VARIABLE(vartype,varname) \
EXTERN  __attribute__((section(".data"))) vartype varname

HAL_DEFGLOB_VARIABLE(machbstart_t,kmachbsp);


初始化图形显示驱动
下面,我们在 Cosmos/hal/x86/ 下的 bdvideo.c 文件中,写好 init_bdvideo 函数。

void init_bdvideo()
{
    dftgraph_t *kghp = &kdftgh;
    //初始化图形数据结构,里面放有图形模式,分辨率,图形驱动函数指针
    init_dftgraph();
    //初始bga图形显卡的函数指针
    init_bga();
    //初始vbe图形显卡的函数指针
    init_vbe();
    //清空屏幕 为黑色
    fill_graph(kghp, BGRA(0, 0, 0));
    //显示背景图片 
    set_charsdxwflush(0, 0);
    hal_background();
    return;
}


        init_defgraph() 函数初始了 dftgraph_t 结构体类型的变量 kdftgh,我们在 halglobal.c 文件中定义这个变量,结构类型我们这样来定义。


typedef struct s_DFTGRAPH
{
    u64_t gh_mode;         //图形模式
    u64_t gh_x;            //水平像素点
    u64_t gh_y;            //垂直像素点
    u64_t gh_framphyadr;   //显存物理地址 
    u64_t gh_fvrmphyadr;   //显存虚拟地址
    u64_t gh_fvrmsz;       //显存大小
    u64_t gh_onepixbits;   //一个像素字占用的数据位数
    u64_t gh_onepixbyte;
    u64_t gh_vbemodenr;    //vbe模式号
    u64_t gh_bank;         //显存的bank数
    u64_t gh_curdipbnk;    //当前bank
    u64_t gh_nextbnk;      //下一个bank
    u64_t gh_banksz;       //bank大小
    u64_t gh_fontadr;      //字库地址
    u64_t gh_fontsz;       //字库大小
    u64_t gh_fnthight;     //字体高度
    u64_t gh_nxtcharsx;    //下一字符显示的x坐标
    u64_t gh_nxtcharsy;    //下一字符显示的y坐标
    u64_t gh_linesz;       //字符行高
    pixl_t gh_deffontpx;   //默认字体大小
    u64_t gh_chardxw;
    u64_t gh_flush;
    u64_t gh_framnr;
    u64_t gh_fshdata;      //刷新相关的
    dftghops_t gh_opfun;   //图形驱动操作函数指针结构体
}dftgraph_t;
typedef struct s_DFTGHOPS
{
    //读写显存数据
    size_t (*dgo_read)(void* ghpdev,void* outp,size_t rdsz);
    size_t (*dgo_write)(void* ghpdev,void* inp,size_t wesz);
    sint_t (*dgo_ioctrl)(void* ghpdev,void* outp,uint_t iocode);
    //刷新
    void   (*dgo_flush)(void* ghpdev);
    sint_t (*dgo_set_bank)(void* ghpdev, sint_t bnr);
    //读写像素
    pixl_t (*dgo_readpix)(void* ghpdev,uint_t x,uint_t y);
    void   (*dgo_writepix)(void* ghpdev,pixl_t pix,uint_t x,uint_t y);
    //直接读写像素 
    pixl_t (*dgo_dxreadpix)(void* ghpdev,uint_t x,uint_t y);
    void   (*dgo_dxwritepix)(void* ghpdev,pixl_t pix,uint_t x,uint_t y);
    //设置x,y坐标和偏移
    sint_t (*dgo_set_xy)(void* ghpdev,uint_t x,uint_t y);
    sint_t (*dgo_set_vwh)(void* ghpdev,uint_t vwt,uint_t vhi);
    sint_t (*dgo_set_xyoffset)(void* ghpdev,uint_t xoff,uint_t yoff);
    //获取x,y坐标和偏移
    sint_t (*dgo_get_xy)(void* ghpdev,uint_t* rx,uint_t* ry);
    sint_t (*dgo_get_vwh)(void* ghpdev,uint_t* rvwt,uint_t* rvhi);
    sint_t (*dgo_get_xyoffset)(void* ghpdev,uint_t* rxoff,uint_t* ryoff);
}dftghops_t;
//刷新显存
void flush_videoram(dftgraph_t *kghp)
{
    kghp->gh_opfun.dgo_flush(kghp);
    return;
}


        把这些实际的图形驱动函数的地址填入了这个结构体中,然后通过这个结构体,我们就可以调用到相应的函数了
下面我们要把这些函数调用起来:

//在halinit.c文件中
void init_hal()
{
    init_halplaltform();
    return;
}
//在hal_start.c文件中
void hal_start()
{
    init_hal();//初始化hal层,其中会调用初始化平台函数,在那里会调用初始化图形驱动
    for(;;);
    return;
}


然后我们make vboxtest 就能看见图
注意三个文件加 chmod +x 


初始化内存


        首先,我们在 Cosmos/hal/x86/ 下建立一个 halmm.c 文件,用于初始化内存,为了后面的内存管理器作好准备。
        hal 层的内存初始化比较容易,只要向内存管理器提供内存空间布局信息就可以。
        Cosmos 的内存管理器需要保存更多的信息,最好是顺序的内存布局信息,这样可以增加额外的功能属性,同时降低代码的复杂度。
        BIOS 提供的结构无法满足前面这些要求。不过我们也有办法解决,只要以 BIOS 提供的结构为基础,设计一套新的数据结构就搞定了。这个结构可以这样设计。

#define PMR_T_OSAPUSERRAM 1
#define PMR_T_RESERVRAM 2
#define PMR_T_HWUSERRAM 8
#define PMR_T_ARACONRAM 0xf
#define PMR_T_BUGRAM 0xff
#define PMR_F_X86_32 (1<<0)
#define PMR_F_X86_64 (1<<1)
#define PMR_F_ARM_32 (1<<2)
#define PMR_F_ARM_64 (1<<3)
#define PMR_F_HAL_MASK 0xff

typedef struct s_PHYMMARGE
{
    spinlock_t pmr_lock;//保护这个结构是自旋锁
    u32_t pmr_type;     //内存地址空间类型
    u32_t pmr_stype;
    u32_t pmr_dtype;    //内存地址空间的子类型,见上面的宏
    u32_t pmr_flgs;     //结构的标志与状态
    u32_t pmr_stus;
    u64_t pmr_saddr;    //内存空间的开始地址
    u64_t pmr_lsize;    //内存空间的大小
    u64_t pmr_end;      //内存空间的结束地址
    u64_t pmr_rrvmsaddr;//内存保留空间的开始地址
    u64_t pmr_rrvmend;  //内存保留空间的结束地址
    void* pmr_prip;     //结构的私有数据指针,以后扩展所用
    void* pmr_extp;     //结构的扩展数据指针,以后扩展所用
}phymmarge_t;


        有些情况下内核要另起炉灶,不想把所有的内存空间都交给内存管理器去管理,所以要保留一部分内存空间,这就是上面结构中那两个 pmr_rrvmsaddr、pmr_rrvmend 字段的作用。
有了数据结构,我们还要写代码来操作它:

u64_t initpmrge_core(e820map_t *e8sp, u64_t e8nr, phymmarge_t *pmargesp)
{
    u64_t retnr = 0;
    for (u64_t i = 0; i < e8nr; i++)
    {
        //根据一个e820map_t结构建立一个phymmarge_t结构
        if (init_one_pmrge(&e8sp[i], &pmargesp[i]) == FALSE)
        {
            return retnr;
        }
        retnr++;
    }
    return retnr;
}
void init_phymmarge()
{
    machbstart_t *mbsp = &kmachbsp;
    phymmarge_t *pmarge_adr = NULL;
    u64_t pmrgesz = 0;
    //根据machbstart_t机器信息结构计算获得phymmarge_t结构的开始地址和大小
    ret_phymmarge_adrandsz(mbsp, &pmarge_adr, &pmrgesz);
    u64_t tmppmrphyadr = mbsp->mb_nextwtpadr;
    e820map_t *e8p = (e820map_t *)((adr_t)(mbsp->mb_e820padr));
    //建立phymmarge_t结构
    u64_t ipmgnr = initpmrge_core(e8p, mbsp->mb_e820nr, pmarge_adr);
    //把phymmarge_t结构的地址大小个数保存machbstart_t机器信息结构中
    mbsp->mb_e820expadr = tmppmrphyadr;
    mbsp->mb_e820exnr = ipmgnr;
    mbsp->mb_e820exsz = ipmgnr * sizeof(phymmarge_t);
    mbsp->mb_nextwtpadr = PAGE_ALIGN(mbsp->mb_e820expadr + mbsp->mb_e820exsz);
    //phymmarge_t结构中地址空间从低到高进行排序,我已经帮你写好了
    phymmarge_sort(pmarge_adr, ipmgnr);
    return;
}


        结合上面的代码,你会发现这是根据 e820map_t 结构数组,建立了一个 phymmarge_t 结构数组,init_one_pmrge 函数正是把 e820map_t 结构中的信息复制到 phymmarge_t 结构中来。
下面我们把这些函数,用一个总管函数调动起来,这个总管函数叫什么名字好呢?当然是 init_halmm,如下所示。

void init_halmm()
{
    init_phymmarge();
    //init_memmgr();
    return;
}


        这里 init_halmm 函数中还调用了 init_memmgr 函数,这个正是这我们内存管理器初始化函数,我会在内存管理的那节课展开讲。而 init_halmm 函数将要被 init_hal 函数调用。

初始化中断


中断被分为两类:

        异常,这是同步的,原因是错误和故障,就像汽车引擎坏了。不修复错误就不能继续运行,所以这时,CPU 会跳到这种错误的处理代码那里开始运行,运行完了会返回。

        中断,这是异步的,我们通常说的中断就是这种类型,它是因为外部事件而产生的,就好像旅游时女朋友来电话了。通常设备需要 CPU 关注时,会给 CPU 发送一个中断信号,所以这时 CPU 会跳到处理这种事件的代码那里开始运行,运行完了会返回。

        在 x86 CPU 上,最多支持 256 个中断,还记得前面所说的中断表和中断门描述符吗,这意味着我们要准备 256 个中断门描述符和 256 个中断处理程序的入口。

下面我们来定义它,如下所示:

typedef struct s_GATE
{
        u16_t   offset_low;     /* 偏移 */
        u16_t   selector;       /* 段选择子 */
        u8_t    dcount;         /* 该字段只在调用门描述符中有效。如果在利用调用门调用子程序时引起特权级的转换和堆栈的改变,需要将外层堆栈中的参数复制到内层堆栈。该双字计数字段就是用于说明这种情况发生时,要复制的双字参数的数量。*/
        u8_t    attr;           /* P(1) DPL(2) DT(1) TYPE(4) */
        u16_t   offset_high;    /* 偏移的高位段 */
        u32_t   offset_high_h;
        u32_t   offset_resv;
}__attribute__((packed)) gate_t;
//定义中断表
HAL_DEFGLOB_VARIABLE(gate_t,x64_idt)[IDTMAX];


        中断表其实是个 gate_t 结构的数组,由 CPU 的 IDTR 寄存器指向,IDTMAX 为 256。
但是光有数组还不行,还要设置其中的数据,下面我们就来设计这个函数,建立一个文件 halsgdidt.c,在其中写一个函数,代码如下。


//vector 向量也是中断号
//desc_type 中断门类型,中断门,陷阱门
//handler 中断处理程序的入口地址
//privilege 中断门的权限级别
void set_idt_desc(u8_t vector, u8_t desc_type, inthandler_t handler, u8_t privilege)
{
    gate_t *p_gate = &x64_idt[vector];
    u64_t base = (u64_t)handler;
    p_gate->offset_low = base & 0xFFFF;
    p_gate->selector = SELECTOR_KERNEL_CS;
    p_gate->dcount = 0;
    p_gate->attr = (u8_t)(desc_type | (privilege << 5));
    p_gate->offset_high = (u16_t)((base >> 16) & 0xFFFF);
    p_gate->offset_high_h = (u32_t)((base >> 32) & 0xffffffff);
    p_gate->offset_resv = 0;
    return;
}


        上面的代码,正是按照要求,把这些数据填入中断门描述符中的。有了中断门之后,还差中断处理程序,
中断处理程序只负责这三件事:

  • 保护 CPU 寄存器,即中断发生时的程序运行的上下文。
  • 调用中断处理程序,这个程序可以是修复异常的,可以是设备驱动程序中对设备响应的程序。
  • 恢复 CPU 寄存器,即恢复中断时程序运行的上下文,使程序继续运行。
  • 以上这些操作又要用汇编代码才可以编写,我觉得这是内核中最重要的部分,所以我们建立一个文件,并用 kernel.asm 命名。

我们先来写好完成以上三个功能的汇编宏代码,避免写 256 遍同样的代码,代码如下所示。


//保存中断后的寄存器
%macro  SAVEALL  0
  push rax
  push rbx
  push rcx
  push rdx
  push rbp
  push rsi
  push rdi
  push r8
  push r9
  push r10
  push r11
  push r12
  push r13
  push r14
  push r15
  xor r14,r14
  mov r14w,ds
  push r14
  mov r14w,es
  push r14
  mov r14w,fs
  push r14
  mov r14w,gs
  push r14
%endmacro
//恢复中断后寄存器
%macro  RESTOREALL  0
  pop r14
  mov gs,r14w
  pop r14 
  mov fs,r14w
  pop r14
  mov es,r14w
  pop r14
  mov ds,r14w
  pop r15
  pop r14
  pop r13
  pop r12
  pop r11
  pop r10
  pop r9
  pop r8
  pop rdi
  pop rsi
  pop rbp
  pop rdx
  pop rcx
  pop rbx
  pop rax
  iretq
%endmacro
//保存异常下的寄存器
%macro  SAVEALLFAULT 0
  push rax
  push rbx
  push rcx
  push rdx
  push rbp
  push rsi
  push rdi
  push r8
  push r9
  push r10
  push r11
  push r12
  push r13
  push r14
  push r15
  xor r14,r14
  mov r14w,ds
  push r14
  mov r14w,es
  push r14
  mov r14w,fs
  push r14
  mov r14w,gs
  push r14
%endmacro
//恢复异常下寄存器
%macro  RESTOREALLFAULT  0
  pop r14
  mov gs,r14w
  pop r14 
  mov fs,r14w
  pop r14
  mov es,r14w
  pop r14
  mov ds,r14w
  pop r15
  pop r14
  pop r13
  pop r12
  pop r11
  pop r10
  pop r9
  pop r8
  pop rdi
  pop rsi
  pop rbp
  pop rdx
  pop rcx
  pop rbx
  pop rax
  add rsp,8
  iretq
%endmacro
//没有错误码CPU异常
%macro  SRFTFAULT 1
  push    _NOERRO_CODE
  SAVEALLFAULT
  mov r14w,0x10
  mov ds,r14w
  mov es,r14w
  mov fs,r14w
  mov gs,r14w
  mov   rdi,%1 ;rdi, rsi
  mov   rsi,rsp
  call   hal_fault_allocator
  RESTOREALLFAULT
%endmacro
//CPU异常
%macro  SRFTFAULT_ECODE 1
  SAVEALLFAULT
  mov r14w,0x10
  mov ds,r14w
  mov es,r14w
  mov fs,r14w
  mov gs,r14w
  mov   rdi,%1
  mov   rsi,rsp
  call   hal_fault_allocator
  RESTOREALLFAULT
%endmacro
//硬件中断
%macro  HARWINT  1
  SAVEALL
  mov r14w,0x10
  mov ds,r14w
  mov es,r14w
  mov fs,r14w
  mov gs,r14w
  mov  rdi, %1
  mov   rsi,rsp
  call    hal_intpt_allocator
  RESTOREALL
%endmacro


        其实最重要的只有两个指令:push、pop,这两个正是用来压入寄存器和弹出寄存器的,正好可以用来保存和恢复 CPU 所有的通用寄存器。
        有的 CPU 异常,CPU 自动把异常码压入到栈中,而有的 CPU 异常没有异常码,为了统一,我们对没有异常码的手动压入一个常数,维持栈的平衡。
下面我们在 kernel.asm 中写好它们:

/除法错误异常 比如除0
exc_divide_error:
  SRFTFAULT 0
//单步执行异常
exc_single_step_exception:
  SRFTFAULT 1
exc_nmi:
  SRFTFAULT 2
//调试断点异常
exc_breakpoint_exception:
  SRFTFAULT 3
//溢出异常
exc_overflow:
  SRFTFAULT 4
//段不存在异常
exc_segment_not_present:
  SRFTFAULT_ECODE 11
//栈异常
exc_stack_exception:
  SRFTFAULT_ECODE 12
//通用异常
exc_general_protection:
  SRFTFAULT_ECODE 13
//缺页异常
exc_page_fault:
  SRFTFAULT_ECODE 14
hxi_exc_general_intpfault:
  SRFTFAULT 256
//硬件1~7号中断
hxi_hwint00:
  HARWINT  (INT_VECTOR_IRQ0+0)
hxi_hwint01:
  HARWINT  (INT_VECTOR_IRQ0+1)
hxi_hwint02:
  HARWINT  (INT_VECTOR_IRQ0+2)
hxi_hwint03:
  HARWINT  (INT_VECTOR_IRQ0+3)
hxi_hwint04:
  HARWINT  (INT_VECTOR_IRQ0+4)
hxi_hwint05:
  HARWINT  (INT_VECTOR_IRQ0+5)
hxi_hwint06:
  HARWINT  (INT_VECTOR_IRQ0+6)
hxi_hwint07:
  HARWINT  (INT_VECTOR_IRQ0+7)


        为了突出重点,这里没有全部展示代码 ,你只用搞清原理就行了。那有了中断处理程序的入口地址,下面我们就可以在 halsgdidt.c 文件写出函数设置中断门描述符了,代码如下。

void init_idt_descriptor()
{
//一开始把所有中断的处理程序设置为保留的通用处理程序
    for (u16_t intindx = 0; intindx <= 255; intindx++)
    {
        set_idt_desc((u8_t)intindx, DA_386IGate, hxi_exc_general_intpfault, PRIVILEGE_KRNL);
    }
    set_idt_desc(INT_VECTOR_DIVIDE, DA_386IGate, exc_divide_error, PRIVILEGE_KRNL);
    set_idt_desc(INT_VECTOR_DEBUG, DA_386IGate, exc_single_step_exception, PRIVILEGE_KRNL);
    set_idt_desc(INT_VECTOR_NMI, DA_386IGate, exc_nmi, PRIVILEGE_KRNL);
    set_idt_desc(INT_VECTOR_BREAKPOINT, DA_386IGate, exc_breakpoint_exception, PRIVILEGE_USER);
    set_idt_desc(INT_VECTOR_OVERFLOW, DA_386IGate, exc_overflow, PRIVILEGE_USER);
//篇幅所限,未全部展示
    set_idt_desc(INT_VECTOR_PAGE_FAULT, DA_386IGate, exc_page_fault, PRIVILEGE_KRNL);
    set_idt_desc(INT_VECTOR_IRQ0 + 0, DA_386IGate, hxi_hwint00, PRIVILEGE_KRNL);
    set_idt_desc(INT_VECTOR_IRQ0 + 1, DA_386IGate, hxi_hwint01, PRIVILEGE_KRNL);
    set_idt_desc(INT_VECTOR_IRQ0 + 2, DA_386IGate, hxi_hwint02, PRIVILEGE_KRNL);
    set_idt_desc(INT_VECTOR_IRQ0 + 3, DA_386IGate, hxi_hwint03, PRIVILEGE_KRNL);
    //篇幅所限,未全部展示
     return;
}


        上面的代码已经很明显了,一开始把所有中断的处理程序设置为保留的通用处理程序,避免未知中断异常发生了 CPU 无处可去,然后对已知的中断和异常进一步设置,这会覆盖之前的通用处理程序,这样就可以确保万无一失。
        下面我们把这些代码整理一下,安装到具体的调用路径上,让上层调用者调用到就好了。我们依然在 halintupt.c 文件中写上 init_halintupt() 函数:

void init_halintupt()
{
    init_idt_descriptor();
    init_intfltdsc();
    return;
}


        到此为止,CPU 体系层面的中断就初始化完成了。你会发现,我们在 init_halintupt() 函数中还调用了 init_intfltdsc() 函数,这个函数是干什么的呢?请往下看。
        前面我们只是解决了中断的 CPU 相关部分,而 CPU 只是响应中断,但是并不能解决产生中断的问题。

        比如缺页中断来了,我们要解决内存地址映射关系,程序才可以继续运行。再比如硬盘中断来了,我们要读取硬盘的数据,要处理这问题,就要写好相应的处理函数。因为有些处理是内核所提供的,而有些处理函数是设备驱动提供的,想让它们和中断关联起来,就要好好设计中断处理框架了。

中断框架


下面我们来画幅图,描述中断框架的设计:

在这里插入图片描述

        可以看到,中断、异常分发器的左侧的东西我们已经处理完成,下面需要写好中断、异常分发器和中断异常描述符。
        先来搞定中断异常描述,结合框架图,中断异常描述也是个表,它在 C 语言中就是个结构数组,让我们一起来写好这个数组:

typedef struct s_INTFLTDSC{    
    spinlock_t  i_lock;    
    u32_t       i_flg;    
    u32_t       i_stus;    
    uint_t      i_prity;        //中断优先级    
    uint_t      i_irqnr;        //中断号    
    uint_t      i_deep;         //中断嵌套深度    
    u64_t       i_indx;         //中断计数    
    list_h_t    i_serlist;      //也可以使用中断回调函数的方式
    uint_t      i_sernr;        //中断回调函数个数   
    list_h_t    i_serthrdlst;   //中断线程链表头    
    uint_t      i_serthrdnr;    //中断线程个数    
    void*       i_onethread;    //只有一个中断线程时直接用指针    
    void*       i_rbtreeroot;   //如果中断线程太多则按优先级组成红黑树
    list_h_t    i_serfisrlst;      
    uint_t      i_serfisrnr;       
    void*       i_msgmpool;     //可能的中断消息池    
    void*       i_privp;    
    void*       i_extp;
}intfltdsc_t;


        中断可以由线程的方式执行,也可以是一个回调函数,该函数的地址放另一个结构体中,这个结构体我已经帮你写好了,如下所示。

typedef drvstus_t (*intflthandle_t)(uint_t ift_nr,void* device,void* sframe); //中断处理函数的指针类型
typedef struct s_INTSERDSC{    
    list_h_t    s_list;        //在中断异常描述符中的链表
    list_h_t    s_indevlst;    //在设备描述描述符中的链表
    u32_t       s_flg;        
    intfltdsc_t* s_intfltp;    //指向中断异常描述符 
    void*       s_device;      //指向设备描述符
    uint_t      s_indx;    
    intflthandle_t s_handle;   //中断处理的回调函数指针
}intserdsc_t;


        如果内核或者设备驱动程序要安装一个中断处理函数,就要先申请一个 intserdsc_t 结构体,然后把中断函数的地址写入其中,最后把这个结构挂载到对应的 intfltdsc_t 结构中的 i_serfisrlst 链表中。

        因为我们的计算机中可能有很多设备,每个设备都可能产生中断,但是中断控制器的中断信号线是有限的。你可以这样理解:中断控制器最多只能产生几十号中断号,而设备不止几十个,所以会有多个设备共享一根中断信号线。这就导致一个中断发生后,无法确定是哪个设备产生的中断,所以我们干脆让设备驱动程序来决定,因为它是最了解设备的。这里我们让这个 intfltdsc_t 结构上的所有中断处理函数都依次执行,查看是不是自己的设备产生了中断,如果是就处理,不是则略过。
        好,明白了这两个结构之后,我们就要开始初始化了。首先是在 halglobal.c 文件定义 intfltdsc_t 结构。


//定义intfltdsc_t结构数组大小为256
HAL_DEFGLOB_VARIABLE(intfltdsc_t,machintflt)[IDTMAX];


下面我们再来实现中断、异常分发器函数,如下所示。


//中断处理函数
void hal_do_hwint(uint_t intnumb, void *krnlsframp)
{    
    intfltdsc_t *ifdscp = NULL;    
    cpuflg_t cpuflg;
    //根据中断号获取中断异常描述符地址    
    ifdscp = hal_retn_intfltdsc(intnumb);
    //对断异常描述符加锁并中断    
    hal_spinlock_saveflg_cli(&ifdscp->i_lock, &cpuflg);    
    ifdscp->i_indx++;    
    ifdscp->i_deep++;
    //运行中断处理的回调函数
    hal_run_intflthandle(intnumb, krnlsframp);    
    ifdscp->i_deep--;
    //解锁并恢复中断状态    
    hal_spinunlock_restflg_sti(&ifdscp->i_lock, &cpuflg);    
    return;
}
//异常分发器
void hal_fault_allocator(uint_t faultnumb, void *krnlsframp)
{
    //我们的异常处理回调函数也是放在中断异常描述符中的
    hal_do_hwint(faultnumb, krnlsframp);
    return;
}
//中断分发器
void hal_hwint_allocator(uint_t intnumb, void *krnlsframp)
{
    hal_do_hwint(intnumb, krnlsframp);
    return;
}


        前面的代码确实是按照我们的中断框架设计实现的,下面我们去实现 hal_run_intflthandle 函数,它负责调用中断处理的回调函数。


void hal_run_intflthandle(uint_t ifdnr, void *sframe)
{    
    intserdsc_t *isdscp;    
    list_h_t *lst;
    //根据中断号获取中断异常描述符地址    
    intfltdsc_t *ifdscp = hal_retn_intfltdsc(ifdnr);
    //遍历i_serlist链表    
    list_for_each(lst, &ifdscp->i_serlist)    
    {   
        //获取i_serlist链表上对象即intserdsc_t结构
        isdscp = list_entry(lst, intserdsc_t, s_list);  
        //调用中断处理回调函数      
        isdscp->s_handle(ifdnr, isdscp->s_device, sframe);    
    }
    return;
}



        上述代码已经很清楚了,循环遍历 intfltdsc_t 结构中,i_serlist 链表上所有挂载的 intserdsc_t 结构,然后调用 intserdsc_t 结构中的中断处理的回调函数。

中断控制器


        我们把 CPU 端的中断搞定了以后,还有设备端的中断,这个可以交给设备驱动程序,但是 CPU 和设备之间的中断控制器,还需要我们出面解决。
        多个设备的中断信号线都会连接到中断控制器上,中断控制器可以决定启用或者屏蔽哪些设备的中断,还可以决定设备中断之间的优先线,所以它才叫中断控制器。

        x86 平台上的中断控制器有多种,最开始是 8259A,然后是 IOAPIC,最新的是 MSI-X。为了简单的说明原理,我们选择了 8259A 中断控制器。
        下面我们来做代码初始化,我们程序员可以向 8259A 写两种命令字: ICW 和 OCW;ICW 这种命令字用来实现 8259a 芯片的初始化。而 OCW 这种命令用来向 8259A 发布命令,以对其进行控制。OCW 可以在 8259A 被初始化之后的任何时候被使用。我已经把代码定好了,放在了 8259.c 文件中,如下所示:


void init_i8259()
{
    //初始化主从8259a
    out_u8_p(ZIOPT, ICW1);    
    out_u8_p(SIOPT, ICW1);    
    out_u8_p(ZIOPT1, ZICW2);    
    out_u8_p(SIOPT1, SICW2);    
    out_u8_p(ZIOPT1, ZICW3);    
    out_u8_p(SIOPT1, SICW3);    
    out_u8_p(ZIOPT1, ICW4);    
    out_u8_p(SIOPT1, ICW4);
    //屏蔽全部中断源
    out_u8_p(ZIOPT1, 0xff);    
    out_u8_p(SIOPT1, 0xff);        
    return;
}


进入内核层


        hal 层的初始化已经完成,按照前面的设计,我们的 Cosmos 还有内核层,我们下面就要进入到内核层,建立一个文件,写上一个函数,作为本课程的结尾。
        但是这个函数是个空函数,目前什么也不做,它是为 Cosmos 内核层初始化而存在的,但是由于课程只进行到这里,所以我只是写个空函数,为后面的课程做好准备。由于内核层是从 hal 层进入的,必须在 hal_start() 函数中被调用,所以在此完成这个函数——init_krl()。

void init_krl()
{ 
    //禁止函数返回    
    die(0);    
    return;
}


下面我们在 hal_start() 函数中调用它就行了,如下所示

void hal_start()
{   
    //初始化Cosmos的hal层 
    init_hal();
    //初始化Cosmos的内核层    
    init_krl();    
    return;
}



        从上面的代码中,不难发现 Cosmos 的 hal 层初始化完成后,就自动进入了 Cosmos 内核层的初始化。

总结
        一个 C 函数,干的第一件重要工作就是调用 hal 层的初始化函数。这个初始化函数首先初始化了平台,初始化了机器信息结构供内核的其它代码使用,还初始化了我们图形显示驱动、显示了背景图片;其次是初始化了内存管理相关的数据结构;接着初始了中断,中断处理框架是两层,所以最为复杂;最后初始化了中断控制器。

​​​​​​​

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值