Xv6源代碼之boot

一 基本原理

       計算機加電啓動後,CPU一開始會執行稱爲BIOS(基本輸入/輸出系統)的程序,該程序存儲在主板上的非易失性存儲器(ROM)中。 BIOS的工作是準備硬件,然後將控制權交給操作系統。具體來說,計算機系統的 BIOS 在完成一系列的初始化後會選擇一個啓動設備(例如硬盤、光盤、軟盤等),並且讀取該設備的第一扇區(磁盤最開始的 512 個字節),如果發現它以 0xaa55 結束,則 BIOS 認爲它是一個引導扇區,將其裝載到指定的內存位置(CS 設置爲 0x0000,IP設置爲 0x7c00,即物理內存 0x07c00)內存中,然後跳轉到 0x07c00 處將控制權徹底交給這段引導代碼。到此,計算機不再由 BIOS 中固有的程序來控制,而是變成由操作系統的一部分來控制。當引導加載程序開始執行時,處理器模擬Intel 8088,加載程序的工作是將處理器置於更現代的操作模式,將xv6內核從磁盤加載到內存中,然後將控制權交給內核。 xv6引導加載器包含兩個源文件,一個是以16位和32位x86彙編(bootasm.S)的組合編寫的,一個是C(bootmain.c)。

二 實模式與保護模式

       自從 1969 年推出第一個微處理器以來, Intel 處理器就在不斷地更新換代,從 8086、 8088、80286,到 80386、80486、奔騰、奔騰 II、奔騰 4 等,其體系結構也在不斷變化。從 80386開始,增加了一些新的功能,彌補了 8086 的缺陷。這其中包括內存保護、多任務以及使用640KB 以上的內存等,但仍然保持和 8086 家族的兼容性。也就是說 80386 仍然具備了 8086和 80286 的所有功能,但是在功能上有了很大的增強。早期的處理器工作在實地址模式
RM(Real-address Mode)之下的,80286 以後引入了保護模式 PM(Protected-address Mode),而在 80386 以後保護模式又進行了很大的改進。在 80386 中,保護模式爲程序員提供了更好的保護,提供了更多的內存。

      早期的PC是基於16位的Intel 8088的處理器的,其操作模式稱爲“實地址模式”,只支持1MB的物理內存,因此早期的PC的物理內存是從0x00000000到0x000FFFFF,而不是結束於0xFFFFFFFF。物理內存的前640KB被標記爲了“Low Memory”,這一塊區域是早期的PC 唯一可以使用的 RAM。而從 0x000A0000 到0x000FFFFF 的 384KB 的區域是被硬件保留着用於特殊用途的,比如像作爲 VGA 的顯示輸出的緩存或者是被當作保護系統固化指令的非易失性存儲器。這一部分內存區域中最重要的應該是保存在 0x000F0000 到 0x001000000 處佔據 64KB 的 BIOS(基本輸入輸出系統)。

    在 RM 模式下,對內存地址的訪問是通過 Segment:Offset 的方式來進行的,其中 Segment是一個段的 BaseAddress(Segment 的最大長度是 64KB,這是 16-bit 系統所能表示的最大長度)。而 Offset 則是相對於此 Segment Base Address 的偏移量。 BaseAddress +Offset 就是一個內存絕對地址。由此可以看出,一個段具備兩個因素: BaseAddress 和 Limit(段的最大長度)。對一個內存地址的訪問需要指出:使用哪個段?以及相對於這個段 BaseAddress 的 Offset,這個 Offset 應該小於此段的 Limit(對於 16-bit 系統,Limit 默認爲最大長度 64KB,而 16-bit的 Offset 也永遠不可能大於此 Limit)。在實際編程時,使用 16-bit 段寄存器 CS(Code Segment),DS(Data Segment),SS(Stack Segment)來指定 Segment,CPU 將段寄存器中的數值向左偏移4-bit,放到 20-bit 的地址線上就成爲 20-bit 的 Base Address。   

       在 PM 模式下,內存的管理模式分爲兩種,段模式和頁模式。其中段模式是必不可少的,頁模式是可選的,且是基於段模式的——如果使用頁模式,則是段頁式。因此 PM 模式下的內存管理模式事實上是:純段模式和段頁式。

  (1) 分段存儲管理機制

        這裏暫且只考慮段模式,訪問一個內存地址自然使用 Segment:Offset 的方式。由於 PM模式運行在 32-bit 系統上,那麼 Segment 的兩個因素:BaseAddress 和 Limit 也都是 32 位的。IA-32 允許將一個段的 BaseAddress 設爲 32-bit 所能表示的任何值。另外,PM 又爲段模式提供了保護機制,也就說一個段的描述符需要規定對自身的訪問權限(Access)。所以,在 PM 模式下,對一個段的描述包括 3 方面因素:[BaseAddress,Limit,Access],它們加在一起被放在一個 64-bit 長的數據結構中,被稱爲段描述符,其結構如圖 2-1 所示。通常,將這些 64-bit 的段描述符放入一個全局數組中,稱之爲全局描述符表(GDT)。 GDT 可以被放在內存的任何位置,爲了跟蹤 GDT,處理器內部有一個 48 位的寄存器,稱爲全局描述符寄存器(GDTR),其結構如圖 2-2 所示。該寄存器分爲兩部分,分別是 32 位的線性地址和 16 位的邊界。爲了引用 GDT 的段描述符所描述的段,需要通過存放在段寄存器中的 16-bit 結構作爲下標間接應用。這個數據結構叫做 SegmentSelector——段選擇子,如圖 2-3 所示。

                                                               圖 2-1 段描述符結構

                                                                  圖 2-2 GDTR 結構

                                                                 圖 2-3 段選擇子結構

        例如,系統的“段選擇子”爲:0x0008,對應二進制串 0000 0000 0000 1000,指定了GDT 中具有 RPL=0 的段 1,其索引字段值爲 1。邏輯地址 0x0008:0x0000 如何轉換爲線性地址呢?在 PM 模式下,當需要引用一個內存地址時,使用的仍然是 Segment:Offset 模式,具體操作是:在相應的段寄存器裝入 SegmentSelector,按照這個 SegmentSelector 可以到 GDT 或LDT 中找到相應的 SegmentDescriptor,這個 SegmentDescriptor 中記錄了此段的 BaseAddress,然後加上 Offset,就得到了最後的內存地址。如圖 2-4 所示。

                                                                      圖 2-4 地址轉換

(2) A20 地址線

       在8086、8088中,有20根地址線,所以尋址範圍是1M,但8086/8088是16位的地址模式,即只能表示64K的範圍。爲了能訪問1M的內存採取了分段的模式。16位段基址:16位偏移,達到了0x10FFEF,但是8086/8088的內存不可能超過1MB,所以當時的程序超過1MB時會自動回捲。但是到了80386地址線達到32根,芯片也達到32-bit,尋址能力達到4GB,但是爲了向後兼容,IBM採用了一個控制方法,用鍵盤控制器上的一個剩餘的控制線來控制,即A20控制線。

 

        A20 對應於地址中的第 20-bit(從 0 開始數)的特殊處理(也就是對第 21 根地址線的處理)。對於 80386 極其隨後的 32-bit 芯片來說,如果 A20Gate 被禁止,則其第 20-bit 在 CPU 做地址訪問的時候是無效的,永遠只能被作爲 0;如果 A20Gate 被打開,則其第 20-bit 是有效的,其值既可以是 0,又可以是 1。

                                                              圖2-5 A20 Gate
        所以,在保護模式下,如果 A20Gate 被禁止,則可以訪問的內存只能是奇數 1M 段,即1M,3M,5M...,也就是 00000-FFFFF,200000-2FFFFF,300000-3FFFFF...。如果 A20Gate 被打開,則可以訪問的內存則是連續的。

                                                                 圖 2-6 地址空間
        多數 PC 都使用鍵盤控制器(8042 芯片)來處理 A20Gate。

