• 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个练习。。。

  • 相关阅读:
    sql server 2008 64位连接sql 2000服务器的时候出现
    MySQL忘记密码怎么修改密码
    vs2015 行数统计
    javascript和c#aes加密方法互解
    vs2015 不能启动 iis express
    修改oracle的字符集操作方法
    PL/SQL Developer连接本地Oracle 11g 64位数据库
    CodeSmith Generator 7.0.2激活步骤
    PetaPoco利用ODP.NET Managed Driver连接Oracle
    解决Chrome插件安装时出现的“程序包无效”问题亲测可用
  • 原文地址:https://www.cnblogs.com/LuoboLiam/p/13561056.html
Copyright © 2020-2023  润新知