引導代碼位於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內聯彙編的方式操作端口,不得不說,內聯彙編確實不簡單。