学习操作系统原理最好的方法是自己写一个简单的操作系统。
之前我们在电脑的启动过程中介绍过boot程序的主要任务就是加载并运行loader程序,本讲我们就来实现。
本讲代码文件共2个:
- boot.asm
- loader.asm
一、代码及讲解
本讲所用到的知识点都是之前已经用过的,只是在本讲中综合应用了一下。
关于如何读取文件在上一讲中已经介绍过了,我们只要在上讲代码中把要读取的文件名改成loader的文件名"LOADER BIN"即可读取loader程序文件。
本讲的boot.asm就是在上讲的基础上稍微改了下,加了3处提示语句。程序一开始先清屏并在屏幕上输出字符串“GrapeOS boot start.”。然后从硬盘根目录查找LOADER.BIN程序文件,如果没有找到文件则在屏幕上输出字符串“Loader not found.”,如果找到了文件则在屏幕上输出字符串“Loader found.”。如果找到了文件则读取文件内容,读取完后通过jmp指令跳转到loader在内存中的起始地址,这样就完成了加载并运行loader。
boot.asm中的代码如下:
;--------------------定义常量-------------------- ;FAT16目录项中各成员的偏移量: ;名称 偏移 长度 描述 DIR_Name equ 0 ;11 文件名8B,扩展名3B DIR_Attr equ 11 ;1 目录项属性 ;Reserved equ 12 ;10 保留位 DIR_WrtTime equ 22 ;2 最后一次写入时间 DIR_WrtDate equ 24 ;2 最后一次写入日期 DIR_FstClus equ 26 ;2 起始簇号 DIR_FileSize equ 28 ;4 文件大小 BOOT_ADDRESS equ 0x7c00 ;boot程序加载到内存的地址。 DISK_BUFFER equ 0x7e00 ;读磁盘临时存放数据用的缓存区,放到boot程序之后。 DISK_SIZE_M equ 4 ;磁盘容量,单位M。 FAT1_SECTORS equ 32 ;FAT1占用扇区数 ROOT_DIR_SECTORS equ 32 ;根目录占用扇区数 SECTOR_NUM_OF_FAT1_START equ 1 ;FAT1表起始扇区号 SECTOR_NUM_OF_ROOT_DIR_START equ 33 ;根目录区起始扇区号 SECTOR_NUM_OF_DATA_START equ 65 ;数据区起始扇区号,对应簇号为2。 SECTOR_CLUSTER_BALANCE equ 63 ;簇号加上该值正好对应扇区号。 FILE_NAME_LENGTH equ 11 ;文件名8字节加扩展名3字节共11字节。 DIR_ENTRY_SIZE equ 32 ;目录项为32字节。 DIR_ENTRY_PER_SECTOR equ 16 ;每个扇区能存放目录项的数目。 LOADER_ADDRESS equ 0x1000 ;loader程序加载到内存的地址。 STACK_BOTTOM equ LOADER_ADDRESS ;栈底地址(把栈放到loader程序前面) VIDEO_SEGMENT_ADDRESS equ 0xb800 ;显存的段地址(默认显示模式为25行80列字符模式) VIDEO_CHAR_MAX_COUNT equ 2000 ;默认屏幕最多显示字符数。 ;--------------------MBR开始-------------------- org BOOT_ADDRESS jmp boot_start nop ;FAT16参数区: BS_OEMName db 'GrapeOS ' ;厂商名称(8字节,含空格) BPB_BytesPerSec dw 0x0200 ;每扇区字节数 BPB_SecPerClus db 0x01 ;每簇扇区数 BPB_RsvdSecCnt dw 0x0001 ;保留扇区数(引导扇区的扇区数) BPB_NumFATs db 0x01 ;FAT表的份数 BPB_RootEntCnt dw 0x0200 ;根目录可容纳的目录项数 BPB_TotSec16 dw 0x2000 ;扇区总数(4MB) BPB_Media db 0xf8 ;介质描述符 BPB_FATSz16 dw 0x0020 ;每个FAT表扇区数 BPB_SecPerTrk dw 0x0020 ;每磁道扇区数 BPB_NumHeads dw 0x0040 ;磁头数 BPB_hiddSec dd 0x00000000 ;隐藏扇区数 BPB_TotSec32 dd 0x00000000 ;如果BPB_TotSec16是0,由这个值记录扇区数。 BS_DrvNum db 0x80 ;int 13h的驱动器号 BS_Reserved1 db 0x00 ;未使用 BS_BootSig db 0x29 ;扩展引导标记 BS_VolID dd 0x00000000 ;卷序列号 BS_VolLab db 'Grape OS ';卷标(11字节,含空格) BS_FileSysType db 'FAT16 ' ;文件系统类型(8字节,含空格) ;通过以上参数可知硬盘容量为4MB,共8K个扇区。扇区具体分布如下: ;区域名 扇区数 扇区号 字节偏移 说明 ;引导扇区 1个扇区 扇区0 0x0000~0x01ff ;FAT1表 32个扇区 扇区1~32 0x0200~0x41ff 可记录8K-2个簇 ;FAT2表 无 无 无 无 ;根目录区 32个扇区 扇区33~64 0x4200~0x81ff 可容纳512个目录项 ;数据区 8127个扇区 扇区65~0x1fff 0x8200~0x3fffff ;--------------------程序开始-------------------- boot_start: ;初始化寄存器 mov ax,cs mov ds,ax mov es,ax ;cmpsb会用到ds:si和es:di mov ss,ax mov sp,STACK_BOTTOM mov ax,VIDEO_SEGMENT_ADDRESS mov gs,ax ;本程序中gs专用于指向显存段 ;清屏 call func_clear_screen ;打印字符串:"GrapeOS boot start." mov si,boot_start_string mov di,0 ;在屏幕第1行显示 call func_print_string ;读取loader文件开始 ;读取根目录的第1个扇区(1个扇区可以存放16个目录项,我们用到的文件少,不会超过16个。) mov esi,SECTOR_NUM_OF_ROOT_DIR_START mov di,DISK_BUFFER call func_read_one_sector ;在16个目录项中通过文件名查找文件 cld ;cld将标志位DF置0,在串处理指令中控制每次操作后让si和di自动递增。std相反。下面repe cmpsb会用到。 mov bx,0 ;用bx记录遍历第几个目录项。 next_dir_entry: mov si,bx shl si,5 ;乘以32(目录项的大小) add si,DISK_BUFFER ;源地址指向目录项中的文件名。 mov di,loader_file_name_string ;目标地址指向loader程序在硬盘中的正确文件名。 mov cx,FILE_NAME_LENGTH ;字符比较次数为FAT16文件名长度,每比较一个字符,cx会自动减一。 repe cmpsb ;逐字节比较ds:si和es:di指向的两个字符串。 jcxz loader_found ;当cx为0时跳转。cx为0表示上面比较的两个字符串相同。找到了loader文件。 inc bx cmp bx,DIR_ENTRY_PER_SECTOR jl next_dir_entry ;检查下一个目录项。 jmp loader_not_found ;没有找到loader文件。 loader_found: ;打印字符串:"Loader found." mov si,loader_found_string mov di,80 ;在屏幕第2行显示 call func_print_string ;从目录项中获取loader文件的起始簇号 shl bx,5 ;乘以32 add bx,DISK_BUFFER mov bx,[bx+DIR_FstClus] ;loader的起始簇号 ;读取FAT1表的第1个扇区(我们用到的文件少且小,只用到了该扇区中的簇号) mov esi,SECTOR_NUM_OF_FAT1_START mov di,DISK_BUFFER ;放到boot程序之后 call func_read_one_sector ;按簇读loader mov bp,LOADER_ADDRESS ;loader文件内容读取到内存中的起始地址 read_loader: xor esi,esi ;esi清零 mov si,bx ;簇号 add esi,SECTOR_CLUSTER_BALANCE mov di,bp call func_read_one_sector add bp,512 ;下一个目标地址 ;获取下一个簇号(每个FAT表项为2字节) shl bx,1 ;乘2,每个FAT表项占2个字节 mov bx,[bx+DISK_BUFFER] ;判断下一个簇号 cmp bx,0xfff8 ;大于等于0xfff8表示文件的最后一个簇 jb read_loader ;jb无符号小于则跳转,jl有符号小于则跳转。 read_loader_finish: ;读取loader文件结束 jmp LOADER_ADDRESS ;跳转到loader在内存中的地址 loader_not_found: ;没有找到loader文件。 ;打印字符串:"Loader not found." mov si,loader_not_found_string mov di,80 ;在屏幕第2行显示 call func_print_string stop: hlt jmp stop ;清屏函数(将屏幕写满空格就实现了清屏) ;输入参数:无。 ;输出参数:无。 func_clear_screen: mov ah,0x00 ;黑底黑字 mov al,' ' ;空格 mov cx,VIDEO_CHAR_MAX_COUNT ;循环控制 .start_blank: mov bx,cx ;以下3行表示bx=(cx-1)*2 dec bx shl bx,1 mov [gs:bx],ax ;[gs:bx]表示字符对应的显存地址(从屏幕右下角往前清屏) loop .start_blank ret ;打印字符串函数。 ;输入参数:ds:si,di。 ;输出参数:无。 ;ds:si 表示字符串起始地址,以0为结束符。 ;di 表示字符串在屏幕上显示的起始位置(0~1999)。 func_print_string: mov ah,0x07 ;ah表示字符属性,0x07表示黑底白字。 shl di,1 ;乘2(屏幕上每个字符对应2个显存字节)。 .start_char: mov al,[si] cmp al,0 jz .end_print mov [gs:di],ax ;将字符和属性放到对应的显存中。 inc si add di,2 jmp .start_char .end_print: ret ;读取硬盘1个扇区(主硬盘控制器主盘) ;输入参数:esi,ds:di。 ;esi LBA扇区号 ;ds:di 将数据写入到的内存起始地址 ;输出参数:无。 func_read_one_sector: ;第1步:检查硬盘控制器状态 mov dx,0x1f7 .not_ready1: nop ;nop相当于稍息 hlt相当于睡觉 in al,dx ;读0x1f7端口 and al,0xc0 ;第7位为1表示硬盘忙,第6位为1表示硬盘控制器已准备好,正在等待指令。 cmp al,0x40 ;当第7位为0,且第6位为1,则进入下一个步。 jne .not_ready1 ;若未准备好,则继续判断。 ;第2步:设置要读取的扇区数 mov dx,0x1f2 mov al,1 out dx,al ;读取1个扇区 ;第3步:将LBA地址存入0x1f3~0x1f6 mov eax,esi ;LBA地址7~0位写入端口0x1f3 mov dx,0x1f3 out dx,al ;LBA地址15~8位写入端口写入0x1f4 shr eax,8 mov dx,0x1f4 out dx,al ;LBA地址23~16位写入端口0x1f5 shr eax,8 mov dx,0x1f5 out dx,al ;第4步:设置device端口 shr eax,8 and al,0x0f ;LBA第24~27位 or al,0xe0 ;设置7~4位为1110,表示LBA模式,主盘 mov dx,0x1f6 out dx,al ;第5步:向0x1f7端口写入读命令0x20 mov dx,0x1f7 mov al,0x20 out dx,al ;第6步:检测硬盘状态 .not_ready2: nop ;nop相当于稍息 hlt相当于睡觉 in al,dx ;读0x1f7端口 and al,0x88 ;第7位为1表示硬盘忙,第3位为1表示硬盘控制器已准备好数据传输。 cmp al,0x08 ;当第7位为0,且第3位为1,进入下一步。 jne .not_ready2 ;若未准备好,则继续判断。 ;第7步:从0x1f0端口读数据 mov cx,256 ;每次读取2字节,一个扇区需要读256次。 mov dx,0x1f0 .go_on_read: in ax,dx mov [di],ax add di,2 loop .go_on_read ret loader_file_name_string:db "LOADER BIN",0 ;loader程序在硬盘中存储的文件名,共11个字节,含空格。 boot_start_string:db "GrapeOS boot start.",0 loader_not_found_string:db "Loader not found.",0 loader_found_string:db "Loader found.",0 times 510-($-$$) db 0 db 0x55,0xaa
目前我们的loader程序非常简单,只是向屏幕输出一行字符串“GrapeOS loader start.”。
loader.asm中的代码如下:
org 0x1000 ;打印字符串:"GrapeOS loader start." mov si,loader_start_string mov di,160 ;屏幕第3行显示 call func_print_string stop: hlt jmp stop ;打印字符串函数 ;输入参数:ds:si,di。 ;输出参数:无。 ;si 表示字符串起始地址,以0为结束符。 ;di 表示字符串在屏幕上显示的起始位置(0~1999) func_print_string: mov ah,0x07 ;ah 表示字符属性 黑底白字 shl di,1 ;乘2(屏幕上每个字符对应2个显存字节) .start_char: mov al,[si] cmp al,0 jz .end_print mov [gs:di],ax inc si add di,2 jmp .start_char .end_print: ret loader_start_string:db "GrapeOS loader start.",0
二、程序演示
编译boot和loader:
[root@CentOS7 Lesson24]# nasm boot.asm -o boot.bin [root@CentOS7 Lesson24]# nasm loader.asm -o loader.bin
将boot写入到虚拟硬盘的第一个扇区:
[root@CentOS7 Lesson24]# dd conv=notrunc if=boot.bin of=/media/VMShare/GrapeOS.img
运行QEMU:
C:UsersCYJ>qemu-system-i386 d:GrapeOSVMShareGrapeOS.img
运行截图如下:

上面截图上显示“Loader not found.”,因为loader.bin文件还没有放入虚拟硬盘里,下面我们来放入。
[root@CentOS7 Lesson24]# mount /media/VMShare/GrapeOS.img /mnt/ -t msdos -o loop [root@CentOS7 Lesson24]# cp loader.bin /mnt/ [root@CentOS7 Lesson24]# sync [root@CentOS7 Lesson24]# umount /mnt/
重新运行QUME,截图如下:

上面截图中显示“GrapeOS loader start.”,说明已成功加载并运行loader。
视频版地址:https://www.bilibili.com/video/BV1KV4y197sa/
配套的代码与资料在:https://gitee.com/jackchengyujia/grapeos-course
GrapeOS操作系统交流QQ群:643474045