日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

MIT JOS学习笔记01:环境配置、Boot Loader(2016.10.22)

發布時間:2024/4/14 编程问答 55 豆豆
生活随笔 收集整理的這篇文章主要介紹了 MIT JOS学习笔记01:环境配置、Boot Loader(2016.10.22) 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

未經許可謝絕以任何形式對本文內容進行轉載!

一、環境配置

  關于MIT課程中使用的JOS的配置教程網上已經有很多了,在這里就不做介紹,個人使用的是Ubuntu 16.04 + qemu。另注,本文章中貼出的代碼均是JOS中未經修改的源代碼,其中有一些細節是MIT課程中要求學生自己實現的。

二、Boot Loader代碼分析

  1.boot.S(AT&T匯編格式)

1 #include <inc/mmu.h> 2 3 # Start the CPU: switch to 32-bit protected mode, jump into C. 4 # The BIOS loads this code from the first sector of the hard disk into 5 # memory at physical address 0x7c00 and starts executing in real mode 6 # with %cs=0 %ip=7c00. 7 8 .set PROT_MODE_CSEG, 0x8 # kernel code segment selector 9 .set PROT_MODE_DSEG, 0x10 # kernel data segment selector 10 .set CR0_PE_ON, 0x1 # protected mode enable flag 11 12 .globl start 13 start: 14 .code16 # Assemble for 16-bit mode 15 cli # Disable interrupts 16 cld # String operations increment 17 18 # Set up the important data segment registers (DS, ES, SS). 19 xorw %ax,%ax # Segment number zero 20 movw %ax,%ds # -> Data Segment 21 movw %ax,%es # -> Extra Segment 22 movw %ax,%ss # -> Stack Segment 23 24 # Enable A20: 25 # For backwards compatibility with the earliest PCs, physical 26 # address line 20 is tied low, so that addresses higher than 27 # 1MB wrap around to zero by default. This code undoes this. 28 seta20.1: 29 inb $0x64,%al # Wait for not busy 30 testb $0x2,%al 31 jnz seta20.1 32 33 movb $0xd1,%al # 0xd1 -> port 0x64 34 outb %al,$0x64 35 36 seta20.2: 37 inb $0x64,%al # Wait for not busy 38 testb $0x2,%al 39 jnz seta20.2 40 41 movb $0xdf,%al # 0xdf -> port 0x60 42 outb %al,$0x60 43 44 # Switch from real to protected mode, using a bootstrap GDT 45 # and segment translation that makes virtual addresses 46 # identical to their physical addresses, so that the 47 # effective memory map does not change during the switch. 48 lgdt gdtdesc 49 movl %cr0, %eax 50 orl $CR0_PE_ON, %eax 51 movl %eax, %cr0 52 53 # Jump to next instruction, but in 32-bit code segment. 54 # Switches processor into 32-bit mode. 55 ljmp $PROT_MODE_CSEG, $protcseg 56 57 .code32 # Assemble for 32-bit mode 58 protcseg: 59 # Set up the protected-mode data segment registers 60 movw $PROT_MODE_DSEG, %ax # Our data segment selector 61 movw %ax, %ds # -> DS: Data Segment 62 movw %ax, %es # -> ES: Extra Segment 63 movw %ax, %fs # -> FS 64 movw %ax, %gs # -> GS 65 movw %ax, %ss # -> SS: Stack Segment 66 67 # Set up the stack pointer and call into C. 68 movl $start, %esp 69 call bootmain 70 71 # If bootmain returns (it shouldn't), loop. 72 spin: 73 jmp spin 74 75 # Bootstrap GDT 76 .p2align 2 # force 4 byte alignment 77 gdt: 78 SEG_NULL # null seg 79 SEG(STA_X|STA_R, 0x0, 0xffffffff) # code seg 80 SEG(STA_W, 0x0, 0xffffffff) # data seg 81 82 gdtdesc: 83 .word 0x17 # sizeof(gdt) - 1 84 .long gdt # address gdt boot.S

?  boot.S的代碼如上所示,這部分代碼的作用是將處理器從實模式切換到保護模式,然后再進行后續的加載內核程序的操作。為什么要讓處理器切換到保護模式下工作?這就要從PC物理內存的分布來考慮,以現在4GB的內存為例,PC物理內存的分布大致可以用以下的圖來表示:

在早期16bits的8088處理器上,地址總線為20位,可尋址空間為2^20,因此只能訪問最下方1MB的內存。之后隨著技術發展,Intel公司的處理器發展到了32bits尋址,為了兼容原來的軟硬件,保留了PC物理內存中最下方1MB內存的布局和使用方式,把這之上的內存高地址部分設置為擴展內存(盡管這部分內存有一部分預留給了32bits的設備,如上圖所示),而只有處理器工作在保護模式下才能訪問到這部分擴展內存(詳見其他關于保護模式的分析)。

  在boot.S的開頭,使用了.set匯編偽指令定義了Boot Loader代碼段和數據段的段選擇子(Segment Selector)和標志位CR0_PE_ON(這個標志位與切換到保護模式有關,具體會在后面介紹)。這之后,進行了一系列的初始化,包括關中斷、DF寄存器復位、用0初始化部分段寄存器等(還有一部分是通過in、out等匯編指令和端口交換字節信息,因為個人對硬件端口和工作原理并不熟悉,這部分的分析可能以后補上)。再之后就是關鍵的從實模式切換到保護模式的代碼:

1 lgdt gdtdesc 2 movl %cr0, %eax 3 orl $CR0_PE_ON, %eax 4 movl %eax, %cr0

其中第1行使用lgdt指令載入了事先定義好的GDT(全局描述符表,Global Descriptor Table),這張GDT的內容在boot.S的末尾:

1 # Bootstrap GDT 2 .p2align 2 # force 4 byte alignment 3 gdt: 4 SEG_NULL # null seg 5 SEG(STA_X|STA_R, 0x0, 0xffffffff) # code seg 6 SEG(STA_W, 0x0, 0xffffffff) # data seg 7 8 gdtdesc: 9 .word 0x17 # sizeof(gdt) - 1 10 .long gdt # address gdt

值得注意的是,lgdt指令需要的參數共6bytes,其中低位的2bytes表示該GDT的大小,高位的4bytes表示指向該GDT的32bits基址(gdtdesc所指向的參數滿足這一要求),使用lgdt指令的目的是在實模式切換到保護模式之前進行初始化。接著我們來分析GDT的結構(上述代碼的3-6行),其中SEG_NULL、SEG宏均是在“inc/mmu.h”中定義的宏:

1 #define SEG_NULL \ 2 .word 0, 0; \ 3 .byte 0, 0, 0, 0 4 #define SEG(type,base,lim) \ 5 .word (((lim) >> 12) & 0xffff), ((base) & 0xffff); \ 6 .byte (((base) >> 16) & 0xff), (0x90 | (type)), \ 7 (0xC0 | (((lim) >> 28) & 0xf)), (((base) >> 24) & 0xff)

從宏定義來看,SEG_NULL定義了一個空段(根據習慣,GDT的第一個段都是空段),接著

1 SEG(STA_X|STA_R, 0x0, 0xffffffff) # code seg

定義了可執行的(STA_X)、可讀的(STA_R)、基址為0x0且大小為0xffffffff(即占整個PC內存、大小為4GB的)的代碼段,而

1 SEG(STA_W, 0x0, 0xffffffff) # data seg

定義了可讀的(STA_W,取這個值時該段不可執行)、基址為0x0且大小為0xffffffff(同上)的數據段,以上就是boot.S中預定義的GDT的內容。

  然后我們再回到從模式切換部分的匯編代碼的第2-4行(值得注意的是,AT&T匯編和Intel匯編的源操作數和目標操作數的順序不同),這3行代碼利用eax寄存器,讓原本的控制寄存器CR0和標志位CR0_PE_ON進行or運算(實質上是將CR0寄存器的第0位置1),將處理器由實模式切換到了保護模式。為什么這3行代碼能夠做到模式的切換?這就需要了解CR0各位表示的含義:

CR0各位含義
比特位簡寫全稱描述
0PEProtected Mode Enable保護模式使能,PE=1表示CPU處于保護模式,PE=0表示CPU處于實模式
1MPMonitor co-processor協處理器監控,MP=1表示協處理器在工作,MP=0表示協處理器未工作
2EMEmulation協處理器仿真,當MP=0且EM=1表示正在使用軟件仿真協處理器工作
3TSTask switched任務轉換,每次任務轉換時,TS=1表示任務轉換完畢
4ETExtension type處理器擴展類型,表示所擴展的協處理器的類型,ET=0表示80287,ET=1表示80387
5NENumeric error數值異常中斷控制,如果運行協處理器指令發生故障,NE=1表示使用異常中斷處理,NE=0表示用外部中斷處理
16WPWrite protect寫保護,WP=1表示對只讀頁面進行寫操作時會產生頁故障
18AMAlignment mask對齊標志,AM=1表示允許對齊檢查,AM=0表示不允許對其檢查
29NWNot-write through和CD一起控制CPU內部Cache,NW=0且CD=0表示Cache使能,其他組合參見Intel手冊
30CDCache disable同上
31PGPaging頁式管理機制使能,PG=1表示頁式管理機制工作,PG=0表示不工作

可以看出,CPU的工作模式是依靠CR0的PE位控制的,因此要想切換到保護模式,只需要把CR0的PE位置為1。

  在CPU完成從實模式到保護模式的切換之后,boot.S使用一條ljmp指令跳轉到模式切換后的第一條指令地址:

1 # Jump to next instruction, but in 32-bit code segment. 2 # Switches processor into 32-bit mode. 3 ljmp $PROT_MODE_CSEG, $protcseg

還記得boot.S開頭的兩個段選擇子嗎?這里的PROT_MODE_CSEG就是其中的一個標記了代碼段的段選擇子,在解釋為什么PROT_MODE_CSEG要定義為0x8之前,我們有必要先了解一下實模式和保護模式下尋址方式的不同。

  在實模式下,要想尋址某個內存單元,需要知道所在段的基地址base和它在段中的偏移量offset,由公式:base<<4 + offset得到。而在保護模式下,尋址不再需要段的基地址,而是換成了段選擇子。段選擇子的結構如下:

其中RPL(第0、1位)表示特權請求級,TI(第2位)表示描述符表標識符(用于區分GDT和LDT),Index(第3-15位)表示描述符在描述符表中的索引(從0計起)。現在我們再來考慮為什么PROT_MODE_CSEG定義為0x8。我們把PROT_MODE_CSEG用二進制的段選擇子的結構表示為:0000 0000?0000?1000,根據段選擇子的結構可以得到:

RPL = 00(0),TI = 0,Index =?0000 0000?0000?1(1)

這說明該段選擇子訪問RPL為0的、GDT中的第1個段。那這個段究竟是什么?我們回顧一下之前在boot.S中預定義的GDT:

1 gdt: 2 SEG_NULL # null seg 3 SEG(STA_X|STA_R, 0x0, 0xffffffff) # code seg 4 SEG(STA_W, 0x0, 0xffffffff) # data seg

其中第1個段就是代碼段(注意是從0開始計數),另一個段選擇子PROT_MODE_DSEG的分析同上。到這里上述ljmp指令的功能就很清楚了:跳轉到由PROT_MODE_CSEG段選擇子和protcseg偏移量指定的代碼入口處。而跳轉到的代碼所完成的工作是:通過.code32偽指令編碼的對各段寄存器的初始化,隨后利用start標號指向的地址作為esp調用main.c中的bootmain()函數(注意bootmain()函數不需要參數,因此也就沒有參數的壓棧操作)。

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

  到此boot.S的功能結束。

  2.main.c

