mit 6.828 lab 代碼和筆記,以及中文註釋源代碼已放置在github中:
https://github.com/yunwei37/xv6-labs
init
-
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