首页 > 其他分享 >一个操作系统的设计与实现——第5章 加载内核

一个操作系统的设计与实现——第5章 加载内核

时间:2023-11-12 09:57:12浏览次数:29  
标签:... 表项 操作系统 扇区 段程序 内核 硬盘 加载

一直以来,我们都在使用汇编语言对MBR编程,但对于操作系统这样的复杂程序来说,使用汇编语言是比较困难的。本章将实现操作系统内核的加载与进入。

5.1 读硬盘的实现原理

操作系统存储于硬盘中,现在需要将其读出至内存。想要读硬盘,就需要依次进行以下操作:

  1. 设定读取的扇区数
  2. 设定起始扇区号
  3. 发送读硬盘命令
  4. 等待硬盘准备完毕
  5. 将硬盘中的数据读出

5.1.1 设定读取的扇区数

读取的扇区数需要写入到0x1f2端口。这是一个8位端口,如果向此端口写入0,则读取的扇区数为256个;否则,读取的扇区数就是写入的值。

5.1.2 设定起始扇区号

我们的操作系统使用的是具有28位逻辑扇区号的硬盘。然而,硬盘中用于存放逻辑扇区号的端口全都是8位的。也就是说,一共需要"3.5个端口"来存放逻辑扇区号。"3.5个端口"显然是不存在的,实际使用的是3个8位端口,加上1个8位端口的低4位,共同凑成28位的逻辑扇区号。这些端口如下表所示:

端口号 作用
0x1f3 存放逻辑扇区号的0~7位
0x1f4 存放逻辑扇区号的8~15位
0x1f5 存放逻辑扇区号的16~23位
0x1f6 低4位存放逻辑扇区号的24~27位,高4位固定为0xe

5.1.3 发送读硬盘命令

0x1f7端口是一个既可读又可写的8位端口。如果向此端口写入数据,其用于接收命令。读硬盘的命令是0x20

5.1.4 等待硬盘准备完毕

硬盘接收到0x20命令后就会开始准备。在此期间,需要不断的读0x1f7端口以查询硬盘状态。对于读取到的这个8位整数,只需要关注其中的两位:

  1. 如果第7位为1,表示硬盘忙,其他位都无效;如果第7位为0,表示硬盘不忙,其他位有效
  2. 如果第3位为1,表示硬盘已经准备就绪;如果第3位为0,表示硬盘尚未准备就绪

综上,等待的目标是第7位为0且第3位为1。

5.1.5 将硬盘中的数据读出

0x1f0端口用于读取数据。这是一个16位的端口。当硬盘准备完毕后,可以通过(大量的)in指令或insw指令将数据读出。

5.2 编译内核

本章代码5/Kernel.c是用于测试的内核。

内核在编译时不能依赖任何已有的库,且需要一些特殊设定。

请看本章代码5/Makefile

第3行,将内核编译成库文件。

在我们的操作系统中,会使用一些与C语言标准库重名的函数,命令中的-fno-builtin用于关闭GCC对这些函数名的警告。

第4行,将库文件链接,得到可执行文件。

-Ttext-segment 0x0用于设定内核在内存中的起始加载地址。事实上,最低1K内存中存储的是中断向量表,但我们并不使用这个表,所以可以直接覆盖这段内存。

-e main用于设定入口点,如果没有这个设置,链接器会提示"找不到_start"。读者可能会疑惑:C语言的入口点难道不是main函数吗?这个问题将在后续章节中讨论。此外,读者也可以将代码中的main改成_start,并去除-e main,也能通过链接。

第6行,将内核写入虚拟硬盘。在我们的操作系统中,内核使用99个扇区,与MBR使用的1个扇区共同凑整到100个扇区。

5.3 ELF文件

在Linux中,不管是上文得到的Kernel.o文件还是Kernel文件,其格式都是ELF,即可执行与可链接格式(Executable and Linkable Format,ELF)。ELF格式适用于多种文件,包括静态链接库,动态链接库,可执行程序等,在我们的操作系统中,只需要关注可执行程序。

ELF文件的加载分为两个阶段,首先需要将整个文件读入内存的一个缓冲区中,在这里解析该文件,并按文件中提供的信息将程序加载到目的地址。