1 #include <inc/x86.h> 2 #include <inc/elf.h> 3 4 /********************************************************************** 5 * This a dirt simple boot loader, whose sole job is to boot 6 * an ELF kernel image from the first IDE hard disk. 7 * 8 * DISK LAYOUT 9 * * This program(boot.S and main.c) is the bootloader. It should 10 * be stored in the first sector of the disk. 11 * 12 * * The 2nd sector onward holds the kernel image. 13 * 14 * * The kernel image must be in ELF format. 15 * 16 * BOOT UP STEPS 17 * * when the CPU boots it loads the BIOS into memory and executes it 18 * 19 * * the BIOS intializes devices, sets of the interrupt routines, and 20 * reads the first sector of the boot device(e.g., hard-drive) 21 * into memory and jumps to it. 22 * 23 * * Assuming this boot loader is stored in the first sector of the 24 * hard-drive, this code takes over... 25 * 26 * * control starts in boot.S -- which sets up protected mode, 27 * and a stack so C code then run, then calls bootmain() 28 * 29 * * bootmain() in this file takes over, reads in the kernel and jumps to it. 30 **********************************************************************/ 31 32 #define SECTSIZE 512 33 #define ELFHDR ((struct Elf *) 0x10000) // scratch space 34 35 void readsect(void*, uint32_t); 36 void readseg(uint32_t, uint32_t, uint32_t); 37 38 void 39 bootmain(void) 40 { 41 struct Proghdr *ph, *eph; 42 43 // read 1st page off disk 44 readseg((uint32_t) ELFHDR, SECTSIZE*8, 0); 45 46 // is this a valid ELF? 47 if (ELFHDR->e_magic != ELF_MAGIC) 48 goto bad; 49 50 // load each program segment (ignores ph flags) 51 ph = (struct Proghdr *) ((uint8_t *) ELFHDR + ELFHDR->e_phoff); 52 eph = ph + ELFHDR->e_phnum; 53 for (; ph < eph; ph++) 54 // p_pa is the load address of this segment (as well 55 // as the physical address) 56 readseg(ph->p_pa, ph->p_memsz, ph->p_offset); 57 58 // call the entry point from the ELF header 59 // note: does not return! 60 ((void (*)(void)) (ELFHDR->e_entry))(); 61 62 bad: 63 outw(0x8A00, 0x8A00); 64 outw(0x8A00, 0x8E00); 65 while (1) 66 /* do nothing */; 67 } 68 69 // Read 'count' bytes at 'offset' from kernel into physical address 'pa'. 70 // Might copy more than asked 71 void 72 readseg(uint32_t pa, uint32_t count, uint32_t offset) 73 { 74 uint32_t end_pa; 75 76 end_pa = pa + count; 77 78 // round down to sector boundary 79 pa &= ~(SECTSIZE - 1); 80 81 // translate from bytes to sectors, and kernel starts at sector 1 82 offset = (offset / SECTSIZE) + 1; 83 84 // If this is too slow, we could read lots of sectors at a time. 85 // We'd write more to memory than asked, but it doesn't matter -- 86 // we load in increasing order. 87 while (pa < end_pa) { 88 // Since we haven't enabled paging yet and we're using 89 // an identity segment mapping (see boot.S), we can 90 // use physical addresses directly. This won't be the 91 // case once JOS enables the MMU. 92 readsect((uint8_t*) pa, offset); 93 pa += SECTSIZE; 94 offset++; 95 } 96 } 97 98 void 99 waitdisk(void) 100 { 101 // wait for disk reaady 102 while ((inb(0x1F7) & 0xC0) != 0x40) 103 /* do nothing */; 104 } 105 106 void 107 readsect(void *dst, uint32_t offset) 108 { 109 // wait for disk to be ready 110 waitdisk(); 111 112 outb(0x1F2, 1); // count = 1 113 outb(0x1F3, offset); 114 outb(0x1F4, offset >> 8); 115 outb(0x1F5, offset >> 16); 116 outb(0x1F6, (offset >> 24) | 0xE0); 117 outb(0x1F7, 0x20); // cmd 0x20 - read sectors 118 119 // wait for disk to be ready 120 waitdisk(); 121 122 // read a sector 123 insl(0x1F0, dst, SECTSIZE/4); 124 } main.c

  在分析main.c的功能之前,先介紹3種將要使用到的結構體:Elf(Executable and Linkable Format)、Proghdr(Program Header)、Secthdr(Section Header),這三種結構體在“inc/elf.h”中定義如下(詳見:https://en.wikipedia.org/wiki/Executable_and_Linkable_Format):

1 #define ELF_MAGIC 0x464C457FU /* "\x7FELF" in little endian */ 2 3 struct Elf { 4 uint32_t e_magic; //此處必須與ELF_MAGIC相等,否則不是有效Elf文件 5 uint8_t e_elf[12]; // 6 uint16_t e_type; // 7 uint16_t e_machine; //標明支持的指令集結構 8 uint32_t e_version; // 9 uint32_t e_entry; //kernel進程開始執行的入口地址 10 uint32_t e_phoff; //Program Header表的偏移量 11 uint32_t e_shoff; //Section Header表的偏移量 12 uint32_t e_flags; // 13 uint16_t e_ehsize; //Elf文件頭的大小 14 uint16_t e_phentsize; // 15 uint16_t e_phnum; //Program Header表中的條目數量 16 uint16_t e_shentsize; // 17 uint16_t e_shnum; //Section Header表中的條目數量 18 uint16_t e_shstrndx; // 19 }; 20 21 struct Proghdr { 22 uint32_t p_type; //標明該段的類型 23 uint32_t p_offset; //該段在文件鏡像中的偏移量 24 uint32_t p_va; //該段在內存中的虛擬地址 25 uint32_t p_pa; //在使用相對物理地址的系統中,表示該段在內存中的物理地址 26 uint32_t p_filesz; //該段在文件鏡像中的大小(以bytes計,可以為0) 27 uint32_t p_memsz; //該段在內存中的大小(以bytes計,可以為0) 28 uint32_t p_flags; // 29 uint32_t p_align; // 30 }; 31 32 struct Secthdr { 33 uint32_t sh_name; // 34 uint32_t sh_type; // 35 uint32_t sh_flags; // 36 uint32_t sh_addr; //該節在內存中的虛擬地址(對已經加載的扇區而言) 37 uint32_t sh_offset; //該節在文件鏡像中的偏移量 38 uint32_t sh_size; //該節在文件鏡像中的大小(以bytes計,可以為0) 39 uint32_t sh_link; // 40 uint32_t sh_info; // 41 uint32_t sh_addralign; // 42 uint32_t sh_entsize; // 43 };

?  接下來我們從函數的角度來分析main.c都做了什么工作。在main.c的開頭處我們看到兩行函數頭,分別是readsect(void *, uint32_t)和readseg(uint32_t, uint32_t, uint32_t),緊接著是在boot.S中被調用的bootmain()函數體。我們依次對這三個函數進行觀察:

  首先是readsect(void *dst, uint32_t offset)。該函數中的函數通過__asm __volatile()間接使用了in和out匯編指令(詳見“inc/x86.h”)對指定編號(在函數中,編號參數被定義為offset)的磁盤扇區進行讀操作,并把讀到的信息寫到dst指向的內存中。

  其次是readseg(uint32_t pa, uint32_t count, uint32_t offset)。從接口上看,該函數是向pa所指向的地址中讀入count個bytes的、在磁盤上偏移量為offset的數據。而在函數的實現中,該函數對數據的讀取是借助readsect(void *dst, uint32_t offset),以扇區為單位進行的,即通過offset計算所在的磁盤扇區,然后再將整個扇區讀入,最終達到讀入所有數據的目的。

  最后我們對bootmain()函數進行觀察:

  在bootmain()函數的開頭,先通過

1 readseg((uint32_t) ELFHDR, SECTSIZE*8, 0);

從磁盤偏移量為0的地方向ELFHDR這個指針指向的地址(在main.c中該地址由宏定義為0x10000)讀入1個頁(512 * 8 = 4096 bytes,即8個扇區)大小的數據。隨后對讀入的kernel是否為Elf格式進行了校驗。若校驗不通過,向特定端口輸出特定字信息后進入死循環(bad標號指向的代碼,此處也通過__asm __volatile()間接使用了out匯編指令)。若通過文件格式校驗,則通過ph和eph兩個Proghdr *類型的指針加載該Elf文件中的所有程序段(其中ph指向Program Header表的表頭,eph指向Program Header表的表尾,每一次指針的自加操作都是指向當前表中的下一條目,表中條目的數量都保存在ELFHDR->e_phnum中),更具體地說,對于表中的每一項ph(即每個程序段),通過

1 readseg(ph->p_pa, ph->p_memsz, ph->p_offset);

將ph所對應的偏移量為ph->offset的、大小為ph->memsz的程序段從磁盤中都入到ph->p_pa指向的物理內存中。個人認為,上述部分代碼實質上是將整個kernel程序從磁盤部署到內存中等待執行。而最后一行

1 ((void (*)(void)) (ELFHDR->e_entry))();

顯然是通過函數指針進行函數調用,這里的ELFHDR->e_entry指向的地址就是上面一開始所介紹的kernel進程的第一條指令的地址,也就是說,這條C語句執行過后,Boot Loader就將執行權交給了kernel,因為理論上kernel并不返回,所以這是Boot Loader所執行的最后一條語句(如果kernel返回說明操作系統出現嚴重錯誤,跳轉到bad標號指向的代碼執行)。

  到此main.c的功能結束。到此整個Boot Loader的功能結束。

轉載于:https://www.cnblogs.com/LostChristmas/p/5987381.html

總結

以上是生活随笔為你收集整理的MIT JOS学习笔记01:环境配置、Boot Loader(2016.10.22)的全部內容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。