通过阅读bootmain.c,了解bootloader如何加载ELF文件。通过分析源代码和通过qemu来运行并调试bootloader&OS, 问题1:
bootloader如何读取硬盘扇区的?
参考手册上说明这里用的是PIO方式读取磁盘的,但是网上查阅资料说这种方式CPU利用率比较低,现在都用的DMA方式。这里我们跟着源码一步一步来:
第一步在main函数的第89行:
readseg((uintptr_t)ELFHDR, SECTSIZE * 8, 0);这里调用了一个函数,我们看一下这个函数(68行开始):
/* 这段代码我看了好久才理解(不知道为啥,总感觉怪怪的) 这里是把硬盘的offset读取count个字节到内存va处 因为硬盘一次最少读取一个扇区,为了保证在原va与offset对其,va向前移动一下。(然后多读取一个扇区) 这里可能会影响到其他数据,不够这段代码应该没有体现到。 */ 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); } }这里调用了readsect函数,我们来看一下这个函数(第44行):
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); }这里调用了waitdisk函数,我们看一下这个函数,在37行;
static void waitdisk(void) { while ((inb(0x1F7) & 0xC0) != 0x40) /* do nothing */; }往下调用了inb函数,这里就不看了。其实这里就很清楚了,类似与开启A20地址总线遇到的那个,这里要等磁盘空闲。
然后根据实验手册的 硬盘访问概述可以的到第49行到第53行给出的指令是告诉磁盘自己要读多少扇区。(0x1F6的第四位表示主从盘,高4-7不知道表示什么,也没查到),第47行0x20信号表示要读扇区信号。
发出这个命令后就可以进行读写磁盘了,我们看insl函数的展开:
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"); }这段内联汇编暂时跳过,以后有需要继续来填坑。
这里大概就到这了。
问题2:
bootloader是如何加载ELF格式的OS?
先看代码
if (ELFHDR->e_magic != ELF_MAGIC) { goto bad; } struct proghdr *ph, *eph; // load each program segment (ignores ph flags) ph = (struct proghdr *)((uintptr_t)ELFHDR + ELFHDR->e_phoff); eph = ph + ELFHDR->e_phnum; for (; ph < eph; ph ++) { readseg(ph->p_va & 0xFFFFFF, ph->p_memsz, ph->p_offset); } // call the entry point from the ELF header // note: does not return ((void (*)(void))(ELFHDR->e_entry & 0xFFFFFF))();这里ph是数据表的起始位置,eph是数据表的结束位置,循环把数据装入内存。
((void (*)(void))(ELFHDR->e_entry & 0xFFFFFF))();这段代码是找到内核入口,进行执行。
