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

原文地址:https://www.cnblogs.com/LuoboLiam/p/13548788.html