草庐IT

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

成宇佳的技术博客 2023-03-28 原文

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


之前我们在电脑的启动过程中介绍过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的更多相关文章

  1. ruby - 如何在续集中重新加载表模式? - 2

    鉴于我有以下迁移:Sequel.migrationdoupdoalter_table:usersdoadd_column:is_admin,:default=>falseend#SequelrunsaDESCRIBEtablestatement,whenthemodelisloaded.#Atthispoint,itdoesnotknowthatusershaveais_adminflag.#Soitfails.@user=User.find(:email=>"admin@fancy-startup.example")@user.is_admin=true@user.save!ende

  2. ruby - RuntimeError(自动加载常量 Apps 多线程时检测到循环依赖 - 2

    我收到这个错误:RuntimeError(自动加载常量Apps时检测到循环依赖当我使用多线程时。下面是我的代码。为什么会这样?我尝试多线程的原因是因为我正在编写一个HTML抓取应用程序。对Nokogiri::HTML(open())的调用是一个同步阻塞调用,需要1秒才能返回,我有100,000多个页面要访问,所以我试图运行多个线程来解决这个问题。有更好的方法吗?classToolsController0)app.website=array.join(',')putsapp.websiteelseapp.website="NONE"endapp.saveapps=Apps.order("

  3. ruby-on-rails - 使用 config.threadsafe 时从 lib/加载模块/类的正确方法是什么!选项? - 2

    我一直致力于让我们的Rails2.3.8应用程序在JRuby下正确运行。一切正常,直到我启用config.threadsafe!以实现JRuby提供的并发性。这导致lib/中的模块和类不再自动加载。使用config.threadsafe!启用:$rubyscript/runner-eproduction'pSim::Sim200Provisioner'/Users/amchale/.rvm/gems/jruby-1.5.1@web-services/gems/activesupport-2.3.8/lib/active_support/dependencies.rb:105:in`co

  4. Observability:从零开始创建 Java 微服务并监控它 (二) - 2

    这篇文章是继上一篇文章“Observability:从零开始创建Java微服务并监控它(一)”的续篇。在上一篇文章中,我们讲述了如何创建一个Javaweb应用,并使用Filebeat来收集应用所生成的日志。在今天的文章中,我来详述如何收集应用的指标,使用APM来监控应用并监督web服务的在线情况。源码可以在地址 https://github.com/liu-xiao-guo/java_observability 进行下载。摄入指标指标被视为可以随时更改的时间点值。当前请求的数量可以改变任何毫秒。你可能有1000个请求的峰值,然后一切都回到一个请求。这也意味着这些指标可能不准确,你还想提取最小/

  5. ruby-on-rails - 从应用程序中自定义文件夹内的命名空间自动加载 - 2

    我们目前正在为ROR3.2开发自定义cms引擎。在这个过程中,我们希望成为我们的rails应用程序中的一等公民的几个类类型起源,这意味着它们应该驻留在应用程序的app文件夹下,它是插件。目前我们有以下类型:数据源数据类型查看我在app文件夹下创建了多个目录来保存这些:应用/数据源应用/数据类型应用/View更多类型将随之而来,我有点担心应用程序文件夹被这么多目录污染。因此,我想将它们移动到一个子目录/模块中,该子目录/模块包含cms定义的所有类型。所有类都应位于MyCms命名空间内,目录布局应如下所示:应用程序/my_cms/data_source应用程序/my_cms/data_ty

  6. ruby - 如何使用 Selenium Webdriver 根据 div 的内容执行操作? - 2

    我有一个使用SeleniumWebdriver和Nokogiri的Ruby应用程序。我想选择一个类,然后对于那个类对应的每个div,我想根据div的内容执行一个Action。例如,我正在解析以下页面:https://www.google.com/webhp?sourceid=chrome-instant&ion=1&espv=2&ie=UTF-8#q=puppies这是一个搜索结果页面,我正在寻找描述中包含“Adoption”一词的第一个结果。因此机器人应该寻找带有className:"result"的div,对于每个检查它的.descriptiondiv是否包含单词“adoption

  7. ruby-on-rails - 如何处理 Grape 中特定操作的过滤器之前? - 2

    我正在我的Rails项目中安装Grape以构建RESTfulAPI。现在一些端点的操作需要身份验证,而另一些则不需要身份验证。例如,我有users端点,看起来像这样:moduleBackendmoduleV1classUsers现在如您所见,除了password/forget之外的所有操作都需要用户登录/验证。创建一个新的端点也没有意义,比如passwords并且只是删除password/forget从逻辑上讲,这个端点应该与用户资源。问题是Grapebefore过滤器没有像except,only这样的选项,我可以在其中说对某些操作应用过滤器。您通常如何干净利落地处理这种情况?

  8. ruby-on-rails - 在 Ruby on Rails 中发送响应之前如何等待多个异步操作完成? - 2

    在我做的一些网络开发中,我有多个操作开始,比如对外部API的GET请求,我希望它们同时开始,因为一个不依赖另一个的结果。我希望事情能够在后台运行。我找到了concurrent-rubylibrary这似乎运作良好。通过将其混合到您创建的类中,该类的方法具有在后台线程上运行的异步版本。这导致我编写如下代码,其中FirstAsyncWorker和SecondAsyncWorker是我编写的类,我在其中混合了Concurrent::Async模块,并编写了一个名为“work”的方法来发送HTTP请求:defindexop1_result=FirstAsyncWorker.new.async.

  9. ruby - 在 Ruby 中是否有一种惯用的方法来操作 2 个数组? - 2

    a=[3,4,7,8,3]b=[5,3,6,8,3]假设数组长度相同,是否有办法使用each或其他一些惯用方法从两个数组的每个元素中获取结果?不使用计数器?例如获取每个元素的乘积:[15,12,42,64,9](0..a.count-1).eachdo|i|太丑了...ruby1.9.3 最佳答案 使用Array.zip怎么样?:>>a=[3,4,7,8,3]=>[3,4,7,8,3]>>b=[5,3,6,8,3]=>[5,3,6,8,3]>>c=[]=>[]>>a.zip(b)do|i,j|c[[3,5],[4,3],[7,6],

  10. ruby-on-rails - 如何让 Rails View 返回其关联的操作名称? - 2

    我有一个非常简单的Controller来管理我的Rails应用程序中的静态页面:classPagesController我怎样才能让View模板返回它自己的名字,这样我就可以做这样的事情:#pricing.html.erb#-->"Pricing"感谢您的帮助。 最佳答案 4.3RoutingParametersTheparamshashwillalwayscontainthe:controllerand:actionkeys,butyoushouldusethemethodscontroller_nameandaction_nam

随机推荐