Linux-0.11内核分析01:从开机加电到执行main函数之前的过程

1. 启动BIOS,准备实模式下的中断向量表和中断服务程序(ISR)

  • 按下电源键,处理器(IA-32)进入16位实模式,从CS:IP=0xFFFF0处开始运行。
  • BIOS程序在主板的一块ROM芯片中,该芯片无需初始化即可直接读取,被接在处理器的0xFE000-0xFFFFF地址处。
  • BIOS程序的入口地址被设计为0xFFFF0,因此上电过后处理器实际上运行的是BIOS程序。
  • BIOS程序将中断向量表放在内存的0x00000处,长度为1KB(256个中断,每个中断向量为4B),在0x00400处放BIOS数据区,长度为256B,在0x0E05B的处放ISR,大小约8KB。

2. 加载操作系统内核程序并为保护模式做准备

  • BIOS触发int 0x19中断,在中断向量表中查询到该ISR的地址为0x0E6F2,处理器跳转到此处执行。
  • 该ISR将读取软盘第一个扇区(512B)的数据到内存0x07C00处,这个扇区中的程序由bootsect.s文件编译生成,这是Linux 0.11的第一个程序。
  • 退出中断后,处理器跳回BIOS程序段,BIOS再将CS=0, IP=0x07C0,这样就可以跳转到0x07C00处执行。
  • bootsect.s程序第一件事是拷贝0x07C00开始的内容到0x90000处,长度为512B,即: 将bootsect.s自身移动到0x90000处。这一步目的是按照自己的需求安排内存。
    boot/bootsect.s
    ------------------------------------------------------
    mov ax,#BOOTSEG
    mov ds,ax           ; ds=0x07C0
    mov ax,#INITSEG
    mov es,ax           ; es=0x9000
    mov cx,#256         ; 循环次数256次
    sub si,si           ; si=0x0000
    sub di,di           ; di=0x0000
    rep
    movw                ; 一次mov一个字,ds:si-->es:di
  • 拷贝完成后,CS=0x9000,处理器跳转到CS:IP处执行。这里巧妙利用了jmpi指令。
    boot/bootsect.s
    ------------------------------------------------------
    jmpi    go,INITSEG  ; jmpi 为段间跳转指令,即跳转到go标号处,然后将cs=INITSEG
    go: 
    mov ax,cs
  • 将DS, ES, SS都设置为0x9000,SP设置为0xFF00,此时栈地址为SS:SP=0x9FF00。设置栈地址后就可以使用PUSH和POP指令了。
    boot/bootsect.s
    ------------------------------------------------------
    go: 
        mov ax,cs
        mov ds,ax
        mov es,ax
    ! put stack at 0x9ff00.
        mov ss,ax
        mov sp,#0xFF00      ! arbitrary value >>512
  • 将软盘中第二个扇区开始的4个扇区(setup.s)拷贝到0x90200中,刚好紧挨bootsect的结尾。该拷贝使用int 0x13中断完成。
    boot/bootsect.s
    ------------------------------------------------------
    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
  • 将软盘中第六个扇区开始的240个扇区拷贝到0x10000往后的120KB的空间中,这120KB的内容就是内核程序,也就是system。该拷贝过程与第二扇区的拷贝基本相同。
  • 确认根设备号,并跳转到0x90200(setup.s)中执行。跳转仍然使用jmpi指令
    boot/bootsect.s
    ------------------------------------------------------
        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
    ; 跳转到0x90200,即setup.s
        jmpi    0,SETUPSEG
  • setup.s开始,此时中断向量表和ISR还在0x0开始地址,所以可以利用BIOS提供的ISR将一些机器系统数据存放到0x90000-0x901FD处,共占510B,刚好将bootsect.s程序覆盖,bootsect不再使用。以下省略了大部分代码。
    boot/setup.s
    ------------------------------------------------------
    start:
        ; 保存光标位置
        mov ax,#INITSEG ! this is done in bootsect already, but...
        mov ds,ax
        mov ah,#0x03    ! read cursor pos
        xor bh,bh
        int 0x10        ! save it in known place, con_init fetches
        mov [0],dx      ! it from 0x90000.
        ; 保存外接内存大小
        mov ah,#0x88
        int 0x15
        mov [2],ax
        ...

