首先编写mbr.S,将写入磁盘的第一个扇区,用于加载loader。
%include "boot.inc"
SECTION MBR vstart=0x7c00 ;起始地址编译在0x7c00
mov ax,cs
mov ds,ax
mov es,ax
mov ss,ax
mov fs,ax
mov sp,0x7c00
mov ax,0xb800
mov gs,ax
;这个时候 ds = es = ss = 0 栈指针指向MBR开始位置
首先告诉编译器将程序的起始地址编译为0x7c00。
接下来用cs寄存器去初始化其他栈寄存器,由于我们是从jmp 0 : 0x7c00 (cs:ip) 指令过来的,所以此时cs为0。
接着初始化栈指针,由于MBR加载到0x7c00以上的位置,所以此时0x7c00一下的内存是安全的。
;清屏
;利用0x06号功能,上卷全部行,即可清屏
;INT 0x10 功能号:0x06 功能描述:上卷窗口
;输入:
;AH 功能号 = 0x06
;AL = 上卷行数 (如果为0,表示全部)
;BH = 上卷行属性
;(CL,CH) = 窗口左上角的(x,y)位置
;(DL,DH) = 窗口右下角的(x,y)位置
;无返回值
mov ax,0600h
mov bx,0700h
mov cx,0
mov dx,184fh
;因为VGA文本模式中,一行只能容纳80个字符,共25行
;下标从0开始,所以0x18 = 24, 0x4f = 79
int 0x10
接着利用BIOS的int10中断进行清屏操作,BIOS的int10号中断负责有关打印的例程。功能号0x06用于上卷行。具体介绍见注释。
mov eax,LOADER_START_SECTOR ;起始扇区lba地址
mov bx,LOADER_BASE_ADDR ;写入的地址
mov cx,4 ;待读入的扇区数
call rd_disk_m_16 ;以下读取程序的起始部分
jmp LOADER_BASE_ADDR + 0x300
;功能:读取硬盘n个扇区
rd_disk_m_16: ;eax = LBA扇区号 bx = 将数据写入的内存地址 cx = 读入的扇区数
mov esi,eax ;备份eax
mov di,cx ;备份cx
;读写硬盘
;第一步:设置要读取的扇区数
mov dx,0x1f2
mov al,cl
out dx,al
mov eax,esi ;恢复eax
;第二步:将LBA地址存入0x1f3 ~ 0x1f6
;LBA地址7~0位写入端口0x1f3
mov dx,0x1f3
out dx,al
;LBA地址15~8位写入端口0x1f4
mov cl,8
shr eax,cl
mov dx,0x1f4
out dx,al
;LBA地址23~16位写入端口0x1f5
shr eax,cl
mov dx,0x1f5
out dx,al
shr eax,cl
and al,0x0f ;LBA第24~27位
or al,0xe0 ;设置7~4位为1110,表示LBA模式
mov dx,0x1f6
out dx,al
;第三步:向0x1f7端口写入读命令,0x20
mov dx,0x1f7
mov al,0x20
out dx,al
;第四步:检测硬盘状态
.not_ready:
;同一端口,写时表示写入命令数,读时表示读入硬盘状态
nop
in al,dx
and al,0x88 ;第3位为1表示硬盘控制器已准备好数据传输
;第7位为1表示硬盘忙
cmp al,0x08
jnz .not_ready ;若未准备好,继续等
;第五步:从0x1f0端口读数据
mov ax,di
mov dx,256
mul dx
mov cx,ax
;di为要读取的扇区数,一个扇区有512字节,每次读入一个字
;共需di*512/2次
mov dx,0x1f0
.go_on_read:
in ax,dx
mov [bx],ax
add bx,2
loop .go_on_read
ret
;字符串声明 db == define byte dw == define word ascii一个字符占一个字节
;预留两个字节 其余空余的全部用0填满 为使检测当前扇区最后两字节为0x55 0xaa 检测是否为有效扇区
;510 = 512字节-2预留字节 再减去(当前位置偏移量-段开始位置偏移量)求出来的是剩余空间
times 510 - ($ - $$) db 0
db 0x55,0xaa
GDT是全局描述符表(Global Descriptor Table),用于索引描述符
寄存器GDTR(GDT Register)用来存储GDT的内存地址及大小,GDTR是一个48位寄存器
GDTR第0-15位为GDT界限,第16-47位为GDT内存起始地址
对此寄存器的访问只能通过lgdt指令,指令格式为 lgdt 48位内存数据
进入保护模式后,段的信息增加了很多,需要段描述符来描述一个段的各种属性。在保护模式下,内存访问依旧是“段基址:段内偏移地址”的形式,但段寄存器中是选择子,而不是段基址。
段基址在段描述符中,给出的选择子索引到描述符后,CPU自动从中取出段基址,再加上段内偏移地址便可得到物理地址。
注意:在保护模式下已经是32位地址线和32位寄存器,不需要再将段基址乘以16
其中0-2位为RPL位,表示请求特权级,0为最高特权级
段界限表示最高拓展到多少,实际的段界限边界值 = (描述符中段界限 + 1) * (段界限的粒度大小:4KB 或者 1) - 1
S表示该段是系统段还是数据段,为0表示系统段(硬件运行相关),为1表示数据段(软件运行相关)
DPL (Descriptor Privilege Level) 即描述符特权级,0为最高级别
P(Present),即段是否在内存中,存在则为1,不存在则为0。
AVL (AVaiLable),保留字段,软件可自行使用。
L 字段,用来设置是否为64位代码段,为1表示64位代码段,为0表示32位代码段
D/B字段,用来指定有效地址(端内偏移地址)及操作数的大小。对代码段来说,为D位,1表示32位,0表示16位。对栈段来说是B位,用来指定操作数大小,为1表示32位操作数,为0表示16位操作数
G表示粒度,为0则表示1字节,为1表示4KB字节
Type字段要和S字段搭配使用,具体表示如下图
%include "boot.inc"
section loader vstart=LOADER_BASE_ADDR
LOADER_STACK_TOP equ LOADER_BASE_ADDR
;构建gdt以其内部的描述符
GDT_BASE: dd 0x00000000
dd 0x00000000
CODE_DESC: dd 0x0000FFFF
dd DESC_CODE_HIGH4
DATA_STACK_DESC: dd 0x0000FFFF
dd DESC_DATA_HIGH4
VIDEO_DESC: dd 0x80000007 ;limit = (0xbffff-0xb8000)/4k = 0x7
dd DESC_VIDEO_HIGH4 ;此时dpl为0
GDT_SIZE equ $ - GDT_BASE
GDT_LIMIT equ GDT_SIZE - 1
times 60 dq 0 ;此处预留60个描述符空位
SELECTOR_CODE equ (0x0001<<3) + TI_GDT + RPL0 ;相当于(CODE_DESC - GDT_BASE)/8 + TI_GDT + RPL0
SELECTOR_DATA equ (0x0002<<3) + TI_GDT + RPL0 ;同上
SELECTOR_VIDEO equ (0x0003<<3) + TI_GDT + RPL0 ;同上
;total_mem_bytes 用于保存内存容量,以字节为单位
;当前偏移loader.bin文件头0x200字节
;loader.bin的加载地址是0x900
;故total_mem_bytes的地址是0xb00
total_mem_bytes dd 0
;以下是gdt的指针,前2字节是gdt界限,后4字节是gdt起始地址
gdt_ptr dw GDT_LIMIT
dd GDT_BASE
首先在GDT中构建代码段,栈段和显存段的段描述符。并预留60个段描述符位置。
注意:GDT的第0个描述符不可用,为了避免选择子忘记初始化而误访问到第0个段描述符。
接下来利用BIOS 0x15中断的e820h子功能检测内存容量
;人工对齐,total_mem_bytes(4) + gdt_ptr(6) + ards_buf(244) + ards_nr(2) = 256 字节
ards_buf times 244 db 0
ards_nr dw 0 ;用于记录ARDS结构体数量
loader_start:
; int 15h eax = 0000E820h, edx = 534D4150h ('SMAP') 获取内存布局
xor ebx, ebx ;第一次调用时,ebx要为0
mov edx, 0x534d4150 ;edx只赋值一次,循环体中不会改变
mov di, ards_buf ;ards结构缓冲区
.e820_mem_get_loop: ;循环获取每个ARDS内存范围描述结构
mov eax, 0x0000e820 ;执行int 0x15后, eax会变为0x534d4150
;所以每次int前都要将eax更新为功能号
mov ecx, 20 ;ARDS地址范围描述符结构大小是20字节
int 0x15
add di, cx ;使di增加20字节指向下一个位置
inc word [ards_nr] ;记录ARDS的数量
cmp ebx, 0 ;若ebx为0,说明ards全部返回
;当前已是最后一个
jnz .e820_mem_get_loop
;在所有ards中,
;找出base_add_low + length_low 的最大值,即内存的容量
mov cx, [ards_nr]
;遍历每一个ARDS结构体,循环次数是ARDS的数量
mov ebx, ards_buf
xor edx, edx ;edx是最大内存的数量,在此先清0
.find_max_mem_area:
;无需判断type是否为1,最大的内存块一定是可被使用的
mov eax, [ebx] ;base_add_low
add eax, [ebx+8] ;length_low
add ebx, 20 ;指向缓冲区中下一个ARDS结构
cmp edx, eax
;冒泡排序,找出最大,edx寄存器始终是最大的内存容量
jge .next_ards
mov edx, eax
.next_ards:
loop .find_max_mem_area
jmp .mem_get_ok
.mem_get_ok:
;将内存转为byte后存入total_mem_bytes处
mov [total_mem_bytes], edx
;---准备进入保护模式---
;1 打开A20
;2 加载gdt
;3 将cr0的pe位置1
;第一步:打开A20
in al,0x92
or al,0000_0010B
out 0x92, al
;第二步:加载GDT
lgdt [gdt_ptr]
;第三步:cr0第3位置1
mov eax, cr0
or eax, 0x00000001
mov cr0, eax
jmp dword SELECTOR_CODE:p_mode_start ;刷新流水线
[bits 32]
;伪指令,接下来将按照32位质量格式编译
p_mode_start:
mov ax, SELECTOR_DATA
mov ds, ax
mov es, ax
mov ss, ax
mov esp,LOADER_STACK_TOP
mov ax, SELECTOR_VIDEO
mov gs, ax
mov byte [gs:160], 'P'
jmp $
1.打开A20地址总线
2.加载GDT到GDTR中
3.将控制寄存器CR0的PE位(Protection Enable)置1
注意:开启保护模式后,将使用32位指令格式,为了避免之前送上流水线的16位格式指令引发错误,需要刷新流水线。