(3) CR0 控制寄存器

        80386 有 4 個 32 位的控制寄存器,名字分別爲 CR0、CR1、CR2 和 CR3。這些寄存器僅能夠由系統程序通過 MOV 指令訪問。其中 CR0 格式如圖 2-7 所示。

                                                                    圖 2-7CR0 寄存器
        控制寄存器 CR0 包含系統整體的控制標誌,它控制或指示出整個系統的運行狀態或條件。其中 PE 位爲保護模式開啓位(Protection Enable,第 0 位),如果設置了該位,就會使處理器開始在保護模式下運行。
        系統切換到保護模式,實際就是把 PE 位設置爲 1。當然爲了把系統切換到保護模式,還要做一些其他的事情。啓動程序必須要對系統的段寄存器和控制寄存器進行初始化。把PE 位設置爲 1 以後,還要執行跳轉指令。過程簡述如下。
Step 1: 創建 GDT 表。
Step 2: 用 lgdt 命令加載 gdtr。
Step 3: 啓用 A20 地址線。
Step 4: 通過置 CR0 的 PE 位爲 1。
Step 5: 執行跳轉,進入保護模式。

bootasm.S:

#include "asm.h"
#include "memlayout.h"
#include "mmu.h"

# Start the first CPU: switch to 32-bit protected mode, jump into C.C函數負責將內存載入
# The BIOS loads this code from the first sector of the hard disk into
# memory at physical address 0x7c00 and starts executing in real mode
# with %cs=0 %ip=7c00.

.code16                       # Assemble for 16-bit mode
.globl start
start:
  cli                         # 關中斷; 

  # 寄存器置爲0
  xorw    %ax,%ax             # Set %ax to zero
  movw    %ax,%ds             # -> Data Segment
  movw    %ax,%es             # -> Extra Segment
  movw    %ax,%ss             # -> Stack Segment

  # Physical address line A20 is tied to zero so that the first PCs 
  # with 2 MB would run software that assumed 1 MB.  Undo that.
  #打開A20Gate
seta20.1:
  inb     $0x64,%al               # Wait for not busy
  testb   $0x2,%al
  jnz     seta20.1


  movb    $0xd1,%al               # 0xd1 -> port 0x64  把0xd1這條數據寫入到0x64端口 理解爲下一個寫入0x60端口的數據是一個控制指令

  outb    %al,$0x64

seta20.2:
  inb     $0x64,%al               # Wait for not busy
  testb   $0x2,%al
  jnz     seta20.2

  movb    $0xdf,%al               # 0xdf -> port 0x60 0Xdf指令含義就是使能A20線
  outb    %al,$0x60

  # Switch from real to protected mode.  Use a bootstrap GDT that makes
  # virtual addresses map directly to physical addresses so that the
  # effective memory map doesn't change during the transition.
  lgdt    gdtdesc  #利用lgdt指令加載GDTR寄存器 gdtdesc是一個標識符,標識着一個內存地址
  movl    %cr0, %eax #設置CR0
  orl     $CR0_PE, %eax
  movl    %eax, %cr0

//PAGEBREAK!
  # Complete the transition to 32-bit protected mode by using a long jmp
  # to reload %cs and %eip.  The segment descriptors are set up with no
  # translation, so that the mapping is still the identity mapping.
  ljmp    $(SEG_KCODE<<3), $start32

.code32  # Tell assembler to generate 32-bit code now.
start32:
  # Set up the protected-mode data segment registers
  movw    $(SEG_KDATA<<3), %ax    # Our data segment selector
  movw    %ax, %ds                # -> DS: Data Segment
  movw    %ax, %es                # -> ES: Extra Segment
  movw    %ax, %ss                # -> SS: Stack Segment
  movw    $0, %ax                 # Zero segments not ready for use
  movw    %ax, %fs                # -> FS
  movw    %ax, %gs                # -> GS

  # Set up the stack pointer and call into C.
  movl    $start, %esp
  call    bootmain

  # If bootmain returns (it shouldn't), trigger a Bochs
  # breakpoint if running under Bochs, then loop.
  movw    $0x8a00, %ax            # 0x8a00 -> port 0x8a00
  movw    %ax, %dx
  outw    %ax, %dx
  movw    $0x8ae0, %ax            # 0x8ae0 -> port 0x8a00
  outw    %ax, %dx
spin:
  jmp     spin

