1.1
ld bin/kernel ld -m elf_i386 -nostdlib -T tools/kernel.ld -o bin/kernel obj/kern/init/init.o obj/kern/libs/stdio.o obj/kern/libs/readline.o obj/kern/debug/panic.o obj/kern/debug/kdebug.o obj/kern/debug/kmonitor.o obj/kern/driver/clock.o obj/kern/driver/console.o obj/kern/driver/picirq.o obj/kern/driver/intr.o obj/kern/trap/trap.o obj/kern/trap/vectors.o obj/kern/trap/trapentry.o obj/kern/mm/pmm.o obj/libs/string.o obj/libs/printfmt.o
ld將.o文件整合成可執行文件kernel,而這些.o文件是Makefile文件通過命令使用gcc把有關kernel的.c文件編譯生成
+ ld bin/bootblock
ld -m elf_i386 -nostdlib -N -e start -Ttext 0x7C00 obj/boot/bootasm.o obj/boot/bootmain.o -o obj/bootblock.o 'obj/bootblock.out' size: 488 bytes
build 512 bytes boot sector: 'bin/bootblock' success!
同理ld也將.o文件整合成可執行文件bootblock,大小為488字節,但還是放入512字節扇區中,但是,而這些.o文件也是Makefile文件通過命令使用gcc把有關bootloader的.c文件編譯生成
dd if=/dev/zero of=bin/ucore.img count=10000
記錄了10000+0 的讀入 記錄了10000+0 的寫出 5120000 bytes (5.1 MB, 4.9 MiB) copied, 0.0278414 s, 184 MB/s
創建10000塊扇區,每個扇區512字節,制成ucore.img虛擬磁盤
dd if=bin/bootblock of=bin/ucore.img conv=notrunc
記錄了1+0 的讀入 記錄了1+0 的寫出 512 bytes copied, 0.000466728 s, 1.1 MB/s
將bootblock存到ucore.img虛擬磁盤的第一塊
dd if=bin/kernel of=bin/ucore.img seek=1 conv=notrunc
記錄了146+1 的讀入 記錄了146+1 的寫出 74828 bytes (75 kB, 73 KiB) copied, 0.00037979 s, 197 MB/s
將kernel存到ucore.img虛擬磁盤的第二塊及之后幾塊,注意seek1,最終ucore.img虛擬磁盤制作完成
注意:ucore很特別,第一扇區存bootloader,第二扇區以及之后存kernel...........
1.2
通過sign.c(生成一個符合標准的主引導扇區)文件可以得到:
char buf[512]; memset(buf, 0, sizeof(buf)); buf[510] = 0x55; buf[511] = 0xAA;
扇區大小為512字節,最后兩字節為0x55和0xAA
2.1

