學習的框架如下:
1.80386的分段和分頁管理
2.80386的保護模式
3.Linux0.11的初始化,主要分析內存管理和使用部分
下面將按Linux的啟動過程進行分析
80386上電之后進行BIOS的自檢,自檢完成后將軟驅或者硬盤中的引導程序拷貝到0x7C00中,并跳轉到這個程序之中,這個時候80386處于實模式中.
Linux0.11中這個引導程序為Bootsect.s
剛進入Bootsect.s中時的寄存器值如下:
EAX : 0xAA55
ECX : 0xF0001
EDX : 0x0
EBX : 0x0
ESP : 0xFFFE
EBP : 0x0
ESI : 0x733F
EDI : 0xFFDE
EIP : 0x7C00
EFLAGS : 0x282
CS : 0x0
SS : 0x0
DS : 0x0
ES : 0x0
FS : 0x0
GS : 0x0
Bootsect.s的代碼如下:
SYSSIZE = 0x3000
.globl begtext, begdata, begbss, endtext, enddata, endbss
.text
begtext:
.data
begdata:
.bss
begbss:
.text
SETUPLEN = 4 ! nr of setup-sectors
BOOTSEG = 0x07c0 ! original address of boot-sector
INITSEG = 0x9000 ! we move boot here - out of the way
SETUPSEG = 0x9020 ! setup starts here
SYSSEG = 0x1000 ! system loaded at 0x10000 (65536).
ENDSEG = SYSSEG + SYSSIZE ! where to stop loading
ROOT_DEV = 0x306
entry start
start:
//取得自檢完成后CPU執(zhí)行引導程序的首地址
mov ax,#BOOTSEG
//將該地址設為數據段的段基址
mov ds,ax
//取得bootsect.s將復制到的地址
mov ax,#INITSEG
//將該地址設為附加段的段基址
mov es,ax
//設置計數器為256
mov cx,#256
//清零si寄存器 -> ds:si = 0x07C0:0x0000
sub si,si
//清零di寄存器 -> es:di = 0x9000:0x0000
sub di,di
//直到cx為0之前重復執(zhí)行movw
rep
//拷貝ds:si所指的數據到es:di
//每拷貝1次,si di自增 , 每次拷貝一個字
movw
//跳躍到INITSEG的偏移go的位置上
//執(zhí)行完之后cs為INITSEG,ip為go
//也就是跳轉到復制的bootsect.s中繼續(xù)執(zhí)行
jmpi go,INITSEG
go:
//取得代碼段寄存器cs的值
//也就是INITSEG,0x9000
mov ax,cs
//將cs的值賦給數據段寄存器ds
mov ds,ax
//將cs的值賦給附加段寄存器es
mov es,ax
//將cs的值賦給堆棧指針寄存器ss
mov ss,ax
//設置堆棧指針偏移寄存器sp的值為0xFF00
//則椢虮茫空間為0x90000 - 0x9FF00
mov sp,#0xFF00 ! arbitrary value >>512
//加載setup.s程序到地址0x90200中
load_setup:
mov dx,#0x0000 ! drive 0, head 0
mov cx,#0x0002 ! sector 2, track 0
mov bx,#0x0200 ! address = 512, in INITSEG
mov ax,#0x0200+SETUPLEN ! service 2, nr of sectors
int 0x13 ! read it
jnc ok_load_setup ! ok - continue
mov dx,#0x0000
mov ax,#0x0000 ! reset the diskette
int 0x13
j load_setup
ok_load_setup:
mov dl,#0x00
mov ax,#0x0800 ! AH=8 is get drive parameters
int 0x13
mov ch,#0x00
seg cs
mov sectors,cx
mov ax,#INITSEG
mov es,ax
mov ah,#0x03 ! read cursor pos
xor bh,bh
int 0x10
mov cx,#24
mov bx,#0x0007 ! page 0, attribute 7 (normal)
mov bp,#msg1
mov ax,#0x1301 ! write string, move cursor
int 0x10
mov ax,#SYSSEG
mov es,ax ! segment of 0x010000
call read_it
call kill_motor
seg cs
mov ax,root_dev
cmp ax,#0
jne root_defined
seg cs
mov bx,sectors
mov ax,#0x0208 ! /dev/ps0 - 1.2Mb
cmp bx,#15
je root_defined
mov ax,#0x021c ! /dev/PS0 - 1.44Mb
cmp bx,#18
je root_defined
undef_root:
jmp undef_root
root_defined:
seg cs
mov root_dev,ax
//加載完成,跳轉到setup.s中
//0x90200也就是0x9020:0
jmpi 0,SETUPSEG
sread: .word 1+SETUPLEN ! sectors read of current track
head: .word 0 ! current head
track: .word 0 ! current track
read_it:
mov ax,es
test ax,#0x0fff
die: jne die ! es must be at 64kB boundary
xor bx,bx ! bx is starting address within segment
rp_read:
mov ax,es
cmp ax,#ENDSEG ! have we loaded all yet?
jb ok1_read
ret
ok1_read:
seg cs
mov ax,sectors
sub ax,sread
mov cx,ax
shl cx,#9
add cx,bx
jnc ok2_read
je ok2_read
xor ax,ax
sub ax,bx
shr ax,#9
ok2_read:
call read_track
mov cx,ax
add ax,sread
seg cs
cmp ax,sectors
jne ok3_read
mov ax,#1
sub ax,head
jne ok4_read
inc track
ok4_read:
mov head,ax
xor ax,ax
ok3_read:
mov sread,ax
shl cx,#9
add bx,cx
jnc rp_read
mov ax,es
add ax,#0x1000
mov es,ax
xor bx,bx
jmp rp_read
read_track:
push ax
push bx
push cx
push dx
mov dx,track
mov cx,sread
inc cx
mov ch,dl
mov dx,head
mov dh,dl
mov dl,#0
and dx,#0x0100
mov ah,#2
int 0x13
jc bad_rt
pop dx
pop cx
pop bx
pop ax
ret
bad_rt: mov ax,#0
mov dx,#0
int 0x13
pop dx
pop cx
pop bx
pop ax
jmp read_track
kill_motor:
push dx
mov dx,#0x3f2
mov al,#0
outb
pop dx
ret
sectors:
.word 0
msg1:
.byte 13,10
.ascii "Loading system ..."
.byte 13,10,13,10
.org 508
root_dev:
.word ROOT_DEV
boot_flag:
.word 0xAA55
.text
endtext:
.data
enddata:
.bss
endbss:
Bootsect.s首先將自身復制到地址0x90200中,并跳轉到復制后的地址中執(zhí)行,如下圖所示:
執(zhí)行jmpi go,INITSEG后就由開始的Bootsect.s跳轉到復制后的Bootsect.s中的標號go處繼續(xù)執(zhí)行.
然后Bootsect.s把Setup.s從磁盤中讀取到內存位置0x90200處,如下圖所示:
加載完Setup.s后在屏幕上打印"Loading system ...".
接著把SYSTEM,也就是LINUX0.11的內核讀取到內存位置0x10000處,如下圖所示:
然后使用指令jmpi 0,SETUPSEG跳轉到0x90200地址處的第一條指令繼續(xù)執(zhí)行,也就是進入到了Setup.s中
剛進入Setup.s中時的寄存器值如下:
EAX : 0x301
ECX : 0x111600
EDX : 0xE00
EBX : 0x0
ESP : 0xFF00
EBP : 0x13F
ESI : 0x200
EDI : 0xEFDF
EIP : 0x0
EFLAGS : 0x202
CS : 0x9020
SS : 0x9000
DS : 0x9000
ES : 0x4000
FS : 0x0
GS : 0x0
Setup.s的代碼如下:
INITSEG = 0x9000 ! we move boot here - out of the way
SYSSEG = 0x1000 ! system loaded at 0x10000 (65536).
SETUPSEG = 0x9020 ! this is the current segment
.globl begtext, begdata, begbss, endtext, enddata, endbss
.text
begtext:
.data
begdata:
.bss
begbss:
.text
entry start
start:
//設置ax為0x9000,也就是bootsect.s的起始地址
mov ax,#INITSEG ! this is done in bootsect already, but...
//將該地址賦給數據段寄存器ds
mov ds,ax
//設置ah為0x03,為讀取光標位置做準備
mov ah,#0x03 ! read cursor pos
//清零bh
xor bh,bh
//啟用10號BOIS中斷中的0x03號功能來讀取數據
int 0x10 ! save it in known place, con_init fetches
//將讀取到得數據保存在 ds:0 中 , 也就是 9000:0 -> 0x90000
mov [0],dx ! it from 0x90000.
//設置ah為0x88,為讀取內存大小做準備
mov ah,#0x88
//啟動15號BIOS中斷中的0x88號功能來讀取數據
int 0x15
//將讀取到的數據保存在 ds:2 中,也就是9000:2 -> 0x90002
mov [2],ax
mov ah,#0x0f
int 0x10
mov [4],bx ! bh = display page
mov [6],ax ! al = video mode, ah = window width
mov ah,#0x12
mov bl,#0x10
int 0x10
mov [8],ax
mov [10],bx
mov [12],cx
mov ax,#0x0000
mov ds,ax
lds si,[4*0x41]
mov ax,#INITSEG
mov es,ax
mov di,#0x0080
mov cx,#0x10
rep
movsb
mov ax,#0x0000
mov ds,ax
lds si,[4*0x46]
mov ax,#INITSEG
mov es,ax
mov di,#0x0090
mov cx,#0x10
rep
movsb
mov ax,#0x01500
mov dl,#0x81
int 0x13
jc no_disk1
cmp ah,#3
je is_disk1
no_disk1:
mov ax,#INITSEG
mov es,ax
mov di,#0x0090
mov cx,#0x10
mov ax,#0x00
rep
stosb
is_disk1:
//禁止中斷
cli ! no interrupts allowed !
//設置ax為0x0000,這也是system模塊將要復制到的位置
mov ax,#0x0000
//設置si和di的遞增方向為向前
cld ! 'direction'=0, movs moves forward
do_move:
//設置附加段寄存器的值為ax
mov es,ax ! destination segment
//ax的值自增0x1000
add ax,#0x1000
//檢測ax的值是否達到0x9000
cmp ax,#0x9000
//達到則跳到end_move
jz end_move
//將數據段寄存器的值設為ax
mov ds,ax ! source segment
//清零di
sub di,di
//清零si
sub si,si
//設置計數寄存器的值為0x8000 , 拷貝0x8000個字 , 在8086中也就是64k字節(jié),每字2個字節(jié)
mov cx,#0x8000
//直到cx為0之前重復執(zhí)行movsw
rep
//拷貝ds:si的數據到es:di , si di自增 , 每次拷貝一個字 (movsw和movw一樣?)
movsw
//跳回到do_move
jmp do_move
//拷貝system模塊完成
end_move:
//設置ax的值為SETUPSEG , 也就是0x9020
mov ax,#SETUPSEG ! right, forgot this at first. didn''t work :-)
//設置數據段寄存器為SETUPSEG,也就是0x9020
mov ds,ax
//加載中斷描述符表地址為idt_48
lidt idt_48 ! load idt with 0,0
//加載全局描述表地址為gdt_48
lgdt gdt_48 ! load gdt with whatever appropriate
call empty_8042
mov al,#0xD1 ! command write
out #0x64,al
call empty_8042
mov al,#0xDF ! A20 on
out #0x60,al
call empty_8042
mov al,#0x11 ! initialization sequence
out #0x20,al ! send it to 8259A-1
.word 0x00eb,0x00eb ! jmp $+2, jmp $+2
out #0xA0,al ! and to 8259A-2
.word 0x00eb,0x00eb
mov al,#0x20 ! start of hardware int''s (0x20)
out #0x21,al
.word 0x00eb,0x00eb
mov al,#0x28 ! start of hardware int''s 2 (0x28)
out #0xA1,al
.word 0x00eb,0x00eb
mov al,#0x04 ! 8259-1 is master
out #0x21,al
.word 0x00eb,0x00eb
mov al,#0x02 ! 8259-2 is slave
out #0xA1,al
.word 0x00eb,0x00eb
mov al,#0x01 ! 8086 mode for both
out #0x21,al
.word 0x00eb,0x00eb
out #0xA1,al
.word 0x00eb,0x00eb
mov al,#0xFF ! mask off all interrupts for now
out #0x21,al
.word 0x00eb,0x00eb
out #0xA1,al
//設置保護模式比特位
mov ax,#0x0001 ! protected mode (PE) bit
//加載機器狀態(tài)字
lmsw ax ! This is
//跳躍到臨時全局表中的第2項中
//8轉換為段選擇符格式為1000,低3位為屬性
//Index部分為1,也就是0x1,第2個描述符
//0x0為第1個描述符
jmpi 0,8 ! jmp offset 0 of segment 8 (cs)
empty_8042:
.word 0x00eb,0x00eb
in al,#0x64 ! 8042 status port
test al,#2 ! is input buffer full?
jnz empty_8042 ! yes - loop
ret
gdt:
//全局表的第1項為空
.word 0,0,0,0 ! dummy
//全局表的第2項,這里為代碼段描述符
//因為0代表4KB,所以2048-1=2047
.word 0x07FF ! 8Mb - limit=2047 (2048*4096=8Mb)
//基地址為0
.word 0x0000 ! base address=0
// P=1,S=1,TYPE=1010
.word 0x9A00 ! code read/exec
// G=1,D/B=1
.word 0x00C0 ! granularity=4096, 386
//全局表的第3項,這里為數據段描述符
//因為0代表4KB,所以2048-1=2047
.word 0x07FF ! 8Mb - limit=2047 (2048*4096=8Mb)
//基地址為0
.word 0x0000 ! base address=0
// P=1,S=1,TYPE=0010
.word 0x9200 ! data read/write
// G=1,D/B=1
.word 0x00C0 ! granularity=4096, 386
idt_48:
//限長為0
.word 0 ! idt limit=0
//基地址為0
.word 0,0 ! idt base=0L
gdt_48:
//256個描述符,每個8字節(jié),256*8 = 2048字節(jié)
.word 0x800 ! gdt limit=2048, 256 GDT entries
//基地址為0x90200 + gdt (0x200 = 512) -> (SETUPSEG) + gdt
.word 512+gdt,0x9 ! gdt base = 0X9xxxx
.text
endtext:
.data
enddata:
.bss
endbss:
Setup.s首先讀取BIOS自檢時設置好的內存,顯示卡,硬盤等信息,保存到內核中的對應地址中,然后將System模塊從0x10000處移動到0x00000處,如下圖所示:
然后準備進入保護模式之前的處理,首先加載一個臨時的GDT表和設置IDT表基址寄存器,因為在進入保護模式之前關閉了中斷,所以再開啟中斷之前不會讀取IDT表的項目,所以把IDTR的基地址設置成0x0也不用擔心會產生錯誤,如下圖所示:
加載完成后便開啟保護模式,然后跳到全局描述符表中的第2個描述符的偏移0x0處繼續(xù)執(zhí)行,第2個描述符為代碼段描述符,其基地址為0x0,呢么就是執(zhí)行物理地址0x0處的指令,setup.s程序之前將System模塊移動到了0x0地址處,而System模塊中的head.s代碼處于模塊頭,也就是在0x0地址上,所以這里會執(zhí)行head.s的代碼.
這里介紹一下實模式和保護模式尋址的不同.
在實模式中尋址分為段地址和偏移地址,段提供一個0x0-0xFFFF的范圍,偏移地址在這個范圍內進行定位,段地址由段寄存器中的值向左移動4位得出.
例如要表示0x90200這個地址,可以寫成0x9000:0x200,0x9000向左移動4位得0x90000,再加上偏移地址0x200,就是0x90000+0x200=0x90200,也可以寫成0x9020:0x0,0x9020向左移動4位得0x90200,再加上偏移地址0x0,就是0x90200+0=0x90200.
而在保護模式中,尋址依然分為段地址和偏移地址,不過段地址不再由段寄存器直接給出,段寄存器給出的是一個索引值,要在一個表中根據這個索引值得出段地址.
例如0x8:0x0,0x8換成2進制為1000,其中低3位為索引的屬性,呢么Index就是1,也就是說0x8表示取表中的第1個段描述符,假設該段描述符提供的段地址為0x1000,呢么0x8:0x0就是尋址0x1000+0x0=0x1000.
剛進入head.s中時的寄存器值如下:
EAX : 0x1
ECX : 0x110000
EDX : 0x1181
EBX : 0x3
ESP : 0xFF00
EBP : 0x13F
ESI : 0x0
EDI : 0x0
EIP : 0x0
EFLAGS : 0x46
CS : 0x8
SS : 0x9000
DS : 0x9020
ES : 0x8000
FS : 0x0
GS : 0x0
head.s的代碼如下:
/*
* linux/boot/head.s
*
* (C) 1991 Linus Torvalds
*/
.text
.globl _idt,_gdt,_pg_dir,_tmp_floppy_area
_pg_dir:
startup_32:
//將eax寄存器的值設置為0x10
//0x10,換算成段描述符也就是10000,低3位為屬性
//也就是index段為10,也就是0x2,也就是第3個描述符
movl $0x10,%eax
//設置數據段寄存器的值為0x10,也就是數據描述符
mov %ax,%ds
//設置附加段寄存器的值為0x10,也就是數據描述符
mov %ax,%es
//設置附加數據段寄存器fs的值為0x10,也就是數據描述符
mov %ax,%fs
//設置附加數據段寄存器gs的值為0x10,也就是數據描述符
mov %ax,%gs
//設置堆棧指針指向_stack_start
lss _stack_start,%esp
//設置中斷描述符表
call setup_idt
//設置全局描述符表
call setup_gdt
//因為更改了全局描述表基地址寄存器
//需要重新加載一次段寄存器
//將eax寄存器的值設置為0x10
movl $0x10,%eax # reload all the segment registers
//設置數據段寄存器的值為0x10,也就是數據描述符
mov %ax,%ds # after changing gdt. CS was already
//設置附加段寄存器的值為0x10,也就是數據描述符
mov %ax,%es # reloaded in 'setup_gdt'
//設置附加數據段寄存器fs的值為0x10,也就是數據描述符
mov %ax,%fs
//設置附加數據段寄存器gs的值為0x10,也就是數據描述符
mov %ax,%gs
//設置堆棧指針指向_stack_start
lss _stack_start,%esp
xorl %eax,%eax
1: incl %eax # check that A20 really IS enabled
movl %eax,0x000000 # loop forever if it isn''t
cmpl %eax,0x100000
je 1b
movl %cr0,%eax # check math chip
andl $0x80000011,%eax # Save PG,PE,ET
orl $2,%eax # set MP
movl %eax,%cr0
call check_x87
jmp after_page_tables
check_x87:
fninit
fstsw %ax
cmpb $0,%al
je 1f /* no coprocessor: have to set bits */
movl %cr0,%eax
xorl $6,%eax /* reset MP, set EM */
movl %eax,%cr0
ret
.align 2
1: .byte 0xDB,0xE4 /* fsetpm for 287, ignored by 387 */
ret
setup_idt:
//設置edx寄存器的值為ignore_int函數的地址
lea ignore_int,%edx
//設置eax寄存器的值為0x00080000 , 也就是段選擇符為0x0008 , 偏移地址的0-15位為0x0
movl $0x00080000,%eax
//設置偏移地址的0-15位為edx中的低16位也就是dx中的值
movw %dx,%ax /* selector = 0x0008 = cs */
//設置P=1,DPL=0,D=1,TYPE=110,為中斷門
movw $0x8E00,%dx /* interrupt gate - dpl=0, present */
//設置edi寄存器的值為_idt的地址,也就是中段描述符表的地址
lea _idt,%edi
//設置計數寄存器的值為256
mov $256,%ecx
rp_sidt:
//設置edi所指地址的值為eax
movl %eax,(%edi)
//設置edi所指地址+4的地址的值為edx
movl %edx,4(%edi)
//使edi指向下一個中斷描述符
addl $8,%edi
//減少計數寄存器
dec %ecx
//檢測計數寄存器是否為0,不為0則跳回到rp_sidt
jne rp_sidt
//裝載中斷描述符寄存器
lidt idt_descr
//返回到調用setup_idt的地方
ret
setup_gdt:
//裝載全局描述符寄存器
lgdt gdt_descr
//返回到調用setup_gdt的地方
ret
.org 0x1000
pg0:
.org 0x2000
pg1:
.org 0x3000
pg2:
.org 0x4000
pg3:
.org 0x5000
_tmp_floppy_area:
.fill 1024,1,0
after_page_tables:
//壓入main的參數envp
pushl $0 # These are the parameters to main :-)
//壓入main的參數argv
pushl $0
//壓入main的參數argc
pushl $0
//壓入main的返回地址,地址為L6
pushl $L6 # return address for main, if it decides to.
//壓入main的地址,當執(zhí)行ret的時候就會轉入到main函數中
pushl $_main
jmp setup_paging
L6:
jmp L6 # main should never return here, but
# just in case, we know what happens.
int_msg:
.asciz "Unknown interrupt\n\r"
.align 2
ignore_int:
pushl %eax
pushl %ecx
pushl %edx
push %ds
push %es
push %fs
movl $0x10,%eax
mov %ax,%ds
mov %ax,%es
mov %ax,%fs
pushl $int_msg
call _printk
popl %eax
pop %fs
pop %es
pop %ds
popl %edx
popl %ecx
popl %eax
iret
.align 2
setup_paging:
//5個頁表,一共1024*5個頁面,設置計數寄存器
movl $1024*5,%ecx /* 5 pages - pg_dir+4 page tables */
//清零eax
xorl %eax,%eax
//清零edi
xorl %edi,%edi /* pg_dir is at 0x000 */
//拷貝eax的值到edi的地址上,直到ecx為0,也就是清零所有頁幀
cld;rep;stosl
// P=1,R/W=1,U/S=1,pg0地址為0x1000,其中低12位用于存儲頁屬性,實際為0x1007
movl $pg0+7,_pg_dir /* set present bit/user r/w */
// P=1,R/W=1,U/S=1,pg1地址為0x2000,其中低12位用于存儲頁屬性,實際為0x2007
movl $pg1+7,_pg_dir+4 /* --------- " " --------- */
// P=1,R/W=1,U/S=1,pg2地址為0x3000,其中低12位用于存儲頁屬性,實際為0x3007
movl $pg2+7,_pg_dir+8 /* --------- " " --------- */
// P=1,R/W=1,U/S=1,pg3地址為0x4000,其中低12位用于存儲頁屬性,實際為0x4007
movl $pg3+7,_pg_dir+12 /* --------- " " --------- */
//設置edi指向pg3頁表的最后一頁
movl $pg3+4092,%edi
//設置頁的地址為16MB中的最后一頁,屬性為P=1,R/W=1,U/S=1
movl $0xfff007,%eax /* 16Mb - 4096 + 7 (r/w user,p) */
//方向位向前,edi向低地址移動
std
//拷貝eax中的內容到es:edi所指向的地址中,數據長度為l->long
1: stosl /* fill pages backwards - more efficient :-) */
//減少一頁,每頁為4K字節(jié)
subl $0x1000,%eax
//當eax大于或者等于0則向前跳轉到符號1處
jge 1b
//清零eax
xorl %eax,%eax /* pg_dir is at 0x0000 */
//清零cr3控制寄存器,也就是設置CR3中的頁目錄表基地址為0x0,指向_pg_dir
movl %eax,%cr3 /* cr3 - page directory start */
//讀取cr0中的數據到eax中
movl %cr0,%eax
//置PG標志為1
orl $0x80000000,%eax
//將置位后的eax回寫到cr0中,這時候開始就啟動分頁了
movl %eax,%cr0 /* set paging (PG) bit */
//跳到之前壓入的main函數中
ret /* this also flushes prefetch-queue */
.align 2
.word 0
idt_descr:
//設置限長,每個中段描述符為8個字節(jié),中段描述符256個,呢么大小就是256*8
.word 256*8-1 # idt contains 256 entries
//設置基地址為_idt
.long _idt
.align 2
.word 0
gdt_descr:
//設置限長,每個描述符為8個字節(jié),描述符256個,呢么大小就是256*8
.word 256*8-1 # so does gdt (not that that''s any
//設置基地址為_gdt
.long _gdt # magic number, but it works for me :^)
.align 3
//中段描述符表
//256項,每項8字節(jié),每項填充為0
_idt: .fill 256,8,0 # idt is uninitialized
_gdt:
//第1項為空
.quad 0x0000000000000000 /* NULL descriptor */
//第2項為系統(tǒng)代碼描述符
// G=1,D/B=1
// P=1,S=1,TYPE=1010
// 基地址為0
// 因為0代表4KB,(4096 - 1)*4KB = 16MB
.quad 0x00c09a0000000fff /* 16Mb */
//第3項為系統(tǒng)數據描述符
// G=1,D/B=1
// P=1,S=1,TYPE=0010
// 基地址為0
// 因為0代表4KB,(4096 - 1)*4KB = 16MB
.quad 0x00c0920000000fff /* 16Mb */
//第4項為空
.quad 0x0000000000000000 /* TEMPORARY - don't use */
//252項,每項8字節(jié),每項填充為0
.fill 252,8,0 /* space for LDT's and TSS's etc */
head.s首先初始化中斷描述符表中的項,然后設置IDTR,完成后設置新的GDT表中的項,然后重新設置GDTR,使其指向新的GDT表,如下圖:
然后head.s將main函數的參數和返回地址壓入棧中,跳轉到分頁初始化中,Linux0.11在head.s中預留了5張頁,每張頁1024項,第1張頁用來填寫頁目錄項,其余4張頁填寫頁表項,每張頁可尋址4MB地址空間,4張頁表尋址16MB,也就是Linux0.11默認支持的最大內存大小,如下圖:
完成之后設置CR3寄存器為0x0,也就是頁目錄表的基地址.
分頁設置完成后打開分頁屬性,之后保護模式下的地址經過分段處理后還要進行分頁處理.
最后將執(zhí)行中斷返回,跳轉到之前壓入的main函數中.
介紹一下分頁的尋址方法,分頁的尋址方法和保護模式下的尋址方法差不多,也是進行查表尋址,在分頁管理中,把32位的地址分成了3個部分:
- 偏移地址:0-11位.
- 頁表索引號:12-21位.
- 頁目錄索引號:22-31位.
舉個例子, 0x00405008,將這個地址拆成2進制,就是0000 0000 0100 0000 0101 0000 0000 1000,從右往左計算,0到11位為偏移地址,呢么偏移地址就是0x8,12到21位為頁表號,呢么頁表號就是0x5,22位到31位為頁目錄號,呢么頁目錄號就是0x4.
尋址過程如下:首先取得頁目錄表的基地址,該地址存在CR3中,假設CR3的值為0x0,然后根據頁目錄表的基地址(0x0)和頁目錄號(0x4)計算對應的頁目錄項,在頁目錄項中取得頁表的基地址, 假設0x4號頁目錄中的頁表基地址為0x4000,然后根據頁表的基地址(0x1000)和頁表號(0x5)計算對應的頁表項, 在頁表項中取得頁面的基地址, 假設0x4號頁表中的頁面基地址為0x9000,呢么最后0x00405008所指的物理地址為0x9000+0x8 = 0x9008,過程如下圖所示:
main函數的代碼如下:
void main(void)
{
//指向地址0x901FC,這個地址保存了根文件系統(tǒng)所在設備號
ROOT_DEV = ORIG_ROOT_DEV;
//指向地址0x90080,這個地址保存了硬盤參數表基址
drive_info = DRIVE_INFO;
//保存在0x90002地址處的數據為擴展內存的大小,單位為1KB
//這里計算內存的大小
//計算的方法為1MB+擴展內存的大小*1KB
memory_end = (1<<20) + (EXT_MEM_K<<10);
//最小單位為1KB,舍棄不足1KB的部分
memory_end &= 0xfffff000;
//檢測內存大小是否大于16MB
if (memory_end > 16*1024*1024)
//大于16MB則只要16MB
memory_end = 16*1024*1024;
//檢測內存大小是否大于12MB
if (memory_end > 12*1024*1024)
//大于12MB則設置緩沖區(qū)的結束位置為 4MB處
buffer_memory_end = 4*1024*1024;
//小于12MB則檢測是否大于6MB
else if (memory_end > 6*1024*1024)
//大于6MB則設置緩沖區(qū)的結束位置為2MB處
buffer_memory_end = 2*1024*1024;
//小于6MB
else
//設置緩沖區(qū)的結束位置為1MB處
buffer_memory_end = 1*1024*1024;
//設置主內存的起始位置為緩沖區(qū)的結束位置
main_memory_start = buffer_memory_end;
#ifdef RAMDISK
main_memory_start += rd_init(main_memory_start, RAMDISK*1024);
#endif
//初始化內存管理
mem_init(main_memory_start,memory_end);
trap_init();
blk_dev_init();
chr_dev_init();
tty_init();
time_init();
//初始化調度程序
sched_init();
buffer_init(buffer_memory_end);
hd_init();
floppy_init();
//打開中斷
sti();
//切換到task0中繼續(xù)執(zhí)行接下來的代碼
move_to_user_mode();
//創(chuàng)建一個新進程task1完成init函數
if (!fork())
{ /* we count on this going ok */
init();
}
//task0負責進程調度
for(;;) pause();
}
main函數首先根據內存的不同大小設置主內存區(qū)域的開始和結束地址對于不同的內存大小,LINUX0.11對于主內存區(qū)實現了3種不同的分配方案:
- 內存大小在12MB到16MB范圍之內,則主內存區(qū)從4MB開始到最大.
- 內存大小在6MB到12MB范圍之內,則主內存區(qū)從2MB開始到最大.
- 內存大小在6MB之內,則主內存區(qū)從1MB開始到最大.
在以后的分析中我們假設內存的大小為16MB,不使用RAMDISK,之后的初始化函數中主要關注mem_init
,sched_init, sti, move_to_user_mode和fork.
首先進入到mem_init中,mem_init的代碼如下:
void mem_init(long start_mem, long end_mem)
{
int i;
//設置內存地址的結束位置
HIGH_MEMORY = end_mem;
//歷遍內存管理數組,進行初始化
for (i=0 ; i<PAGING_PAGES ; i++)
//設置為已使用
mem_map[i] = USED;
//計算主內存區(qū)域的起始位置在第幾個頁幀
i = MAP_NR(start_mem);
//計算主內存區(qū)域的大小
end_mem -= start_mem;
//計算主內存區(qū)域占用多少個頁
end_mem >>= 12;
//歷遍主內存區(qū)域的頁
while (end_mem-->0)
//設置內存管理數組對應的頁為未使用
mem_map[i++]=0;
}
在LINUX0.11中使用一個mem_map的unsigned char數組來管理內存的分配狀態(tài),這個數組用于管理物理內存地址1M以上的頁面,其中的每一項都對應內存中的一個頁面, mem_map中有3840項,最大可管理3840*4KB=15MB的內存,對于物理內存不足16MB的情況,LINUX0.11將mem_map中對應的項設置為已使用,不進行分配,從而在邏輯上消除了不對稱的影響.
上圖展示了一個擁有15MB內存時候mem_map的映像圖,低于4MB,也就是內核區(qū)域設置為已使用,不進行分配,高于15MB,也就是高于物理內存的部分也設置為已使用,主內存區(qū)域設置為0,也就是未使用.
首先將mem_map中的項全部設置為已使用,如下圖
然后根據主內存區(qū)域的起始位置和結束位置將mem_map數組中的對應項設置為未使用,如下圖
mem_init完成后來到sched_init中, sched_init的代碼如下:
void sched_init(void)
{
int i;
struct desc_struct * p;
if (sizeof(struct sigaction) != 16)
panic("Struct sigaction MUST be 16 bytes");
//將全局描述符表中的第5項設為init_task.task.tss
set_tss_desc(gdt+FIRST_TSS_ENTRY,&(init_task.task.tss));
//將全局描述符表中的第6項設為init_task.task.ldt
set_ldt_desc(gdt+FIRST_LDT_ENTRY,&(init_task.task.ldt));
//指向全局描述符表中的第7項
p = gdt+2+FIRST_TSS_ENTRY;
//初始化進程管理數組
for(i=1;i<NR_TASKS;i++)
{
task[i] = NULL;
//初始化tss描述符,清零
p->a=p->b=0;
p++;
//初始化ldt描述符,清零
p->a=p->b=0;
p++;
}
//清除NT標志,這樣在之后執(zhí)行中斷返回的時候不會導致嵌套執(zhí)行
//將flag寄存器的值壓棧
//pushfl;
//修改棧中剛壓進的flag的值,置NT標志為0
//andl $0xffffbfff,(%esp) ;
//彈出修改的值給flag寄存器
//popfl
__asm__("pushfl ; andl $0xffffbfff,(%esp) ; popfl");
//將任務0的tss描述符裝載到任務寄存器tr中
ltr(0);
//將任務0的ldt描述符裝載到局部描述符表寄存器中
lldt(0);
//初始化8253定時器
outb_p(0x36,0x43); /* binary, mode 3, LSB/MSB, ch 0 */
outb_p(LATCH & 0xff , 0x40); /* LSB */
outb(LATCH >> 8 , 0x40); /* MSB */
//設置時鐘中斷處理函數
set_intr_gate(0x20,&timer_interrupt);
//設置中斷控制器,允許時鐘中斷
outb(inb_p(0x21)&~0x01,0x21);
//設置系統(tǒng)調用處理函數
set_system_gate(0x80,&system_call);
}
在分析sched_init前先分析一下TSS(任務狀態(tài)段描述符)和LDT(局部段描述符表).
TSS(任務狀態(tài)段描述符)用于保存任務狀態(tài),任務狀態(tài)的結構如下:
struct tss_struct {
//前一進程任務的TSS的描述符的地址
long back_link;
//存放進程任務在特權級0運行時的堆棧指針
long esp0;
long ss0;
//存放進程任務在特權級1運行時的堆棧指針
long esp1;
long ss1;
//存放進程任務在特權級2運行時的堆棧指針
long esp2;
long ss2;
//頁目錄基地址寄存器
long cr3;
//指令指針
long eip;
//標志寄存器
long eflags;
//通用寄存器
long eax,ecx,edx,ebx;
//變址寄存器
long esp;
long ebp;
long esi;
long edi;
//段寄存器
long es;
long cs;
long ss;
long ds;
long fs;
long gs;
//任務的LDT選擇符
long ldt;
//I/O比特位圖的基地址
long trace_bitmap;
//協(xié)處理器信息
struct i387_struct i387;
};
任務狀態(tài)保存了任務運行時的寄存器信息,這樣在任務切換中就能迅速得到原先任務的狀態(tài),并恢復,繼續(xù)執(zhí)行原本的指令流.
LDT(局部段描述符表)是全局段描述符表的補充,用于存放任務自己的段描述符信息,如何判斷一個索引值是LDT中的項還是GDT中的項取決于索引值中的TI屬性.
索引,也就是段選擇符的格式如下:
- RPI : 0-1位 : 請求特權級.
- TI : 2位 : 當TI為0時,說明使用的是GDT,當TI為1時,說明使用的是LDT.
- Index : 3-15位 : 段描述符的索引號.
舉個例子,0x8,轉換成2進制就是1000,呢么該索引使用GDT表中的第0x1項;0xC,轉換成2進制就是1100,呢么該索引使用LDT表中的第0x1項.
init_task是Linux0.11中靜態(tài)分配好的任務,他處于任務結構數組task中的第0項,所以俗稱task0.
sched_init首先設置GDT表中的第5項指向task0的TSS,第6項指向task0的LDT.
set_tss_desc是一個宏,代碼如下:
#define set_tss_desc(n,addr) _set_tssldt_desc(((char *) (n)),addr,"0x89")
set_ldt_desc也是一個宏,代碼如下:
#define set_ldt_desc(n,addr) _set_tssldt_desc(((char *) (n)),addr,"0x82")
他們都調用了_set_tssldt_desc, _set_tssldt_desc的代碼如下:
#define _set_tssldt_desc(n,addr,type) \
__asm__ (
//設置段限長的0-15位為0x68
"movw $104,%1\n\t" \
//設置基地址的0-15位為eax的低16位
"movw %%ax,%2\n\t" \
//將eax高16位的內容移動到低16位中
"rorl $16,%%eax\n\t" \
//設置基地址的16-23位為eax低16位中的低8位
"movb %%al,%3\n\t" \
//設置TYPE為type,P,DPL,S為0
"movb $" type ",%4\n\t" \
//設置G,D/B,保留,AVL和段限長的16-19位為0
"movb $0x00,%5\n\t" \
//設置基地址的16-23位為eax低16位中的高8位
"movb %%ah,%6\n\t" \
//清零eax
"rorl $16,%%eax" \
//eax中存儲addr
//%1表示地址n,也就是段限長的0-15位
//%2表示地址n偏移2個字節(jié),也就是基地址的0-15位
//%3表示地址n偏移4個字節(jié),也就是基地址的16-23位
//%4表示地址n偏移5個字節(jié),也就是P,DPL,S,TYPE
//%5表示地址n偏移6個字節(jié),也就是G,D/B,保留,AVL和段限長的16-19位
//%6表示地址n偏移7個字節(jié),也就是基地址的24-31位
::"a" (addr), "m" (*(n)), "m" (*(n+2)), "m" (*(n+4)), \
"m" (*(n+5)), "m" (*(n+6)), "m" (*(n+7)) \
)
設置完成后的GDT表如下: