通过阅读bootmain.c,了解bootloader如何加载ELF文件。通过分析源代码和通过qemu来运行并调试bootloader&OS,
提示:可阅读“硬盘访问概述”,“ELF执行文件格式概述”这两小节。
假定进入了保护模式之后,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去执行
ELF(Executable and linking format)文件格式是Linux系统下的一种常用目标文件(object file)格式,有三种主要类型:
ELF文件结构:
首先,ELF文件格式提供了两种视图,分别是链接视图和执行视图。
链接视图是以节(section)为单位,执行视图是以段(segment)为单位。链接视图就是在链接时用到的视图,而执行视图则是在执行时用到的视图。上图左侧的视角是从链接来看的,右侧的视角是执行来看的。可以看出,一个segment可以包含数个section。
本文关注执行,结构体Proghdr是用于描述段 (segment) 的 program header,可有多个。
ELF header在文件开始处描述了整个文件的组织。ELF的文件头包含整个执行文件的控制结构。
两个结构体都定义在elf.h中:
struct elfhdr { //ELF文件头 uint magic; // must equal ELF_MAGIC uchar elf[12]; ushort type; ushort machine; uint version; uint entry; // 程序入口的虚拟地址 uint phoff; // program header起始位置 uint shoff; //section header起始位置 uint flags; ushort ehsize; // ELF文件头本身大小 ushort phentsize; ushort phnum; // program header个数 ushort shentsize; ushort shnum; ushort shstrndx; };
struct proghdr {//程序表头 uint type; // 段类型 uint offset; // 段相对于ELF文件开头的偏移 uint va; // 段的第一个字节将被放到内存中的虚拟地址 uint pa;// 物理地址 uint filesz; uint memsz; // 段在内存映像中占用的字节数,就是在内存中的大小 uint flags; // 读,写,执行权限 uint align; };
bootmain()函数的作用是加载 ELF格式的ucore操作系统,
#include <defs.h> #include <x86.h> #include <elf.h> /* ********************************************************************* * 这是一个非常简单的引导加载程序,它的唯一工作就是引导 * 来自第一个IDE硬盘的ELF内核映像 * * 磁盘布局 * 这个程序(bootasm).S和bootmain.c是引导加载程序。 * 应该存储在磁盘的第一个扇区。 * * *第二个扇区包含内核映像。 * * * 内核映像必须是ELF格式。 * * 开机步骤 * * 当CPU启动时,它将BIOS加载到内存中并执行它 * * * BIOS初始化设备,设置中断例程,以及 * 读取启动设备(硬盘)的第一个扇区 * 进入内存并跳转到它。 * * * Assuming this boot loader is stored in the first sector of the * hard-drive, this code takes over... * * * 控制启动bootasm.S -- 设置保护模式, * 和一个堆栈,C代码然后运行,然后调用bootmain() * * * bootmain()在这个文件中接管,读取内核并跳转到它 * */ // 扇区(sector)大小512 unsigned int SECTSIZE = 512 ; // 将0x10000设为内核起始地址 struct elfhdr * ELFHDR = ((struct elfhdr *)0x10000) ; // scratch space /* waitdisk - wait for disk ready */ static void waitdisk(void) { while ((inb(0x1F7) & 0xC0) != 0x40) /* do nothing */; } /* readsect - read a single sector at @secno into @dst */ static void readsect(void *dst, uint32_t secno) { // wait for disk to be ready waitdisk(); outb(0x1F2, 1); // count = 1 outb(0x1F3, secno & 0xFF); outb(0x1F4, (secno >> 8) & 0xFF); outb(0x1F5, (secno >> 16) & 0xFF); outb(0x1F6, ((secno >> 24) & 0xF) | 0xE0); outb(0x1F7, 0x20); // cmd 0x20 - read sectors // wait for disk to be ready waitdisk(); // read a sector insl(0x1F0, dst, SECTSIZE / 4); } /* * * readseg - read @count bytes at @offset from kernel into virtual address @va, * might copy more than asked. * */ //读取segment static void readseg(uintptr_t va, uint32_t count, uint32_t offset) { uintptr_t end_va = va + count; // round down to sector boundary va -= offset % SECTSIZE; // translate from bytes to sectors; kernel starts at sector 1 uint32_t secno = (offset / SECTSIZE) + 1; // If this is too slow, we could read lots of sectors at a time. // We'd write more to memory than asked, but it doesn't matter -- // we load in increasing order. for (; va < end_va; va += SECTSIZE, secno ++) { readsect((void *)va, secno); } } /* bootmain - the entry of bootloader */ void bootmain(void) { // read the 1st page off disk // 从 0 开始读取 8*512 = 4096 byte 的内容到 ELFHDR readseg((uintptr_t)ELFHDR, SECTSIZE * 8, 0); // is this a valid ELF? // 通过储存在头部的e_magic判断是否是合法的ELF文件 if (ELFHDR->e_magic != ELF_MAGIC) { goto bad; } struct proghdr *ph, *eph; // load each program segment (ignores ph flags) // 获得程序头表的起始位置 ph ph = (struct proghdr *)((uintptr_t)ELFHDR + ELFHDR->e_phoff); // 获取程序头表结束的位置 eph eph = ph + ELFHDR->e_phnum; // 按照描述表将ELF文件中数据载入内存 for (; ph < eph; ph ++) { // 根据每个 program header 读取 segment // 从 p_offset 开始拷贝 p_memsz 个 byte 到 p_pa readseg(ph->p_va & 0xFFFFFF, ph->p_memsz, ph->p_offset); } // call the entry point from the ELF header // note: does not return // ELF文件0x1000位置后面的0xd1ec比特被载入内存0x00100000 // ELF文件0xf000位置后面的0x1d20比特被载入内存0x0010e000 // 根据ELF头部储存的入口信息,找到内核的入口 ((void (*)(void))(ELFHDR->e_entry & 0xFFFFFF))(); //跳到内核程序入口地址,将cpu控制权交给ucore内核代码 bad: outw(0x8A00, 0x8A00); outw(0x8A00, 0x8E00); /* do nothing */ while (1); }
bootasm.S完成了bootloader的大部分功能,包括打开A20,初始化GDT,进入保护模式,更新段寄存器的值,建立堆栈
接下来bootmain完成bootloader剩余的工作,就是把内核从硬盘加载到内存中来,并把控制权交给内核。
现在看不懂这个函数具体怎么实现的没关系,后面会有具体的解释。只需要知道它的功能就行。
读硬盘扇区的代码如下:
/* readsect - read a single sector at @secno into @dst */ static void readsect(void *dst, uint32_t secno) { // wait for disk to be ready waitdisk(); //读取扇区内容 //outb(使用内联汇编实现),设置读取扇区的数目为1 outb(0x1F2, 1); // count = 1 outb(0x1F3, secno & 0xFF); outb(0x1F4, (secno >> 8) & 0xFF); outb(0x1F5, (secno >> 16) & 0xFF); outb(0x1F6, ((secno >> 24) & 0xF) | 0xE0); outb(0x1F7, 0x20); // cmd 0x20 - read sectors // 上面四条指令联合制定了扇区号 // 在这4个字节联合构成的32位参数中 // 29-31位强制设为1 // 28位(=0)表示访问"Disk 0" // 0-27位是28位的偏移量 // wait for disk to be ready waitdisk(); //将扇区内容加载到内存中虚拟地址dst // read a sector insl(0x1F0, dst, SECTSIZE / 4);//也用内联汇编实现 }
就是把硬盘上的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
的实现如下:
// x86.h static inline void insl(uint32_t port, void *addr, int cnt) { asm volatile ( "cld;" "repne; insl;" : "=D" (addr), "=c" (cnt) : "d" (port), "0" (addr), "1" (cnt) : "memory", "cc"); }
读取硬盘扇区的步骤:
while ((inb(0x1F7) & 0xC0) != 0x40)
,意思是不断查询读0x1F7寄存器的最高两位,直到最高位为0、次高位为1(这个状态应该意味着磁盘空闲)才返回。0x10000
处,并把这里强制转换成elfhdr
使用;e_magic
字段;之前已经看了readsect函数, readsect
从设备的第secno扇区读取数据到dst位置
static void readsect(void *dst, uint32_t secno)
readseg简单包装了readsect,可以从设备读取任意长度的内容。
static void readseg(uintptr_t va, uint32_t count, uint32_t offset) { uintptr_t end_va = va + count; // round down to sector boundary va -= offset % SECTSIZE; // translate from bytes to sectors; kernel starts at sector 1 uint32_t secno = (offset / SECTSIZE) + 1; // 加1因为0扇区被引导占用 // ELF文件从1扇区开始 for (; va < end_va; va += SECTSIZE, secno ++) { readsect((void *)va, secno); } }
最后是bootmain函数:
/* bootmain - the entry of bootloader */ void bootmain(void) { // read the 1st page off disk // 从 0 开始读取 8*512 = 4096 byte 的内容到 ELFHDR readseg((uintptr_t)ELFHDR, SECTSIZE * 8, 0); // is this a valid ELF? // 通过储存在头部的e_magic判断是否是合法的ELF文件 if (ELFHDR->e_magic != ELF_MAGIC) { goto bad; } struct proghdr *ph, *eph; // load each program segment (ignores ph flags) // 获得程序头表的起始位置 ph ph = (struct proghdr *)((uintptr_t)ELFHDR + ELFHDR->e_phoff); // 获取程序头表结束的位置 eph eph = ph + ELFHDR->e_phnum; // 按照描述表将ELF文件中数据载入内存 for (; ph < eph; ph ++) { // 根据每个 program header 读取 segment // 从 p_offset 开始拷贝 p_memsz 个 byte 到 p_pa readseg(ph->p_va & 0xFFFFFF, ph->p_memsz, ph->p_offset); } // call the entry point from the ELF header // note: does not return // ELF文件0x1000位置后面的0xd1ec比特被载入内存0x00100000 // ELF文件0xf000位置后面的0x1d20比特被载入内存0x0010e000 // 根据ELF头部储存的入口信息,找到内核的入口 ((void (*)(void))(ELFHDR->e_entry & 0xFFFFFF))(); //跳到内核程序入口地址,将cpu控制权交给ucore内核代码 bad: outw(0x8A00, 0x8A00); outw(0x8A00, 0x8E00); /* do nothing */ while (1); }