自己动手做一个CS,是一件有趣的事情。在编写的过程中,能够学习知识,增长见识。看到自己编写的系统在虚拟机上成功的运行起来,也是一件让人愉悦的事情。
本Boot源自《x86汇编语言-从实模式到保护模式》第17章的代码,在其基础上进行了有限的修改。
该Boot主要实现了以下一些功能:
1. 启动图形模式(因为整个CS都是在图形模式下运行);
2. 启动32位保护模式;
3. 启动内存页管理模式;
4. 将0-1M内存空间映射到0xc0000000-0xc0100000处(3G处);
5. 将CS代码从硬盘拷贝到0xc0010000处;
6. 将GDT、堆栈指针调整到0xc0000000以上;
7. 跳转到CS代码处开始运行;
详细的基础内容,请参考《x86汇编语言-从实模式到保护模式》一书,里面有非常详细的解答。
使用nasm进行编译,运行的虚拟机为virtualbox 4.3.24。
Boot具体的实现代码如下(mbr.asm):
;===============================================================================
;=== 本代码为系统Boot,功能包括: ===
;=== 1.进入32位保护模式 ===
;=== 2.启动页管理模式 ===
;=== 3.将0-1M内存空间映射到0xc0000000-0xc0100000处 ===
;=== -其中,系统代码在0xc0010000处开始; ===
;=== -系统进程的页表1指向新页面,保障后面加载用户进程的顺利进行 ===
;=== 4.调整gdt、堆栈指针到0xc0000000以上 ===
;=== 5.跳转到系统代码开始运行 ===
;===============================================================================
;===============================================================================
;=== 该处用于保存常量 ===
;===============================================================================
core_base_address equ 0x00010000 ;常数,内核加载的起始内存地址(64K起始)
core_start_sector equ 0x00000001 ;常数,内核的起始逻辑扇区号
core_sector_num equ 0x00000032 ;常数,内核代码的扇区个数
;-------------------------------------------------------------------------------
gdt_base_address equ 0x00008000 ;常数,gdt表的起始地址
gdt_code_high_dd equ 0x00cf9800 ;常数,gdt表中代码段的高双字
gdt_code_low_dd equ 0x0000ffff ;常数,gdt表中代码段的低双字
gdt_data_high_dd equ 0x00cf9200 ;常数,gdt表中数据段的高双字
gdt_data_low_dd equ 0x0000ffff ;常数,gdt表中数据段的低双字
boot_code_addr equ 0x00007000 ;常数,boot代码页的硬件地址
;-------------------------------------------------------------------------------
page_dir_address equ 0x1000 ;常数,页目录硬件地址(第1页)
page_1_address equ 0x2000 ;常数,第一页页表的硬件地址(第2页)
page_1_new_addr equ 0x3000 ;常数,将代码放入3G地址后,更新页表1地址
;===============================================================================
;=== 该处开始Boot代码 ===
;===============================================================================
SECTION mbr vstart=0x00007c00
cli ;中断机制尚未工作
xor ax, ax ;初始化ds和ss段寄存器
mov ds, ax
mov ax, cs
mov ss, ax
mov sp, 0x7c00
;-------------------------------------------------------------------------------
sti ;开中断,使BIOS中断正常运行
mov ax, 0x13 ;设置为图形模式320×200-256色
int 0x10
cli ;关中断
;------------------------在gdt表中安装代码和数据段------------------------------
;计算GDT所在的逻辑段地址
mov eax,[cs:pgdt+0x02] ;GDT的32位物理地址
;跳过0#号描述符的槽位
;创建1#描述符,保护模式下的代码段描述符
mov dword [eax+0x08],gdt_code_low_dd ;基地址为0,界限0xFFFFF,DPL=00
mov dword [eax+0x0c],gdt_code_high_dd ;4KB粒度,代码段描述符,向上扩展
;创建2#描述符,保护模式下的数据段和堆栈段描述符
mov dword [eax+0x10],gdt_data_low_dd ;基地址为0,界限0xFFFFF,DPL=00
mov dword [eax+0x14],gdt_data_high_dd ;4KB粒度,数据段描述符,向上扩展
;初始化描述符表寄存器GDTR
mov word [cs:pgdt],23 ;描述符表的界限(三个描述符)
lgdt [cs:pgdt] ;将gdt写入gdt寄存器
;----------------------------启动32位保护模式-----------------------------------
in al,0x92 ;南桥芯片内的端口
or al,0000_0010B
out 0x92,al ;打开A20
mov eax,cr0
or eax,1
mov cr0,eax ;设置PE位
;以下进入保护模式... ...
jmp dword 0x0008:flush ;16位的描述符选择子:32位偏移
;清流水线并串行化处理器
[bits 32]
flush:
mov eax,0x00010 ;加载数据段(4GB)选择子
mov ds,eax
mov es,eax
mov fs,eax
mov gs,eax
mov ss,eax ;加载堆栈段(4GB)选择子
mov esp,0x00007c00 ;堆栈指针,从0x7c00开始向下走
mov byte [0xa0000], 0x55 ;显示白点
;-------------------------将系统从硬盘读到64K处--------------------------------
mov eax, core_start_sector ;从逻辑扇区1处开始读取
mov ebx, core_base_address ;将系统放到64K开始处
mov ecx, core_sector_num ;连续读32个扇区共16K字节
rdcore:
call read_hard_disk_0
inc eax ;下一个逻辑扇区号
loop rdcore
;-----------------------------初始化页目录和页表--------------------------------
page:
mov eax, page_dir_address ;页目录最后一项指向页目录本身
mov ebx, eax
or ebx, 0x3 ;页硬件地址再加上存在位。0x3为RW=1,P=1
mov dword [eax+4092],ebx ;让最后一个页目录指向页目录页自己
;----------------------------将地址投影到高3G处---------------------------------
;将页表1的前256项初始化成硬件地址0x0~0x100000(1M)
mov eax, page_1_address
xor ebx, ebx
xor edx, edx
mov edx, 0x3 ;增加页存在位(P=1)和权限位
mov ecx, 256 ;一共初始化256页,共1M
page_1_init:
mov dword [eax+ebx], edx
add ebx, 4
add edx, 0x1000 ;每次增加一页
loop page_1_init
;让页目录表的第一项和第0x300项指向页表1
mov eax, page_dir_address
mov edx, page_1_address
add edx, 0x3
mov dword [eax+0x0], edx ;页目录第0项指向它,否则启动页管理后cp会不知所措
mov dword [eax+0xc00], edx ;页目录第0x300项,对应线性地址0xc0000000起始
;-----------------------------启动页式管理机制----------------------------------
;令CR3寄存器指向页目录,并正式开启页功能
mov eax, page_dir_address ;PCD=PWT=0
mov cr3,eax
;将GDT的线性地址映射到从0xc0000000开始的相同位置
sgdt [pgdt]
mov ebx,[pgdt+2]
add dword [pgdt+2],0xc0000000 ;GDTR也用的是线性地址
lgdt [pgdt]
mov eax,cr0
or eax,0x80000000
mov cr0,eax ;开启分页机制
;调整堆栈指针
add esp, 0xc0000000
;测试一下页管理开启情况
mov eax, paged
add eax, 0xc0000000
jmp eax
paged:
mov byte [0xa0002], 0x55 ;显示白点
;更新系统进程的页表1
mov eax, page_1_new_addr
add eax, 0x3
mov [0xfffff000], eax ;更新系统进程页表一的地址
mov byte [0xc00a0004], 0x55 ;显示白点
;-----------------------------进入系统开始执行----------------------------------
;直接跳到core代码起始处开始执行
mov eax, core_base_address
add eax, 0xc0000000
jmp eax
;-----------------------------读取硬盘扇区函数----------------------------------
read_hard_disk_0: ;从硬盘读取一个逻辑扇区
;EAX=逻辑扇区号
;DS:EBX=目标缓冲区地址
;返回:EBX=EBX+512
push eax
push ecx
push edx
push eax
mov dx,0x1f2
mov al,1
out dx,al ;读取的扇区数
inc dx ;0x1f3
pop eax
out dx,al ;LBA地址7~0
inc dx ;0x1f4
mov cl,8
shr eax,cl
out dx,al ;LBA地址15~8
inc dx ;0x1f5
shr eax,cl
out dx,al ;LBA地址23~16
inc dx ;0x1f6
shr eax,cl
or al,0xe0 ;第一硬盘 LBA地址27~24
out dx,al
inc dx ;0x1f7
mov al,0x20 ;读命令
out dx,al
.waits:
in al,dx
and al,0x88
cmp al,0x08
jnz .waits ;不忙,且硬盘已准备好数据传输
mov ecx,256 ;总共要读取的字数
mov dx,0x1f0
.readw:
in ax,dx
mov [ebx],ax
add ebx,2
loop .readw
pop edx
pop ecx
pop eax
ret
;===============================================================================
;=== 该处开始全局变量 ===
;===============================================================================
pgdt dw 0
dd gdt_base_address ;GDT的物理/线性地址:32K处开始
;-------------------------------------------------------------------------------
times 510-($-$$) db 0
db 0x55,0xaa