【转】Linux0.11下的内存管理学习笔记(1)

学习的框架如下:
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个部分:

  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,过程如下图所示:

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

上图展示了一个拥有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;i<NR_TASKS;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表如下:

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,458评论 4 363
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,454评论 1 294
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,171评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,062评论 0 207
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,440评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,661评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,906评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,609评论 0 200
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,379评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,600评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,085评论 1 261
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,409评论 2 254
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,072评论 3 237
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,088评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,860评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,704评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,608评论 2 270

推荐阅读更多精彩内容