# Bootstrap GDT 設置了臨時的GDT
.p2align 2                                # force 4 byte alignment
gdt:
  SEG_NULLASM                             # null seg
  SEG_ASM(STA_X|STA_R, 0x0, 0xffffffff)   # code seg代碼段
  SEG_ASM(STA_W, 0x0, 0xffffffff)         # data seg數據段

gdtdesc:
  .word   (gdtdesc - gdt - 1)             # sizeof(gdt) - 1

  .long   gdt                             # address gdt    

bootmain.c

           bootmain.c目的是在磁盤的第二個扇區開頭找到內核程序並加載到內存中。
// Boot loader.
//
// Part of the boot block, along with bootasm.S, which calls bootmain().
// bootasm.S has put the processor into protected 32-bit mode.
// bootmain() loads an ELF kernel image from the disk starting at
// sector 1 and then jumps to the kernel entry routine.
//內核二進制文件是ELF格式的,所以bootmain通過ELF文件格式可以得到內核的程序入口
#include "types.h"
#include "elf.h"
#include "x86.h"
#include "memlayout.h"

#define SECTSIZE  512

void readseg(uchar*, uint, uint); //從硬盤中讀取數據

void
bootmain(void)
{
  struct elfhdr *elf;
  struct proghdr *ph, *eph;
  void (*entry)(void);
  uchar* pa;

  elf = (struct elfhdr*)0x10000;  // scratch space臨時空間

  // Read 1st page off disk從磁盤讀取第一頁的內容到ELFHEAD(0x10000)處 相當於把操作系統映像文件的elf頭部讀取出來放到內存中
  readseg((uchar*)elf, 4096, 0);

  // Is this an ELF executable? 對elf頭部信息進行驗證 elf頭部信息的Magic字段是整個頭部信息的開端。
  if(elf->magic != ELF_MAGIC)

    return;  // let bootasm.S handle error

elf文件:elf是一種文件格式,主要被用來把程序存放到磁盤上。一個elf文件包括多個段,elf文件的頭部就是用來描述這個elf文件如何在存儲器中存儲。




  // Load each program segment (ignores ph flags).
  ph = (struct proghdr*)((uchar*)elf + elf->phoff); //Program Header Table 其中存放着程序中所有段的信息
  eph = ph + elf->phnum;
  for(; ph < eph; ph++){ //把內核中的各個段從外存讀入內存中
    pa = (uchar*)ph->paddr;
    readseg(pa, ph->filesz, ph->off);
    if(ph->memsz > ph->filesz)
      stosb(pa + ph->filesz, 0, ph->memsz - ph->filesz);
  }

  // Call the entry point from the ELF header.
  // Does not return!
  entry = (void(*)(void))(elf->entry); //跳轉到操作系統內核程序的起始指令處
  entry();
}

void
waitdisk(void)
{
  // Wait for disk ready.
  while((inb(0x1F7) & 0xC0) != 0x40)
    ;
}


// Read a single sector at offset into dst.
//讀取第offset塊磁盤到dst
void
readsect(void *dst, uint offset)
{
  // Issue command.
  waitdisk();
  outb(0x1F2, 1);   // count = 1
  outb(0x1F3, offset);
  outb(0x1F4, offset >> 8);
  outb(0x1F5, offset >> 16);
  outb(0x1F6, (offset >> 24) | 0xE0);
  outb(0x1F7, 0x20);  // cmd 0x20 - read sectors

  // Read data.
  waitdisk();
  insl(0x1F0, dst, SECTSIZE/4);

}

// Read 'count' bytes at 'offset' from kernel into physical address 'pa'.
// Might copy more than asked.
//從磁盤中讀取數據的函數,從硬盤中讀取數據到pa開始的內存地址,count是字節數目,offset是開始的字節數。
void
readseg(uchar* pa, uint count, uint offset)
{
  uchar* epa;
  epa = pa + count;
// Round down to sector boundary.
//扇區是磁盤可以尋址的最小單位

  pa -= offset % SECTSIZE;

  // Translate from bytes to sectors; kernel starts at sector 1.
  offset = (offset / SECTSIZE) + 1;

  // 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.
  for(; pa < epa; pa += SECTSIZE, offset++)
    readsect(pa, offset);

 

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章