ELF文件的开头记录了一些最重要的信息,以下只列出和可执行程序有关的部分:

相对于文件开头的偏移量 字节数 含义
0x0 1 魔数0x7f
0x1 3 字符串ELF
... ... ...
0x18 4 入口地址
0x1c 4 程序头表相对于文件开头的偏移量
... ... ...
0x26 2 程序头表中每个表项的大小
0x28 2 程序头表中表项的数量
... ... ...

程序头表中存放的是可执行程序的加载信息。其由一组表项构成,每个表项的结构如下:

相对于表项开头的偏移量 字节数 含义
0x0 4 类型
0x4 4 这段程序相对于ELF文件开头的偏移量
0x8 4 这段程序需要加载到的地址
... ... ...
0x10 4 这段程序的大小
0x14 4 这段程序要求的内存大小
... ... ...

在我们的操作系统中,只需要关注类型为0x1的表项,这个类型表示可加载的程序段。

之所以要区分"这段程序的大小"和"这段程序要求的内存大小",是因为BSS段的存在。所以,加载一段程序时要分两步:

  1. 使用"这段程序的大小",将这段程序加载到目的地址中
  2. 计算这段程序要求的内存大小 - 这段程序的大小,记作N,将这段程序需要加载到的地址 + 这段程序的大小后面的N字节清零,这样就完成了BSS段的加载

综上,加载并进入一个ELF文件需要以下步骤:

  1. 将ELF文件读取至缓冲区
  2. 取偏移量0x1c处的4字节整数,将其与ELF文件的起始地址相加,得到程序头表的起始地址
  3. 取偏移量0x26处的2字节整数,这是程序头表中每个表项的大小
  4. 取偏移量0x28处的2字节整数,这是程序头表中表项的数量
  5. 遍历程序头表,将其中每一段类型为0x1的程序加载到目的地址中,并将BSS段清零
  6. jmp到偏移量0x18处的入口地址

5.4 加载内核的实现

请看本章代码5/Mbr.s

第1~49行与上一章一致,用于进入保护模式和分页模式。

第51~53行,设定读取的扇区数为99。

第55~68行,设定起始扇区号为1。

第70~72行,向硬盘发送读命令。此时,硬盘开始准备。

第74~79行,不断读取0x1f7端口,以等待硬盘准备完毕。等待的目标已于上文中讨论过:第7位为0且第3位为1。

第81~84行,将硬盘中的数据读出。参数如下:

  1. 端口号:0x1f0
  2. 目的地址:0x80000。这个地址离0xa0000有128K,远远超过我们的操作系统的大小
  3. 读取次数:99 * 512 / 2。读取的扇区数是99,一个扇区是512字节,insw指令一次读取2字节,所以可以使用这个公式计算读取次数

至此,ELF文件已经加载到0x80000处,接下来需要解析这个文件。

第86~92行,读取ELF文件头中的多项信息,列举如下:

  1. EBX中存放的是程序头表地址。请注意:0x8001c处的数值是一个偏移量,其需要与0x80000相加才能得到内存地址
  2. EDX中存放的是程序头表中每个表项的大小
  3. ECX中存放的是程序头表中表项的数量

第98~99行,判断表项的类型,只需要类型为0x1的表项。

第101~105行,将这段程序加载到目的地址中,参数如下:

  1. ESI中存放的是源地址。这里同样需要注意:[ebx + 0x4]处的数值只是一个偏移量,其需要与0x80000相加才能得到内存地址
  2. EDI中存放的是目的地址
  3. ECX中存放的是这段程序的大小

第107~110行,构造BSS段。这里的EDI沿用了上面rep movsb指令的结果;ECX被设定为[ebx + 0x14] - [ebx + 0x10]

第114~117行,通过循环解析程序头表中的每个表项。

第119行,跳转至内核的入口地址。

至此,我们已经正式进入操作系统内核。

标签:...,表项,操作系统,扇区,段程序,内核,硬盘,加载
From: https://www.cnblogs.com/yingyulou/p/17825517.html

相关文章

  • 一个操作系统的设计与实现——第9章 硬盘驱动
    操作系统应当具备读写硬盘的能力。因此,本章将要实现的是硬盘驱动。硬盘驱动由两个函数构成:读硬盘函数与写硬盘函数。9.1读硬盘想要读硬盘,就需要提供以下三个信息:起始扇区号读取的扇区数数据存储的地址需要注意的是:读取的扇区数只能是一个8字节的整数。由于读硬盘需要使......
  • 一个操作系统的设计与实现——第8章 内存管理系统
    计算机上的任何程序,包括操作系统自己,都需要使用内存。因此,操作系统需要实现内存管理系统,以进行内存的分配和回收。在我们的操作系统中,内存管理系统由两部分组成:页分配器与页回收器。本章将实现这两个部分。8.1从虚拟地址到物理地址回顾CPU对内存地址的转换过程:使用段寄存器......
  • 一个操作系统的设计与实现——第7章 中断
    7.1什么是中断中断是一种能够随时打断CPU正常工作的机制。这句话看着挺别扭的,CPU工作的好好的,为什么要"随时打断"它?这是因为,CPU需要为诸多外部设备提供服务,以键盘为例,当键盘上的键被按下时,CPU需要对此做出响应和处理,如果不能及时响应,我们会说:"电脑很卡";如果一直都不能响应,我们会......
  • 一个操作系统的设计与实现——第6章 显卡驱动
    进入内核以后,应该做些什么呢?本章将实现一个最容易看到效果的模块:显卡驱动。6.1什么是驱动驱动这个词听起来很高大上,但实际上很简单,就是硬件的接口函数。在软件工程中,可以使用接口封装和简化设计,硬件也是一样。例如:想要读硬盘,需要很多指令设定好几个端口,然后等待硬盘就绪,最后才......
  • 一个操作系统的设计与实现——第12章 任务(三):3特权级任务
    特权级是保护模式的核心概念之一,但我们的操作系统一直没有引入这个概念。这是因为,特权级只有在3特权级任务存在时才有意义。本章将要实现的是3特权级任务的加载与任务切换。12.1特权级12.1.1特权级的功能特权级(PrivilegeLevel),是保护模式中用于限制任务权限的机制。特权级有4......
  • 一个操作系统的设计与实现——第11章 任务(二):0特权级任务
    上一章中,我们的操作系统已经支持内核共享,这为任务的加载和运行做好了准备。本章将要实现的是0特权级任务的加载与任务切换。11.1任务切换的原理11.1.1协同式与抢占式任务切换如果CPU上只运行着Kernel.c的main函数,那么情况非常简单,只需要不断执行下一条指令即可。然而,如果现......
  • 一个操作系统的设计与实现——第10章 任务(一):共享内核
    一直以来,我们的操作系统在启动后,运行的都是Kernel.c中的main函数。只运行这一个函数是不够的,操作系统应当有能力加载并运行其他程序。从本章开始,将使用四章的篇幅讨论操作系统如何加载并运行任务。这里的任务(Task)与进程(Process)是同义词,在操作系统领域中,任务这个词更为常用,请读者......
  • 一个操作系统的设计与实现——第13章 任务(四):任务回收
    在前面的两章中,我们的操作系统均不支持任务回收,所以任务不能退出。本章将要实现的是任务回收功能。13.1任务回收的原理如果一个任务位于任务队列中,其就会被运行。所以,如果一个任务的运行已经结束,它就应该从任务队列中删除。仅仅将任务从任务队列中删除是不够的,这是因为任务还......
  • windows操作系统中如何将apache zookeeper安装为系统服务
    记录一下,网上有些资料是错的,prunsrv的参数要以--开头!一下载最新版的apachecommonsdaemonhttp://archive.apache.org/dist/commons/daemon/binaries/windows/commons-daemon-1.3.4-bin-windows.zip解压,复制两个exe文件到zk的bin目录下二编写服务停止脚本(启动脚本不用自己......
  • 内核目录结构
    内核目录结构arch:这个文件夹包含了一个Kconfig文件,它用于设置这个目录里的源代码编译所需的一系列设定。每个支持的处理器架构都在它相应的文件夹中。如,Alpha处理器的源代码在alpha文件夹中。请记住,随着时间的推移,一些新的处理器将被支持,有些会被放弃。block --此文件夹包含块......