XV6學習筆記(1) : 啟動與載入

周小倫發表於2021-08-16

XV6學習筆記(1)

1. 啟動與載入

首先我們先來分析pc的啟動。其實這個都是老生常談了,但是還是很重要的(也不知道面試官考不考這玩意),

1. 啟動的第一件事-bios

首先啟動的第一件事就是執行bios,這個時候我們的機器位於真實模式,也就是16位地址。這個時候能訪問的空間只有1mb

  1. 就是設定cs暫存器的值為0xFFFF, ip的值為0x0000
  2. 這個就是bios的地址,然後我們會去執行bios執行各種對硬體的檢查
  3. 但是xv6和之前的jos(也就是828)中都沒有這樣做,作為一個精簡的os系統,

2. bootloader的彙編程式

我們的載入程式位於第一個扇區內。第一個扇區地址為0x7c00。會在bios結束之後跳轉到這裡來

整個bootloader程式分為兩個部分。第一部分是彙編程式,第二部分則是c語言

  1. 第一件做的事情是關中斷 + 清空暫存器

  2. 第二件就是開啟A20.

    開啟A20是非常重要的一件事情。這是突破16位的關鍵。這裡參考了別人的部落格

    我們具體來看 xv6 的實現程式碼

    seta20.1:
      inb     $0x64,%al               # Wait for not busy
      testb   $0x2,%al
      jnz     seta20.1
    
      movb    $0xd1,%al               # 0xd1 -> port 0x64
      outb    %al,$0x64
    
    seta20.2:
      inb     $0x64,%al               # Wait for not busy
      testb   $0x2,%al
      jnz     seta20.2
    
      movb    $0xdf,%al               # 0xdf -> port 0x60
      outb    %al,$0x60
    

    這裡 bootasm.S用了兩個方法 seta20.1seta20.2 來實現通過 804x 鍵盤控制器開啟 A20 gate。

    第一步是向 804x 鍵盤控制器的 0x64 埠傳送命令。這裡傳送的命令是 0xd1,這個命令的意思是要向鍵盤控制器的 P2 寫入資料。這就是 seta20.1 程式碼段所做的工作(具體的解釋可以參看我在程式碼中寫的註釋)。

    第二步就是向鍵盤控制器的 P2 埠寫資料了。寫資料的方法是把資料通過鍵盤控制器的 0x60 埠寫進去。寫入的資料是 0xdf,因為 A20 gate 就包含在鍵盤控制器的 P2 埠中,隨著 0xdf 的寫入,A20 gate 就被開啟了。

    接下來要做的就是進入“保護模式”了。

  3. 準備GDT表

    進入保護模式之後,我們的定址就要根據段地址 + 段內偏移來做了,所有這個全域性段描述表非常關鍵啊

    GDT 表裡的每一項叫做“段描述符”,用來記錄每個記憶體分段的一些屬性資訊,每個“段描述符”佔 8 位元組,我們先來看一眼這個段描述符的具體結構:

  1. GDT 也搞定了,接下來我們就要把我們剛剛在記憶體中設定好的 GDT 的位置告訴 CPU。CPU 單獨為我們準備了一個暫存器叫做 GDTR 用來儲存我們 GDT 在記憶體中的位置和我們 GDT 的長度。

    GDTR 暫存器一共 48 位,其中高 32 位用來儲存我們的 GDT 在記憶體中的位置,其餘的低 16 位用來存我們的 GDT 有多少個段描述符。並且還專門提供了一個指令用來讓我們把 GDT 的地址和長度傳給 GDTR 暫存器,來看 xv6 的程式碼:

lgdt   gdtdesc

而這個 gdtdesc 和 gdt 一起放在了 bootasm.S 檔案的最底部,我們看一眼:

gdtdesc:  
.word   (gdtdesc - gdt - 1)            # 16 位的 gdt 大小sizeof(gdt) - 1 
.long   gdt                            # 32 位的 gdt 所在實體地址
  1. 在xv6中,我們的cpu利用四個控制暫存器來進行一些狀態控制,想要進入保護模式需要修改cr0暫存器

  • PG    為 0 時代表只使用分段式,不使用分頁式
             為 1 是啟用分頁式
  • PE    為 0 時代表關閉保護模式,執行在真實模式下
             為 1 則開啟保護模式

最後看一下在xv6中如何做到開啟保護模式的

  lgdt    gdtdesc
  movl    %cr0, %eax
  orl     $CR0_PE_ON, %eax
  movl    %eax, %cr0

而這裡其實就是把 cr0 暫存器的值 或上 $CR0_PE_ON的值。而. CR0_PE_ON = 0x0......1

這裡的意思就是開啟保護模式

  1. 進入c語言之前的一些彙編
 # Jump to next instruction, but in 32-bit code segment.
  # Switches processor into 32-bit mode.
  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
  
  # Set up the stack pointer and call into C.
  movl    $start, %esp
  call bootmain

3. bootloader的c語言程式

  1. 首先去磁碟第一個扇區讀取核心的ELF檔案
  2. 判斷是否是一個有效的ELF標頭檔案
  3. 然後逐段把作業系統從磁碟中讀到核心中
  4. 最後執行核心的程式,此後作業系統就交由核心處理
void
bootmain(void)
{
	struct Proghdr *ph, *eph;
	int i;

	// read 1st page off disk
	readseg((uint32_t) ELFHDR, SECTSIZE*8, 0);

	// is this a valid ELF?
	if (ELFHDR->e_magic != ELF_MAGIC)
		goto bad;

	// load each program segment (ignores ph flags)
	ph = (struct Proghdr *) ((uint8_t *) ELFHDR + ELFHDR->e_phoff);
	eph = ph + ELFHDR->e_phnum;
	for (; ph < eph; ph++) {
		// p_pa is the load address of this segment (as well
		// as the physical address)
		readseg(ph->p_pa, ph->p_memsz, ph->p_offset);
		for (i = 0; i < ph->p_memsz - ph->p_filesz; i++) {
			*((char *) ph->p_pa + ph->p_filesz + i) = 0;
		}
	}

	// call the entry point from the ELF header
	// note: does not return!
	((void (*)(void)) (ELFHDR->e_entry))();

bad:
	outw(0x8A00, 0x8A00);
	outw(0x8A00, 0x8E00);
	while (1)
		/* do nothing */;
}

2. 執行核心

好文分享

kernel.ld中有一些關於核心的設定

SECTIONS
{
	/* Link the kernel at this address: "." means the current address */
	. = 0xF0100000;

	/* AT(...) gives the load address of this section, which tells
	   the boot loader where to load the kernel in physical memory */
	.text : AT(0x100000) {
		*(.text .stub .text.* .gnu.linkonce.t.*)
	}

	PROVIDE(etext = .);	/* Define the 'etext' symbol to this value */

	.rodata : {
		*(.rodata .rodata.* .gnu.linkonce.r.*)
	}

這裡設定了核心的程式碼段位於記憶體中的0x100000位置,而所對應的虛擬地址為0xF0100000

好了下面就可以去entry.S看一下核心的程式碼了

1. 設定頁表開啟分頁

  1. 對於64位機,CR3暫存器也從32位變成了64位,它的主要功能還是用來存放頁目錄表實體記憶體基地址,每當程式切換時,Linux就會把下一個將要執行程式的頁目錄表實體記憶體基地址等資訊存放到CR3暫存器中。

  1. 首先開啟4MB記憶體頁。這裡是通過設定cr4暫存器的PSE位來實現的
.globl entry
entry:
  # Turn on page size extension for 4Mbyte pages
  movl    %cr4, %eax
  orl     $(CR4_PSE), %eax
  movl    %eax, %cr4
 
  1. 設定頁目錄開啟頁表

這裡通過程式碼我們可以得到頁表的基地址就在entrypgdir中,這個變數可以在main.c中找到

開啟頁表就是通過調整cr0暫存器的位來實現的

 # Set page directory
  movl    $(V2P_WO(entrypgdir)), %eax
  movl    %eax, %cr3
  # Turn on paging.
  movl    %cr0, %eax
  orl     $(CR0_PG|CR0_WP), %eax
  movl    %eax, %cr0
// Boot page table used in entry.S and entryother.S.
// Page directories (and page tables), must start on a page boundary,
// hence the "__aligned__" attribute.  
// Use PTE_PS in page directory entry to enable 4Mbyte pages.
__attribute__((__aligned__(PGSIZE)))
pde_t entrypgdir[NPDENTRIES] = {
  // Map VA's [0, 4MB) to PA's [0, 4MB)
  [0] = (0) | PTE_P | PTE_W | PTE_PS,
  // Map VA's [KERNBASE, KERNBASE+4MB) to PA's [0, 4MB)
  [KERNBASE>>PDXSHIFT] = (0) | PTE_P | PTE_W | PTE_PS,
};

//PAGEBREAK!
// Blank page.

將這些巨集定義都轉義過來我們看看這個頁表的樣子

unsigned int entrypgdir[1024] = {
    [0] = 0 | 0x001 | 0x002 | 0x080,  // 0x083 = 0000 1000 0011
    [0x80000000 >> 22] = 0 | 0x001 | 0x002 | 0x080  // 0x083
};

當然這裡只是一個臨時頁表。這裡只有兩個頁表項 0x000000000x80000000,而且兩個頁表項索引的記憶體實體地址都是 0 ~ 4MB

把虛擬地址空間的地址範圍:0x80100000 -0x80500000,對映到實體地址範圍:0x00000000 - 0x00400000上面。也可以把虛擬地址範圍:0x00000000 - 0x00400000,同樣對映到實體地址範圍:0x00000000~0x00400000上面。任何不再這兩個虛擬地址範圍內的地址都會引起一個硬體異常。雖然只能對映這兩塊很小的空間,但是已經足夠剛啟動程式的時候來使用了。

這裡的jos裡地址就是0xF0100000,不過邏輯都是一模一樣的

  1. 設定核心棧以及跳轉到c語言到main.c
  # Set up the stack pointer.
  movl $(stack + KSTACKSIZE), %esp

  # Jump to main(), and switch to executing at
  # high addresses. The indirect call is needed because
  # the assembler produces a PC-relative instruction
  # for a direct jump.
  mov $main, %eax
  jmp *%eax

.comm stack, KSTACKSIZE

3. main.c

// Bootstrap processor starts running C code here.
// Allocate a real stack and switch to it, first
// doing some setup required for memory allocator to work.
int
main(void)
{
  kinit1(end, P2V(4*1024*1024)); // phys page allocator
  kvmalloc();      // kernel page table
  mpinit();        // collect info about this machine
  lapicinit();
  seginit();       // set up segments
  cprintf("\ncpu%d: starting xv6\n\n", cpu->id);
  picinit();       // interrupt controller
  ioapicinit();    // another interrupt controller
  consoleinit();   // I/O devices & their interrupts
  uartinit();      // serial port
  pinit();         // process table
  tvinit();        // trap vectors
  binit();         // buffer cache
  fileinit();      // file table
  iinit();         // inode cache
  ideinit();       // disk
  if(!ismp)
    timerinit();   // uniprocessor timer
  startothers();   // start other processors
  kinit2(P2V(4*1024*1024), P2V(PHYSTOP)); // must come after startothers()
  userinit();      // first user process
  // Finish setting up this processor in mpmain.
  mpmain();
}

這裡做了各種對於os的初始化。接下來我們將會看到 xv6 的核心是如何實現記憶體管理、程式管理、IO 操作等化作業系統所應該具有的功能,同時會結合jos也就是mit6.828進行對比一下。

相關文章