练习四 分析bootloader加载ELF格式的OS的过程
(1)、bootloader是如何读取硬盘扇区的?
(2)、bootloader是如何加载ELF格式的OS的?
首先要介绍一下 对于bootloader访问硬盘时都是LBA模式的PIO方式,也就是说所有的I/O操作都是通过CPU访问硬盘的I/O地址寄存器完成。操作系统位于第一个硬盘上,而访问第一个硬盘的扇区可以设置I/O端口0x1f0~0x1f7来改变地址寄存器实现。下述表格所显示的即为0x1f0~0x1f7所对应的功能:
I/o地址 | 功能 |
---|---|
0x1f0 | 读数据,当0x1f7不为忙状态时,可以读 |
0x1f1 | 可获得详细的错误信息 |
0x1f2 | 与读写的扇区数量,每次读写前,都需要表明要读写几个扇区 |
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端口读数据 |
硬盘的数据存储在硬盘的扇区中,每个扇区的大小为512B,读取一个扇区的流程如下:
1、等待磁盘准备好
2、发出读取扇区的命令
3、等待磁盘准备好
4、把磁盘扇区读取到内存
#define SECTSIZE 512 //表示一个扇区的大小
#define ELFHDR ((struct elfhdr *)0x10000) //表示虚拟地址的起始地址
下述函数实现的是将一个扇区读取到内存的代码段
static void
waitdisk(void) {
/*
*0x1F7表示0号硬盘的状态寄存器,当状态寄存器的最高两位是01时,表示空闲状态
*inb(0x1F7) & 0xC0 表示将0x1F7端口所代表的状态寄存器的值和0xC0做与操作 观察0x1F7的最高两位是否是01
*如果是01,表示空闲,跳出循环,如果不是,则继续循环。
*/
while ((inb(0x1F7) & 0xC0) != 0x40);
}
//从secno指定的扇区读取数据到dst位置
static void
readsect(void *dst, uint32_t secno) {
waitdisk(); //等待磁盘准备好
//outb表示只读取一个字节的数据到I/O端口中
outb(0x1F2, 1); // 设置读取扇区的数目为1
outb(0x1F3, secno & 0xFF); //将要读取的扇区编号写入到0x1F3端口所代表额寄存器中
outb(0x1F4, (secno >> 8) & 0xFF); //用来存放读写柱面的低八位字节
outb(0x1F5, (secno >> 16) & 0xFF); //用来存放读写柱面的高两位字节
outb(0x1F6, ((secno >> 24) & 0xF) | 0xE0); //用来存放要读写的磁盘号及磁头号
outb(0x1F7, 0x20); //发出读取磁盘扇区的命令
waitdisk(); //等待磁盘准备好
insl(0x1F0, dst, SECTSIZE / 4); //将磁盘扇区的数据读到dst内存中
}
下面的程序段表示将第一个硬盘的8个扇区依次读入到内存中
static void
/*
*下面的第一个参数表示的虚拟地址的起始地址
*第二个参数表示读取数据的总大小
*第三个表示偏移量
*/
readseg(uintptr_t va, uint32_t count, uint32_t offset) {
uintptr_t end_va = va + count; //end_va表示读取数据的结束地址
va -= offset % SECTSIZE; //SECTSIZE表示的是一个扇区的长度,这里用起始地址减去偏移地址 得到的是块的首地址
uint32_t secno = (offset / SECTSIZE) + 1; //存取我们需要读取的磁盘的位置
//通过for循环将end_va和va地址之间的数据读取到内存中
for (; va < end_va; va += SECTSIZE, secno ++) {
readsect((void *)va, secno); //每次循环将secno读取到va内存的位置
}
}
下述函数是bootloader的入口函数
该函数用于加载elf格式的os
void
bootmain(void) {
// read the 1st page off disk
readseg((uintptr_t)ELFHDR, SECTSIZE * 8, 0);//将硬盘的8个扇区大小的数据读取到ELFHDR所开始的内存地址中 ELF的头占据8个扇区的大小
//读取完毕之后,在加载操作之前首先需要对ELFHDR进行判断,观察是否是一个合法的ELF头部
if (ELFHDR->e_magic != ELF_MAGIC) {
goto bad; //如果ELF头部不合法 则跳转到bad标号处
}
struct proghdr *ph, *eph; //proghdr表示存储程序块的结构体 在elf.h中定义 ph在这里定义的是指向elf的首地址 eph在这里表示指向elf的尾地址
// load each program segment (ignores ph flags)
ph = (struct proghdr *)((uintptr_t)ELFHDR + ELFHDR->e_phoff); //e_phoff表示elf程序头表的偏移地址 ph为基地址加上偏移地址 指的是首地址
eph = ph + ELFHDR->e_phnum; //ph为首地址 e_phnum表示program head表的入口数目 eph指向elf文件的尾地址
for (; ph < eph; ph ++) { //通过for循环 讲elf文件的内容读取到p_va的虚存中
readseg(ph->p_va & 0xFFFFFF, ph->p_memsz, ph->p_offset);
}
//最后调用ELF header表头中的内核入口地址, 实现 内核链接地址 转化为 加载地址
((void (*)(void))(ELFHDR->e_entry & 0xFFFFFF))();
bad:
outw(0x8A00, 0x8A00);
outw(0x8A00, 0x8E00);
/* do nothing */
while (1);
}