• ucore-lab1-ex3


    分析 bootloader 进入保护模式的过程

    (要求在报告中写出分析)

    BIOS 将通过读取硬盘主引导扇区到内存,并转跳到对应内存中的位置执行 bootloader。请分析 bootloader 是如何完成从实模式进入保护模式的。提示:需要阅读小节“保护模式和分段机制”和 lab1/boot/bootasm.S 源码,了解如何从实模式切换到保护模式。

    代码简析

    因为8086的地址线是20bit,但是数据处理位宽是16bit,无法直接寻址实模式规定的1M地址空间,于是引入了一种地址转换机制,地址用(segment:offset)表示,segment和offset分别是16bit寄存器,物理地址用segment<<4+offset表示,所以不难计算最大的地址是0xffff0+0xffff=0x10ffef,约为1088KB,大于1MB,如果发生超过1MB的寻址,并不会认为寻址异常并且会在20bit处截断,例如 0x100000 会被认为是 0x0。这个现象叫做内存回绕(memory wrapping)。

    但是在80286上出现了问题,因为80286提供了24bit地址线,且提供了保护模式,这样可以访问的内存达到了16M,这时如果访问 0x100000 ,系统将实际访问这块内存,而不是访问 0x0 这块内存。因此为了保证向下兼容性,也就是人为的控制地址的长度,IBM使用键盘控制器上的一根输出线管理第21根地址线,叫做 A20 Gate。当 A20 打开的时候,寻址可以超过1M,关闭的时候不能超过 1M。

    下面讨论如何开启A20。

    PC机刚出现的时候,也许是为了降低成本,工程师使用8042键盘控制器来控制A20,但是实际上A20与键盘管理没有任何关系,下面是8042的逻辑图:

    开启部分在指导书中写的很详细,不再赘述,直接看bootasm中的汇编代码:

    .code16                                             # Assemble for 16-bit mode
        cli                                             # Disable interrupts
        cld                                             # String operations increment
    
        # Set up the important data segment registers (DS, ES, SS).
        xorw %ax, %ax                                   # Segment number zero
        movw %ax, %ds                                   # -> Data Segment
        movw %ax, %es                                   # -> Extra Segment
        movw %ax, %ss                                   # -> Stack Segment
    

    首先关中断,之后DF置为0,规定字符处理方向为从前向后,查阅i386关于初始的文档发现要把DS、ES和SS初始化为0,那么首先通过xor把%ax清零,之后分别赋给%ds、%es和%ss。

    因为现在处在实模式,如果想使用保护模式就需要打开A20,所以:

    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
    

    先读64h端口到%al,然后检查bit1是否为1,如果是,说明inputbuffer中还有数据,此时ZF=1,跳转到seta20.1;否则inputbuffer为空,这时向 64h 发送 0xd1,表示要写 output port 的 P2 端口;之后类似的等待inputbuffer为空,将 0xdf 输出到 0x60 端口,作为写入的参数,根据图示,此时A20置为1(11011111),A20打开。

    通过lgdt gdtdesc加载我们已经创建好的GDT:

    # 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中只有3个段描述符,所以sizeof(gdt)=24Byte,但是为什么要减一不是很懂,同时SEG_*的定义在asm.h中:

    #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
    

    我们不妨看看代码段的段描述符:

    发现base都是0,limit是0xfffff,G是1,说明访存粒度是4KB,所以此时逻辑地址等于物理地址。

    加载完GDTR之后,

        # Switch from real to protected mode, using a bootstrap GDT
        # and segment translation that makes virtual addresses
        # identical to physical addresses, so that the
        # effective memory map does not change during the switch.
        lgdt gdtdesc
        movl %cr0, %eax
        orl $CR0_PE_ON, %eax
        movl %eax, %cr0
    
        # Jump to next instruction, but in 32-bit code segment.
        # Switches processor into 32-bit mode.
        ljmp $PROT_MODE_CSEG, $protcseg
    

    可以参考i386的manual文档,以及保护模式的初始化,我们只需要设置CR0寄存器的PE(protection enabled)位即可。注意:x86汇编的mov指令从左往右读,和mips指令mv有区别。

    接下来使用ljmp把%cs替换为段选择子,index指向第一个段(data segment),在protcseg中,设置ds、es、fs、gs和ss几个寄存器,只有初始化frame pointer和stack pointer,再调用bootmain进行kernel的加载。

    至此,bootloader从实模式进入保护模式。主要的步骤:

    1. 初始化(关中断等);
    2. 打开 A20 Gate;
    3. 设置 GDT 和 GDTR 后加载;
    4. cr0 的 PE 置为1;
    5. 初始化段寄存器、栈指针等,加载内核。

    概念辨析

    一些问题

    在A20 Gate的激活过程中,指导书提到要禁止键盘输入指令:

    但是代码中好像并没有实现禁止键盘输入的指令,没看到类似下面代码的实现。

    movb $0xad, %al                                 # 0xad -> port 0x64
    outb %al, $0x64
    

    这里的原因暂时不清楚。

    参考资料

    激活A20地址详解
    Intel 80386 Programmer's Reference Manual

  • 相关阅读:
    韩寒做错了(update 4 12)。
    放弃IE6。
    阿弥陀佛,我没有“抄袭”。
    婚姻。
    爆牙齿饭否?
    地震之后——和妈妈对话。
    8年前,《西班牙,我为你哭泣。》
    在等决赛中提问。
    地震之后——中国互联网在黑夜中哭泣。
    年轻。
  • 原文地址:https://www.cnblogs.com/LuoboLiam/p/13548788.html
Copyright © 2020-2023  润新知