学习的框架如下:
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执行引导程序的首地址
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之前重复执行movw
rep
//拷贝ds:si所指的数据到es:di
//每拷贝1次,si di自增 , 每次拷贝一个字
movw
//跳跃到INITSEG的偏移go的位置上
//执行完之后cs为INITSEG,ip为go
//也就是跳转到复制的bootsect.s中继续执行
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中,并跳转到复制后的地址中执行,如下图所示:
执行jmpi go,INITSEG后就由开始的Bootsect.s跳转到复制后的Bootsect.s中的标号go处继续执行.
然后Bootsect.s把Setup.s从磁盘中读取到内存位置0x90200处,如下图所示:
加载完Setup.s后在屏幕上打印"Loading system ...".
接着把SYSTEM,也就是LINUX0.11的内核读取到内存位置0x10000处,如下图所示:
然后使用指令jmpi 0,SETUPSEG跳转到0x90200地址处的第一条指令继续执行,也就是进入到了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字节,每字2个字节
mov cx,#0x8000
//直到cx为0之前重复执行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
//加载机器状态字
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字节,256*8 = 2048字节
.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处继续执行,第2个描述符为代码段描述符,其基地址为0x0,呢么就是执行物理地址0x0处的指令,setup.s程序之前将System模块移动到了0x0地址处,而System模块中的head.s代码处于模块头,也就是在0x0地址上,所以这里会执行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的地址,当执行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字节
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个字节,中段描述符256个,呢么大小就是256*8
.word 256*8-1 # idt contains 256 entries
//设置基地址为_idt
.long _idt
.align 2
.word 0
gdt_descr:
//设置限长,每个描述符为8个字节,描述符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字节,每项填充为0
_idt: .fill 256,8,0 # idt is uninitialized
_gdt:
//第1项为空
.quad 0x0000000000000000 /* NULL descriptor */
//第2项为系统代码描述符
// G=1,D/B=1
// P=1,S=1,TYPE=1010
// 基地址为0
// 因为0代表4KB,(4096 - 1)*4KB = 16MB
.quad 0x00c09a0000000fff /* 16Mb */
//第3项为系统数据描述符
// 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字节,每项填充为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,也就是页目录表的基地址.
分页设置完成后打开分页属性,之后保护模式下的地址经过分段处理后还要进行分页处理.
最后将执行中断返回,跳转到之前压入的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,这个地址保存了根文件系统所在设备号
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则设置缓冲区的结束位置为 4MB处
buffer_memory_end = 4*1024*1024;
//小于12MB则检测是否大于6MB
else if (memory_end > 6*1024*1024)
//大于6MB则设置缓冲区的结束位置为2MB处
buffer_memory_end = 2*1024*1024;
//小于6MB
else
//设置缓冲区的结束位置为1MB处
buffer_memory_end = 1*1024*1024;
//设置主内存的起始位置为缓冲区的结束位置
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中继续执行接下来的代码
move_to_user_mode();
//创建一个新进程task1完成init函数
if (!fork())
{ /* we count on this going ok */
init();
}
//task0负责进程调度
for(;;) pause();
}
main函数首先根据内存的不同大小设置主内存区域的开始和结束地址对于不同的内存大小,LINUX0.11对于主内存区实现了3种不同的分配方案:
- 内存大小在12MB到16MB范围之内,则主内存区从4MB开始到最大.
- 内存大小在6MB到12MB范围之内,则主内存区从2MB开始到最大.
- 内存大小在6MB之内,则主内存区从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>= 12;
//历遍主内存区域的页
while (end_mem-->0)
//设置内存管理数组对应的页为未使用
mem_map[i++]=0;
}
在LINUX0.11中使用一个mem_map的unsigned char数组来管理内存的分配状态,这个数组用于管理物理内存地址1M以上的页面,其中的每一项都对应内存中的一个页面, mem_map中有3840项,最大可管理3840*4KB=15MB的内存,对于物理内存不足16MB的情况,LINUX0.11将mem_map中对应的项设置为已使用,不进行分配,从而在逻辑上消除了不对称的影响.
上图展示了一个拥有15MB内存时候mem_map的映像图,低于4MB,也就是内核区域设置为已使用,不进行分配,高于15MB,也就是高于物理内存的部分也设置为已使用,主内存区域设置为0,也就是未使用.
首先将mem_map中的项全部设置为已使用,如下图
然后根据主内存区域的起始位置和结束位置将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;ia=p->b=0;
p++;
//初始化ldt描述符,清零
p->a=p->b=0;
p++;
}
//清除NT标志,这样在之后执行中断返回的时候不会导致嵌套执行
//将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);
//设置系统调用处理函数
set_system_gate(0x80,&system_call);
}
在分析sched_init前先分析一下TSS(任务状态段描述符)和LDT(局部段描述符表).
TSS(任务状态段描述符)用于保存任务状态,任务状态的结构如下:
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;
//协处理器信息
struct i387_struct i387;
};
任务状态保存了任务运行时的寄存器信息,这样在任务切换中就能迅速得到原先任务的状态,并恢复,继续执行原本的指令流.
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中静态分配好的任务,他处于任务结构数组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个字节,也就是基地址的0-15位
//%3表示地址n偏移4个字节,也就是基地址的16-23位
//%4表示地址n偏移5个字节,也就是P,DPL,S,TYPE
//%5表示地址n偏移6个字节,也就是G,D/B,保留,AVL和段限长的16-19位
//%6表示地址n偏移7个字节,也就是基地址的24-31位
::"a" (addr), "m" (*(n)), "m" (*(n+2)), "m" (*(n+4)), \
"m" (*(n+5)), "m" (*(n+6)), "m" (*(n+7)) \
)
设置完成后的GDT表如下: