练习四:分析bootloader加载ELF格式的OS的过程。
1.题目要求
通过阅读bootmain.c,了解bootloader如何加载ELF文件。通过分析源代码和通过qemu来运行并调试bootloader&OS,
- bootloader如何读取硬盘扇区的?
- bootloader是如何加载ELF格式的OS?
提示:可阅读“硬盘访问概述”,“ELF执行文件格式概述”这两小节。
2.整个流程
假定进入了保护模式之后,bootloader需要能够加载ELF文件。因为kenerl(就是ucore os)是以ELF的形式存在硬盘上的。
bootloader如何读取硬盘扇区的?就是说boot loader能够访问硬盘,bootloader把硬盘数据读取出来之后,要把其中ELF格式文件给分析出来。从而知道ucore它的代码段应该放在什么地方,应该有多大一块空间放这个代码段数据。哪一段空间是放数据段的数据,然后把它加载到内存中去,同时还知道跳转到ucore哪个位置去执行。
读取扇区是readsect函数,用到了in b,out b这种机器指令。in b,out b的实现都是内联汇编来实现的,它采取了一种IO空间的地址寻址方式,能够把外设的数据给读到内存中来,这也是x86里面的寻址方式。除了正常的memory方式之外,还有IO这一种寻址方式。
readsect函数这一块其实不用仔细去看,只需要知道bootloader从哪开始把相应的扇区给读进来,记忆它读多大,读完之后它就需要去进一步的分析。这个分析呢,需要去了解相应的ELF格式。
在bootmain函数中,有对ELF的格式判断,它怎么知道都进来这个扇区的数据是一个ELF格式的文件呢?它其实是读取了ELF的header,然后判断它的一个特殊的成员变量e_magic,看它是否等于一个特定的值,就认为确实是一个合法的ELF格式的文件。
在bootmain.c中有更详细的把ELF文件读取进来的一段判断。它怎么能够根据ELFheader和proghdr程序头来读出相应的代码段和数据段,然后加到相应的地方去。
就是决定了bootloader把这个加载完之后,到底跳转到什么地方去,把控制权交给ucore去执行
3.预备知识
3.1ELF文件格式
ELF(Executable and linking format)文件格式是Linux系统下的一种常用目标文件(object file)格式,有三种主要类型:
- 用于执行的可执行文件(executable file),用于提供程序的进程映像,加载到内存执行。 这也是本实验的OS文件类型。
- 用于连接的可重定位文件(relocatable file),可与其它目标文件一起创建可执行文件和共享目标文件。
- 共享目标文件(shared object file),连接器可将它与其它可重定位文件和共享目标文件连接成其它的目标文件,动态连接器又可将它与可执行文件和其它共享目标文件结合起来创建一个进程映像。
ELF文件结构:
首先,ELF文件格式提供了两种视图,分别是链接视图和执行视图。
链接视图是以节(section)为单位,执行视图是以段(segment)为单位。链接视图就是在链接时用到的视图,而执行视图则是在执行时用到的视图。上图左侧的视角是从链接来看的,右侧的视角是执行来看的。可以看出,一个segment可以包含数个section。
本文关注执行,结构体Proghdr是用于描述段 (segment) 的 program header,可有多个。
ELF header在文件开始处描述了整个文件的组织。ELF的文件头包含整个执行文件的控制结构。
两个结构体都定义在elf.h中:
bootmain()函数的作用是加载 ELF格式的ucore操作系统,
3.2 bootmain()函数
bootasm.S完成了bootloader的大部分功能,包括打开A20,初始化GDT,进入保护模式,更新段寄存器的值,建立堆栈
接下来bootmain完成bootloader剩余的工作,就是把内核从硬盘加载到内存中来,并把控制权交给内核。
现在看不懂这个函数具体怎么实现的没关系,后面会有具体的解释。只需要知道它的功能就行。
4. 问题解答
4.1问题一:bootloader如何读取硬盘扇区的?
读硬盘扇区的代码如下:
就是把硬盘上的kernel,读取到内存中
从outb()可以看出这里是用LBA模式的PIO(Program IO)方式来访问硬盘的(即所有的IO操作是通过CPU访问硬盘的IO地址寄存器完成)。从磁盘IO地址和对应功能表可以看出,该函数一次只读取一个扇区。
| IO地址 | 功能 |
|---|---|
| 0x1f0 | 读数据,当0x1f7不为忙状态时,可以读。 |
| 0x1f2 | 要读写的扇区数,每次读写前,你需要表明你要读写几个扇区。最小是1个扇区 |
| 0x1f3 | 如果是LBA模式,就是LBA参数的0-7位 |
| 0x1f4 | 如果是LBA模式,就是LBA参数的8-15位 |
| 0x1f5 | 如果是LBA模式,就是LBA参数的16-23位 |
| 0x1f6 | 第0~3位:如果是LBA模式就是24-27位 第4位:为0主盘;为1从盘 |
| 0x1f7 | 状态和命令寄存器。操作时先给命令,再读取,如果不是忙状态就从0x1f0端口读数据 |
其中insl的实现如下:
读取硬盘扇区的步骤:
- 等待硬盘空闲。waitdisk的函数实现只有一行:
while ((inb(0x1F7) & 0xC0) != 0x40),意思是不断查询读0x1F7寄存器的最高两位,直到最高位为0、次高位为1(这个状态应该意味着磁盘空闲)才返回。 - 硬盘空闲后,发出读取扇区的命令。对应的命令字为0x20,放在0x1F7寄存器中;读取的扇区数为1,放在0x1F2寄存器中;读取的扇区起始编号共28位,分成4部分依次放在0x1F3~0x1F6寄存器中。
- 发出命令后,再次等待硬盘空闲。
- 硬盘再次空闲后,开始从0x1F0寄存器中读数据。注意insl的作用是"That function will read cnt dwords from the input port specified by port into the supplied output array addr.",是以dword即4字节为单位的,因此这里SECTIZE需要除以4.
4.2 问题二:bootloader如何加载ELF格式的OS
- 从硬盘读了8个扇区数据到内存
0x10000处,并把这里强制转换成elfhdr使用; - 校验
e_magic字段; - 根据偏移量分别把程序段的数据读取到内存中。
之前已经看了readsect函数, readsect从设备的第secno扇区读取数据到dst位置
readseg简单包装了readsect,可以从设备读取任意长度的内容。
最后是bootmain函数:
__EOF__

本文链接:https://www.cnblogs.com/huilinmumu/p/16222172.html
关于博主:评论和私信会在第一时间回复。或者直接私信我。
版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!
声援博主:如果您觉得文章对您有帮助,可以点击文章右下角【推荐】一下。您的鼓励是博主的最大动力!

