02/bootsect.s
.text
.globl start
.include "kernel.inc"
include the above file
.code16
start:
jmp code
gdt:
.quad 0x0000000000000000 # null descriptor
.quad 0x00cf9a000000ffff # cs
.quad 0x00cf92000000ffff # ds
.quad 0x0000000000000000 # reserved for further use
.quad 0x0000000000000000 # reserved for further use
gdt_48:
.word .-gdt-1 # 当前地址减gdt地址减1得到GDT的长度 # 知道是长度这个属性了,这样子计算的,那个小点,估计代表的是当前地址,看的时候要注意
.long GDT_ADDR
code:
xorw %ax, %ax
movw %ax, %ds # 数据段 = 0x0000
movw %ax, %ss # 堆栈段 = 0x0000
movw $0x1000,%sp # 保护模式前用的堆栈,不要让他覆盖到7c00处的引导程序即可
# 我们将加载内核到地址 0x10000
movw $0x1000,%ax #ax=0x1000
movw %ax, %es #es=ax
xorw %bx, %bx # es:bx 加载内核的目标地址
movw $KERNEL_SECT,%cx #cx= KERNEL_SECT, 72 # 内核大小,单位是,36k对于现在来说已经足够了
movw $1, %si # 0,跳过去,所以是1
rd_kern:
call read_sect # 入口参数:si是起始扇区数,es:bx是指定内存地址,从哪儿读到内存es:bx处的呢
addw $512, %bx #增量为512,一个扇区的大小
incw %si #si扇区的个数
loop rd_kern
我们先把内核读到0x10000这个临时地址,然后再把它搬移到0x0(进入保护模式后搬移)。这个函数讲起来有些烦,读者可以自己分析:)
cli # 就要进入保护模式了,所以关掉实模式下的中断
cld # 将内核的前512字节移到0x0
movw $0x1000,%ax #ax=0x1000
movw %ax, %ds #ds=ax
movw $0x0000,%ax #ax=0x0000
movw %ax, %es #es=ax
xorw %si, %si #si=0
xorw %di, %di #di=0
movw $512>>2,%cx #cx=128??????,控制传输的次数明白为啥是128了,因为movsl这个指令一次传送的是四个字节(双字)
rep
movsl #将DS:SI这段地址的N个字节复制到ES:DI指向的地址,恩,很有感觉了
为什么要这样做?因为内核的这个部分是load.s这个文件编译出来的(本课后面会介绍到),load.s会读取“真正的内核”到0x200处,但是在这一课,我们只准备打印"Hello World!",除此之外什么都不做。
xorw %ax, %ax
movw %ax, %ds # 复位 ds 为 0x0000
movw $GDT_ADDR>>4,%ax # (0x80000 + 256 * 8) >> 2 ####IDT_ADDR= 0x80000
# IDT_SIZE,=(256*8)
# GDT_ADDR= (IDT_ADDR+IDT_SIZE) # GDT 在 IDT的后面
movw %ax, %es # gdt所在的数据段
movw $gdt, %si #我感觉gdt标号在编译好的目标中应该相当于一个偏移的感觉
xorw %di, %di # 从ds:si 拷贝到 es:di中
movw $GDT_SIZE>>2,%cx # 拷贝数据段中的gdt到指定地址
rep
movsl
#打开A20 Gate Option 详细 为了读取1M以上的地址空间
# * 读60h端口,读output buffer
# * 写60h端口,写input buffer
# * 读64h端口,读Status Register
#seta20.1: inb $0x64,%al # Get status 读状态寄存器 in al,0x64
# testb $0x2,%al # Busy? test al,0x02
# jnz seta20.1 # Yes 占用则跳转
# movb $0xd1,%al # Command: Write
# outb %al,$0x64 # output port
#seta20.2: inb $0x64,%al # Get status
# testb $0x2,%al # Busy?
# jnz seta20.2 # Yes
# movb $0xdf,%al # Enable
# outb %al,$0x60 # A20
enable_a20:
inb $0x64, %al #inbyte(猜测的),读64h端口状态
testb $0x2, %al #看看是否繁忙
jnz enable_a20
movb $0xbf, %al
outb %al, $0x64 #貌似为什么不是60端口呢???,难道状态口和读写口都不分的吗
这种开启a20地址线的方法来自一本书:"The Undocumented PC",中文纸版是《PC技术内幕》,可惜已绝版。a20地址线通过键盘控制器一个端口使能(ibm早期这样设计),当系统启动时,该地址线是关闭的,使能它之后才能访问1MB以外的地址空间。
lgdt gdt_48 # 加载gdt地址到寄存器中
# 进入保护模式
movl %cr0, %eax
orl $0x1, %eax
movl %eax, %cr0 # 使能CR0 控制寄存器中的PE位(即第0位)
现在我们已经进入到保护模式了,是不是简单的另你不敢相信?呵呵
ljmp $CODE_SEL, $0x0
我们还需要进行一个绝对地址跳转,因为解码管线中预取了16位指令,需要刷新成后面的32位指令。关于ia32的指令预取和解码管线,网络上有很多相关的文章,建议读者阅读一下相关文章。这个指令跳转到0x08描述符选择子指向的偏移0x的指令处,并开始执行,这个描述符即GDT中的第2项:内核代码段描述符。代码就是load.s的开始处,一会我们开始分析load.s这个程序。
bootsector.s中的函数:
# 输入: si: LBA 地址,从0开始
# 输出 es:bx 读取扇区到这个内存地址
read_sect:
pushw %ax
pushw %cx
pushw %dx
pushw %bx
movw %si, %ax
xorw %dx, %dx
movw $18, %bx # 对于1.44M软盘:每磁道18扇区
divw %bx
incw %dx
movb %dl, %cl # cl = 扇区号
xorw %dx, %dx
movw $2, %bx # 每磁道2磁头
divw %bx
movb %dl, %dh # 磁头
xorb %dl, %dl # 软驱号
movb %al, %ch # 柱面
popw %bx # 读取到:es:bx
rp_read:
movb $0x1, %al # 读1个扇区
movb $0x2, %ah
int $0x13
jc rp_read
popw %dx
popw %cx
popw %ax
ret
.org 0x1fe, 0x90 # 填充nop指令,机器码是0x90
.word 0xaa55
Hello World Comes Back
当我们进入到保护模式后,所有的通用寄存器和段寄存器保持原来实模式的值,代码段从特权级0开始执行。load.s文件将从地址0处开始执行。