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內聯彙編的方式操作端口,不得不說,內聯彙編確實不簡單。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章