自己动手从零写桌面操作系统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
自己动手从零写桌面操作系统GrapeOS系列教程——24.加载并运行loader的更多相关文章
- Yaf零基础学习总结5-Yaf类的自动加载
Yaf零基础学习总结5-Yaf类的自动加载 框架的一个重要功能就是类的自动加载了,在第一个demo的时候我们就约定自己的项目的目录结构,框架就基于这个目录结构来自动加载需要的类文件. Yaf在自启动的 ...
- 操作系统---在内核中重新加载GDT和堆栈
摘要 用BIOS方式启动计算机后,BIOS先读取引导扇区,引导扇区再从外部存储设备中读取加载器,加载器读取内核.进入内核后,把加载器中建立的GDT复制到内核中. 这篇文章的最大价值也许在末尾,对C语言 ...
- 不用写代码也能做表单 —— 加载meta即可
做增删改查要写多少代码? 一个表单一套代码,十个表单十套代码吗? 我这么懒,怎么会写这么多代码? 我想做到:即使一百个表单也只需要一套代码(而且不需要复制粘贴).实现多个表单,只需要加载不同的meta ...
- 麒麟操作系统上安装docker并加载镜像
最近需要在政务云系统中部署深度学习环境,其使用麒麟操作系统并与互联网相互隔离,无法使用常规的指令行方式进行安装.参考docker官方文档并经过多次尝试,使用离线安装的方式完成了环境的部署.这里做一下笔 ...
- 自己写CPU第九阶段(3)——加载存储指令说明2(swl、swr)
我们会继续上传新书<q=%E8%87%AA%E5%B7%B1%E5%8A%A8%E6%89%8B%E5%86%99CPU&ie=utf-8&src=se_lighten_quot ...
- 分享个刚写好的 android 的 ListView 动态加载类,功能全而代码少。
(转载声明出处:http://www.cnblogs.com/linguanh/) 简介: 该ListView 实现动态加载数据,为了方便用户充分地自定义自己的数据源.点击事件,等核心操作, ...
- 别人写的一个Bootstrap系列教程
http://www.cnblogs.com/lansy/category/659061.html
- 深入浅出JVM(一):你写得.java文件是如何被加载到内存中执行的
众所周知,.java文件需要经过编译生成.class文件才能被JVM执行. 其中,JVM是如何加载.class文件,又做了些什么呢? .class文件通过 加载->验证->准备->解 ...
- 自制 os 极简教程1:写一个操作系统有多难
为什么叫极简教程呢?听我慢慢说 不知道正在阅读本文的你,是否是因为想自己动手写一个操作系统.我觉得可能每个程序员都有个操作系统梦,或许是想亲自动手写出来一个,或许是想彻底吃透操作系统的知识.不论是为了 ...
- linux内核剖析(零)linux系统启动过程详解-开机加电后发生了什么
本文参考了如下文章 深入理解linux启动过程 mbr (主引导记录(Master Boot Record)) 电脑从开机加电到操作系统main函数之前执行的过程 详解linux系统的启动过程及系统初 ...
随机推荐
- STM32自学笔记
1位带操作 第一种位带操作 #define BITBAND_REG(Reg,Bit) (*((uint32_t volatile*)(0x42000000u + (((uint32_t)&(R ...
- UR #3 核聚变反应强度( gcd )
tags: -分解质因数 , gcd 题目大意 给定\(n\)个数,求\(a_1\)与\(a_i\)次小公约数 分析 易知次小公约数是\(\gcd\)的因数,于是用\(\gcd\)除去它的最小质因子即 ...
- 新centos6 静态ip 放行端口 hosts主机名 jdk环境变量
0 jdk 环境变量 vi /etc/profile source /etc/profile 刷新环境变量 在尾部增加如下代码: #JDK全局环境变量配置export JAVA_HOME=/usr/ ...
- CGTime CMTimeRange CMTimeMapping 小结
CMTime CMTimeRange CMTimeMapping 在使用 AVFoundation 框架处理多媒体资源时,通常会用到一些在 CoreMedia 框架中定义的结构体, 这里对其中描述时间 ...
- InnoDB的全文检索
InnoDB的全文检索 注:全文为MySQL官网5.7的文档(MySQL 8.0的文档与此几乎一致) MySQL 5.6 不支持中.日.韩语,因为无法对其分词,5.7版本引入NGram(基于字符)对中 ...
- 【Android异常】关于静态注册BroadcastReceiver接收不到系统广播的问题
如果你静态注册的广播无法接收到消息,请先检查下:你的安卓版本是不是8.0+ 前言Google官方声明:Beginning with Android 8.0 (API level 26), the sy ...
- python投票一致性指数(IVC)实现代码
毕业论文中用于计算联合国会员国间在联合国大会上的投票一致性(IVC) import pandas as pd import sqlite3 import networkx as nx import t ...
- Jmeter-接口测试(三)
一.jmeter接口关联 1.正则表达式实现接口关联 正则表达式可以这样测试 2.jsonpath表达式实现接口关联(只能作用于返回值是token的) 从根目录开始找$.token 从任意目录开始找( ...
- webstrom破解
1.下载webstrom补丁 链接:https://pan.baidu.com/s/1I93J_JOlbZzkoqV4EsJlpQ 提取码:kopn (永久有效) 2.将补丁复 ...
- python开头
python识别的正则模式 coding[:=]\s*([-\w.]+)#coding:utf8 ???不要用 #coding=utf-8#coding:utf-8# -*- coding: ut ...