0000 0010 实现一个最简单的内核

一.  PC 机的引导流程

        写操作系统要用汇编和 C 语言,尽管这个 Hello OS 很小,但也要用到两种编程语言。其实,现有的商业操作系统都是用这两种语言开发出来的。

          我们也不打算从 PC 的引导程序开始写起,原因是目前我们的知识储备还不够,所以先借用一下 GRUB 引导程序,只要我们的 PC 机上安装了 Ubuntu Linux 操作系统,GRUB 就已经存在了。

 

        在写 Hello OS 之前,我们先要搞清楚 Hello OS 的引导流程,如下图所示:

 

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

二.  Hello OS 引导汇编代码

        明白了 PC 机的启动流程,下面只剩下我们的 Hello OS 了,我们马上就去写好它。

        我们先来写一段汇编代码。这里我要特别说明一个问题:为什么不能直接用 C?

        C 作为通用的高级语言,不能直接操作特定的硬件,而且 C 语言的函数调用、函数传参,都需要用栈。

        栈简单来说就是一块内存空间,其中数据满足后进先出的特性,它由 CPU 特定的栈寄存器指向,所以我们要先用汇编代码处理好这些 C 语言的工作环境。


;*******第一部分***********
MBT_HDR_FLAGS EQU 0x00010003
MBT_HDR_MAGIC EQU 0x1BADB002 ;多引导协议头魔数
MBT_HDR2_MAGIC EQU 0xe85250d6 ;第二版多引导协议头魔数
global _start ;导出_start符号
extern main ;导入外部的main函数符号
[section .start.text] ;定义.start.text代码节
[bits 32] ;汇编成32位代码
_start:
jmp _entry
ALIGN 8
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
;以上是GRUB所需要的头
ALIGN 8
mbt2_hdr:
DD MBT_HDR2_MAGIC
DD 0
DD mbt2_hdr_end - mbt2_hdr
DD -(MBT_HDR2_MAGIC + 0 + (mbt2_hdr_end - mbt2_hdr))
DW 2, 0
DD 24
DD mbt2_hdr
DD _start
DD 0
DD 0
DW 3, 0
DD 12
DD _entry
DD 0
DW 0, 0
DD 8
mbt2_hdr_end:
;以上是GRUB2所需要的头
;包含两个头是为了同时兼容GRUB、GRUB2
;*****************************************
ALIGN 8
;*****************第二部分******************
_entry:
;关中断
cli
;关不可屏蔽中断
in al, 0x70
or al, 0x80
out 0x70,al
;重新加载GDT
lgdt [GDT_PTR]
;*******************************************
jmp dword 0x8 :_32bits_mode
;**************第三部分*********************
_32bits_mode:
;下面初始化C语言可能会用到的寄存器
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
;初始化栈,C语言需要栈才能工作
mov esp,0x9000
;调用C语言函数main
call main
;***********************************************
;让CPU停止执行指令
halt_step:
halt
jmp halt_step
;****************第四部分*************************
GDT_START:
knull_dsc: dq 0
kcode_dsc: dq 0x00cf9e000000ffff
kdata_dsc: dq 0x00cf92000000ffff
k16cd_dsc: dq 0x00009e000000ffff
k16da_dsc: dq 0x000092000000ffff
GDT_END:
GDT_PTR:
GDTLEN dw GDT_END-GDT_START-1
GDTBASE dd GDT_START

以上的汇编代码(/lesson01/HelloOS/entry.asm)分为 4 个部分:

        1. 第一部分:用汇编定义的 GRUB 的多引导协议头,其实就是一定格式的数据,我们的 Hello OS 是用 GRUB 引导的,当然要遵循 GRUB 的多引导协议标准,让 GRUB 能识别我们的 Hello OS。之所以有两个引导头,是为了兼容 GRUB1 和 GRUB2。

        2. 第二部分:关掉中断,设定 CPU 的工作模式。你现在可能不懂,没事儿,后面 CPU 相关的课程我们会专门再研究它。

        3. 第三部分:初始化 CPU 的寄存器和 C 语言的运行环境。

        4. 第四部分:GDT_START 开始的,是 CPU 工作模式所需要的数据,同样,后面讲 CPU 时会专门介绍。

 三.   Hello OS 的主函数

        不知道你有没有发现一个问题? 上面的汇编代码调用了 main 函数,而在其代码中并没有看到其函数体,而是从外部引入了一个符号。

        那是因为这个函数是用 C 语言写的在(/lesson01/HelloOS/main.c)中,最终它们分别由 nasm 和 GCC 编译成可链接模块,由 LD 链接器链接在一起,形成可执行的程序文件:


#include "vgastr.h"
void main()
{
  printf("Hello OS!");
  return;
} 

        以上这段代码,你应该很熟悉了吧?不过这不是应用程序的 main 函数,而是 Hello OS 的 main 函数。

        其中的 printf 也不是应用程序库中的那个 printf 了,而是需要我们自己实现了。

四.   控制计算机屏幕

        接着我们再看下显卡,这和我们接下来要写的代码有直接关联。

        计算机屏幕显示往往是显卡的输出,显卡有很多形式:集成在主板的叫集显,做在 CPU 芯片内的叫核显,独立存在通过 PCIE 接口连接的叫独显,性能依次上升,价格也是。

        独显的高性能是游戏玩家们所钟爱的,3D 图形显示往往要涉及顶点处理、多边形的生成和变换、纹理、着色、打光、栅格化等。而这些任务的计算量超级大,所以独显往往有自己的 RAM、多达几百个运算核心的处理器。因此独显不仅仅是可以显示图像,而且可以执行大规模并行计算,比如“挖矿”。

        我们要在屏幕上显示字符,就要编程操作显卡。

        其实无论我们 PC 上是什么显卡,它们都支持一种叫 VESA 的标准,这种标准下有两种工作模式:字符模式和图形模式。显卡们为了兼容这种标准,不得不自己提供一种叫 VGABIOS 的固件程序。

        下面,我们来看看显卡的字符模式的工作细节。它把屏幕分成 24 行,每行 80 个字符,把这(24*80)个位置映射到以 0xb8000 地址开始的内存中,每两个字节对应一个字符,其中一个字节是字符的 ASCII 码,另一个字节为字符的颜色值。如下图所示:

         这里先提个醒:C 语言字符串是以 0 结尾的,其字符编码通常是 utf8,而 utf8 编码对 ASCII 字符是兼容的,即英文字符的 ASCII 编码和 utf8 编码是相等的.


void _strwrite(char* string)
{
  char* p_strdst = (char*)(0xb8000);//指向显存的开始地址
  while (*string)
  {
    *p_strdst = *string++;
    p_strdst += 2;
  }
  return;
}

void printf(char* fmt, ...)
{
  _strwrite(fmt);
  return;
}

        代码很简单,printf 函数直接调用了 _strwrite 函数,而 _strwrite 函数正是将字符串里每个字符依次定入到 0xb8000 地址开始的显存中,而 p_strdst 每次加 2,这也是为了跳过字符的颜色信息的空间。

        到这,Hello OS 相关的代码就写好了,下面就是编译和安装了。

五.   编译和安装 Hello OS

        Hello OS 的代码都已经写好,这时就要进入安装测试环节了。在安装之前,我们要进行系统编译,即把每个代码模块编译最后链接成可执行的二进制文件。

        你可能觉得我在小题大做,编译不就是输入几条命令吗,这么简单的工作也值得一说?

        确实,对于我们 Hello OS 的编译工作来说特别简单,因为总共才三个代码文件,最多四条命令就可以完成。但是以后我们 Hello OS 的文件数量会爆炸式增长,一个成熟的商业操作系统更是多达几万个代码模块文件,几千万行的代码量,是这世间最复杂的软件工程之一。所以需要一个牛逼的工具来控制这个巨大的编译过程。

make 工具

        自行查博客学习

编译

        下面我们用一张图来描述我们 Hello OS 的编译过程,如下所示:

 

 安装 Hello OS

        经过上述流程,我们就会得到 Hello OS.bin 文件,但是我们还要让 GRUB 能够找到它,才能在计算机启动时加载它。这个过程我们称为安装,不过这里没有写安装程序,得我们手动来做。

        经研究发现,GRUB 在启动时会加载一个 grub.cfg 的文本文件,根据其中的内容执行相应的操作,其中一部分内容就是启动项。

        GRUB 首先会显示启动项到屏幕,然后让我们选择启动项,最后 GRUB 根据启动项对应的信息,加载 OS 文件到内存。下面来看看我们 Hello OS 的启动项:


menuentry 'HelloOS' {
     insmod part_msdos #GRUB加载分区模块识别分区
     insmod ext2 #GRUB加载ext文件系统模块识别ext文件系统
     set root='hd0,msdos4' #注意boot目录挂载的分区,这是我机器上的情况
     multiboot2 /boot/HelloOS.bin #GRUB以multiboot2协议加载HelloOS.bin
     boot #GRUB启动HelloOS.bin
}

  

      如果你不知道你的 boot 目录挂载的分区,可以在 Linux 系统的终端下输入命令:df /boot/,就会得到如下结果:


文件系统          1K-块    已用     可用      已用% 挂载点
/dev/sda4      48752308 8087584 38158536   18%    /

         其中的“sda4”就是硬盘的第四个分区,但是 GRUB 的 menuentry 中不能写 sda4,而是要写“hd0,msdos4”,这是 GRUB 的命名方式,hd0 表示第一块硬盘,结合起来就是第一块硬盘的第四个分区。

        把上面启动项的代码插入到你的 Linux 机器上的 /boot/grub/grub.cfg 文件中,然后把 Hello OS.bin 文件复制到 /boot/ 目录下,最后重启计算机,你就可以看到 Hello OS 的启动选项了。

        选择 Hello OS,按下 Enter 键,这样就可以成功启动我们自己的 Hello OS 了。

重点回顾

       首先,我们了解了从按下 PC 机电源开关开始,PC 机的引导过程。它从 CPU 上电,到加载 BIOS 固件,再由 BIOS 固件对计算机进行自检和默认的初始化,并加载 GRUB 引导程序,最后由 GRUB 加载具体的操作系统。

        其次,就到了我们这节课最难的部分,即用汇编语言和 C 语言实现我们的 Hello OS。

                第一步,用汇编程序初始化 CPU 的寄存器、设置 CPU 的工作模式和栈,最重要的是加入了 GRUB 引导协议头;第二步,切换到 C 语言,用 C 语言写好了主函数和控制显卡输出的函数,其间还了解了显卡的一些工作细节。

        最后,就是编译和安装 Hello OS 了。我们用了 make 工具编译整个代码,其实 make 会根据一些规则调用具体的 nasm、gcc、ld 等编译器,然后形成 Hello OS.bin 文件,你把这个文件写复制到 boot 分区,写好 GRUB 启动项,这样就好了。

思考题

        以上 printf 函数定义,其中有个形式参数很奇怪,请你思考下:为什么是“…”形式参数,这个形式参数有什么作用?

参考文章:手写操作系统(1) - 知乎

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

实现一个 Linux 内核中断程序,需要了解以下几个步骤: 1. 定义中断处理程序(handler) 2. 注册中断处理程序 3. 触发中断 以下是一个简单的 Linux 内核中断程序的代码示例: ```c #include <linux/interrupt.h> #include <linux/kernel.h> irqreturn_t my_interrupt(int irq, void *dev_id) { printk("Interrupt occurred!\n"); return IRQ_HANDLED; } static int __init my_init(void) { int irq = 1; // 中断号 int result = request_irq(irq, my_interrupt, IRQF_SHARED, "my_interrupt", &my_dev_id); if (result) { printk(KERN_ERR "Failed to register interrupt handler!\n"); return -EFAULT; } else { printk(KERN_INFO "Interrupt handler registered successfully!\n"); return 0; } } static void __exit my_exit(void) { free_irq(1, &my_dev_id); printk(KERN_INFO "Interrupt handler unregistered successfully!\n"); } module_init(my_init); module_exit(my_exit); MODULE_LICENSE("GPL"); ``` 在这个示例中,我们定义了一个名为 `my_interrupt` 的中断处理程序,当中断被触发时,该函数会输出一条信息并返回 `IRQ_HANDLED` 表示中断已被处理。 在 `my_init` 函数中,我们使用 `request_irq` 函数注册了中断处理程序。这个函数的第一个参数是中断号,第二个参数是中断处理程序的函数指针,第三个参数是中断标志,第四个参数是中断处理程序的名字,第五个参数是中断处理程序的设备 ID。 最后,在 `my_exit` 函数中,我们使用 `free_irq` 函数释放了中断处理程序。 要触发中断,可以使用硬件或软件方法,具体取决于你的应用场景。例如,如果你正在编写一个驱动程序,你可以在设备上触发中断来测试你的驱动程序是否能够正确响应中断。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值