【完整代码已经归档到 : https://github.com/linzhanglong/mini_bootloader】
我们知道,系统启动会加载磁盘的MBR扇区到内存0x7c00那里去执行。但是毕竟MBR扇区只有512个字节,如果要实现设置GDT,读取内核,引导内核等功能,这512字节显得力不从心。所以我们这里第一步就是通过MBR去磁盘读取一块更大的空间到内存然后去执行这部分代码(称为 stage2)。这里我们尤其要注意一点就是,系统刚从BIOS启动执行磁盘MBR数据,CPU的工作模式还是实模式,寄存器都是16位的。通过段寄存器,最大可以支持的内存是[0xffff,0xffff] = 0xffff * 16 + 0xffff = 1M。总共只能索引1M的内存,而每一个段内只能索引64K范围。
我们这里先规划我们的内存和磁盘布局:
我们现在实现的代码就是通过MBR代码,把stage2的代码加载到内存0x9000执行。
首先我们要先实现一个读取磁盘的函数接口,对于磁盘的结构理解,网上很多资源:http://www.cnblogs.com/joydinghappy/articles/2511948.html
我们这里要借助BIOS的中断服务来实现磁盘的读操作:
我们看一下对应的BIOS如何读取磁盘:
使用的中断号:
INT 13h AH=02h: Read Sectors From Drive[edit]
传入的参数:
AH 02h
AL Sectors To Read Count
CH Cylinder
CL Sector
DH Head
DL Drive
ES:BX Buffer Address Pointer
结果的返回:
CF Set On Error, Clear If No Error
AH Return Code
AL Actual Sectors Read Count
数据的存放:
磁盘数据存放的内存地址: ES:BX
注意事项:
1. CX寄存器包含了圆柱面号和扇区号,其中同心圆号占10bit,扇区号占6bit【所以一个圆盘最大扇区数目就是64 * 1024】,所以CX[0-5]表示扇区号,CX[6-15]表示同心圆号
CX = ---CH--- ---CL---
cylinder : 76543210 98
sector : 543210
2. 根据第一点我们可以知道,这里我们最大索引的磁盘空间是: 256 Head * 1024 Cylinder * 64 Sector * 512 byte = 8G
3. 因为数据拷贝存放的地址是放在ES:BX起始地址,我们知道一个段可以索引的大小是1M(BX:0xFFFF),所以拷贝的数据大小+拷贝的其实地址BX最好小于1M。
现在开始实现我们的读磁盘函数(文件名字 read_disk.asm):
;
;@ brief 这里实现把磁盘的数据拷贝到内存
;@ param dh:cx 从哪个扇区开始拷贝。al表示磁头号,bx决定哪个同心圆哪个扇区
;@ param al 拷贝多少个扇区数据
;@ return 如果成功,函数直接返回,数据存放到ES:BX的地址。
; 如果失败,打印一条错误日志,然后卡主
;@ notes 调用者需要先设置好数据的拷贝地址 : ES:BX的地址
;
read_diskdata:
pusha
mov [READ_SECTER_NR], al
;开始拷贝的扇区地址分为 (Head)8bit:(Cylinder)10bit:(Sector)6bit
;其中Head -> DH, Cylinder -> CH, Sector -> CL
;这里目前只支持拷贝drive0,也就是磁盘hda --> 0x80
mov dl, 0x80
;开始拷贝磁盘数据
mov ah, 0x02
int 0x13
;开始判断磁盘拷贝结果,如果CF表示磁盘拷贝失败
jc _READ_ERR
;判断拷贝的磁盘扇区数目是不是和我们要拷贝的一样,不是也报错
mov dl, al ;保存实际的拷贝的扇区数目到dl
mov al, [READ_SECTER_NR] ;我们想要拷贝的扇区数目al
cmp dl, al
jne _READ_ERR
;成功拷贝,打印一条日志,然后返回
mov bx, DISK_READ_OK
call print_string
popa
ret
_READ_ERR:
mov bx, ax
call print_hex
mov bx, DISK_READ_ERR
call print_string
jmp $ ;卡主
ret ;Never go there
;定义打印的字符串
DISK_READ_OK db 'Disk Read Ok', 0
DISK_READ_ERR db 'Disk Read Error', 0
;拷贝的扇区数目我们需要保存起来
READ_SECTER_NR equ 0x00
实现完磁盘的读取函数接口,现在我们就开始写MBR代码,MBR代码512字节大小,实现的功能,就是把磁盘地址512到512+32K(也就是第二个扇区开始,读取64个扇区)的数据加载到内存0x9000地址,然后跳过去执行。直接上代码-(文件名字 mbr.asm):
;
; Date 2017/11/28
; Authon: linzhanglong
; notes: 这里是MBR分区,用途:用于加载stage2代码到指定内存位置,然后执行
[org 0x7c00]
;定义几个变量
;根据https://www.kernel.org/doc/Documentation/x86/boot.txt,
;定义stage2的代码长度,要求512字节对齐!
;mbr工作在实模式,一个段内索引最大64k,这里定义stage2 32k大小
STAGE2_LEN equ 0x8000 ;32k
STAGE2_MAGIC equ 0x4433 ;用于校验我们是不是拷贝正常
; step 1 先初始化号堆栈,免得出异常[xxxxx, 0x8000]
mov bp, 0x8000
mov sp, bp
; step 2 先初始化stage2内存拷贝地址[0x9000, 0x9000 + 64k]
mov ax, 0x900
mov es, ax
mov bx, 0
; step 3 从磁盘位置[0x200, 0x200 + 64k] => [2 sector, 2 + 128 sector]
; 拷贝stage2代码到内存
mov dh, 0 ;(Head)8bit
mov cx, 2 ;(Cylinder)10bit:(Sector)6bit
mov ax, STAGE2_LEN
shr ax, 9 ;al <----- setor num
; step 4开始拷贝数据
call read_diskdata
;check load ok,直接读取stage2在内存最后两个字节,判断是不是魔数 STAGE2_MAGIC
;是表示我们加载没有错误,不是表示我们加载有问题
mov bx, STAGE2_LEN
sub bx, 2
mov ax, [es:bx]
cmp ax, STAGE2_MAGIC
jne _CHECK_ERR
mov bx , MSG_LOAD_STAGE2_OK
call print_string
jmp 0x9000 ;跳到stage2代码执行
_CHECK_ERR:
mov bx, MSG_LOAD_STAGE2_ERR
call print_string
jmp $
%include "print.asm"
%include "read_disk.asm"
MSG_LOAD_STAGE2_ERR db 'Load stage2 Err', 0
MSG_LOAD_STAGE2_OK db 'Load stage2 Ok', 0
times 510-($-$$) db 0
dw 0xaa55
最后就是stage2的代码了,目前这格代码只是打印一条日志,后面有时间在实现stage2的真正代码功能(文件名字 boot_kernel.asm):
;
; Date 2017/11/28
; Authon: linzhanglong
; notes: 这里是加载内核,目前先实现一个个打印功能。
[org 0x9000]
;目前只是简单打印一条日志,后面在实现功能
mov bx, MSG_BOOT_KERNEL_TEST
call print_string
jmp $
%include "print.asm"
%include "read_disk.asm"
MSG_BOOT_KERNEL_TEST db 'Boot kernel test', 0
times 32766-($-$$) db 1
dw 0x4433
# !/bin/bash
rm -rf ./raw_disk
nasm mbr.asm -o mbr
if [[ $? != 0 ]];then
exit 1
fi
#写入MBR
dd if=./mbr of=./raw_disk bs=512 count=1
if [[ $? != 0 ]];then
exit 1
fi
nasm boot_kernel.asm -o boot_kernel
#写入stage2
dd if=./boot_kernel of=./raw_disk bs=512 seek=1
if [[ $? != 0 ]];then
exit 1
fi
#启动qemu
qemu-kvm raw_disk -vnc :6
exit 0
最终的结果图: