分析 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从实模式进入保护模式。主要的步骤:
- 初始化(关中断等);
- 打开 A20 Gate;
- 设置 GDT 和 GDTR 后加载;
- cr0 的 PE 置为1;
- 初始化段寄存器、栈指针等,加载内核。
概念辨析
一些问题
在A20 Gate的激活过程中,指导书提到要禁止键盘输入指令:
但是代码中好像并没有实现禁止键盘输入的指令,没看到类似下面代码的实现。
movb $0xad, %al # 0xad -> port 0x64
outb %al, $0x64
这里的原因暂时不清楚。