练习3

练习3: 分析bootloader进入保护模式的过程

练习3文档

相关阅读:保护模式和分段机制

  • 为何开启A20,以及如何开启A20

  • 如何初始化GDT表

  • 如何使能和进入保护模式

相关文件

为何开启A20,如何开启A20

背景知识

  1. A20是什么?

    A20为处理器的第21根地址线。A即Address。A0是第一根地址线,以此类推,A19是第20根地址线,A20是第21根地址线。

  2. 开启A20是什么?

    开启A20指的是开启A20 Gate

    A20 Gate是80286处理器为了兼容8086处理器的程序而设置的,一个控制第21根地址线(A20 Line)的与门。

  3. 为什么要有A20 Gate

    8086处理器有20根地址线(A0 ~ A19),可以寻址1MB内存。当物理地址达到0xFFFFF (11111111111111111111B)时,再加1,由于只能维持20位地址,进位会丢失导致地址变为0x00000 (00000000000000000000B)。

    而80286处理器有24根地址线(A0 ~ A23),可以寻址16M内存。当物理地址达到0x0FFFFF (000011111111111111111111B),再加1,得到的地址为0x100000 (000100000000000000000000)。若运行8086处理器的程序,会导致寻址出现错误。

    为了让80286处理器兼容8086处理器的程序,IBM在主板上添加了A20 Gate,当A20 Gate关闭(值为0)时,在与门的操作下,A20 Line的值始终为0,所以当0x(0)FFFFF [(0000)11111111111111111111B]增加1时80286处理器可以得到跟8086处理器一样的结果0x(0)00000 [(0000)00000000000000000000]。

  4. 如何开启A20 Gate

    A20 Gate的控制阀门在8042键盘控制器(8042芯片)内,为输出端口P2的第二个bit(P21引脚)。

    为开启A20 Gate,只需向P2端口写入第二个bit为1的一个字节即可。

问题分析

  1. 为何开启A20?

    为了使CPU可以正常访问高于16位的地址,为进入可寻址32位的保护模式做准备。

  2. 如何开启A20?

    以下为开启A20的代码:

        #    File:    bootasm.S
        #    Line:    25-43
    
        # Enable A20:
        #  For backwards compatibility with the earliest PCs, physical
        #  address line 20 is tied low, so that addresses higher than
        #  1MB wrap around to zero by default. This code undoes this.
    seta20.1:
        inb $0x64, %al                                  # Wait for not busy(8042 input buffer empty).
        testb $0x2, %al
        jnz seta20.1
    
        movb $0xd1, %al                                 # 0xd1 -> port 0x64
        outb %al, $0x64                                 # 0xd1 means: write data to 8042's P2 port
    
    seta20.2:
        inb $0x64, %al                                  # Wait for not busy(8042 input buffer empty).
        testb $0x2, %al
        jnz seta20.2
    
        movb $0xdf, %al                                 # 0xdf -> port 0x60
        outb %al, $0x60                                 # 0xdf = 11011111, means set P2's A20 bit(the 1 bit) to 1

    8042芯片的I/O端口为0x60~0x6f,实际上IBM PC/AT使用的只有0x60和0x64两个端口。

    8042芯片有四个8 bit长的寄存器:Input Buffer, Output Buffer, Status Register, Control Register

    为向输出端口P2写入数据,需要先将要写入的数据传入8042芯片的Output Buffer,然后让8042芯片会将Output Buffer的内容写入输出端口P2

    Output Buffer写入数据的流程为:向0x64端口发送0xd1,表示写入,然后再将要写入的数据传入0x60端口。

    在向Output Buffer写入数据前,需要先读取Status Register的数据已获取芯片状态,得到的结果第二个bit表示0x60和0x64端口是否有数据,若无数据才可正常写入。

    以下为具体步骤:

        #    File:    bootasm.S
        #    Line:    29-32
    
    seta20.1:
        inb $0x64, %al
        testb $0x2, %al
        jnz seta20.1

    首先,从0x64端口读入数据存入al,将al的值和0x02(00000010B)进行与操作,若得到结果不为0,即0x60和0x64端口内有数据,则跳转回标记seta20.1并重复这一操作,直到端口内无数据,则进行下一步。

        #    File:    bootasm.S
        #    Line:    34-35
    
        movb $0xd1, %al
        outb %al, $0x64

    然后将0xd1经al写入0x64端口,表示将要向0x60端口传入要写进输出端口P2的数据。

        #    File:    bootasm.S
        #    Line:    37-43
    
    seta20.2:
        inb $0x64, %al
        testb $0x2, %al
        jnz seta20.2
    
        movb $0xdf, %al
        outb %al, $0x60

    这里与seta20.1部分类似,从0x64端口获取状态,直到0x60和0x64端口内没有数据,即可执行下一部分。

    然后经al向0x60端口写入数据以传入Output Buffer,然后8042芯片自动将Output Buffer中的数据写入输出端口P2

    在这段代码中写入0x60端口的数据为0xdf (11011111B),其第二个bit为1,即表示开启A20 Gate。另外7个bit表示其它参数,这里不再做了解。

参考文献

如何初始化GDT表

背景知识

  1. 什么是GDT表

    Global Descriptor Table(全局描述符表)是IA32架构CPU的一种数据结构,它保存多个段描述符,其起始地址保存在全局描述符表寄存器(GDTR)中。GDTR长48位,其中高32位为基地址,低16位为段界限。

    其中,全局描述符表中第一个段描述符设定为空段描述符。GDTR中的段界限以字节为单位。对于含有N个描述符的描述符表的段界限通常可设为8*N-1。

    GDT表由汇编指令lgdt加载,该指令将GDT表地址加载到GDTR中。

  2. GDT表的作用

    在保护模式下,大多数内存管理和中断服务例程都是通过GDT表控制的。每个描述符(Descriptor)都存储着CPU可能需要的对象信息。

  3. GDT表存储的内容

    • 空描述符(不会被加载)

    • 一个代码段描述符(在内核中,其类型应设为0x9A)

    • 一个数据段描述符(其类型应设为0x92)

    • 一个TSS(Task State Segment 任务段描述符)(uCore中没有使用)

    • ...

问题分析

  1. 如何初始化GDT表

        #    File:    bootasm.S
        #    Line:    49
    
        lgdt gdtdesc

    gdtdesc即GDT表的地址(见下文),使用lgdt指令加载即可。

  2. uCore中GDT表的格式

        #    File:    bootasm.S
        #    Line:    77-86
    
    # Bootstrap GDT
    .p2align 2                                          # force 4 byte alignment
    gdt:
        SEG_NULLASM                                     # null seg
        SEG_ASM(STA_X|STA_R, 0x0, 0xffffffff)           # code seg for bootloader and kernel
        SEG_ASM(STA_W, 0x0, 0xffffffff)                 # data seg for bootloader and kernel
    
    gdtdesc:
        .word 0x17                                      # sizeof(gdt) - 1
        .long gdt                                       # address gdt

    gdt为存储的两个段描述符(以及之前的一个空的段描述符),gdtdesc为GDT表的段界限及段描述符(gdt)的起始地址。两个段描述符分别为:代码段描述符,数据段描述符。

    以下为代码中创建段描述符的宏以及其参数。

     //    File:    asm.h
     //    Line:    4-23
    
     /* Assembler macros to create x86 segments */
    
     /* Normal segment */
     #define SEG_NULLASM                                             \
         .word 0, 0;                                                 \
         .byte 0, 0, 0, 0
    
     #define SEG_ASM(type,base,lim)                                  \
         .word (((lim) >> 12) & 0xffff), ((base) & 0xffff);          \
         .byte (((base) >> 16) & 0xff), (0x90 | (type)),             \
             (0xC0 | (((lim) >> 28) & 0xf)), (((base) >> 24) & 0xff)
    
     /* Application segment type bits */
     #define STA_X       0x8     // Executable segment
     #define STA_E       0x4     // Expand down (non-executable segments)
     #define STA_C       0x4     // Conforming code segment (executable only)
     #define STA_W       0x2     // Writeable (non-executable segments)
     #define STA_R       0x2     // Readable (executable segments)
     #define STA_A       0x1     // Accessed

    由以上代码可知,代码描述符的类型(type)值为(0x90 | (STA_X|STA_R))(0x90 | 0x0A) = 0x9A,数据段描述符的类型(type)值为(0x90 | STA_W)(0x90 | 0x02) = 0x92,与文档描述一致。

参考文献

如何使能和进入保护模式

背景知识

  1. 什么是保护模式

    保护模式与实模式相对应,是在80286处理器之后现代Intel处理器主要的操作模式。在80386处理器或更高版本上,32位保护模式允许使用多个虚拟空间,每个虚拟空间最多具有4GB的可寻址内存,并能通过Rings限制可用的指令集使系统能够执行严格的内存和硬件I/O保护。

问题分析

  1. 如何进入保护模式

    在进入保护模式前,需要进行一系列操作:

    1. 禁用所有中断(执行cli命令)

    2. 启用A20 Line

    3. 使用适用于代码、数据和堆栈的段描述符加载GDT表

    做完准备工作后,就可以切换到保护模式了。

    控制实模式与保护模式切换的开关位于CR0寄存器。CR0是处理器内部的控制寄存器(Control Register, CR),它是32位的寄存器,包含了一系列用于控制处理器操作模式和运行状态的标志位,它的第1位是保护模式允许位,把该位置设为1后处理器就会进入保护模式。

     #    File:    bootasm.S
     #    Line:    50-52
    
     movl %cr0, %eax
     orl $CR0_PE_ON, %eax
     movl %eax, %cr0

    由于CR0是32位寄存器,所以我们需要先将其值存入EAX寄存器。EAX寄存器是一个32位寄存器,常用的AX寄存器是EAX的低16位。

    在存入数据后,对EAX的值与常量CR0_PE_ON进行位或操作,常量CR0_PE_ON的值为0x1,进行位或操作后可使EAX内数值的第1位变为1,即 将CR0状态标志位的保护模式允许设为打开。

    然后将EAX寄存器内的值传入CR0,这时CR0内第1位的值变成了1,其它位的值也没有发生变化,这就标志着保护模式的正常启动。

参考文献

  • 《x86汇编语言:从实模式到保护模式》,电子工业出版社;

Last updated