《ucore lab1 exercise4》實驗報告


資源

  1. ucore在線實驗指導書
  2. 我的ucore實驗代碼

題目:分析bootloader加載ELF格式的OS的過程

通過閱讀bootmain.c,了解bootloader如何加載ELF文件。通過分析源代碼和通過qemu來運行並調試bootloader&OS,理解:

  1. bootloader如何讀取硬盤扇區的?
  2. bootloader是如何加載ELF格式的OS?

解答

問題1:bootloader如何讀取硬盤扇區

分析原理

閱讀材料其實已經給出了讀一個扇區的大致流程:

  1. 等待磁盤准備好
  2. 發出讀取扇區的命令
  3. 等待磁盤准備好
  4. 把磁盤扇區數據讀到指定內存

實際操作中,需要知道怎樣與硬盤交互。閱讀材料中同樣給出了答案:所有的IO操作是通過CPU訪問硬盤的IO地址寄存器完成。硬盤共有8個IO地址寄存器,其中第1個存儲數據,第8個存儲狀態和命令,第3個存儲要讀寫的扇區數,第4~7個存儲要讀寫的起始扇區的編號(共28位)。了解這些信息,就不難編程實現啦。

分析代碼

bootloader讀取扇區的功能是在boot/bootmain.c的readsect函數中實現的,先貼代碼:

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);
}

根據代碼可以得出讀取硬盤扇區的步驟:

  1. 等待硬盤空閑。waitdisk的函數實現只有一行:while ((inb(0x1F7) & 0xC0) != 0x40),意思是不斷查詢讀0x1F7寄存器的最高兩位,直到最高位為0、次高位為1(這個狀態應該意味着磁盤空閑)才返回。

  2. 硬盤空閑后,發出讀取扇區的命令。對應的命令字為0x20,放在0x1F7寄存器中;讀取的扇區數為1,放在0x1F2寄存器中;讀取的扇區起始編號共28位,分成4部分依次放在0x1F3~0x1F6寄存器中。

  3. 發出命令后,再次等待硬盤空閑。

  4. 硬盤再次空閑后,開始從0x1F0寄存器中讀數據。注意insl的作用是"That function will read cnt dwords from the input port specified by port into the supplied output array addr.",是以dword即4字節為單位的,因此這里SECTIZE需要除以4.

問題2: bootloader如何加載ELF格式的OS

分析原理

首先從原理上分析加載流程。

  1. bootloader要加載的是bin/kernel文件,這是一個ELF文件。其開頭是ELF header,ELF Header里面含有phoff字段,用於記錄program header表在文件中的偏移,由該字段可以找到程序頭表的起始地址。程序頭表是一個結構體數組,其元素數目記錄在ELF Header的phnum字段中。

  2. 程序頭表的每個成員分別記錄一個Segment的信息,包括以下加載需要用到的信息:

    • uint offset; // 段相對文件頭的偏移值,由此可知怎么從文件中找到該Segment
    • uint va; // 段的第一個字節將被放到內存中的虛擬地址,由此可知要將該Segment加載到內存中哪個位置
    • uint memsz; // 段在內存映像中占用的字節數,由此可知要加載多少內容
  3. 根據ELF Header和Program Header表的信息,我們便可以將ELF文件中的所有Segment逐個加載到內存中

分析代碼

bootloader加載os的功能是在bootmain函數中實現的,先貼代碼:

void bootmain(void) {
    // read the 1st page off disk
    readseg((uintptr_t)ELFHDR, SECTSIZE * 8, 0);

    // is this a valid ELF?
    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))();
}
  1. 首先從硬盤中將bin/kernel文件的第一頁內容加載到內存地址為0x10000的位置,目的是讀取kernel文件的ELF Header信息。

  2. 校驗ELF Header的e_magic字段,以確保這是一個ELF文件

  3. 讀取ELF Header的e_phoff字段,得到Program Header表的起始地址;讀取ELF Header的e_phnum字段,得到Program Header表的元素數目。

  4. 遍歷Program Header表中的每個元素,得到每個Segment在文件中的偏移、要加載到內存中的位置(虛擬地址)及Segment的長度等信息,並通過磁盤I/O進行加載

  5. 加載完畢,通過ELF Header的e_entry得到內核的入口地址,並跳轉到該地址開始執行內核代碼

調試代碼

  1. 輸入make debug啟動gdb,並在bootmain函數入口處即0x7d0d設置斷點,輸入c跳到該入口

  2. 單步執行幾次,運行到call readseg處,由於該函數會反復讀取硬盤,為節省時間,可在下一條語句設置斷點,避免進入到readseg函數內部反復執行循環語句。(或者直接輸入n即可,不用這么麻煩)

  3. 執行完readseg后,可以通過x/xw 0x10000查詢ELF Header的e_magic的值,查詢結果如下,確實與0x464c457f相等,所以校驗成功。注意,我們的硬件是小端字節序(這從asm文件的匯編語句和二進制代碼的對比中不難發現),因此0x464c45實際上對應字符串"elf",最低位的0x7f字符對應DEL。

(gdb) x/xw 0x10000
0x10000:        0x464c457f
  1. 繼續單步執行,由0x7d2f mov 0x1001c,%eax可知ELF Header的e_phoff字段將加載到eax寄存器,0x1001c相對0x10000的偏移為0x1c,即相差28個字節,這與ELF Header的定義相吻合。執行完0x7d2f處的指令后,可以看到eax的值變為0x34,說明program Header表在文件中的偏移為0x34,則它在內存中的位置為0x10000 + 0x34 = 0x10034.查詢0x10034往后8個字節的內容如下所示:
(gdb) x/8xw 0x10034
0x10034:        0x00000001      0x00001000      0x00100000      0x00100000
0x10044:        0x0000dac4      0x0000dac4      0x00000005      0x00001000

可以結合代碼中定義的Program Header結構來理解這8個字節的含義。

struct proghdr {
    uint32_t p_type;   // loadable code or data, dynamic linking info,etc.
    uint32_t p_offset; // file offset of segment
    uint32_t p_va;     // virtual address to map segment
    uint32_t p_pa;     // physical address, not used
    uint32_t p_filesz; // size of segment in file
    uint32_t p_memsz;  // size of segment in memory (bigger if contains bss)
    uint32_t p_flags;  // read/write/execute bits
    uint32_t p_align;  // required alignment, invariably hardware page size
};

還可以使用readelf -l bin/kernel來查詢kernel文件各個Segment的基本信息,以作對比。查詢結果如下所示,可見與gdb調試結果是一致的。

Program Headers:
  Type           Offset   VirtAddr   PhysAddr   FileSiz MemSiz  Flg Align
  LOAD           0x001000 0x00100000 0x00100000 0x0dac4 0x0dac4 R E 0x1000
  LOAD           0x00f000 0x0010e000 0x0010e000 0x00aac 0x01dc0 RW  0x1000
  GNU_STACK      0x000000 0x00000000 0x00000000 0x00000 0x00000 RWE 0x10
  1. 繼續單步執行,由0x7d34 movzwl 0x1002c,%esi可知ELF Header的e_phnum字段將加載到esi寄存器,執行完x07d34處的指令后,可以看到esi的值變為3,這說明一共有3個segment。

  2. 后面是通過磁盤I/O完成三個Segment的加載,不再贅述。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM