练习3:分析bootloader进入保护模式的过程
1.开启A20的原因
原来的8086和8088只有20位地址线,寻址空间为1024KB。而PC机的寻址结构是segment:offset,segment和offset均为16位,能表达的最大寻址空间为10ffefh,约为1088KB,超过了20位地址线的寻址空间。在8086中,由于没有第21位地址线,在寻址超过20位地址线的寻址空间而小于PC机最大寻址空间的地址时(100000h–10ffefh),会得到寻址地址减去100000h后地址的内容。80286和80386的出现使PC机的地址线从20位变成24位又变成32位,100000h以上的寻址空间可以访问。其中,100000h–10ffefh称为高端内存区,10ffefh以上称为扩展内存。为了保持与8086的兼容,PC机设计了A20 Gate开关,开关关闭时,第21位地址线不能用,在实模式下,80286和其后续系统所表现的行为和8086/8088所表现的完全一样;开关打开时,第21位地址线可以正常使用。在保护模式下,如果不打开A20 Gate,第21位地址线永远为0,那么系统只能访问奇数兆的内存,即只能访问0–1M、2-3M、4-5M…,这显然是不行的。因此要进入保护模式,应开启A20 Gate。
2.bootloader进入保护模式的过程
.set PROT_MODE_CSEG, 0x8 # kernel code segment selector
.set PROT_MODE_DSEG, 0x10 # kernel data segment selector
.set CR0_PE_ON, 0x1 # protected mode enable flag
前面两个段选择子的作用是提供了gdt中代码段和数据段的索引,后面的CR0_PE_ON
变量是开启A20地址线的标志,为1是开启保护模式。
关闭中断
start:
.code16 # Assemble for 16-bit mode
cli # Disable interrupts
cld # String operations increment
接下来的步骤不能被任何事情打扰,因此使用cli指令关闭中断。因为目前还处于实模式,所以需要这里的.code16告诉编译器代码以16位模式编译。
清理寄存器
# 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
开启A20
A20 Gate被定义在output port(64h)的bit 1上,这个端口的操作是通过向64h发送命令,然后在60h进行读写的方式完成,只要对其操作就可以控制A20的开关。但当我们要向8042的输入缓冲区里写数据时,里面可能还有其他的数据没有被处理,所以我们要禁止键盘操作,并等数据缓冲区里没有数据后,才能开启A20。
首先等待input buffer为空
seta20.1:
inb $0x64, %al # Wait for not busy(8042 input buffer empty).
testb $0x2, %al
jnz seta20.1
inb指令表示从I/O端口读取一个字节,testb指令表示执行and指令,但不会保存and执行的结果,而是根据and的结果设置flags寄存器,jnz指令表示ZF不为1时跳转。
inb $0x64, %al
是指从Status Register(64h)读取一个字节,并放入al寄存器中。之后testb $0x2, %al
判断al寄存器中第二位的值,即Status Register中的bit 1是不是1。如果是1,则代表输入缓冲中有数据,ZF被置位为1;如果是0,则代表输入缓冲中已经没有数据,ZF被置位为0。jnz seta20.1
判断ZF是否为1,如果为1,则代表输入缓冲中有数据,还要继续等待其为空,因此跳转到该程序段开头继续执行;如果ZF为0,程序继续向下执行。
接下来发送写Output Port命令
movb $0xd1, %al # 0xd1 -> port 0x64
outb %al, $0x64 # 0xd1 means: write data to 8042's P2 port
outb指令表示向I/O端口写入一个字节。
0xd1命令代表写output port,这段代码表示向64h端口发送0xd1命令,以便接下来向60h写入Output Port。
再次等待input buffer为空
seta20.2:
inb $0x64, %al # Wait for not busy(8042 input buffer empty).
testb $0x2, %al
jnz seta20.2
向写60h端口发送将A20 Gate置为1的指令(0xdf)
movb $0xdf, %al # 0xdf -> port 0x60
outb %al, $0x60 # 0xdf = 11011111, means set P2's A20 bit(the 1 bit) to 1
接下来开始初始化GDT表。
将GDT的入口地址装入GDTR(全局描述符表寄存器),从此以后,CPU就根据此寄存器中的内容作为GDT的入口来访问GDT
lgdt gdtdesc
这段代码中,lgdt指令是将GDT入口地址和大小以gdtdesc的结构放入寄存器里里。
设置cr0寄存器
movl %cr0, %eax
orl $CR0_PE_ON, %eax
movl %eax, %cr0
orl是按位或操作指令。
cr0主要用于存储两个标志位——保护模式和分页。打开保护模式标志位,相当于按下了保护模式的开关。cr0寄存器的第0位就是这个开关,这里通过将CR0_PE_ON或cr0寄存器,将第0位置1。
切换32位模式
# Jump to next instruction, but in 32-bit code segment.
# Switches processor into 32-bit mode.
ljmp $PROT_MODE_CSEG, $protcseg
.code32 # Assemble for 32-bit mode
protcseg:
ljmp $PROT_MODE_CSEG, $protcseg
是远跳转指令。PROT_MODE_CSEG为0000 0000 0000 1000,是GDT中的第1个段,即代码段。protcseg是偏移量。
.code32的作用和前面提到.code16对应,因为已经打开了cr0的保护模式标志,所以cpu按保护模式的方式寻址,这里应该告诉编译器切换到32位模式。protcseg为段内偏移。
重新初始化各个段寄存器,将代码段的开始地址赋值给各个寄存器
# Set up the protected-mode data segment registers
movw $PROT_MODE_DSEG, %ax # Our data segment selector
movw %ax, %ds # -> DS: Data Segment
movw %ax, %es # -> ES: Extra Segment
movw %ax, %fs # -> FS
movw %ax, %gs # -> GS
movw %ax, %ss # -> SS: Stack Segment
建立堆栈
# Set up the stack pointer and call into C. The stack region is from 0--start(0x7c00)
movl $0x0, %ebp
movl $start, %esp
至此,从实模式转换为保护模式完成。
call函数将控制权交给bootmain
call bootmain
# If bootmain returns (it shouldn't), loop.
spin:
jmp spin
构建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
第一个描述符必须是Null Segment,也就是全0。接下来是代码段和数据段,代码段是可读可执行的, 数据段是可写的。
三个参数分别是类型、基地址和长度。
构造GDTR的内容
gdtdesc:
.word 0x17 # sizeof(gdt) - 1
.long gdt # address gdt
GDTR由gdt大小和基地址构成,高位是大小,低位由地址组成。