MIT 6.828 操作系統工程 lab1 2018 fall part1 & part2 筆記 and 中文註釋源代碼閱讀

mit 6.828 lab 代碼和筆記,以及中文註釋源代碼已放置在github中:
https://github.com/yunwei37/xv6-labs

init

  1. setup

    實驗內容採用git分發:

    git clone https://pdos.csail.mit.edu/6.828/2018/jos.git lab
    

    測試的話可以使用:

    make grade
    
    

Part 1: PC Bootstrap

  • 需要了解x86彙編以及內聯彙編的寫法,參看:

    http://www.delorie.com/djgpp/doc/brennan/brennan_att_inline_djgpp.html
    https://pdos.csail.mit.edu/6.828/2018/readings/pcasm-book.pdf

  • 運行 qemu

    cd lab
    make 
    make qemu
    
    
  • PC的物理地址空間:

    +------------------+  <- 0xFFFFFFFF (4GB)
    |      32-bit      |
    |  memory mapped   |
    |     devices      |
    |                  |
    /\/\/\/\/\/\/\/\/\/\
    
    /\/\/\/\/\/\/\/\/\/\
    |                  |
    |      Unused      |
    |                  |
    +------------------+  <- depends on amount of RAM
    |                  |
    |                  |
    | Extended Memory  |
    |                  |
    |                  |
    +------------------+  <- 0x00100000 (1MB)
    |     BIOS ROM     |
    +------------------+  <- 0x000F0000 (960KB)
    |  16-bit devices, |
    |  expansion ROMs  |
    +------------------+  <- 0x000C0000 (768KB)
    |   VGA Display    |
    +------------------+  <- 0x000A0000 (640KB)
    |                  |
    |    Low Memory    |
    |                  |
    +------------------+  <- 0x00000000
    
    
  • 使用 gdb 調試qemu:

打開新的窗口:

cd lab
make qemu-gdb

在另外一個終端:

make
make gdb

開始使用gdb調試,首先進入實模式;

  • IBM PC從物理地址0x000ffff0開始執行,該地址位於爲ROM BIOS保留的64KB區域的最頂部。
  • PC從CS = 0xf000和IP = 0xfff0開始執行。
  • 要執行的第一條指令是jmp指令,它跳轉到分段地址 CS = 0xf000和IP = 0xe05b。

物理地址 = 16 *網段 + 偏移量

然後,BIOS所做的第一件事就是jmp倒退到BIOS中的較早位置;

Part 2: The Boot Loader 引導加載程序

PC的軟盤和硬盤分爲512個字節的區域,稱爲扇區。

當BIOS找到可引導的軟盤或硬盤時,它將512字節的引導扇區加載到物理地址0x7c00至0x7dff的內存中,然後使用jmp指令將CS:IP設置爲0000:7c00,將控制權傳遞給引導程序裝載機。

引導加載程序必須執行的兩個主要功能:

  • 將處理器從實模式切換到 32位保護模式;
  • 通過x86的特殊I / O指令直接訪問IDE磁盤設備寄存器,從硬盤讀取內核;

引導加載程序的源代碼:

boot/boot.S

#include <inc/mmu.h>

# 啓動CPU:切換到32位保護模式,跳至C代碼;
# BIOS將該代碼從硬盤的第一個扇區加載到
# 物理地址爲0x7c00的內存,並開始以實模式執行
# %cs=0 %ip=7c00.

.set PROT_MODE_CSEG, 0x8         # 內核代碼段選擇器
.set PROT_MODE_DSEG, 0x10        # 內核數據段選擇器
.set CR0_PE_ON,      0x1         # 保護模式啓用標誌

.globl start
start:
  .code16                     # 彙編爲16位模式
  cli                         # 禁用中斷
  cld                         # 字符串操作增量,將標誌寄存器Flag的方向標誌位DF清零。
                              # 在字串操作中使變址寄存器SI或DI的地址指針自動增加,字串處理由前往後。

  # 設置重要的數據段寄存器(DS,ES,SS)
  xorw    %ax,%ax             # 第零段
  movw    %ax,%ds             # ->數據段
  movw    %ax,%es             # ->額外段
  movw    %ax,%ss             # ->堆棧段

  # 啓用A20:
  #   爲了與最早的PC向後兼容,物理
  #   地址線20綁在低電平,因此地址高於
  #   1MB會被默認返回從零開始。  這邊代碼撤消了此操作。
seta20.1:
  inb     $0x64,%al               # 等待其不忙狀態
  testb   $0x2,%al
  jnz     seta20.1

  movb    $0xd1,%al               # 0xd1 -> 端口 0x64
  outb    %al,$0x64

seta20.2:
  inb     $0x64,%al               # 等待其不忙狀態
  testb   $0x2,%al
  jnz     seta20.2

  movb    $0xdf,%al               # 0xdf -> 端口 0x60
  outb    %al,$0x60

  # 使用引導GDT從實模式切換到保護模式
  # 並使用段轉換以保證虛擬地址和它們的物理地址相同
  # 因此
  # 有效內存映射在切換期間不會更改。
  lgdt    gdtdesc
  movl    %cr0, %eax
  orl     $CR0_PE_ON, %eax
  movl    %eax, %cr0
  
  # 跳轉到下一條指令,但還是在32位代碼段中。
  # 將處理器切換爲32位指令模式。
  ljmp    $PROT_MODE_CSEG, $protcseg

  .code32                     # 32位模式彙編
protcseg:
  # 設置保護模式數據段寄存器
  movw    $PROT_MODE_DSEG, %ax    # 我們的數據段選擇器
  movw    %ax, %ds                # -> DS: 數據段
  movw    %ax, %es                # -> ES:額外段
  movw    %ax, %fs                # -> FS
  movw    %ax, %gs                # -> GS
  movw    %ax, %ss                # -> SS: 堆棧段
  
  # 設置堆棧指針並調用C代碼,bootmain
  movl    $start, %esp
  call bootmain

  # 如果bootmain返回(不應該這樣),則循環
spin:
  jmp spin

# Bootstrap GDT
.p2align 2                                # 強制4字節對齊 
gdt:
  SEG_NULL				# 空段
  SEG(STA_X|STA_R, 0x0, 0xffffffff)	# 代碼段
  SEG(STA_W, 0x0, 0xffffffff)	        # 數據部分

gdtdesc:
  .word   0x17                            # sizeof(gdt) - 1
  .long   gdt                             # address gdt

boot/main.c


#include <inc/x86.h>
#include <inc/elf.h>

/**********************************************************************
 * 這是一個簡單的啓動裝載程序,唯一的工作就是啓動
 * 來自第一個IDE硬盤的ELF內核映像。
 *
 * 磁盤佈局
 *  * 此程序(boot.S和main.c)是引導加載程序。這應該
 *    被存儲在磁盤的第一個扇區中。
 *
 *  * 第二個扇區開始保存內核映像。
 *
 *  * 內核映像必須爲ELF格式。
 *
 * 啓動步驟
 *  * 當CPU啓動時,它將BIOS加載到內存中並執行
 *
 *  *  BIOS初始化設備,中斷例程集以及
 *    讀取引導設備的第一個扇區(例如,硬盤驅動器)
 *    進入內存並跳轉到它。
 *
 *  * 假設此引導加載程序存儲在硬盤的第一個扇區中
 *    此代碼接管...
 *
 *  * 控制從boot.S開始-設置保護模式,
 *    和一個堆棧,然後運行C代碼,然後調用bootmain()
 *
 *  * 該文件中的bootmain()會接管,讀取內核並跳轉到該內核。
 **********************************************************************/

#define SECTSIZE	512
#define ELFHDR		((struct Elf *) 0x10000) // /暫存空間

void readsect(void*, uint32_t);
void readseg(uint32_t, uint32_t, uint32_t);

void
bootmain(void)
{
	struct Proghdr *ph, *eph;

	// 從磁盤讀取第一頁
	readseg((uint32_t) ELFHDR, SECTSIZE*8, 0);

	// 這是有效的ELF嗎?
	if (ELFHDR->e_magic != ELF_MAGIC)
		goto bad;

	// 加載每個程序段(忽略ph標誌)
	ph = (struct Proghdr *) ((uint8_t *) ELFHDR + ELFHDR->e_phoff);
	eph = ph + ELFHDR->e_phnum;
	for (; ph < eph; ph++)
		// p_pa是該段的加載地址(同樣
		// 是物理地址)
		readseg(ph->p_pa, ph->p_memsz, ph->p_offset);

	// 從ELF標頭中調用入口點
	// 注意:不返回!
	((void (*)(void)) (ELFHDR->e_entry))();

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

// 從內核將“偏移”處的“計數”字節讀取到物理地址“ pa”中。
// 複製數量可能超過要求
void
readseg(uint32_t pa, uint32_t count, uint32_t offset)
{
	uint32_t end_pa;

	end_pa = pa + count;

	// 向下舍入到扇區邊界
	pa &= ~(SECTSIZE - 1);

	// 從字節轉換爲扇區,內核從扇區1開始
	offset = (offset / SECTSIZE) + 1;

	// 如果速度太慢,我們可以一次讀取很多扇區。
	// 我們向內存中寫入的內容超出了要求,但這沒關係 --
	// 我們以遞增順序加載.
	while (pa < end_pa) {
		// 由於尚未啓用分頁,因此我們正在使用
		// 一個特定的段映射 (參閱 boot.S), 我們可以
		// 直接使用物理地址.  一旦JOS啓用MMU
		// ,就不會這樣了
		readsect((uint8_t*) pa, offset);
		pa += SECTSIZE;
		offset++;
	}
}

void
waitdisk(void)
{
	// 等待磁盤重新運行
	while ((inb(0x1F7) & 0xC0) != 0x40)
		/* do nothing */;
}

void
readsect(void *dst, uint32_t offset)
{
	// 等待磁盤準備好
	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 - 讀取扇區

	// 等待磁盤準備好
	waitdisk();

	// 讀取一個扇區
	insl(0x1F0, dst, SECTSIZE/4);
}


加載內核

  • ELF二進制文件:

    可以將ELF可執行文件視爲具有加載信息的標頭,然後是幾個程序段,每個程序段都是要在指定地址加載到內存中的連續代碼或數據塊。ELF二進制文件以固定長度的ELF標頭開頭,其後是可變長度的程序標頭, 列出了要加載的每個程序段。

執行objdump -h obj/kern/kernel,查看內核可執行文件中所有部分的名稱,大小和鏈接地址的完整列表:

  • .text:程序的可執行指令。

  • .rodata:只讀數據,例如C編譯器生成的ASCII字符串常量。

  • .data:數據部分保存程序的初始化數據,例如用int x = 5等初始化程序聲明的全局變量;

  • VMA 鏈接地址,該節期望從中執行的內存地址。

  • LMA 加載地址,

obj/kern/kernel:     file format elf32-i386

Sections:
Idx Name          Size      VMA       LMA       File off  Algn
  0 .text         00001acd  f0100000  00100000  00001000  2**4
                  CONTENTS, ALLOC, LOAD, READONLY, CODE
  1 .rodata       000006bc  f0101ae0  00101ae0  00002ae0  2**5
                  CONTENTS, ALLOC, LOAD, READONLY, DATA
  2 .stab         00004291  f010219c  0010219c  0000319c  2**2
                  CONTENTS, ALLOC, LOAD, READONLY, DATA
  3 .stabstr      0000197f  f010642d  0010642d  0000742d  2**0
                  CONTENTS, ALLOC, LOAD, READONLY, DATA
  4 .data         00009300  f0108000  00108000  00009000  2**12
                  CONTENTS, ALLOC, LOAD, DATA
  5 .got          00000008  f0111300  00111300  00012300  2**2
                  CONTENTS, ALLOC, LOAD, DATA
  6 .got.plt      0000000c  f0111308  00111308  00012308  2**2
                  CONTENTS, ALLOC, LOAD, DATA
  7 .data.rel.local 00001000  f0112000  00112000  00013000  2**12
                  CONTENTS, ALLOC, LOAD, DATA
  8 .data.rel.ro.local 00000044  f0113000  00113000  00014000  2**2
                  CONTENTS, ALLOC, LOAD, DATA
  9 .bss          00000648  f0113060  00113060  00014060  2**5
                  CONTENTS, ALLOC, LOAD, DATA
 10 .comment      00000024  00000000  00000000  000146a8  2**0
                  CONTENTS, READONLY

查看引導加載程序的.text部分:

objdump -h obj/boot/boot.out

obj/boot/boot.out:     file format elf32-i386

Sections:
Idx Name          Size      VMA       LMA       File off  Algn
  0 .text         0000019c  00007c00  00007c00  00000074  2**2
                  CONTENTS, ALLOC, LOAD, CODE
  1 .eh_frame     0000009c  00007d9c  00007d9c  00000210  2**2
                  CONTENTS, ALLOC, LOAD, READONLY, DATA
  2 .stab         00000870  00000000  00000000  000002ac  2**2
                  CONTENTS, READONLY, DEBUGGING
  3 .stabstr      00000940  00000000  00000000  00000b1c  2**0
                  CONTENTS, READONLY, DEBUGGING
  4 .comment      00000024  00000000  00000000  0000145c  2**0
                  CONTENTS, READONLY

引導加載程序使用ELF 程序標頭來決定如何加載這些部分,程序標頭指定要加載到內存中的ELF對象的哪些部分以及每個目標地址應占據的位置。

檢查程序頭:objdump -x obj/kern/kernel

ELF對象需要加載到內存中的區域是標記爲“ LOAD”的區域。

Program Header:
    LOAD off    0x00001000 vaddr 0xf0100000 paddr 0x00100000 align 2**12
         filesz 0x00007dac memsz 0x00007dac flags r-x
    LOAD off    0x00009000 vaddr 0xf0108000 paddr 0x00108000 align 2**12
         filesz 0x0000b6a8 memsz 0x0000b6a8 flags rw-
   STACK off    0x00000000 vaddr 0x00000000 paddr 0x00000000 align 2**4
         filesz 0x00000000 memsz 0x00000000 flags rwx

查看內核程序的入口點objdump -f obj/kern/kernel

obj/kern/kernel:     file format elf32-i386
architecture: i386, flags 0x00000112:
EXEC_P, HAS_SYMS, D_PAGED
start address 0x0010000c

  • 在開始時,gdb會提示:The target architecture is assumed to be i8086
  • 切換到保護模式之後(ljmpl $0x8,$0xfd18f指令後),提示: The target architecture is assumed to be i386

練習6:

重置機器(退出QEMU / GDB並再次啓動它們)。在BIOS進入引導加載程序時檢查0x00100000處的8個內存字,然後在引導加載程序進入內核時再次檢查。

進入引導加載程序:

(gdb) x/8x 0x00100000
0x100000:	0x00000000	0x00000000	0x00000000	0x00000000
0x100010:	0x00000000	0x00000000	0x00000000	0x00000000

設置斷點: b *0x7d81

引導加載程序進入內核:

(gdb) x/8x 0x00100000
0x100000:	0x1badb002	0x00000000	0xe4524ffe	0x7205c766
0x100010:	0x34000004	0x2000b812	0x220f0011	0xc0200fd8

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