• 自己动手从零写桌面操作系统GrapeOS系列教程——24.加载并运行loader


    学习操作系统原理最好的方法是自己写一个简单的操作系统。


    之前我们在电脑的启动过程中介绍过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:\Users\CYJ>qemu-system-i386 d:\GrapeOS\VMShare\GrapeOS.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

  • 相关阅读:
    k8s实战系列:3-存储的花样玩法(上)
    edu cf #137 Div.2(A~D)
    【Java】String类的常用方法
    spfa算法判断负环【什么是负环】【出现负环会怎么样】【牢记,此时不是求最短路】
    测试计划包括哪些内容?目的和意义是什么?
    【Proteus仿真】【Arduino单片机】继电器和按键
    如何减轻大语言模型中的幻觉?
    不懂管理,迟早把团队带废(附PPT和表格模板)
    234. 回文链表
    Git进阶之代码回滚、合并代码、从A分支选择N次提交,合并到B分支【revert、merge、rebase、cherry-pick】
  • 原文地址:https://www.cnblogs.com/chengyujia/p/17255709.html