#include // mmu.h 内含有需要使用的宏定义与函数
# Start the CPU: switch to 32-bit protected mode, jump into C. // 这些代码的作用为转换到 32 位保护模式,然后跳转到 main.c
# The BIOS loads this code from the first sector of the hard disk into // BIOS 读取硬盘第一扇区的内容到 0x7c00
# memory at physical address 0x7c00 and starts executing in real mode // 设置 CS、IP 寄存器,执行实模式
# with %cs=0 %ip=7c00.
.set PROT_MODE_CSEG, 0x8 # kernel code segment selector // 内核代码段 selector,用于寻找 GDT 条目
.set PROT_MODE_DSEG, 0x10 # kernel data segment selector // 内核数据段 selector,用于寻找 GDT 条目
.set CR0_PE_ON, 0x1 # protected mode enable flag // 用于设置 CR0 的 PE 位,目的为开启保护模式
.globl start // 设置全局符号 start
start:
.code16 # Assemble for 16-bit mode // 16位指令
cli # Disable interrupts // 屏蔽中断,Bootloader 执行过程中不响应中断
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 // 栈段寄存器
# Enable A20:
# For backwards compatibility with the earliest PCs, physical
# address line 20 is tied low, so that addresses higher than
# 1MB wrap around to zero by default. This code undoes this.
seta20.1:
inb $0x64,%al # Wait for not busy
testb $0x2,%al
jnz seta20.1
movb $0xd1,%al # 0xd1 -> port 0x64
outb %al,$0x64
seta20.2:
inb $0x64,%al # Wait for not busy
testb $0x2,%al
jnz seta20.2
movb $0xdf,%al # 0xdf -> port 0x60
outb %al,$0x60
以上代码用于打开 A20 gate,先来简单介绍下 A20 gate
IO Port | Access Type | Purpose |
---|---|---|
0x60 | Read/Write | Data Port |
0x64 | Read | Status Register |
0x64 | Write | Command Register |
Bit | Meaning |
---|---|
1 |
Input buffer status (0 = empty, 1 = full)
(must be clear before attempting to write data to IO port 0x60 or IO port 0x64)
|
Command Byte |
Meaning |
Response Byte |
0xD1 |
Write next byte to Controller Output Port
Note: Check if output buffer is empty first
|
None |
Bit | Meaning |
---|---|
1 | A20 gate (output) |
# Switch from real to protected mode, using a bootstrap GDT
# and segment translation that makes virtual addresses
# identical to their physical addresses, so that the
# effective memory map does not change during the switch.
lgdt gdtdesc
movl %cr0, %eax
orl $CR0_PE_ON, %eax // 参考文件开头宏定义 CR0_PE_ON = 0x1
movl %eax, %cr0
# Bootstrap GDT
.p2align 2 # force 4 byte alignment
gdt:
SEG_NULL # null seg
SEG(STA_X|STA_R, 0x0, 0xffffffff) # code seg
SEG(STA_W, 0x0, 0xffffffff) # data seg
gdtdesc:
.word 0x17 # sizeof(gdt) - 1
.long gdt # address gdt
/*
* Macros to build GDT entries in assembly. // mmu.h 中的片段
*/
#define SEG_NULL \
.word 0, 0; \
.byte 0, 0, 0, 0
#define SEG(type,base,lim) \ // 权限状态 基地址 大小
.word (((lim) >> 12) & 0xffff), ((base) & 0xffff); \ // 2 个 word 数据,即前 32 位,详细信息看下文图片
.byte (((base) >> 16) & 0xff), (0x90 | (type)), \ // 4 个 byte 数据,即后 32 位
(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
有关 GDT 的作用请自行上网搜索,一些相关资料如下:
The offset is the linear address of the table itself, which means that paging applies. The size is the size of the table subtracted by 1.
GDT Entry 是 GDT 中存放的条目,大小为 8 个字节,下图为结构,Base 代表段基地址,Limit 代表段大小,其他为一些状态权限相关的信息
第 5 行,lgdt 的作用为设置 GDTR,可以看到 gdtdesc 的信息从第 17 行开始
第 18 行,.word 的作用可以理解为设置一个大小为 word 的数据,由于 0x17 = 23,所以 GDTR 中的 Size 为 24,即存在 3 个 GDT Entry
第 19 行,.long 的作用与 .word 同理,可以看到 gdt 的信息从第 12 行开始
第 13、14、15 行,分别设置了NULL段,代码段,数据段,这里调用了 SEG 函数,相关定义可以从 mmu.h 中找到
上面几行执行的结果大概可以视作如下:
GDT[0] = { base = 0x0, limit = 0x0, type = 0x0 }
GDT[1] = { base = 0x0, limit = 0xffffffff, type = 0xA }
GDT[2] = { base = 0x0, limit = 0xffffffff, type = 0x2 }
回到第 6、7、8 行,通过设置 cr0 寄存器的 PE 位,将 CPU 从 real mode 转换到 protected mode,有关这两个模式的信息请自行上网搜索
.set PROT_MODE_CSEG, 0x8 # kernel code segment selector // 文件开头的宏定义,代码段的 selector
.set PROT_MODE_DSEG, 0x10 # kernel data segment selector // 文件开头的宏定义,数据段的 selector
# 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:
# 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
利用 Segment Selector 寻找 GDT Entry,然后根据 GDT Entry 的 Base 寻址,Segment Selector 的结构如下:
索引,即 GDT 表上的索引,用来获取 GDT 中的条目
TI,若为 1 则代表该条目为 LDT,这里没有用到 LDT,相关信息有兴趣可以到网上搜索
RPL,权限等级
接下来看代码:
第 6 行,ljmp 的作用为跳转到指定地址,可以看到 PROT_MODE_CSEG = 0x8 = 0000000000001000,即索引 = 1,TI = 0,RPL = 0
由于在保护模式下,根据上文得出的条目得知 GDT[1] = { base = 0x0, limit = 0xffffffff, type = 0xA }
所以线性地址为 0H + protcseg 的地址 = protcseg 的地址,即跳转到 protcseg 的地址(第 9 行)
第 8 行,代表下面的指令为 32 位指令
第 11 行,与第 6 行同理,得到 GDT[2] = { base = 0x0, limit = 0xffffffff, type = 0x2 } ,然后接下来的几行分别设置各个段寄存器
# Set up the stack pointer and call into C.
movl $start, %esp // 设置栈指针
call bootmain // 调用 bootmain 函数,位于 main.c
# If bootmain returns (it shouldn't), loop.
spin:
jmp spin
接下来,便执行 main.c 中的 bootmain 函数