做者:chuyaoxinhtml
BIOS將經過讀取硬盤主引導扇區到內存,並轉跳到對應內存中的位置執行bootloader。請分析bootloader是如何完成從實模式進入保護模式的。git
提示:須要閱讀小節「保護模式和分段機制」和lab1/boot/bootasm.S源碼,瞭解如何從實模式切換到保護模式,須要瞭解:github
沒有學過彙編的我剛看到源碼時,有點懵逼,因而,我首先查了很多關於彙編的小資料。express
Ucore中用到的是AT&T格式的彙編編程
在 AT&T 彙編格式中bootstrap
寄存器名要加上 '%' 做爲前綴;數組
用 '$' 前綴表示一個當即操做數; 安全
將symbol的值設爲expressionide
屏蔽系統中斷函數
因爲代碼段在實模式下運行,因此要告訴編譯器使用16位的模式編譯
在x86彙編代碼中,標號有惟一的名字加冒號組成。它能夠出如今彙編程序的任何地方,並與緊跟其後的哪行代碼具備相同的地址。
歸納的說 ,當程序中要跳轉到另外一位置時,須要有一個標識來指示新的位置,這就是標號,經過在目標地址的前面放上一個標號,能夠在指令中使用標號來代替直接使用地址。
目標操做數在源操做數的右邊;
操做數的字長由操做符的最後一個字母決定,後綴'b'、'w'、'l'分別表示操做數爲字節(byte,8 比特)、字(word,16 比特)和長字(long,32比特);
一、關閉中斷
二、A20 使能
三、全局描述符表初始化
四、保護模式啓動
五、設置段寄存器(長跳轉更新CS,根據設置好的段選擇子更新其餘段寄存器)
六、設置堆棧,esp 0x700 ebp 0
七、進入bootmain後讀取內核映像到內存,檢查是否合法,並啓動操做系統,控制權交給它
CPU復位(reset)或加電(power on)的時候以實模式啓動,處理器以實模式工做。在實模式下,內存尋址方式和8086相同,由16位段寄存器的內容乘以16(10H)當作段基地址,加上16位偏移地址造成20位的物理地址,最大尋址空間1MB,最大分段64KB。可使用32位指令。32位的x86 CPU用作高速的8086。在實模式下,全部的段都是能夠讀、寫和可執行的。
實模式將整個物理內存當作分段的區域,程序代碼和數據位於不一樣區域,操做系統和用戶程序並無區別對待,並且每個指針都是指向實際的物理地址。這樣,用戶程序的一個指針若是指向了操做系統區域或其餘用戶程序區域,並修改了內容,那麼其後果就極可能是災難性的。經過修改A20地址線能夠完成從實模式到保護模式的轉換。
實模式下,程序地址爲真實的物理地址,能夠訪問任意地址空間,這樣不一樣進程可能訪問到其它進程程序,形成嚴重錯誤。而保護模式下,程序地址爲虛擬地址,而後由OS系統管理內存訪問權限,這樣每一個進程只能訪問分配給本身的物理
內存空間,保證了程序的安全性。例如Linux系統地址訪問採用分頁機制,在加載程序時,由OS分配的進程能夠訪問的物理頁空間,並設置了頁目錄項和頁表項,才能保證程序正常運行。這樣程序運行時地址間接地由OS進行管理,防止進程之間互相影響,所有由OS穩定性保證。
CR0是控制寄存器,其中包含了6個預約義標誌,0位是保護容許位PE(Protedted Enable),用於啓動保護模式。若是PE位置1,則保護模式啓動,若是PE=0,則在實模式下運行。
關於CR0及其餘控制寄存器的詳細內容能夠參考如下連接:https://blog.csdn.net/wyt4455/article/details/8691500
#include <asm.h> # Start the CPU: switch to 32-bit protected mode, jump into C. # The BIOS loads this code from the first sector of the hard disk into # memory at physical address 0x7c00 and starts executing in real mode # with %cs=0 %ip=7c00. .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 # start address should be 0:7c00, in real mode, the beginning address of the running bootloader .globl start start: .code16 # Assemble for 16-bit mode cli # Disable interrupts cld # String operations increment # Set up the important data segment registers (DS, ES, SS). xorw %ax, %ax # Segment number zero movw %ax, %ds # -> Data Segment movw %ax, %es # -> Extra Segment movw %ax, %ss # -> Stack Segment # Enable A20: # For backwards compatibility with the earliest PCs, physical # address line 20 is tied low, so that addresses higher than # 1MB wrap around to zero by default. This code undoes this. seta20.1: inb $0x64, %al # Wait for not busy(8042 input buffer empty). testb $0x2, %al jnz seta20.1 movb $0xd1, %al # 0xd1 -> port 0x64 outb %al, $0x64 # 0xd1 means: write data to 8042's P2 port 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 # Switch from real to protected mode, using a bootstrap GDT # and segment translation that makes virtual addresses # identical to physical addresses, so that the # effective memory map does not change during the switch. lgdt gdtdesc movl %cr0, %eax orl $CR0_PE_ON, %eax movl %eax, %cr0 # 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. The stack region is from 0--start(0x7c00) movl $0x0, %ebp movl $start, %esp call bootmain # If bootmain returns (it shouldn't), loop. spin: jmp spin # Bootstrap GDT .p2align 2 # force 4 byte alignment 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: .word 0x17 # sizeof(gdt) - 1 .long gdt # address gdt
#註釋:
#include <asm.h> asm.h頭文件中包含了一些宏定義,用於定義gdt,gdt是保護模式使用的全局段描述符表,其中存儲着段描述符。 3 # Start the CPU: switch to 32-bit protected mode, jump into C. # The BIOS loads this code from the first sector of the hard disk into # memory at physical address 0x7c00 and starts executing in real mode # with %cs=0 %ip=7c00. 此段註釋說明了要完成的目的:啓動保護模式,轉入C函數。 這裏正好說了一下bootasm.S文件的做用。計算機加電後,由BIOS將bootasm.S生成的可執行代碼從硬盤的第一個扇區複製到內存中的物理地址0x7c00處,並開始執行。 此時系統處於實模式。可用內存很少於1M。 .set PROT_MODE_CSEG, 0x8 # kernel code segment selector .set PROT_MODE_DSEG, 0x10 # kernel data segment selector 這兩個段選擇子的做用實際上是提供了gdt中代碼段和數據段的索引 .set CR0_PE_ON, 0x1 # protected mode enable flag 這個變量是開啓A20地址線的標誌,爲1是開啓保護模式 # start address should be 0:7c00, in real mode, the beginning address of the running bootloader .globl start start: 這兩行代碼至關於定義了C語言中的main函數,start就至關於main,BIOS調用程序時,從這裏開始執行 .code16 # Assemble for 16-bit mode 由於如下代碼是在實模式下執行,因此要告訴編譯器使用16位模式編譯。 cli # Disable interrupts cld # String operations increment 關中斷,設置字符串操做是遞增方向。cld的做用是將direct flag標誌位清零,
這意味着自動增長源索引和目標索引的指令(如MOVS)將同時增長它們。 # Set up the important data segment registers (DS, ES, SS). xorw %ax, %ax # Segment number zero ax寄存器就是eax寄存器的低十六位,使用xorw清零ax,效果至關於movw $0, %ax。 可是好像xorw性能好一些,google了一下沒有獲得好答案 movw %ax, %ds # -> Data Segment movw %ax, %es # -> Extra Segment movw %ax, %ss # -> Stack Segment 將段選擇子清零 # Enable A20: # For backwards compatibility with the earliest PCs, physical # address line 20 is tied low, so that addresses higher than # 1MB wrap around to zero by default. This code undoes this. 準備工做就緒,下面開始動真格的了,激活A20地址位。先翻譯註釋:因爲須要兼容早期pc,物理地址的第20位綁定爲0,因此高於1MB的地址又回到了0x00000. 好了,激活A20後,就能夠訪問全部4G內存了,就可使用保護模式了。 怎麼激活呢,因爲歷史緣由A20地址位由鍵盤控制器芯片8042管理。因此要給8042發命令激活A20 8042有兩個IO端口:0x60和0x64, 激活流程位: 發送0xd1命令到0x64端口 --> 發送0xdf到0x60,done! seta20.1: inb $0x64, %al # Wait for not busy(8042 input buffer empty). testb $0x2, %al jnz seta20.1 #發送命令以前,要等待鍵盤輸入緩衝區爲空,這經過8042的狀態寄存器的第2bit來觀察,而狀態寄存器的值能夠讀0x64端口獲得。 #上面的指令的意思就是,若是狀態寄存器的第2位爲1,就跳到seta20.1符號處執行,知道第2位爲0,表明緩衝區爲空 movb $0xd1, %al # 0xd1 -> port 0x64 outb %al, $0x64 # 0xd1 means: write data to 8042's P2 port 發送0xd1到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 到此,A20激活完成。 # Switch from real to protected mode, using a bootstrap GDT # and segment translation that makes virtual addresses # identical to physical addresses, so that the # effective memory map does not change during the switch. 轉入保護模式,這裏須要指定一個臨時的GDT,來翻譯邏輯地址。這裏使用的GDT經過gdtdesc段定義。
它翻譯獲得的物理地址和虛擬地址相同,因此轉換過程當中內存映射不會改變 lgdt gdtdesc 載入gdt movl %cr0, %eax orl $CR0_PE_ON, %eax movl %eax, %cr0 打開保護模式標誌位,至關於按下了保護模式的開關。cr0寄存器的第0位就是這個開關,經過CR0_PE_ON或cr0寄存器,將第0位置1 # Jump to next instruction, but in 32-bit code segment. # Switches processor into 32-bit mode. ljmp $PROT_MODE_CSEG, $protcseg 因爲上面的代碼已經打開了保護模式了,因此這裏要使用邏輯地址,而不是以前實模式的地址了。 這裏用到了PROT_MODE_CSEG, 他的值是0x8。根據段選擇子的格式定義,0x8就翻譯成: INDEX TI CPL 0000 0000 1 00 0 INDEX表明GDT中的索引,TI表明使用GDTR中的GDT, CPL表明處於特權級。 PROT_MODE_CSEG選擇子選擇了GDT中的第1個段描述符。這裏使用的gdt就是變量gdt。
下面能夠看到gdt的第1個段描述符的基地址是0x0000,因此通過映射後和轉換前的內存映射的物理地址同樣。
.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. The stack region is from 0--start(0x7c00) movl $0x0, %ebp movl $start, %esp call bootmain 棧頂設定在start處,也就是地址0x7c00處,call函數將返回地址入棧,將控制權交給bootmain # If bootmain returns (it shouldn't), loop. spin: jmp spin # Bootstrap GDT .p2align 2 # force 4 byte alignment 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: .word 0x17 # sizeof(gdt) - 1 .long gdt # address gdt
// 用來定義段描述符的宏
#ifndef __BOOT_ASM_H__
#define __BOOT_ASM_H__
// assembler macros to create x86 segments
// 定義了一個空段描述符
#define SEG_NULLASM \
.word 0, 0; \
.byte 0, 0, 0, 0
// 以type,base,lim爲參數定義一個段描述符, 其中的0xC0=(1100)2, 其
// 中的第一個1對應於段描述符中的G位,置1表示段界限以4KB爲單位
// 第二個1對應於段描述符的D位,置1表示這是一個保護模式下的段描述符
// 具體的關於段描述符的格式定義在mmu.h中
// The 0xC0 means the limit is in 4096-byte units
// and (for executable segments) 32-bit mode.
#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)
// 可執行段
#define STA_X 0x8 // Executable segment
// 非可執行段
#define STA_E 0x4 // Expand down (non-executable segments)
// 只能執行的段
#define STA_C 0x4 // Conforming code segment (executable only)
// 可寫段可是不能執行的段
#define STA_W 0x2 // Writeable (non-executable segments)
// 可讀可執行的段
#define STA_R 0x2 // Readable (executable segments)
// 代表描述符是否已被訪問;把選擇字裝入段寄存器時,該位被標記爲1
#define STA_A 0x1 // Accessed
首先說明一點,這是一個歷史遺留問題。
1981年8月,IBM公司最初推出的我的計算機IBM PC使用的CPU是Inter 8088.在該微機中地址線只有20根。在當時內存RAM只有幾百KB或不到1MB時,20根地址線已經足夠用來尋址這些 內存。其所能尋址的最高地址是0xffff,
也就是0x10ffef。對於超出0x100000(1MB)的尋址地址將默認地環繞到0xffef。當IBM公司與1985年引入AT機時,使用的是Inter 80286 CPU,具備24根地址線,最高可尋址16MB,而且有一個與8088那樣實現地址尋址的環繞。
可是當時已經有一些程序是利用這種環繞機制進行工做的。爲了實現徹底的兼容性,IBM公司發明了使用一個開關來開啓或禁止0x100000地址比特位。因爲當時的8042鍵盤控制器上剛好有空閒的端口引腳(輸出端口P2,引腳P21),
因而便使用了該引腳來做爲與門控制這個地址比特位。該信號即被稱爲A20。若是它爲零,則比特20及以上地址都被清除。從而實現了兼容性。
當A20地址線控制禁止時,程序就像運行在8086上,1MB以上的地址是不可訪問的,只能訪問奇數MB的不連續的地址。爲了使能全部地址位的尋址能力,必須向鍵盤控制器8082發送一個命令,鍵盤控制器8042會將A20線置於高電位,使所有32條地址線可用,實現訪問4GB內存。
控制 A20 gate 的方法有 3 種:
1.804x 鍵盤控制器法
2.Fast A20 法
3.BIOS 中斷法
ucore實驗中用了第一種 804x 鍵盤控制器法,這也是最古老且效率最慢的一種。
因爲在機器啓動時,默認條件下,A20地址線是禁止的,因此操做系統必須使用適當的方法來開啓它。
打開A20 Gate的代碼爲:
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
第一步是向 804x 鍵盤控制器的 0x64 端口發送命令。這裏傳送的命令是 0xd1,這個命令的意思是要向鍵盤控制器的 P2 寫入數據。這就是 seta20.1 代碼段所作的工做。
第二步就是向鍵盤控制器的 P2 端口寫數據了。寫數據的方法是把數據經過鍵盤控制器的 0x60 端口寫進去。寫入的數據是 0xdf,由於 A20 gate 就包含在鍵盤控制器的 P2 端口中,隨着 0xdf 的寫入,A20 gate 就被打開了。
接下來要作的就是進入「保護模式」了。
它的GDT全稱是Global Descriptor Table,中文名稱叫「全局描述符表」,想要在「保護模式」下對內存進行尋址就先要有 GDT。GDT 表裏的每一項叫作「段描述符」,用來記錄每一個內存分段的一些屬性信息,每一個「段描述符」佔 8 字節。
在保護模式下,咱們經過設置GDT將內存空間被分割爲了一個又一個的段(這些段是能夠重疊的),這樣咱們就能實現不一樣的程序訪問不一樣的內存空間。這和實模式下的尋址方式是不一樣的, 在實模式下咱們只能使用address = segment << 4 | offset的方式進行尋址(雖然也是segment + offset的,但在實模式下咱們並不會真正的進行分段)。在這種狀況下,任何程序都能訪問整個1MB的空間。而在保護模式下,經過分段的方式,程序並不能訪問整個內存空間
爲了使分段存儲管理機制正常運行,須要創建好段描述符和段描述符表,全局描述符表是一個保存多個段描述符的「數組」,其起始地址保存在全局描述符表寄存器GDTR中。GDTR長48位,其中高32位爲基地址,低16位爲段界限。這裏只須要載入已經靜態存儲在引導區的GDT表和其描述符到GDTR寄存器:
lgdt gdtdesc
#CPU 單獨爲咱們準備了一個寄存器叫作 GDTR 用來保存咱們 GDT 在內存中的位置和咱們 GDT 的長度。
#GDTR 寄存器一共 48 位,其中高 32 位用來存儲咱們的 GDT 在內存中的位置,其他的低 16 位用來存咱們的 GDT 有多少個段描述符。
#16 位最大能夠表示 65536 個數,這裏咱們把單位換成字節,而一個段描述符是 8 字節,因此 GDT 最多能夠有 8192 個段描述符。
#CPU 不只用了一個單獨的寄存器 GDTR 來存儲咱們的 GDT,並且還專門提供了一個指令用來讓咱們把 GDT 的地址和長度傳給 GDTR 寄存器:lgdt gdtdesc
gdtdesc 和 gdt 一塊兒放在了 bootasm.S 文件的最底部
# Bootstrap GDT .p2align 2 # force 4 byte alignment 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: .word 0x17 # sizeof(gdt) - 1 () # 16 位的 gdt 大小sizeof(gdt) - 1
.long gdt # address gdt()# 32 位的 gdt 所在物理地址
48 位傳給了 GDTR 寄存器,到此 GDT 就準備好了
如同 A20 gate 這個開關負責打開 1MB 以上內存尋址同樣,想要進入「保護模式」咱們也須要打開一個開關,這個開關叫「控制寄存器」,x86 的控制寄存器一共有 4 個分別是 CR0、CR一、CR二、CR3(這四個寄存器都是 32 位的),而控制進入「保護模式」的開關在 CR0 上。
CR0中包含了6個預約義標誌,0位是保護容許位PE(Protedted Enable),用於啓動保護模式,若是PE位置1,則保護模式啓動,若是PE=0,則在實模式下運行。
CR0 上和保護模式有關的位,如圖所示:
打開保護模式的代碼爲:
movl %cr0, %eax
orl $CR0_PE_ON, %eax
movl %eax, %cr0
由於咱們沒法直接操做 CR0,因此咱們首先要用一個通用寄存器來保存當前 CR0 寄存器的值,這裏第一行就是用通用寄存器 eax 來保存 cr0 寄存器的值;
而後 CR0_PE 這個宏的定義在 mmu.h 文件中,是個數值 0x00000001,將這個數值與 eax 中的 cr0 寄存器的值作「或」運算後,就保證將 cr0 的第 0 位設置成了 1 即 PE = 1 保證打開了保護模式的開關。
而 cr0 的第 31 位 PG = 0 表示咱們只使用分段式,不使用分頁,這時再將新的計算後的 eax 寄存器中的值寫回到 cr0 寄存器中就完成了到保護模式的切換。
ljmp $PROT_MODE_CSEG, $protcseg
其中protcseg是一個標號(標號的用途在本文中的實驗相關部分已說明)
因爲已經使能了保護模式,因此這裏要使用邏輯地址,而不是以前實模式的地址了
這裏還要注意PROT_MODE_CSEG和PROT_MODE_DSEG,這二者分別定義爲0x8和0x10,表示代碼段和數據段的選擇子。
根據段選擇子的格式定義,0x8就翻譯成:
INDEX TI CPL
注意這裏創建堆棧,ebp寄存器按理來講是棧幀的,可是這裏並不須要把它設置爲0x7c00,由於這裏0x7c00是棧的最高地址,它上面沒有有效內容,而以後由於調用,ebp會被設置爲被調用的那個函數的棧的起始地址,這裏就不用管它了。
Bootload的啓動過程能夠歸納以下:
首先,BIOS將第一塊扇區(存着bootloader)讀到內存中物理地址爲0x7c00的位置,同時段寄存器CS值爲0x0000,IP值爲0x7c00,以後開始執行bootloader程序。CLI屏蔽中斷(屏蔽全部的中斷:爲中斷提供服務一般是操做系統設備驅動程序的責任,所以在bootloader的執行全過程當中能夠沒必要相應任何中斷,中斷屏蔽是經過寫CPU提供的中斷屏蔽寄存器來完成的);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函數;
彙編基本語法簡介
清華大學教學內核ucore學習系列(1) bootloader
ucore-lab1-練習3report
學習xv6從實模式到保護模式
ucore練習三