Lab1:練習4——分析bootloader載入ELF格式的OS的過程

Colorful_i發表於2022-05-04

練習四:分析bootloader載入ELF格式的OS的過程。

1.題目要求

通過閱讀bootmain.c,瞭解bootloader如何載入ELF檔案。通過分析原始碼和通過qemu來執行並除錯bootloader&OS,

  • bootloader如何讀取硬碟扇區的?
  • bootloader是如何載入ELF格式的OS?

提示:可閱讀“硬碟訪問概述”,“ELF執行檔案格式概述”這兩小節。

2.整個流程

假定進入了保護模式之後,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程式頭來讀出相應的程式碼段和資料段,然後加到相應的地方去。

最後一句image-20220503112407992

就是決定了bootloader把這個載入完之後,到底跳轉到什麼地方去,把控制權交給ucore去執行

3.預備知識

3.1ELF檔案格式

ELF(Executable and linking format)檔案格式是Linux系統下的一種常用目標檔案(object file)格式,有三種主要型別:

  • 用於執行的可執行檔案(executable file),用於提供程式的程式映像,載入到記憶體執行。 這也是本實驗的OS檔案型別。
  • 用於連線的可重定位檔案(relocatable file),可與其它目標檔案一起建立可執行檔案和共享目標檔案。
  • 共享目標檔案(shared object file),聯結器可將它與其它可重定位檔案和共享目標檔案連線成其它的目標檔案,動態聯結器又可將它與可執行檔案和其它共享目標檔案結合起來建立一個程式映像。

ELF檔案結構:

image-20220504175154533

首先,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作業系統,

3.2 bootmain()函式

#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剩餘的工作,就是把核心從硬碟載入到記憶體中來,並把控制權交給核心。

現在看不懂這個函式具體怎麼實現的沒關係,後面會有具體的解釋。只需要知道它的功能就行。

4. 問題解答

4.1問題一: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");
}

讀取硬碟扇區的步驟:

  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.

4.2 問題二:bootloader如何載入ELF格式的OS

  1. 從硬碟讀了8個扇區資料到記憶體0x10000處,並把這裡強制轉換成elfhdr使用;
  2. 校驗e_magic欄位;
  3. 根據偏移量分別把程式段的資料讀取到記憶體中。

之前已經看了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);
}

相關文章