【轉】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執(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個部分:

  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,這個地址保存了根文件系統(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種不同的分配方案:

  1. 內存大小在12MB到16MB范圍之內,則主內存區(qū)從4MB開始到最大.
  2. 內存大小在6MB到12MB范圍之內,則主內存區(qū)從2MB開始到最大.
  3. 內存大小在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屬性.
索引,也就是段選擇符的格式如下:

  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中靜態(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表如下:

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末鸠真,一起剝皮案震驚了整個濱河市官撼,隨后出現的幾起案子风皿,更是在濱河造成了極大的恐慌,老刑警劉巖庸诱,帶你破解...
    沈念sama閱讀 216,496評論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件佑稠,死亡現場離奇詭異,居然都是意外死亡猾编,警方通過查閱死者的電腦和手機瘤睹,發(fā)現死者居然都...
    沈念sama閱讀 92,407評論 3 392
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來答倡,“玉大人轰传,你說我怎么就攤上這事”衿玻” “怎么了获茬?”我有些...
    開封第一講書人閱讀 162,632評論 0 353
  • 文/不壞的土叔 我叫張陵港庄,是天一觀的道長。 經常有香客問我恕曲,道長鹏氧,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,180評論 1 292
  • 正文 為了忘掉前任佩谣,我火速辦了婚禮把还,結果婚禮上,老公的妹妹穿的比我還像新娘稿存。我一直安慰自己笨篷,他們只是感情好,可當我...
    茶點故事閱讀 67,198評論 6 388
  • 文/花漫 我一把揭開白布瓣履。 她就那樣靜靜地躺著率翅,像睡著了一般。 火紅的嫁衣襯著肌膚如雪袖迎。 梳的紋絲不亂的頭發(fā)上冕臭,一...
    開封第一講書人閱讀 51,165評論 1 299
  • 那天,我揣著相機與錄音燕锥,去河邊找鬼辜贵。 笑死,一個胖子當著我的面吹牛归形,可吹牛的內容都是我干的托慨。 我是一名探鬼主播,決...
    沈念sama閱讀 40,052評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼暇榴,長吁一口氣:“原來是場噩夢啊……” “哼厚棵!你這毒婦竟也來了?” 一聲冷哼從身側響起蔼紧,我...
    開封第一講書人閱讀 38,910評論 0 274
  • 序言:老撾萬榮一對情侶失蹤婆硬,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后奸例,有當地人在樹林里發(fā)現了一具尸體彬犯,經...
    沈念sama閱讀 45,324評論 1 310
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 37,542評論 2 332
  • 正文 我和宋清朗相戀三年查吊,在試婚紗的時候發(fā)現自己被綠了谐区。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,711評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡菩貌,死狀恐怖卢佣,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情箭阶,我是刑警寧澤虚茶,帶...
    沈念sama閱讀 35,424評論 5 343
  • 正文 年R本政府宣布戈鲁,位于F島的核電站,受9級特大地震影響嘹叫,放射性物質發(fā)生泄漏婆殿。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,017評論 3 326
  • 文/蒙蒙 一罩扇、第九天 我趴在偏房一處隱蔽的房頂上張望婆芦。 院中可真熱鬧,春花似錦喂饥、人聲如沸消约。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,668評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽或粮。三九已至,卻和暖如春捞高,著一層夾襖步出監(jiān)牢的瞬間氯材,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,823評論 1 269
  • 我被黑心中介騙來泰國打工硝岗, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留氢哮,地道東北人。 一個月前我還...
    沈念sama閱讀 47,722評論 2 368
  • 正文 我出身青樓型檀,卻偏偏與公主長得像冗尤,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子胀溺,可洞房花燭夜當晚...
    茶點故事閱讀 44,611評論 2 353

推薦閱讀更多精彩內容