ucore-lab1-ex4

分析 bootloader 加载 ELF 格式的 OS 的过程

通过阅读 bootmain.c,了解 bootloader 如何加载 ELF 文件。通过分析源代码和通过 qemu 来运行并调试 bootloader&OS。

  1. bootloader 如何读取硬盘扇区的?
  2. bootloader 是如何加载 ELF 格式的 OS?

代码简析

硬盘访问概述:

/* waitdisk - wait for disk ready */
static void
waitdisk(void) {
    while ((inb(0x1F7) & 0xC0) != 0x40)
        /* do nothing */;
}

waitdisk 通过从 0x1f7 地址返回 command 寄存器来读取硬盘状态,如果忙就继续等待。( command 寄存器里具体的值暂时不清楚,查阅资料也没查到相关内容)

/* 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);              // 写LBA参数的 0-7bit 
    outb(0x1F4, (secno >> 8) & 0xFF);       // 写LBA参数的 8-15bit 
    outb(0x1F5, (secno >> 16) & 0xFF);      // 写LBA参数的 16-23bit 
    outb(0x1F6, ((secno >> 24) & 0xF) | 0xE0);  // 写LBA参数的 24-27bit,第四位(=0)表示从主盘读入 
    outb(0x1F7, 0x20);                      // cmd 0x20 - read sectors,读扇区

    // wait for disk to be ready
    waitdisk();

    // read a sector
    insl(0x1F0, dst, SECTSIZE / 4);        // in super long ?从 0x1f0 地址处向 dst 处写读取的扇区,操作粒度 G 应为4字节,SECTSIZE=512.
}

之后readseg实现了一个对readsect更高层次的封装,static void readseg(uintptr_t va, uint32_t count, uint32_t offset)实现了在 kernel 所在扇区(在secno那里+1)的 offset 位置向虚拟地址 va 写 count 个字节的功能。这个实现方式挺绕的,但是可以画图分析,画完图就很清晰了。需要注意的是,这段代码会往 va 处 copy 多于 count 的字节,因为是以扇区为粒度 copy 的。注释里认为:We'd write more to memory than asked, but it doesn't matter -- we load in increasing order.,但是还不是很理解,这样不是会覆盖掉其他的数据吗?还是说这时OS还没起来,又因为物理地址等于逻辑地址(参见 lab1-ex3 的段描述符),所以有一定的越界没什么关系?

/* bootmain - the entry of bootloader */
void
bootmain(void) {
    // read the 1st page off disk
    readseg((uintptr_t)ELFHDR, SECTSIZE * 8, 0);               // 从ELFHDR(0x10000)处读第一页(4KB)(为啥读4KB?)

    // is this a valid ELF?
    if (ELFHDR->e_magic != ELF_MAGIC) {
        goto bad;                                              // 检查是否是合法的ELF文件
    }

    struct proghdr *ph, *eph;

    // load each program segment (ignores ph flags)
    ph = (struct proghdr *)((uintptr_t)ELFHDR + ELFHDR->e_phoff);  // ph 指向第一个program header
    eph = ph + ELFHDR->e_phnum;                                    // eph 指向最后一个program header 
    for (; ph < eph; ph ++) {
        readseg(ph->p_va & 0xFFFFFF, ph->p_memsz, ph->p_offset);   // 把每个 segment 上的数据放到对应的 va 处
    }

    // call the entry point from the ELF header
    // note: does not return
    ((void (*)(void))(ELFHDR->e_entry & 0xFFFFFF))();

bad:
    outw(0x8A00, 0x8A00);
    outw(0x8A00, 0x8E00);

    /* do nothing */
    while (1);
}

这里比较难理解的部分是这个& 0xFFFFFF操作,在QQ精彩答疑里找到了回答,但是说的也不是很清楚,我尽可能清楚地进行转述:

这里需要明白链接地址与加载地址:

  1. Link Address 是指编译器指定代码和数据所需要放置的内存地址,由链接器配置。
  2. Load Address 是指程序被实际加载到内存的位置(由程序加载器 ld 配置).

问题在于 kernel 告诉 bootloader 将其加载到 0x100000(Load Address),但是 kernel 代码在生成目标文件时的链接地址是在 0xf0100000,所以符号表等等都是指向 link address 附近的,也就是说ph->p_vaELFHDR->e_entry的值是 0xf0xxxxxx,所以为了把内核加载到正确的位置,需要我们人为的做一个地址转换。

我们发现好像这个问题应该由 gdt 解决,但是为什么失效了呢?我个人觉得可能是因为 gdt 中第一个描述符是 SEG_NULL 的原因。

以上是我的个人理解,不确保正确。

最后找到 ELF-Header 中 kernel 的入口地址,同样的需要人为的进行上述的地址转换。需要注意的是这里采用的是函数调用的形式。关于这行代码,上面的回答已经解释的很清楚了。

至此,bootloader 成功加载了 ELF 格式的 OS kernel,主要步骤是加载对应的扇区到内核的 load address,最后再通过 ELF-header 找到 kernel 入口,通过函数调用进入 kernel 即可。

参考资料

6.828 操作系统 lab1 实验报告
链接地址与加载地址(看图)

顺便吐槽一下,这个lab1简直残暴,一周只写了4个练习。。。

原文地址:https://www.cnblogs.com/LuoboLiam/p/13561056.html