自己動手寫內(nèi)核(第2課:保護(hù)模式)(原創(chuàng))
Posted on 2007-05-08 07:35 天衣有縫 閱讀(2666) 評論(3) 編輯 收藏 引用 所屬分類: os stuff第2課:保護(hù)模式
聲明:轉(zhuǎn)載請保留:
譯者:http://www.shnenglu.com/jinglexy
原作者:xiaoming.mo at skelix dot org
MSN & Email: jinglexy at yahoo dot com dot cn目標(biāo) 下載源程序
如前文所述,系統(tǒng)上電時處理器處于實模式。事實上,它還有另外一種工作模式:保護(hù)模式。skelix從磁盤啟動后即進(jìn)入該模式。在本課中我們進(jìn)入保護(hù)模式并打印"Hello World!"。
保護(hù)模式的優(yōu)點
在實模式下,處理器不能簡單尋址1MB以外的物理地址(實際上用某些方法是可以的),這等內(nèi)存實在是太少了。所以i386系列處理器提供了保護(hù)模式:基于特權(quán)級的保護(hù)和訪問更大的內(nèi)存地址范圍。我們在這里講的是32位保護(hù)模式,16位保護(hù)模式不在討論之列。
保護(hù)模式最大的好處就是可以直接范圍最大4GB的地址空間,但是經(jīng)過多年的更新?lián)Q代,我們的機(jī)器還沒有達(dá)到4GB內(nèi)存,于是引入了虛擬內(nèi)存的概念,它可以使用硬盤存儲空間作為內(nèi)存使用。保護(hù)模式下對內(nèi)存訪問進(jìn)行保護(hù),它阻止用戶程序?qū)?nèi)核代碼或數(shù)據(jù)的訪問,應(yīng)用程序的crash也不會影響到整個系統(tǒng)。單個進(jìn)程可以訪問自己獨有的4GB虛擬地址空間,而不是混亂在整個內(nèi)存里面使用,它是通過地址映射來實現(xiàn)的,即邏輯地址轉(zhuǎn)換成虛擬地址的過程。更詳細(xì)的內(nèi)容可以參考Intel的文檔。
概述運(yùn)行原理
好了,讓我們結(jié)束無聊的理論知識吧,本課的目的是使我們的程序進(jìn)入到保護(hù)模式。在保護(hù)模式中,我們?nèi)匀皇褂枚危ㄊ聦嵣希覀儫o法在處理器上禁用段特性),每個段可以訪問單獨的4GB地址空間。段轉(zhuǎn)載在寄存器中,它表示一個描述符選擇子,和實模式一樣使用cs,ds等16位寄存器。這樣說吧:一個內(nèi)存段描述符寄存器 CS = 0x8,我們可以直接訪問0到
我上面提到段是用選擇子來表示的,這個說法可能不是很準(zhǔn)確,實際上選擇子是段描述符表的索引。這個描述符表是系統(tǒng)所有可以使用的段的地址和范圍表的入口,一個描述符包括段起始地址,長度,類型(數(shù)據(jù)/代碼/門),特權(quán)級等。為了范圍到特定的內(nèi)存地址,段選擇子和偏移地址表示為如下形式:selector:offset,和實模式一樣。例如,我們讓 0x08選擇子指向B8000(視頻內(nèi)存區(qū)域) 開始的內(nèi)存范圍,這樣我們可以使用8:00000000來范圍視頻內(nèi)存區(qū)域的第一個字節(jié)。在系統(tǒng)中存在以下幾種描述符表:GDT(全局描述符表),LDT(局部描述符表),IDT(中斷描述符表)。當(dāng)進(jìn)入到保護(hù)模式后,所有的內(nèi)存范圍都通過GDT或LDT。
在本課中我們使用GDT,正如它的名字“全局”,GDT可以被所有任務(wù)共享。現(xiàn)在我們來使用一個代碼段和一個數(shù)據(jù)段。
下面是代碼段/數(shù)據(jù)段描述符的格式,一個描述符是8字節(jié)長(64位):
63_______________56__55__54__53__52__51_____________48_
| 基地址(31到24位) | G |D/B| X | U | 長度(19到16位) |
|_______________________________________________________|
_47__46__45__44____41______40____39_________________32_
| P | DPL | 類型
| A | 基地址(23到16位) |
|_______________________________________________________|
31____________________________________________________16
|
基地址(15到0位)
|
|_______________________________________________________|
16_____________________________________________________
|
長度(15到0位)
|
|_______________________________________________________|
解釋一下:為什么長度只有20位呢,這是因為粒度一般設(shè)置位4K,所以可以表示0到4GB大小的長度范圍。
表-域說明
長度(位 15-0) |
長度的低16位 |
||||||||
基地址(位 15-0) |
基地址的低16位 |
||||||||
基地址(位 23-16) |
基地址的中16位 |
||||||||
A |
是否已訪問 |
||||||||
類型 |
|
||||||||
DPL |
特權(quán)級:我們只使用兩個,內(nèi)核0級和用戶3級 |
||||||||
P |
存在位,為1表示在內(nèi)存中。一般在虛擬內(nèi)存管理中會使用到這個位。 |
||||||||
長度(位 19-16) |
長度的低8位 |
||||||||
U |
用戶定義位 |
||||||||
X |
恒為0 |
||||||||
D |
32位代碼段還是16位代碼段 |
||||||||
G |
段長度的粒度:4k大小或1字節(jié) |
||||||||
基地址(位 31-24) |
基地址的高16位 |
我們從上面看到,一個描述符保護(hù)32位基地址和20位段長界限等屬性。32位基地址表示32位物理地址,是一個段的開始地址,20位長度界限表示這個段的長度。讀者可能注意到2^20只能表示1M大小范圍。為了訪問4GB地址范圍,描述符中使用了G位來表示粒度。當(dāng)G位為1時,粒度為4K,這是可以訪問的范圍是1M * 4K,即4GB大小;如果G為為0,粒度為1字節(jié),可以訪問的范圍是1M字節(jié)大小。
特權(quán)級保護(hù)是保護(hù)模式的重要概念,為了解釋這個,我們來看一下描述符選擇子。上面已經(jīng)提到了,選擇子是描述符表的一個索引:
15______________________________3___2____1___0__
|
Index |
TI | RPL
|
|_______________________________________________|
RPL |
請求特權(quán)級:requester privilege level |
TI |
使用 GDT(=0) 或者 LDT(=1) |
Index |
描述符表索引值 |
應(yīng)用程序特權(quán)級(PL)和 cs寄存器中的PL(即RPL)是類似的。程序在低的特權(quán)級(即PL值更高)不能訪問高特權(quán)級的數(shù)據(jù)段或執(zhí)行高特權(quán)級的代碼段。當(dāng)選擇子載入到寄存器中時,處理器會檢查CPL和RPL,根據(jù)這兩個PL得到一個EPL(恕我直言,作者增加了一個新的概念并不明智),然后比較EPL和描述符中的DPL。當(dāng)EPL的特權(quán)級更高時,才能正確訪問目標(biāo)段。注意,這里只是大致遵循該法則,處理器還要檢測讀寫屬性,存在位等。正如上面圖所描述,選擇子Index是13位的,所以最多可以索引2^13個描述符,即8096個。這只是在GDT中最多索引的描述符個數(shù),另外每個進(jìn)程都可以有自己的LDT。處理器會保留第一個GDT中的描述符,它應(yīng)當(dāng)被清0,不應(yīng)當(dāng)用作訪問內(nèi)存使用。
進(jìn)入保護(hù)模式
在上一課中,我們從軟盤啟動skelix。現(xiàn)在我們可以執(zhí)行到實模式代碼,并進(jìn)入保護(hù)模式了,一些模式切換的代碼必不可少,并且不準(zhǔn)備讓skelix在返回到那黑暗時代-實模式了。在進(jìn)入保護(hù)模式之前,需要做一些準(zhǔn)備工作,我們先創(chuàng)建GDT:
02/bootsect.s
gdt:
.quad
0x0000000000000000 # 空描述符
.quad
0x00cf9a000000ffff # cs
.quad
0x00cf92000000ffff # ds
.quad
0x0000000000000000 # 用作將來的段描述符
.quad
0x0000000000000000 # 用作將來的段描述符
可以看到,我們在上面定義了5個GDT描述符,但暫時只用到了第2個和第3個。第一個dummy描述符是Intel規(guī)定的,第2個是cs段(代碼段)描述符,下面我們仔細(xì)分析一下這個8字節(jié)值:(紅色表示cs描述符的值域)
Bits 15-0 |
FFFFh |
長度界限低16位 |
Bits 39-16 |
000000h |
段基地址低24位 |
Bit 40 |
0b |
訪問位:設(shè)置為0 |
Bit 41 |
1b |
讀/寫,或讀/執(zhí)行(值表示可讀可執(zhí)行代碼) |
Bit 42 |
0b |
棧還是數(shù)據(jù)段,普通代碼段還是一致代碼段 |
Bit 43 |
1b |
代碼段還是數(shù)據(jù)段 |
Bit 44 |
1b |
代碼數(shù)據(jù)段,還是門描述符 |
Bits 45,46 |
00b |
內(nèi)核特權(quán)級 |
Bit 47 |
1b |
存在位 |
Bits 48-51 |
Fh |
長度界限高4位 |
Bits 52 |
0b |
軟件可用位,設(shè)置為0 |
Bits 53 |
0b |
設(shè)置為恒0 |
Bits 54 |
1b |
32位段還是16位段 |
Bits 55 |
1b |
粒度為4k還是1字節(jié) |
Bits 63-56 |
00h |
段基地址高8位 |
根據(jù)上面的解釋,這個段描述符描述的段從00000000地址開始,界限是FFFFF*4K,即4G的32位代碼段。第3個描述符用于數(shù)據(jù)段或堆棧段,區(qū)別在于第43位,設(shè)置為0表示數(shù)據(jù)段。
好了,還是讓程序的使用來說明一切吧。處理器有幾個專門的寄存器用于保護(hù)模式,GDTR寄存器使用LGDT來加載,GDTR是48位寄存器,低16位表示GDT的長度,高32位表示GDT的基地址。
02/bootsect.s
gdt_48:
.word .-gdt-1 當(dāng)前地址減gdt地址減1得到GDT的長度
.long
GDT_ADDR 這里使用了一些常量,如GDT_ADDR,定義在一個頭文件中
02/include/kernel.inc
.set CODE_SEL, 0x08
# 內(nèi)核代碼段選擇子,二進(jìn)制值是00001000,表示GDT的第2項(索引值為1)
.set DATA_SEL, 0x10 # 內(nèi)核代碼段選擇子
.set IDT_ADDR, 0x80000 # IDT 起始地址
我們將所有數(shù)據(jù)設(shè)置為固定地址,IDT表(后面課程會介紹到)是所有數(shù)據(jù)的起始部分。
.set IDT_SIZE, (256*8) # IDT 大小
.set GDT_ADDR,
(IDT_ADDR+IDT_SIZE) # GDT 在 IDT的后面
我們用GDT_ADDR,而不是用bootsector.s文件中的gdt符合,是因為在進(jìn)入保護(hù)模式后7c00地址將被覆蓋,于是我們把系統(tǒng)中用到的一些表搬移到固定地址。
.set GDT_ENTRIES, 5 # GDT 有 5個描述符
# 空描述符
# 內(nèi)核代碼段描述符
# 內(nèi)核數(shù)據(jù)段描述符
# 當(dāng)前進(jìn)程tss
# 當(dāng)前進(jìn)程ldt
在skelix我們使用了5個GDT描述符,這里我們先介紹前3個,最后兩個將會在后面的課程中介紹。
.set GDT_SIZE, (8*GDT_ENTRIES)
# GDT 大小,每個描述符是8個字節(jié)大小,所以GDT大小是該值,但是我們用的并不是它
.set KERNEL_SECT, 72 # 內(nèi)核大小,單位是,36k對于現(xiàn)在來說已經(jīng)足夠了
.set STACK_BOT, 0xa0000 # 堆棧從640K 內(nèi)存處開始向下增長,應(yīng)該是STACK_TOP才對?
下載我們來看一下引導(dǎo)程序
02/bootsect.s
.text
.globl start
.include "kernel.inc"
include the above file
.code16
start:
jmp
code
gdt:
.quad
0x0000000000000000 # null descriptor
.quad
0x00cf9a000000ffff # cs
.quad
0x00cf92000000ffff # ds
.quad
0x0000000000000000 # reserved for further use
.quad
0x0000000000000000 # reserved for further use
gdt_48:
.word .-gdt-1
.long GDT_ADDR
code:
xorw
%ax, %ax
movw
%ax, %ds # 數(shù)據(jù)段 = 0x0000
movw
%ax, %ss # 堆棧段
= 0x0000
movw
$0x1000,%sp # 保護(hù)模式前用的堆棧,不要讓他覆蓋到7c00處的引導(dǎo)程序即可
# 我們將加載內(nèi)核到地址 0x10000
movw $0x1000,%ax
movw
%ax, %es
xorw
%bx, %bx # es:bx 加載內(nèi)核的目標(biāo)地址
movw
$KERNEL_SECT,%cx
movw
$1, %si # 0,跳過去,所以是1
rd_kern:
call
read_sect # 入口參數(shù):si是起始扇區(qū)數(shù),es:bx是指定內(nèi)存地址
addw
$512, %bx
incw %si
loop rd_kern
我們先把內(nèi)核讀到0x10000這個臨時地址,然后再把它搬移到0x0(進(jìn)入保護(hù)模式后搬移)。這個函數(shù)講起來有些煩,讀者可以自己分析:)
cli #
就要進(jìn)入保護(hù)模式了,所以關(guān)掉實模式下的中斷
cld # 將內(nèi)核的前512字節(jié)移到0x0
movw $0x1000,%ax
movw
%ax, %ds
movw $0x0000,%ax
movw
%ax, %es
xorw
%si, %si
xorw
%di, %di
movw $512>>2,%cx
rep
movsl
為什么要這樣做?因為內(nèi)核的這個部分是load.s這個文件編譯出來的(本課后面會介紹到),load.s會讀取“真正的內(nèi)核”到0x200處,但是在這一課,我們只準(zhǔn)備打印"Hello
World!",除此之外什么都不做。
xorw
%ax, %ax
movw
%ax, %ds # 復(fù)位
ds 為 0x0000
movw
$GDT_ADDR>>4,%ax # (0x80000 +
256 * 8) >> 2
movw
%ax,
%es # gdt所在的數(shù)據(jù)段
movw
$gdt, %si
xorw
%di, %di # 從ds:si 拷貝到 es:di中
movw
$GDT_SIZE>>2,%cx # 拷貝數(shù)據(jù)段中的gdt到指定地址
rep
movsl
enable_a20:
inb
$0x64, %al
testb $0x2, %al
jnz enable_a20
movb $0xbf, %al
outb
%al, $0x64
這種開啟a20地址線的方法來自一本書:"The Undocumented PC",中文紙版是《PC技術(shù)內(nèi)幕》,可惜已絕版。a20地址線通過鍵盤控制器一個端口使能(ibm早期這樣設(shè)計),當(dāng)系統(tǒng)啟動時,該地址線是關(guān)閉的,使能它之后才能訪問1MB以外的地址空間。
lgdt
gdt_48
# 加載gdt地址到寄存器中
# 進(jìn)入保護(hù)模式
movl
%cr0, %eax
orl
$0x1, %eax
movl %eax,
%cr0 # 使能CR0 控制寄存器中的PE位(即第0位)
現(xiàn)在我們已經(jīng)進(jìn)入到保護(hù)模式了,是不是簡單的另你不敢相信?呵呵
ljmp $CODE_SEL, $0x0
我們還需要進(jìn)行一個絕對地址跳轉(zhuǎn),因為解碼管線中預(yù)取了16位指令,需要刷新成后面的32位指令。關(guān)于ia32的指令預(yù)取和解碼管線,網(wǎng)絡(luò)上有很多相關(guān)的文章,建議讀者閱讀一下相關(guān)文章。這個指令跳轉(zhuǎn)到0x08描述符選擇子指向的偏移0x的指令處,并開始執(zhí)行,這個描述符即GDT中的第2項:內(nèi)核代碼段描述符。代碼就是load.s的開始處,一會我們開始分析load.s這個程序。
bootsector.s中的函數(shù):
# 輸入: si: LBA 地址,從0開始
# 輸出
es:bx 讀取扇區(qū)到這個內(nèi)存地址
read_sect:
pushw %ax
pushw %cx
pushw %dx
pushw %bx
movw
%si, %ax
xorw
%dx, %dx
movw
$18, %bx # 對于1.44M軟盤:每磁道18扇區(qū)
divw %bx
incw %dx
movb
%dl, %cl # cl = 扇區(qū)號
xorw
%dx, %dx
movw
$2, %bx # 每磁道2磁頭
divw %bx
movb
%dl, %dh # 磁頭
xorb
%dl, %dl # 軟驅(qū)號
movb
%al, %ch # 柱面
popw %bx
# 讀取到:es:bx
rp_read:
movb $0x1,
%al # 讀1個扇區(qū)
movb
$0x2, %ah
int $0x13
jc rp_read
popw %dx
popw %cx
popw %ax
ret
.org 0x1fe,
0x90
# 填充nop指令,機(jī)器碼是0x90 當(dāng)我們進(jìn)入到保護(hù)模式后,所有的通用寄存器和段寄存器保持原來實模式的值,代碼段從特權(quán)級0開始執(zhí)行。load.s文件將從地址0處開始執(zhí)行。 .text 現(xiàn)在我們用圖來清晰的描述它,引導(dǎo)程序被加載在00007c00,它設(shè)置棧頂在00001000,然后讀取內(nèi)核到00001000,然后把內(nèi)核映象的前一個sector(即load.s)程序讀到地址0。在load.s程序中移到內(nèi)核到地址0。 圖1 圖2 |
| |___________________|a0000
|
| |
內(nèi)核棧 |
| GDT
| | |
| IDT
| | GDT/IDT |
|___________________| 8000:系統(tǒng)數(shù)據(jù) |___________________|80000 |
| | |
|
| | |
|
| | |
| 內(nèi)核
| | |
|
| | |
|___________________|10000 | |
|
| | |
|
| | |
|___________________|7e00 | |
| bootsector.s
| | |
|___________________|7c00 | | |
| | |
|
| |___________________|
|
| | | |
| | | |___________________|1000 | 內(nèi)核 | | stack
| | | |___________________|200 |___________________|200 | load.s | | | |___________________|0 |___________________|0 最后,我們翻開Makefile看看: AS=as
-Iinclude -I選項告訴匯編工具查找頭文件的路徑 內(nèi)核代碼段鏈接在0x0000
.word 0xaa55
02/load.s
.globl pm_mode
.include "kernel.inc"
.org
0 #
告訴加載器,該代碼將從邏輯地址0開始執(zhí)行。它也是物理地址0。
pm_mode:
movl $DATA_SEL,%eax
movw %ax,
%ds
movw
%ax, %es
movw
%ax, %fs
movw
%ax, %gs
movw
%ax, %ss
movl
$STACK_BOT,%esp # 所有數(shù)據(jù)段選擇子設(shè)置為0x10,即GDT的第3項,特權(quán)級為0。這個步驟非常重要!
cld
movl
$0x10200,%esi # 在bootsector程序中,我們將內(nèi)核加載到了0x10200這個地址
movl $0x200,
%edi # 現(xiàn)在把內(nèi)核搬移到0x200
movl
$KERNEL_SECT<<7,%ecx # 拷貝2^7次方個,注意下面是movsl,每次4個字節(jié)
rep
movsl
movb $0x07,
%al #
顏色
movl $msg,
%esi
movl $0xb8000,%edi
1:
cmp
$0,
(%esi) #
打印"Hello World!"字符串
je 1f
movsb
stosb
jmp 1b
1: jmp 1b
msg:
.string "Hello World!\x0"
當(dāng)進(jìn)入到保護(hù)模式后,load.s移到內(nèi)核到它后面,設(shè)置內(nèi)核棧,如圖2。
02/Makefile
LD=ld
KERNEL_OBJS= load.o 到現(xiàn)在為止,內(nèi)核只保護(hù)load.s匯編文件
.s.o:
${AS} -a $< -o $*.o >$*.map
all: final.img
final.img: bootsect kernel
cat bootsect kernel > final.img
@wc -c final.img
bootsect: bootsect.o
${LD} --oformat binary -N -e start -Ttext 0x
kernel: ${KERNEL_OBJS}
${LD} --oformat binary -N -e pm_mode -Ttext 0x0000 -o $@ ${KERNEL_OBJS}
@wc -c kernel
clean:
rm -f *.img kernel bootsect *.o
執(zhí)行make,用vmware運(yùn)行一下剛才的image看看,是不是hello world呢。