3. 开始向32位模式转变,为main函数的调用做准备

  • 关中断,将system从0x10000移动到0x00000地址处,此时一复制,实模式下的中断向量表和ISR就被覆盖了。下面这段代码实际上是将0x10000-0x90000的内容整体左移了0x10000
    boot/setup.s
    ------------------------------------------------------
        cli         ! no interrupts allowed !
        mov ax,#0x0000
        cld         ! 'direction'=0, movs moves forward
    do_move:
        mov es,ax       ! destination segment
        add ax,#0x1000
        cmp ax,#0x9000
        jz  end_move
        mov ds,ax       ! source segment
        sub di,di
        sub si,si
        mov     cx,#0x8000
        rep
        movsw           ; 一次传送一个字
        jmp do_move
  • 建立一个空的中断描述符表(IDT)放在0x0地址处,然后建立一个临时的全局描述符表(GDT)。这两个表可以由用户随意放在内存的合适位置,放置好后使用lidt和lgdt指令将其地址传给IDTR和GDTR寄存器,以后硬件就能够根据IDTR和GDTR的值来找到IDT和GDT。这两个寄存器都是48位的寄存,其中高32位表示表的基地址,低16位表示限长。
    boot/setup.s
    -------------------------------------------------------------------
    end_move:
        mov ax,#SETUPSEG    ; right, forgot this at first. didn't work :-)
        mov ds,ax
        lidt    idt_48      ; load idt with 0,0
        lgdt    gdt_48      ; load gdt with whatever appropriate
    ...
    gdt:
        .word   0,0,0,0     ; dummy

        .word   0x07FF      ; 8Mb - limit=2047 (2048*4096=8Mb)
        .word   0x0000      ; base address=0
        .word   0x9A00      ; code read/exec
        .word   0x00C0      ; granularity=4096, 386

        .word   0x07FF      ; 8Mb - limit=2047 (2048*4096=8Mb)
        .word   0x0000      ; base address=0
        .word   0x9200      ; data read/write
        .word   0x00C0      ; granularity=4096, 386

    idt_48:
        .word   0           ; idt limit=0
        .word   0,0         ; idt base=0L

    gdt_48:
        .word   0x800       ; gdt limit=2048, 256 GDT entries
        .word   512+gdt,0x9 ; gdt base = 0X9xxxx
    ; 这里 0x9是段地址,512+gdt是偏移量,加起来就是0x90200+gdt刚好是前面gdt标号的地址
  • 打开A20,实现32位寻址。A20是通过键盘控制器8042打开的,打开过后可以访问0x100000以上的地址,而不会回滚。
    boot/setup.s
    -------------------------------------------------------------------
    ; A20打开后,可以访问0x100000-0x10FFEF的地址,不会回滚
    call    empty_8042
    mov al,#0xD1        ; command write
    out #0x64,al        ; 访问键盘控制器,打开A20
    call    empty_8042
    mov al,#0xDF        ; A20 on
    out #0x60,al
    call    empty_8042
  • 对中断控制器8259进行重新编程,目的是空出int 0x00~0x1F做内部使用(P23)。
  • 配置CR0,打开32位保护模式,并跳转到内核文件的head.s执行。
    boot/setup.s
    -------------------------------------------------------------------
    ! 切换到保护模式,保护模式下,CS不是代码段基地址,而是代码段选择符
    mov ax,#0x0001  ; protected mode (PE) bit
    lmsw    ax      ; This is it!
    jmpi    0,8     ; jmp offset 0 of segment 8 (cs)

这里执行jmpi后,IP=0,CS=8,由于在保护模式下CS的值表示段选择符,CS=8=0b1000,其中低两位00表示内核特权级,第三位0表示GDT,最高位1表示第1项。
查看GDT的第1项:
0x 00C0 9A00 0000 07FF
0b 0000000011000000 1001101000000000 0000000000000000 0000011111111111
表示:段基址0x00000000,内核特权级,代码段,段限长0x7FF*4KB=8MB(详细解释在P28)
所以这里实际上是跳转到了0x0处执行,也就是head.s

  • 设置页基地址pg_dir,然后设置DS、ES、FS和GS =0x10,表示保护模式下的段选择符
    boot/head.s
    -------------------------------------------------------------------
    .text
    .globl idt,gdt,pg_dir,tmp_floppy_area
    pg_dir:     # 该标号表示分页机制完成后内核的起始地址
    .globl startup_32
    startup_32:
        movl $0x10,%eax
        mov %ax,%ds     # 将ds, es, fs, gs设置为保护模式下的段选择符
        mov %ax,%es     # 0x10=0b10000,和cs=8意义类似
        mov %ax,%fs     # 最后两位00表示内核特权级,第三位0表示GDT
        mov %ax,%gs     # 最高两位10表示第2项
        ...

