#包含头文件"asm.h"
#include <asm.h>
# Start the CPU: switch to 32-bit protected mode, jump into C.
# The BIOS loads this code from the first sector of the hard disk into
# memory at physical address 0x7c00 and starts executing in real mode
# with %cs=0 %ip=7c00.
# BIOS会从第一个扇区中加载此代码到物理内存地址 0x7c00中
# 并且在实模式下以 %cs=0, %ip=7c00开始执行
.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
# start address should be 0:7c00, in real mode, the beginning address of the running bootloader
.globl start
start:
.code16 # Assemble for 16-bit mode
# cli命令是bootLoader的第一条指令,该指令把所有的中断都关闭
cli # Disable interrupts
# CLD使DF复位,即DF=0
# 通过执行cld指令可以控制方向标志DF,决定内存地址是增大(DF=0,向高地址增加)还是减小(DF=1,向地地址减小)
cld # String operations increment
# Set up the important data segment registers (DS, ES, SS).
# 这几条命令把三个段寄存器 ds, es 和 ss都清零
xorw %ax, %ax # Segment number zero
movw %ax, %ds # -> Data Segment
movw %ax, %es # -> Extra Segment
movw %ax, %ss # -> Stack Segment
# Enable A20:
# For backwards compatibility with the earliest PCs, physical
# address line 20 is tied low, so that addresses higher than
# 激活A20地址位。由于需要兼容早起pc,物理地址的第20位绑定为0
# 因此高于1MB的地址又回到0x00000
# 激活了A20,就可以访问所有4G内存,就可以使用保护模式
# A20由键盘控制器芯片8042管理,所以要给8042发送命令激活A20
# 8042有两个IO端口:0x60和0x64
# 激活流程为:
# 1、发送0xd1命令道0x64端口
# 2、发送0xdf道0x60
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
# 至此,A20激活完成
# 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.
# 进入保护模式需要指定一个临时的GDT,来翻译逻辑地址
# 此处使用的GDT通过gdtdesc段定义
# 它翻译得到的物理地址和虚拟地址相同,因此转换过程中的内存映射不发生改变
# 下面一条指令把gdtdesc标识符送入GDTR寄存器中
# 该信息包括GDT表的内存起始地址以及GDT表的长度
# GDTR寄存器由48个比特位构成,低16位表示长度,高32位表示内存中的起始地址
# gdtdesc是个标识符,标识着一个内存地址,从这个地址开始后的6个字节存放着GDT表的长度和起始地址
lgdt gdtdesc
#打开保护模式的标志位:将cr0寄存器的第0位置1
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.
# 上面的代码已经打开了保护模式了,所以这里要使用逻辑地址,而不是之前实模式的地址了。
# 这里用到了PROT_MODE_CSEG, 他的值是0x8。根据段选择子的格式定义,0x8就翻译成:
# INDEX TI CPL
# 0000 0000 0000 1 00 0
# INDEX代表GDT中的索引,TI代表使用GDTR中的GDT, CPL代表处于特权级。
# 需要通过长跳转来设置cs寄存器的值
ljmp $PROT_MODE_CSEG, $protcseg
.code32 # Assemble for 32-bit mode
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)
# 栈顶设定在start(即0x7c00处),call函数将返回地址入栈,将控制权交给bootmain
movl $0x0, %ebp
movl $start, %esp
call bootmain
# If bootmain returns (it shouldn't), loop.
spin:
jmp spin
# Bootstrap GDT
# #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) & oxf)), (((base)>>24) & 0xff)
.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
# 上面定义了3项GDT表的描述符,每项为8个字节,因此GDT表的总大小为24个字节(0x18)
# 这里存的是0x17,这是官方的约定
# 紧接着就是GDT的起始地址
gdtdesc: # 48位,低16位是limit,高32位是GDT表的基地址
# 此处放一个字(2 bytes)
.word 0x17 # sizeof(gdt) - 1
# 此处放4 bytes,为gdt的值(即GDT表的基地址)
.long gdt # address gdt
下面分析代码中所定义的GDT表的表项。GDT表的每一项称作段描述符(Segment Descriptor),每一项的大小为8个字节,段描述符的格式如下图所示:
而代码中宏SEG_ASM的三个参数type、base和limit对应上图中的三个颜色块。代码定义了3个段描述符,第一个是空描述符,第二个是代码段的段描述符,第三个是数据段的段描述符。
根据参数的值,可以知道代码段的段描述符的值为 0x00 CF 9A 00 00 FF FF,对应于段描述符的格式如下图所示:
可以看出,SEG_ASM(STA_X|STA_R, 0x0, 0xffffffff) 定义的GDT表中代码段的段描述符表示该段的基址为0x0,大小为0xFFFFF。
同理可分析SEG_ASM(STA_W, 0x0, 0xffffffff) 的结果。
参考资料:
https://www.cnblogs.com/fatsheep9146/p/5115086.html
https://www.cnblogs.com/zslhg903/p/5781882.html