linux 0.11目录,Linux0.11下的内存管理学习笔记(1)

阅读赵先生的Linux0.11内核分析有一段时间了,最近决定要分析2.6内核中的内存分配,但发觉基础不足,所以回头把Linux0.11中内存管理的部分又看了一下,写下学习笔记以加强自己的理解.

写的不对的地方请大家一定要拍砖指正~ = 3=)/

学习的框架如下:

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中,并跳转到复制后的地址中执行,如下图所示:

57901_090430122206.png

执行jmpi go,INITSEG后就由开始的Bootsect.s跳转到复制后的Bootsect.s中的标号go处继续执行.

然后Bootsect.s把Setup.s从磁盘中读取到内存位置0x90200处,如下图所示:

57901_090430122421.png

加载完Setup.s后在屏幕上打印"Loading system ...".

接着把SYSTEM,也就是LINUX0.11的内核读取到内存位置0x10000处,如下图所示:

57901_090430122434.png

然后使用指令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处,如下图所示:

57901_090430122538.png

然后准备进入保护模式之前的处理,首先加载一个临时的GDT表和设置IDT表基址寄存器,因为在进入保护模式之前关闭了中断,所以再开启中断之前不会读取IDT表的项目,所以把IDTR的基地址设置成0x0也不用担心会产生错误,如下图所示:

57901_090430122546.png

加载完成后便开启保护模式,然后跳到全局描述符表中的第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表,如下图:

57901_090430122452.png

然后head.s将main函数的参数和返回地址压入栈中,跳转到分页初始化中,Linux0.11在head.s中预留了5张页,每张页1024项,第1张页用来填写页目录项,其余4张页填写页表项,每张页可寻址4MB地址空间,4张页表寻址16MB,也就是Linux0.11默认支持的最大内存大小,如下图:

57901_090430122500.png

完成之后设置CR3寄存器为0x0,也就是页目录表的基地址.

分页设置完成后打开分页属性,之后保护模式下的地址经过分段处理后还要进行分页处理.

最后将执行中断返回,跳转到之前压入的main函数中.

介绍一下分页的寻址方法,分页的寻址方法和保护模式下的寻址方法差不多,也是进行查表寻址,在分页管理中,把32位的地址分成了3个部分:

1. 偏移地址:0-11位.

2. 页表索引号:12-21位.

3. 页目录索引号: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,过程如下图所示:

57901_090430122622.png

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种不同的分配方案:

1. 内存大小在12MB到16MB范围之内,则主内存区从4MB开始到最大.

2. 内存大小在6MB到12MB范围之内,则主内存区从2MB开始到最大.

3. 内存大小在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

//设置为已使用

mem_map[i] = USED;

//计算主内存区域的起始位置在第几个页帧

i = MAP_NR(start_mem);

//计算主内存区域的大小

end_mem -= start_mem;

//计算主内存区域占用多少个页

end_mem >>= 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中对应的项设置为已使用,不进行分配,从而在逻辑上消除了不对称的影响.

57901_090430122515.png

上图展示了一个拥有15MB内存时候mem_map的映像图,低于4MB,也就是内核区域设置为已使用,不进行分配,高于15MB,也就是高于物理内存的部分也设置为已使用,主内存区域设置为0,也就是未使用.

首先将mem_map中的项全部设置为已使用,如下图

57901_090430122525.png

然后根据主内存区域的起始位置和结束位置将mem_map数组中的对应项设置为未使用,如下图

57901_090430122508.png

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

{

task[i] = NULL;

//初始化tss描述符,清零

p->a=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属性.

索引,也就是段选择符的格式如下:

1. RPI : 0-1位 : 请求特权级.

2. TI : 2位 : 当TI为0时,说明使用的是GDT,当TI为1时,说明使用的是LDT.

3. 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表如下:

57901_090430122444.png

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值