XV6學習筆記(1)
1. 啟動與加載
首先我們先來分析pc的啟動。其實這個都是老生常談了,但是還是很重要的(也不知道面試官考不考這玩意),
1. 啟動的第一件事-bios
首先啟動的第一件事就是運行bios,這個時候我們的機器位於實模式,也就是16位地址。這個時候能訪問的空間只有1mb
- 就是設置cs寄存器的值為0xFFFF, ip的值為0x0000
- 這個就是bios的地址,然后我們會去運行bios執行各種對硬件的檢查
- 但是xv6和之前的jos(也就是828)中都沒有這樣做,作為一個精簡的os系統,
2. bootloader的匯編程序
我們的引導程序位於第一個扇區內。第一個扇區地址為0x7c00
。會在bios結束之后跳轉到這里來
整個bootloader
程序分為兩個部分。第一部分是匯編程序,第二部分則是c語言
-
第一件做的事情是關中斷 + 清空寄存器
-
第二件就是打開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.1
和seta20.2
來實現通過 804x 鍵盤控制器打開 A20 gate。第一步是向 804x 鍵盤控制器的 0x64 端口發送命令。這里傳送的命令是 0xd1,這個命令的意思是要向鍵盤控制器的 P2 寫入數據。這就是 seta20.1 代碼段所做的工作(具體的解釋可以參看我在代碼中寫的注釋)。
第二步就是向鍵盤控制器的 P2 端口寫數據了。寫數據的方法是把數據通過鍵盤控制器的 0x60 端口寫進去。寫入的數據是 0xdf,因為 A20 gate 就包含在鍵盤控制器的 P2 端口中,隨着 0xdf 的寫入,A20 gate 就被打開了。
接下來要做的就是進入“保護模式”了。
-
准備GDT表
進入保護模式之后,我們的尋址就要根據
段地址 + 段內偏移
來做了,所有這個全局段描述表非常關鍵啊GDT 表里的每一項叫做“段描述符”,用來記錄每個內存分段的一些屬性信息,每個“段描述符”占 8 字節,我們先來看一眼這個段描述符的具體結構:
-
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 所在物理地址
- 在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
這里的意思就是開啟保護模式
- 進入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語言程序
- 首先去磁盤第一個扇區讀取內核的ELF文件
- 判斷是否是一個有效的ELF頭文件
- 然后逐段把操作系統從磁盤中讀到內核中
- 最后執行內核的程序,此后操作系統就交由內核處理
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. 設置頁表開啟分頁
- 對於64位機,CR3寄存器也從32位變成了64位,它的主要功能還是用來存放頁目錄表物理內存基地址,每當進程切換時,Linux就會把下一個將要運行進程的頁目錄表物理內存基地址等信息存放到CR3寄存器中。
- 首先開啟4MB內存頁。這里是通過設置cr4寄存器的PSE位來實現的
.globl entry
entry:
# Turn on page size extension for 4Mbyte pages
movl %cr4, %eax
orl $(CR4_PSE), %eax
movl %eax, %cr4
- 設置頁目錄開啟頁表
這里通過代碼我們可以得到頁表的基地址就在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
};
當然這里只是一個臨時頁表。這里只有兩個頁表項 0x00000000
和 0x80000000
,而且兩個頁表項索引的內存物理地址都是 0 ~ 4MB
把虛擬地址空間的地址范圍:0x80100000 -0x80500000
,映射到物理地址范圍:0x00000000 - 0x00400000
上面。也可以把虛擬地址范圍:0x00000000 - 0x00400000
,同樣映射到物理地址范圍:0x00000000~0x00400000
上面。任何不再這兩個虛擬地址范圍內的地址都會引起一個硬件異常。雖然只能映射這兩塊很小的空間,但是已經足夠剛啟動程序的時候來使用了。
這里的jos里地址就是0xF0100000,不過邏輯都是一模一樣的
- 設置內核棧以及跳轉到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進行對比一下。