經過閱讀bootmain.c,瞭解bootloader如何加載ELF文件。經過分析源代碼和經過qemu來運行並調試bootloader&OS,html
ELF(Executable and linking format)文件格式是Linux系統下的一種經常使用目標文件(object file)格式,有三種主要類型:小程序
簡單的說,BootLoader就是在操做系統運行以前運行的一段小程序。經過這段小程序,能夠初始化硬件設備,從而將系統的軟硬件環境帶到一個合適的狀態,以便爲最終調用操做系統作好準備。對於Bootloader的啓動過程又分爲兩個階段stage1和stage2。函數
stage1所有由彙編編寫,它的主要工做是(1)初始化硬件設備、(2)爲加載Bootlodader的stage2準備RAM空間(3)拷貝Bootloader的stage2到RAM空間(4)設置好堆棧段爲stager2的C語言環境作準備。佈局
stage2所有由C語言編寫,其的主要工做是(1)初始化本階段要使用到的硬件設備(2)將內核映像和根文件系統映像從 flash 上讀到RAM (3)調用內核
post
每種不一樣的CPU體系結構都有不一樣的Bootloader。除了依賴於CPU的體系結構外,Bootloader還依賴於具體的嵌入式板級設備的配置,好比板卡的硬件地址分配,外設芯片類型等。也就是說,對於兩塊不一樣的開發板而言,即便他們是基於同一種CPU而構建的,可是若是他們的硬件資源或配置不一致的話,想要在一塊開發板上運行Bootloader程序也能在另外一塊板子上運行,仍是須要作修改。
ui
#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);
}
bootasm.S完成了bootloader的大部分功能,包括打開A20,初始化GDT,進入保護模式,更新段寄存器的值,創建堆棧this
接下來bootmain完成bootloader剩餘的工做,就是把內核從硬盤加載到內存中來,並把控制權交給內核。url
讀硬盤扇區的代碼以下:spa
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"); }
讀取硬盤扇區的步驟:
等待硬盤空閒。waitdisk的函數實現只有一行:while ((inb(0x1F7) & 0xC0) != 0x40)
,意思是不斷查詢讀0x1F7寄存器的最高兩位,直到最高位爲0、次高位爲1(這個狀態應該意味着磁盤空閒)才返回。
硬盤空閒後,發出讀取扇區的命令。對應的命令字爲0x20,放在0x1F7寄存器中;讀取的扇區數爲1,放在0x1F2寄存器中;讀取的扇區起始編號共28位,分紅4部分依次放在0x1F3~0x1F6寄存器中。
發出命令後,再次等待硬盤空閒。
硬盤再次空閒後,開始從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.
0x10000
處,並把這裏強制轉換成elfhdr
使用;e_magic
字段;