1. 程式人生 > >MIT 6.828課程引導部分的解讀

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內聯彙編的方式操作埠,不得不說,內聯彙編確實不簡單。