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


一、實驗內容

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

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

二、實驗相關

ELF文件格式

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

  • 用於執行的可執行文件(executable file),用於提供程序的進程映像,加載到內存執行。 這也是本實驗的OS文件類型。
  • 用於連接的可重定位文件(relocatable file),可與其它目標文件一起創建可執行文件和共享目標文件。
  • 共享目標文件(shared object file),連接器可將它與其它可重定位文件和共享目標文件連接成其它的目標文件,動態連接器又可將它與可執行文件和其它共享目標文件結合起來創建一個進程映像。
ELF文件有兩種視圖(View),鏈接視圖和執行視圖,如下圖:



鏈接視圖通過Section Header Table描述,執行視圖通過Program Header Table描述。Section Header Table描述了所有Section的信息,包括所在的文件偏移和大小等;Program Header Table描述了所有Segment的信息,即Text Segment, Data Segment和BSS Segment,每個Segment中包含了一個或多個Section。

 

對於加載可執行文件,我們只需關注執行視圖,即解析ELF文件,遍歷Program Header Table中的每一項,把每個Program Header描述的Segment加載到對應的虛擬地址即可,然后從ELF header中取出Entry的地址,跳轉過去就開始執行了。對於ELF格式的內核文件來說,這個工作就需要由Bootloader完成。Bootloader支持ELF內核文件加載之后,用C語言編寫的內核編譯完成之后就不需要objcopy了。
 

Bootloader

我們知道計算機啟動是從BIOS開始,再由BIOS決定從哪個設備啟動以及啟動順序,比如先從DVD啟動再從硬盤啟動等。計算機啟動后,BIOS根據配置找到啟動設備,並讀取這個設備的第0個扇區,把這個扇區的內容加載到0x7c00,之后讓CPU從0x7c00開始執行,這時BIOS已經交出了計算機的控制權,由被加載的扇區程序接管計算機。
這第一個扇區的程序就叫Boot,它一般做一些准備工作,把操作系統內核加載進內存,並把控制權交給內核。由於Boot只能有一個扇區大小,即512字節,它所能做的工作很有限,因此它有可能不直接加載內核,而是加載一個叫Loader的程序,再由Loader加載內核。因為Loader不是BIOS直接加載的,所以它可以突破512字節的程序大小限制(在實模式下理論上可以達到1M)。如果Boot沒有加載Loader而直接加載內核,我們可以把它叫做Bootloader。
Bootloader加載內核就要讀取文件,在實模式下可以用BIOS的INT 13h中斷。內核文件放在哪里,怎么查找讀取,這里牽涉到文件系統,Bootloader要從硬盤(軟盤)的文件系統中查找內核文件,因此Bootloader需要解析文件系統的能力。GRUB是一個專業的Bootloader,它對這些提供了很好的支持。
對於一個Toy操作系統來說,可以簡單處理,把內核文件放到Bootloader之后,即從軟盤的第1個扇區開始,這樣我們可以不需要支持文件系統,直接讀取扇區數據加載到內存即可。

1、Bootloader的作用

簡單的說,BootLoader就是在操作系統運行之前運行的一段小程序。通過這段小程序,可以初始化硬件設備,從而將系統的軟硬件環境帶到一個合適的狀態,以便為最終調用操作系統做好准備。對於Bootloader的啟動過程又分為兩個階段stage1和stage2。

stage1全部由匯編編寫,它的主要工作是(1)初始化硬件設備、(2)為加載Bootlodader的stage2准備RAM空間(3)拷貝Bootloader的stage2到RAM空間(4)設置好堆棧段為stager2的C語言環境做准備。

stage2全部由C語言編寫,其的主要工作是(1)初始化本階段要使用到的硬件設備(2)將內核映像和根文件系統映像從 flash 上讀到RAM (3)調用內核

2、為什么需要Bootloader?

每種不同的CPU體系結構都有不同的Bootloader。除了依賴於CPU的體系結構外,Bootloader還依賴於具體的嵌入式板級設備的配置,比如板卡的硬件地址分配,外設芯片類型等。也就是說,對於兩塊不同的開發板而言,即使他們是基於同一種CPU而構建的,但是如果他們的硬件資源或配置不一致的話,想要在一塊開發板上運行Bootloader程序也能在另一塊板子上運行,還是需要做修改。

bootmain.c代碼

#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()在這個文件中接管,讀取內核並跳轉到它 * */ unsigned int    SECTSIZE  =      512 ; 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. * */
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
  // 首先讀取ELF的頭部
readseg((uintptr_t)ELFHDR, SECTSIZE * 8, 0); // is this a valid ELF?
  // 通過儲存在頭部的幻數判斷是否是合法的ELF文件
if (ELFHDR->e_magic != ELF_MAGIC) { goto bad; } struct proghdr *ph, *eph; // load each program segment (ignores ph flags)
   // ELF頭部有描述ELF文件應加載到內存什么位置的描述表,
   // 先將描述表的頭地址存在ph

ph = (struct proghdr *)((uintptr_t)ELFHDR + ELFHDR->e_phoff); eph = ph + ELFHDR->e_phnum;

   // 按照描述表將ELF文件中數據載入內存
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
   // 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);
}

 

bootmain的內容:

bootasm.S完成了bootloader的大部分功能,包括打開A20,初始化GDT,進入保護模式,更新段寄存器的值,建立堆棧

接下來bootmain完成bootloader剩余的工作,就是把內核從硬盤加載到內存中來,並把控制權交給內核。

三、問題解答

問題一:bootloader如何讀取硬盤扇區的?

讀硬盤扇區的代碼如下:

static voidreadsect(void *dst, uint32_t secno) {

 // wait for disk to be ready waitdisk(); //讀取扇區內容 outb(0x1F2, 1); // count = 1 outb(使用內聯匯編實現),設置讀取扇區的數目為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.

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

  1. 從硬盤讀了8個扇區數據到內存0x10000處,並把這里強制轉換成elfhdr使用;
  2. 校驗e_magic字段;
  3. 根據偏移量分別把程序段的數據讀取到內存中。

首先看readsect函數, readsect從設備的第secno扇區讀取數據到dst位置

 

 1     static void
 2     readsect(void *dst, uint32_t secno) {  3  waitdisk();  4     
 5         outb(0x1F2, 1);                         // 設置讀取扇區的數目為1
 6         outb(0x1F3, secno & 0xFF);  7         outb(0x1F4, (secno >> 8) & 0xFF);  8         outb(0x1F5, (secno >> 16) & 0xFF);  9         outb(0x1F6, ((secno >> 24) & 0xF) | 0xE0); 10             // 上面四條指令聯合制定了扇區號 11             // 在這4個字節線聯合構成的32位參數中 12             // 29-31位強制設為1 13             // 28位(=0)表示訪問"Disk 0" 14             // 0-27位是28位的偏移量
15         outb(0x1F7, 0x20);                      // 0x20命令,讀取扇區
16     
17  waitdisk(); 18 
19         insl(0x1F0, dst, SECTSIZE / 4);         // 讀取到dst位置, 20                                                 // 幻數4因為這里以DW為單位
21     }

 

readseg簡單包裝了readsect,可以從設備讀取任意長度的內容。

 1     static void
 2  readseg(uintptr_t va, uint32_t count, uint32_t offset) {  3         uintptr_t end_va = va + count;  4     
 5         va -= offset % SECTSIZE;  6     
 7         uint32_t secno = (offset / SECTSIZE) + 1;  8         // 加1因為0扇區被引導占用  9         // ELF文件從1扇區開始
10     
11         for (; va < end_va; va += SECTSIZE, secno ++) { 12             readsect((void *)va, secno); 13  } 14     }

在bootmain函數中,

 1     void
 2     bootmain(void) {  3         // 首先讀取ELF的頭部
 4         readseg((uintptr_t)ELFHDR, SECTSIZE * 8, 0);  5     
 6         // 通過儲存在頭部的幻數判斷是否是合法的ELF文件
 7         if (ELFHDR->e_magic != ELF_MAGIC) {  8             goto bad;  9  } 10     
11         struct proghdr *ph, *eph; 12     
13         // ELF頭部有描述ELF文件應加載到內存什么位置的描述表, 14         // 先將描述表的頭地址存在ph
15         ph = (struct proghdr *)((uintptr_t)ELFHDR + ELFHDR->e_phoff); 16         eph = ph + ELFHDR->e_phnum; 17     
18         // 按照描述表將ELF文件中數據載入內存
19         for (; ph < eph; ph ++) { 20             readseg(ph->p_va & 0xFFFFFF, ph->p_memsz, ph->p_offset); 21  } 22         // ELF文件0x1000位置后面的0xd1ec比特被載入內存0x00100000 23         // ELF文件0xf000位置后面的0x1d20比特被載入內存0x0010e000 24 
25         // 根據ELF頭部儲存的入口信息,找到內核的入口
26         ((void (*)(void))(ELFHDR->e_entry & 0xFFFFFF))(); 27     
28  bad: 29         outw(0x8A00, 0x8A00); 30         outw(0x8A00, 0x8E00); 31         while (1); 32     }

 

 

四、參考鏈接

Bootloader的作用、為什么需要Bootloader?

《ucore lab1 exercise4》實驗報告

ucore_lab1

 

 

 

 


免責聲明!

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



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