此处的0x10和之前的CS=8类似,0x10=0b10000,最后两位表示内核特权级,第三位0表示GDT,最高两位10表示表的第二项。
查看GDT第二项:0x00C0 9200 0000 07FF
表示段基址0x0,内核级,数据段,段限长8MB

  • 设置SS为段选择符0x10,同时设置ESP=0x1E25C。
    boot/head.s
    -------------------------------------------------------------------
        lss stack_start,%esp

lss指令是80386以后才有的指令,目的是同时设置SS和ESP。这里stack_start为48位的结构体,其中低32位为栈地址,高16位为0x10,以上指令将esp=栈地址(0x1E25C),然后将ss=0x10。0x10的分析方法和上面类似,是同一个段选择符。

  • 循环填充IDT,长度为256,填充内容是:ignore_int的高16位 | 0x8E00 | 0x0008 | ignore_int的低16位(中断描述符的详细构造在P31),然后将IDTR设置为当前IDT地址。
    boot/head.s
    -------------------------------------------------------------------
        call setup_idt
        ...
    setup_idt:
        lea ignore_int,%edx
        movl $0x00080000,%eax
        movw %dx,%ax        /* selector = 0x0008 = cs */
        movw $0x8E00,%dx   /* interrupt gate - dpl=0, present */
        /* eax= 0x0008<<16 | ignore_int的低16位 
         * edx= ignore_int的高16位<<16 | 0x8E00
         * 中断描述符见P31,表示中断服务程序偏移地址为ignore_int,段选择符
         * 为0x8,P(段存在标志)=1,DPL(特权等级)=00,TYPE(段描述符类型)=0111
         */
        lea idt,%edi
        mov $256,%ecx
        /* 将256个中断描述符全部设置为以上内容,即全部指向ignore_int */
    rp_sidt:
        movl %eax,(%edi)
        movl %edx,4(%edi)   # 表示[edi+4]
        addl $8,%edi
        dec %ecx
        jne rp_sidt
        lidt idt_descr      # 设置IDTR寄存器(48位)的值
        ret
        ...
    ignore_int:
        pushl %eax
        pushl %ecx
        pushl %edx
        ...
    .align 2
    .word 0
    idt_descr:
        .word 256*8-1       # idt contains 256 entries
        .long idt
    idt:    .fill 256,8,0       # idt is uninitialized
                # 在内存中创建256个8字节长区域,初始化为0
  • 重新构建GDT,将第一项和第二项的限长该为0xFFF,也就是16MB,然后给后面的250多项真正的分配了内存空间。
    boot/head.s
    -------------------------------------------------------------------
        call setup_gdt
        ...
    setup_gdt:
        lgdt gdt_descr
        ret
        ...
    .align 2
    .word 0
    gdt_descr:
        .word 256*8-1       ; so does gdt (not that that's any
        .long gdt       ; magic number, but it works for me :^)
    ...
    gdt:    
        .quad 0x0000000000000000    /* NULL descriptor */
        .quad 0x00c09a0000000fff    /* 16Mb */
        .quad 0x00c0920000000fff    /* 16Mb */
        .quad 0x0000000000000000    /* TEMPORARY - don't use */
        .fill 252,8,0           /* space for LDT's and TSS's etc */
  • 重新设置DS、ES、FS、GS和SS = 0x10。
    boot/head.s
    -------------------------------------------------------------------
        movl $0x10,%eax        # reload all the segment registers
        mov %ax,%ds     # after changing gdt. CS was already
        mov %ax,%es     # reloaded in 'setup_gdt'
        mov %ax,%fs
        mov %ax,%gs
        lss stack_start,%esp
  • 通过向0x000000写一个数,然后和0x10000比较是否相等来判断A20是否打开。
    boot/head.s
    -------------------------------------------------------------------
        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
  • 检测x87协处理器是否存在。
    boot/head.s
    -------------------------------------------------------------------
        movl %cr0,%eax      # check math chip
        andl $0x80000011,%eax  # Save PG,PE,ET
    /* "orl $0x10020,%eax" here for 486 might be good */
        orl $2,%eax        # set MP
        movl %eax,%cr0
        call check_x87
        jmp after_page_tables
  • 将L6标号和main函数压栈,然后跳转到setup_paging函数。此时栈顶为main函数地址,目的是head程序执行完返回后,以立即执行main函数,main函数不应该跳出,若跳出则接着执行L6
    boot/head.s
    -------------------------------------------------------------------
    after_page_tables:
        pushl $0       # These are the parameters to main :-)
        pushl $0
        pushl $0
        pushl $L6      # return address for main, if it decides to.
        pushl $main
        jmp setup_paging
    L6:
        jmp L6
  • 开始建立页目录和页表。将0x0开始的5KB内存清零,0x0000~0x1000为页目录,0x1000~0x1FFF为第1页,0x2000~0x2FFF为第2页,0x3000~0x3FFF为第3页,0x4000~0x4FFF为第4页。将页目录的前4项分别指向4个页表,然后在0x4FFB中存储0xFFF007,在0x4FF7中存储0xFFE007……直到将所有的页表填满,此时页表建立完毕,将16MB的内存分为了4K页,每页4KB,使用页目录可查询到各页表,使用各页表可以查询到各页。
    boot/head.s
    -------------------------------------------------------------------
    .org 0x1000
    pg0:

    .org 0x2000
    pg1:

    .org 0x3000
    pg2:

    .org 0x4000
    pg3:

    .org 0x5000
    ...
    .align 2
    setup_paging:
        movl $1024*5,%ecx      /* 5 pages - pg_dir+4 page tables */
        xorl %eax,%eax
        xorl %edi,%edi          /* pg_dir is at 0x000 */
        cld;rep;stosl           /* 清空0x0开始的5K*4B的空间 */
        /* 将后4个页表地址填写到页目录中 */
        movl $pg0+7,pg_dir     /* set present bit/user r/w */
        movl $pg1+7,pg_dir+4       /*  --------- " " --------- */
        movl $pg2+7,pg_dir+8       /*  --------- " " --------- */
        movl $pg3+7,pg_dir+12      /*  --------- " " --------- */
        /* 
         * 16MB内存被分为4K页,每页为4KB
         * 这里填充pg3的最后一个页表项,指向16MB内存的最后一页(0xfff000) 
         * 为了按4字节对齐,这里只取0xfff007的高12位,最低位的7(0x111)
         * 表示用户、读写、存在p,若是0(0x000)表示内核,只读,不存在p 
         */
        movl $pg3+4092,%edi
        movl $0xfff007,%eax        /*  16Mb - 4096 + 7 (r/w user,p) */
        std
    1:  stosl           /* fill pages backwards - more efficient :-) */
        /* 开始循环填充,注意这里的pg0存的是0x0,pg0+4存的是0x1000 */
        subl $0x1000,%eax
        jge 1b

这里页表中存的各页的起始地址,因为要4KB对齐,所以看其中的高12位,低12位表示权限,如这里的0xFFF007,高12位是最后一页的地址,即0xFFF000,低12位的0x7=0b111表示用户、读写、存在p。建立页表的程序被分配在了0x5000以后,因此不会被页表覆盖

  • 将CR3指向页目录表,打开CR0的分页机制开关。
    boot/head.s
    -------------------------------------------------------------------
    /* 将CR3置零,其中的高20位表示页目录基地址,即pg_dir=0x0为也目录的基地址 */
    xorl %eax,%eax      /* pg_dir is at 0x0000 */
    movl %eax,%cr3      /* cr3 - page directory start */
    /* 将CR0的最高位置1,打开分页机制 */
    movl %cr0,%eax
    orl $0x80000000,%eax
    movl %eax,%cr0      /* set paging (PG) bit */
  • 跳转到main函数执行
    boot/head.s
    -------------------------------------------------------------------
    ret

由于前面已经将main函数的地址压到了栈中,因此只需ret即可将main地址弹出给EIP,处理器跳到main函数执行。此时仍处于关中断的状态

猜你喜欢

转载自blog.csdn.net/egean/article/details/81000493