直接make debug,然后gdbinit配置文件設置成:
set architecture i8086 target remote :1234 file bin/kernel break kern_init continue
使用next來單步調試
2.2
在Makefile文件中添加命令
lab1-mon: $(UCOREIMG)
注意此處為TAB開頭$(V)$(TERMINAL) -e "$(QEMU) -S -s -d in_asm -D $(BINDIR)/q.log -parallel stdio -hda $< -serial null" #將qemu日志記錄到q.log中 注意此處為TAB開頭$(V)sleep 2 注意此處為TAB開頭$(V)$(TERMINAL) -e "gdb -x tools/labinit" #使用gdb的配置文件labinit開啟gdb
labinit內容
file bin/kernel #指定gdb調試目標文件
target remote :1234 #把gdb和qemu鏈接
set architecture i8086 #使用實模式16位 b *0x7c00 #在bootloader程序起始處設定斷點,實驗設定的bootloader灰被加載到0x7c00
continue #斷點之后繼續執行 x /2i $pc #顯示對應的匯編指令和下一條匯編指令
可以使用next指令來單步調試
通過輸入
x/i $cs
x/i $eip
我們可以獲取當前 $cs 和 $eip 的值。其中
$cs = 0xf000
$eip = 0xfff0
我們也可以看看這個地址的指令是什么
x/2i 0xffff
得到的結果是
0xffff0: ljmp $0xf000,$0xe05b
2.3
使用x /2i $pc來觀察匯編代碼,然后與bootasm.S文件里16到56行代碼比較即可 2.3
2.4略
3.1
.set PROT_MODE_CSEG, 0x8 # kernel code segment selector,設置內核代碼段選擇符
.set PROT_MODE_DSEG, 0x10 # kernel data segment selector,設置內核數據段選擇符
.set CR0_PE_ON, 0x1 # protected mode enable flag,,設置保護模式使能標志,CR0_PE_ON設置為0x1
cli # Disable interrupts
#關中斷
cld # String operations increment # Set up the important data segment registers (DS, ES, SS). xorw %ax, %ax # Segment number zero,自己跟自己異或結果為0,且放到ax寄存器中 movw %ax, %ds # -> Data Segment,用eax寄存器數據初始化(下同) movw %ax, %es # -> Extra Segment movw %ax, %ss # -> Stack Segment
看bootloader
- 為何開啟A20,以及如何開啟A20
- 如何初始化GDT表
- 如何使能和進入保護模式
1.1為何打開A20?
8088/8086只有20位地址線,按理它的尋址空間是2^20,應該是1024KB,但PC機的尋址結構是segment:offset,segment和offset都是16位的寄存器,最大值是0ffffh,換算成物理地址的計算方法是把segment左移4位,再加上offset,所以segment:offset所能表達的尋址空間最大應為0ffff0h + 0ffffh = 10ffefh(前面的0ffffh是segment=0ffffh並向左移動4位的結果,后面的0ffffh是可能的最大offset),這個計算出的10ffefh大約是1088KB,就是說,segment:offset的地址表達能力超過了20位地址線的物理尋址能力,所以當你訪問大於1M區域是會發生回卷現象,如果你企圖尋址100001h這個地址時,你實際得到的內容是地址00001h上的內容,而下一代的基於Intel 80286 CPU的PC AT計算機系統提供了24根地址線,這樣CPU的尋址范圍變為 2^24=16M,同時也提供了保護模式,可以訪問到1MB以上的內存了,此時如果遇到“尋址超過1MB”的情況,系統不會再“回卷”了,這就造成了向下不兼容,為了保持完全的向下兼容性,IBM決定在PC AT計算機系統上加個硬件邏輯,來模仿以上的回繞特征,PC機在設計上在第21條地址線(也就是A20,原先20條地址線是A0-A19)上做了一個開關,當這個開關打開時,這條地址線和其它地址線一樣可以使用,當這個開關關閉時,第21條地址線(A20)恆為0,這個開關就叫做A20 Gate。
PS. 0000000H-----100000H已經為1M空間,而1088K比1M多的部分就在高端內存區
在實模式下, 由於我們訪問了高端內存區(1088K比1M多的部分),所以我們打開A20gate,在保護模式下,由於使用32位地址線,我們也是打開A20gate,目的是為了訪問更大的內存區域
1.2打開A20 Gate的具體步驟大致如下(參考bootasm.S):
- 等待8042 Input buffer為空;
- 發送Write 8042 Output Port (P2)命令到8042 Input buffer;
- 等待8042 Input buffer為空;
- 將8042 Output Port(P2)得到字節的第2位置1,然后寫入8042 Input buffer
seta20.1: inb $0x64, %al # Wait for not busy(8042 input buffer empty).
#從0x64端口讀入一個字節的數據到al中 testb $0x2, %al
#如果上面的測試中發現al的第2位為0,就不執行該指令 jnz seta20.1 #循環檢查 movb $0xd1, %al # 0xd1 -> port 0x64 outb %al, $0x64 # 0xd1 means: write data to 8042's P2 port #將al中的數據寫入到端口0x64中 seta20.2: inb $0x64, %al # Wait for not busy(8042 input buffer empty). testb $0x2, %al jnz seta20.2 movb $0xdf, %al # 0xdf -> port 0x60 outb %al, $0x60 # 0xdf = 11011111, means set P2's A20 bit(the 1 bit) to 1
2.初始化GDT表
GDT全稱是Global Descriptor Table,也就是全局描述符表。在保護模式下,我們通過設置GDT將內存空間被分割為了一個又一個的segment(這些segment是可以重疊的),這樣我們就能實現不同的程序訪問不同的內存空間。這和實模式下的尋址方式是不同的, 在實模式下我們只能使用address = segment << 4 | offset的方式進行尋址(雖然也是segment + offset的,但在實模式下我們並不會真正的進行分段)。在這種情況下,任何程序都能訪問整個1MB的空間。而在保護模式下,通過分段的方式,程序並不能訪問整個內存空間
在實模式下, 邏輯地址由段選擇子(保存在段寄存器中)和段選擇子偏移量組成. 其中, 段選擇子16bit, 段選擇子偏移量是32bit. 下面是段選擇子的示意圖:

- 在段選擇子中,其中的INDEX[15:3]是GDT的索引。
- TI[2:2]用於選擇表格的類型,1是LDT,0是GDT。
- RPL[1:0]用於選擇請求者的特權級,00最高,11最低
段描述符

主要作用是保存[31..24]的段基址
所以地址轉換方式為:

- 硬件自動將CPU給的邏輯地址分離出段選擇子。
- 利用這個段選擇子在GDT中選擇一個段描述符。
- 將段描述符里的Base Address和邏輯地址的偏移量相加而得到線性地址(沒有頁機制下就是物理地址)。
#define SEG_NULLASM \ .word 0, 0; \ .byte 0, 0, 0, 0 #define SEG_ASM(type,base,lim) \ .word (((lim) >> 12) & 0xffff), ((base) & 0xffff); \ .byte (((base) >> 16) & 0xff), (0x90 | (type)), \ (0xC0 | (((lim) >> 28) & 0xf)), (((base) >> 24) & 0xff)
static inline void lgdt(struct pseudodesc *pd) { asm volatile ("lgdt (%0)" :: "r" (pd)); asm volatile ("movw %%ax, %%gs" :: "a" (USER_DS)); asm volatile ("movw %%ax, %%fs" :: "a" (USER_DS)); asm volatile ("movw %%ax, %%es" :: "a" (KERNEL_DS)); asm volatile ("movw %%ax, %%ds" :: "a" (KERNEL_DS)); asm volatile ("movw %%ax, %%ss" :: "a" (KERNEL_DS)); // reload cs asm volatile ("ljmp %0, $1f\n 1:\n" :: "i" (KERNEL_CS)); }
lgdt gdtdesc
# Bootstrap GDT .p2align 2 # force 4 byte alignment,強制4字節對齊 gdt: SEG_NULLASM # null seg
SEG_ASM(STA_X|STA_R, 0x0, 0xffffffff) # code seg for bootloader and kernel,代碼段描述符
SEG_ASM(STA_W, 0x0, 0xffffffff) # data seg for bootloader and kernel,數據段描述符
gdtdesc: #GDTR所要存的內容 .word 0x17 # sizeof(gdt) - 1 .long gdt # address gdt
理論上GDT可以存在內存中任何位置,但在實模式下只有尋址空間只有1M情況下初始化GDT,GDT只能在這1M空間中,CPU通過lgdt指令讀入GDT的地址,之后我們就可以使用GDT了。
3.如何使能和進入保護模式
CR0中包含了6個預定義標志,0位是保護允許位PE(Protedted Enable),用於啟動保護模式,如果PE位置1,則保護模式啟動,如果PE=0,則在實模式下運行。
movl %cr0, %eax cr0->eax orl $CR0_PE_ON, %eax 相與結果0x1放入eax movl %eax, %cr0 cr0值為0x1
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
設定數據段選擇符到ax寄存器,下面將寄存器初始化 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. The stack region is from 0--start(0x7c00) movl $0x0, %ebp
設置棧指針,並且調用c函數 movl $start, %esp call bootmain # If bootmain returns (it shouldn't), loop.如果bootmain返回的話,就一直循環
spin:
jmp spin
Bootload的啟動過程可以概括如下:
首先,BIOS將第一塊扇區(存着bootloader)讀到內存中物理地址為0x7c00的位置,同時段寄存器CS值為0x0000,IP值為0x7c00,之后開始執行bootloader程序。CLI屏蔽中斷;CLD使DF復位,即DF=0,通過執行cld指令可以控制方向標志DF,決定內存地址是增大(DF=0,向高地址增加)還是減小(DF=1,向地地址減小)。設置寄存器 ax,ds,es,ss寄存器值為0;A20門被關閉,高於1MB的地址都默認回卷到0,所以要激活A20,給8042發命令激活A20,8042有兩個IO端口:0x60和0x64, 激活流程: 發送0xd1命令到0x64端口 --> 發送0xdf到0x60,打開A20門。從實模式轉換到保護模式(實模式將整個物理內存看成一塊區域,程序代碼和數據位於不同區域,操作系統和用戶程序並沒有區別對待,而且每一個指針都是指向實際的物理地址,地址就是IP值。這樣,用戶程序的一個指針如果指向了操作系統區域或其他用戶程序區域,並修改了內容,那么其后果就很可能是災難性的),所以就初始化全局描述符表使得虛擬地址和物理地址匹配可以相互轉換;lgdt匯編指令把通過gdt處理后的(asm.h頭文件中處理函數)描述符表的起始位置和大小存入gdtr寄存器中;將CR0的第0號位設置為1,進入保護模式;指令跳轉由代碼段跳到protcseg的起始位置。設置保護模式下數據段寄存器;設置堆棧寄存器並調用bootmain函數;
4
static void readsect(void *dst, uint32_t secno) { // wait for disk to be ready waitdisk(); //讀取扇區內容 outb(0x1F2, 1); // count = 1 outb使用內聯匯編實現 outb(0x1F3, secno & 0xFF); outb(0x1F4, (secno >> 8) & 0xFF); outb(0x1F5, (secno >> 16) & 0xFF); outb(0x1F6, ((secno >> 24) & 0xF) | 0xE0); outb(0x1F7, 0x20); // cmd 0x20 - read sectors // wait for disk to be ready waitdisk(); //將扇區內容加載到內存中虛擬地址dst // read a sector insl(0x1F0, dst, SECTSIZE / 4); //也用內聯匯編實現 }
static void readseg(uintptr_t va, uint32_t count, uint32_t offset) {//將扇區中數據放到內存中的段中 uintptr_t end_va = va + count; // round down to sector boundary va -= offset % SECTSIZE; // translate from bytes to sectors; kernel starts at sector 1 uint32_t secno = (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 (; va < end_va; va += SECTSIZE, secno ++) { readsect((void *)va, secno); } }
void bootmain(void) { // read the 1st page off disk readseg((uintptr_t)ELFHDR, SECTSIZE * 8, 0);//從硬盤讀取第一頁即ELF文件到內存(ELF文件讀到內存的起始位置,大小,ELF文件偏移)
// is this a valid ELF? if (ELFHDR->e_magic != ELF_MAGIC) {//判斷所得的ELF文件格式是否合法 goto bad; } struct proghdr *ph, *eph;//設定兩個程序頭表指針 // load each program segment (ignores ph flags) ph = (struct proghdr *)((uintptr_t)ELFHDR + ELFHDR->e_phoff);//程序頭表頭指針,ELF文件起始地址加上程序頭表的偏移量(記錄在ELF格式中) eph = ph + ELFHDR->e_phnum; //程序頭表尾指針,e_phnum為程序頭表中段個數(記錄在節區格式中) for (; ph < eph; ph ++) { readseg(ph->p_va & 0xFFFFFF, ph->p_memsz, ph->p_offset); //循環讀取ELF程序頭表中每個段到內存中 } // call the entry point from the ELF header // note: does not return ((void (*)(void))(ELFHDR->e_entry & 0xFFFFFF))(); //跳到內核程序入口地址,將cpu控制權交給ucore內核代碼 bad: outw(0x8A00, 0x8A00); outw(0x8A00, 0x8E00); /* do nothing */ while (1); }
程序頭表中段結構信息
struct proghdr { uint type; // 段類型 uint offset; // 段相對文件頭的偏移值 uint va; // 段的第一個字節將被放到內存中的虛擬地址 uint pa; uint filesz; uint memsz; // 段在內存映像中占用的字節數 uint flags; uint align; };
5.

代碼如下:
void print_stackframe(void) { uint32_t ebp = read_ebp(),eip = read_eip(); //使用內聯函數獲取兩寄存器值 int i,j; for(i = 0;ebp!=0 && i < STACKFRAME_DEPTH;++i){ cprintf("ebp:0x%08x eip:0x%08x args:", ebp, eip); uint32_t *args = (uint32_t*)ebp+2; //觀察原先函數的局部變量值 for(j = 0;j < 4;++j){ cprintf("0x%08x ", args[j]); } cprintf("\n"); print_debuginfo(eip-1); //輸出執行代碼的文件名,行號,函數名,當前eip值相對於函數起始地址偏移量 eip = ((uint32_t*)ebp)[1]; //調整eip到原先函數的call的下一條指令 ebp = ((uint32_t*)ebp)[0]; //ebp換成原先函數的ebp } }
調用C函數的時候,先將參數按照執行順序從后到前壓到棧里,然后壓入call語句的下一條指令的地址,然后將ebp的值壓入棧中,之后將esp的值賦給ebp,然后再調整eip的值為函數入口地址。

6.1
struct gatedesc { unsigned gd_off_15_0 : 16(設置該變量占多少位); // low 16 bits of offset in segment unsigned gd_ss : 16; // segment selector unsigned gd_args : 5; // # args, 0 for interrupt/trap gates unsigned gd_rsv1 : 3; // reserved(should be zero I guess) unsigned gd_type : 4; // type(STS_{TG,IG32,TG32}) unsigned gd_s : 1; // must be 0 (system) unsigned gd_dpl : 2; // descriptor(meaning new) privilege level unsigned gd_p : 1; // Present unsigned gd_off_31_16 : 16; // high bits of offset in segment };
16+16+5+3+...+16 = 64bit = 8byte
其中 gd_off_15_0 : 16 和gd_off_31_16 : 16表示低偏移量和高偏移量再加上gd_ss : 16;為段選擇子到GDT中找到段描述符可得基址,程序入口地址為基址+偏移量
6.2
.globl __alltraps .globl vector0 vector0: pushl $0 pushl $0 jmp __alltraps .globl vector1 vector1: pushl $0 pushl $1 jmp __alltraps .globl vector2 vector2: pushl $0 pushl $2 jmp __alltraps .globl vector3 vector3: pushl $0 pushl $3 jmp __alltraps .globl vector4 vector4: pushl $0 pushl $4 jmp __alltraps .globl vector5 vector5: pushl $0 pushl $5 jmp __alltraps .globl vector6 vector6: pushl $0 pushl $6 jmp __alltraps .globl vector7 vector7: pushl $0 pushl $7 jmp __alltraps
#define SETGATE(gate, istrap, sel, off, dpl) { \ (gate).gd_off_15_0 = (uint32_t)(off) & 0xffff; //0xffff為16位,32位與16位相與只能得到后16位賦值給低16位偏移量\ (gate).gd_ss = (sel); \ (gate).gd_args = 0; \ (gate).gd_rsv1 = 0; \ (gate).gd_type = (istrap) ? STS_TG32 : STS_IG32; \ (gate).gd_s = 0; \ (gate).gd_dpl = (dpl); \ (gate).gd_p = 1; \ (gate).gd_off_31_16 = (uint32_t)(off) >> 16; //32位右移16位剩下0000xxxx賦值給高16位偏移量\ }
void idt_init(void) { extern uintptr_t __vectors[];//引用vector.S文件中數組 int i; for(i = 0;i < sizeof(idt)/sizeof(struct gatedesc);++i){ SETGATE(idt[i],0,GD_KTEXT,__vectors[i],DPL_KERNEL);GD_KTEXT為kernel text,_vector[i]為偏移量,DPL只能為DPL_KERNEL,因為只能在內核下才能執行中斷處理程序 } SETGATE(idt[T_SWITCH_TOU],1,GD_KTEXT,__vectors[T_SWITCH_TOU],DPL_USER);//設定系統調用對應的中斷向量,DPL只能為DPL_USER,因為查找系統調用表時還在用戶態 lidt(&idt_pd);//將IDT位置賦值給IDTR }
時鍾中斷:
static void trap_dispatch(struct trapframe *tf) { char c; switch (tf->tf_trapno) { case IRQ_OFFSET + IRQ_TIMER: ticks++; //使用一個全局變量來記錄時鍾 if (ticks == TICK_NUM) {//TICK_NUM就是固定的100,每到100便調用print_ticks()函數 ticks -= TICK_NUM; print_ticks(); } break;
