MIT 6.828課程引導部分的解讀
引導程式碼位於boot資料夾下,由一個16位與32位彙編混合的彙編檔案(boot.S)和一個C語言檔案(main.c)組成。
程式的入口在boot.S中,採用的是AT&T語法,下面先對這個檔案進行分析:
#include <inc/mmu.h>
在inc資料夾下有一個mmu.h標頭檔案,這裡存放了一些經常會用到的巨集定義
.set PROT_MODE_CSEG, 0x8
.set PROT_MODE_DSEG, 0x10
.set CR0_PE_ON, 0x1
上面聲明瞭一些常量,用來設定一些段暫存器的值
.globl start start:
這裡就要進入正題了,“.globl start”用來告訴編譯器start是程式入口,事實上“.globl”的主要作用是說明“start”這個函式可以被其他檔案的呼叫,編譯器預設的入口標號為“_start”因此,在linux中編譯時,會指定編譯入口,用這個引數“-e start”。
.code16
cli
cld
“.code16”的意思是從這裡往後的程式碼是16位程式,要用16位的編譯模式(16位與32位生成的機器碼是不一樣的,因此不指明的話,會造成不可預計的錯誤。)
“cli”指令用來關掉可遮蔽中斷,為在之後的執行中不受打擾。以後還會開啟的。
“cld”重新確定串操作方向,主要是為了main.c檔案中複製核心程式時使用。
xorw %ax,%ax #將ax清0 ,分別將ds、es、ss的值變成0,因為在之前的BIOS執行中,不能確定這些暫存器的值。
movw %ax,%ds
movw %ax,%es
movw %ax,%ss
seta20.1:
inb $0x64,%al # Wait for not busy
testb $0x2,%al
lgdt gdtdesc
jnz seta20.1 movb $0xd1,%al # 0xd1 -> port 0x64 outb %al,$0x64seta20.2: inb $0x64,%al # Wait for not busy testb $0x2,%al jnz seta20.2 movb $0xdf,%al # 0xdf -> port 0x60 outb %al,$0x60
簡單來講,這段程式碼主要就是一個目的,使能A20地址線,使計算機能夠訪問更大的記憶體空間。具體關於A20地址線網上有很多介紹,這裡不再廢話。要注意的是開啟A20地址線並不不能使計算機直接進入保護模式(32位模式),只是使計算機可以訪問更大的記憶體。
lgdt gdtdesc
這條指令尤為重要,載入全域性描述符(GDT),標號gdtsec處存放了6位元組有關GDT位置的資訊,稍後我們會看到。
movl %cr0, %eax
orl $CR0_PE_ON, %eax
movl %eax, %cr0
這裡將cr0暫存器的0-bit(最低位)置1,告訴cpu進入保護模式,在這裡才算是真正進入了保護模式。
ljmp $PROT_MODE_CSEG, $protcseg
.code32 # Assemble for 32-bit mode
protcseg:
# Set up the protected-mode data segment registers
movw $PROT_MODE_DSEG, %ax # Our data segment selector
movw %ax, %ds # -> DS: Data Segment
movw %ax, %es # -> ES: Extra Segment
movw %ax, %fs # -> FS
movw %ax, %gs # -> GS
movw %ax, %ss # -> SS: Stack Segment
“ljmp”這條指令可能會讓人感到奇怪,因為這個長跳轉指令後的就是他要跳轉的位置。事實上,必須這麼做,在載入GDT以後,由於內部硬體設計的原因,必須要重設所有段暫存器的值,但我們沒有直接改變指令暫存器cs的指令,因此使用一個長跳轉指令改變cs的值(跳轉指令實際上就是在對cs進行操作,使cs指向預定的位置,cup是根據cs指向的位置來執行機器碼的)。
之後的指令就好理解了,因為進入了保護模式,所以“.code32”告訴編譯器使用32位方式編譯程式碼。然後是將其他5個段暫存器的值重新設為0x10。
movl $start, %esp
call bootmain
這裡很簡單,先將棧頂指標指向我們的載入程式首地址,即0x7c00處,由於堆疊是向下生長的,因此實際的堆疊區域變設定為了0x7c00~0x0010這一段。(注:ss被稱為棧底指標,指向堆疊的最底部,sp是棧頂指標,時刻指向當前堆疊第一個元素的位置,esp是32位的,64位是rsp)
最後呼叫bootmain函式,這個函式在main.c檔案中。用來將核心複製到記憶體的指定位置。
spin: #這裡是一個死迴圈,如果bootmain函式發生錯誤返回,計算機就會卡在這兒,就作業系統而言,最好在這裡設定一些提示資訊
jmp spin
# Bootstrap GDT
.p2align 2 # force 4 byte alignment
gdt:
SEG_NULL # null seg
SEG(STA_X|STA_R, 0x0, 0xffffffff) # code seg 程式碼段
SEG(STA_W, 0x0, 0xffffffff) # data seg 資料段
gdtdesc:
.word 0x17 # sizeof(gdt) - 1
.long gdt # address gdt
標號gdt處設計了有3個表項的GDT表,每個表項長度為8位元組,第一個表項必須設定為空的
標號gdtdesc是要往GDTR暫存器里加載的6位元組資訊,GDTR暫存器的低2位元組儲存GDT表的長度,高4位元組儲存GDT表在記憶體中的首地址.由於GDT講解比較繁雜,這裡暫時略過。
下面對main.c檔案分析
首先要說明一下bootmain函式的工作方式。
首先它會判斷要載入的核心檔案是否屬於elf格式,這是一種專為unix系統設計的二進位制檔案,unix/linux下的應用程式均為elf結構,mit 6.828課程也使用了這種結構,結構定義在inc資料夾的elf.h檔案中,如下:
struct Elf {
uint32_t e_magic; // 魔數,elf檔案的前四位元組分別為(0x7f,E,L,F)
uint8_t e_elf[12]; //這12位元組沒有定義
uint16_t e_type; //目標檔案屬性
uint16_t e_machine; //硬體平臺型別
uint32_t e_version; //elf的版本
uint32_t e_entry; //程式入口
uint32_t e_phoff; //程式頭表偏移量,bootmain函式中的第一行的那個結構便是程式表頭的結構
uint32_t e_shoff; //節表頭偏移量,節表多用來儲存程式會用到的資料
uint32_t e_flags; //處理器特定標誌
uint16_t e_ehsize; //elf頭部長度
uint16_t e_phentsize; //程式頭表中一個條目的長度
uint16_t e_phnum; //程式頭表條目數目
uint16_t e_shentsize; //節頭表中一個條目的長度
uint16_t e_shnum; //節頭表條目個數
uint16_t e_shstrndx; //節頭表字符索引
};
來看bootmain函式的第一行:
struct Proghdr *ph, *eph;
聲明瞭兩個指向程式表頭的指標,還沒有定義。
//這裡是將一頁(4kb)核心程式讀入記憶體,放在首地址0x10000處
readseg((uint32_t) ELFHDR, SECTSIZE*8, 0);
if (ELFHDR->e_magic != ELF_MAGIC)//判斷是否為elf檔案,elf結構中的前4位元組為(0x7f,E,L,F)
goto bad;//bad處是一個錯誤處理程式,編寫者只是簡單地做了個迴圈,畢竟是用來學習的
//載入每個程式段
ph = (struct Proghdr *) ((uint8_t *) ELFHDR + ELFHDR->e_phoff);
eph = ph + ELFHDR->e_phnum;//e_phnum儲存了程式段的數量,在這裡將eph指向了最後一個程式段
在這裡,ph指向了核心的程式表頭,eph則指向了最後一個程式段。
for (; ph < eph; ph++)
readseg(ph->p_pa, ph->p_memsz, ph->p_offset);
反覆迴圈,通過readseg函式將核心全部載入到記憶體中。
最後的bad,基本沒有意義。
main.c中還有兩個磁碟操作函式,用來幫助bootmain函式載入核心。
void
readseg(uint32_t pa, uint32_t count, uint32_t offset)
{
uint32_t end_pa;
end_pa = pa + count;
// round down to sector boundary
pa &= ~(SECTSIZE - 1);//確保該地址指向所在扇區的第一個位元組
// translate from bytes to sectors, and kernel starts at sector 1
offset = (offset / SECTSIZE) + 1;//確保偏移量小於512,大於0
// 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.
while (pa < end_pa) {
// Since we haven't enabled paging yet and we're using
// an identity segment mapping (see boot.S), we can
// use physical addresses directly. This won't be the
// case once JOS enables the MMU.
readsect((uint8_t*) pa, offset);//pa的值是0x10000,即核心的地址,
pa += SECTSIZE;
offset++;
}
}
void
waitdisk(void)
{
// wait for disk reaady
//從埠0x1f7獲取一位元組資訊,並判斷最高兩位是否為01(第7位和第6位),否則不斷迴圈
//0x1f7 用來存放硬碟操作後的狀態,其中第7位為1代表控制器忙碌,第六位為1代表磁碟驅動準備好了
//這段程式碼用來判斷硬碟是否可以讀取,不能讀的話計算機就會卡在這裡
while ((inb(0x1F7) & 0xC0) != 0x40)
/* do nothing */;
}
void
readsect(void *dst, uint32_t offset)
{
// wait for disk to be ready
waitdisk();
//0x1f2存放要讀寫的扇區數量
outb(0x1F2, 1); // count = 1
//0x1f3用來存放要讀寫的扇區號碼,就是偏移量
outb(0x1F3, offset);
//0x1f4 用來存放讀寫柱面的低8位位元組
outb(0x1F4, offset >> 8);
//0x1f5 用來存放讀寫柱面的高2位位元組
outb(0x1F5, offset >> 16);
//0x1f6 用來存放要讀寫的磁碟號,磁頭號。7-bit:恆為1;6-bit:恆為0;5-bit:恆為1;
//4-bit:0代表第一塊硬碟,1代表第二塊硬碟
//3~0-bit:用來存放要讀寫的磁頭號
outb(0x1F6, (offset >> 24) | 0xE0);
//0x1f7 用來存放硬碟操作後的狀態,以下為設定為1的情況
//7-bit 控制器忙碌
//6-bit 磁碟驅動器準備好了
//5-bit 寫入錯誤
//4-bit 搜尋完成
//3-bit 扇區緩衝區沒有準備好
//2-bit 是否正確讀取磁碟資料
//1-bit 磁碟每轉一週將此位設定為1
//0-bit 之前的命令因發生錯誤而結束
outb(0x1F7, 0x20); // cmd 0x20 - read sectors
// wait for disk to be ready
waitdisk();//檢查磁碟狀態
// read a sector
insl(0x1F0, dst, SECTSIZE/4);//0x1f0讀寫功能,其內容為正在傳輸的一位元組資料
}
上面已經說得很細了,在inc資料夾的x86.h檔案中可以找的上面用到的一些埠操作函式,使用gcc內聯彙編的方式操作埠,不得不說,內聯彙編確實不簡單。