草庐IT

自己动手从零写桌面操作系统GrapeOS系列教程——23.从硬盘读取文件

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

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


本讲代码文件为boot.asm,要读取的文件为data.txt。

一、在FAT16文件系统中读取文件的流程

在GrapeOS中用到的文件少且小,所有文件都放在了根目录下,数量不会超过16个,占用的簇不会超过254个。所以读取目录项只需要读取根目录的第1个扇区即可,读取FAT表项也只需读取FAT1表的第1个扇区即可。
以下是读取文件的流程图:

二、代码及讲解

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程序加载到内存的地址。
FILE_ADDRESS equ 0x1000 ;文件读到内存中的地址。
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 ;每个扇区能存放目录项的数目。

;--------------------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

;读取文件开始
;读取根目录的第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,read_file_name_string  ;目标地址指向文件在硬盘中的正确文件名。
mov cx,FILE_NAME_LENGTH ;字符比较次数为FAT16文件名长度,每比较一个字符,cx会自动减一。
repe cmpsb ;逐字节比较ds:si和es:di指向的两个字符串。
jcxz file_found ;当cx为0时跳转,cx为0表示上面比较的两个字符串相同。找到了文件。
inc bx
cmp bx,DIR_ENTRY_PER_SECTOR
jl next_dir_entry ;检查下一个目录项。
jmp file_not_found ;没有找到文件。

file_found: ;找到了文件
;从目录项中获取文件的起始簇号
shl bx,5 ;乘以32
add bx,DISK_BUFFER
mov bx,[bx+DIR_FstClus] ;文件的起始簇号

;读取FAT1表的第1个扇区(我们用到的文件少且小,只用到了该扇区中的簇号。)
mov esi,SECTOR_NUM_OF_FAT1_START 
mov di,DISK_BUFFER ;放到boot程序之后
call func_read_one_sector

mov bp,FILE_ADDRESS ;文件内容读取到内存中的起始地址

;按簇号读文件内容
read_file: 
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_file ;jb无符号小于则跳转,jl有符号小于则跳转。

read_file_finish: ;读取文件结束
jmp stop

file_not_found: ;没有找到文件

stop:
hlt
jmp stop

;读取硬盘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

read_file_name_string:db "DATA    TXT",0  ;要读取的文件在硬盘中存储的文件名,共11个字节,含空格。

times 510-($-$$) db 0
db 0x55,0xaa

关于代码的讲解基本都写在注释中了,结合之前讲的内容,大家应该能看懂。

三、通过Linux将文件复制到虚拟硬盘中

本讲要读取的文件是data.txt,如何将该文件复制到虚拟硬盘的FAT16文件系统中呢?我们这里采用的方法是将该虚拟硬盘挂载到Linux系统上,然后就可以将data.txt复制到虚拟硬盘中了。前提是需要先将虚拟硬盘格式化,格式化的方法就是将boot程序写入到虚拟硬盘的第一个扇区。因为boot程序中含有FAT16的结构化数据,Linux系统就知道如何读写该文件系统了。

1.将boot程序写入到虚拟硬盘的第一个扇区

dd if=/dev/zero of=/media/VMShare/GrapeOS.img bs=1M count=4
nasm boot.asm -o boot.bin
dd if=boot.bin of=/media/VMShare/GrapeOS.img conv=notrunc

截图如下:

2.将虚拟硬盘挂载到Linux系统上并将data.txt复制到虚拟硬盘中

mount /media/VMShare/GrapeOS.img /mnt/ -t msdos -o loop
ll /mnt/
cp data.txt /mnt/
sync #数据同步,立马把数据写入硬盘。
ll /mnt/
umount /mnt/

截图如下:

上图中在复制完data.txt后,通过ll /mnt/查看虚拟硬盘根目录,此时虽然看到的文件名是小写“data.txt”,但实际上在虚拟硬盘里存储的文件名已经是全部大写的了,在下面的分析中可以看到。

3.虚拟硬盘数据分析

通过hexdump查看虚拟硬盘数据:

hexdump /media/VMShare/GrapeOS.img -C

截图如下:

截图中的部分数据如下:

000001f0  00 00 00 00 00 00 00 00  00 00 00 00 00 00 55 aa  |..............U.|
00000200  00 00 00 00 00 00 04 00  05 00 ff ff 00 00 00 00  |................|
00000210  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
*
00004200  44 41 54 41 20 20 20 20  54 58 54 20 00 00 00 00  |DATA    TXT ....|
00004210  00 00 00 00 00 00 fa 4e  78 56 03 00 58 04 00 00  |.......NxV..X...|
00004220  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|

前面我们计算过,FAT1表的起始扇区是扇区1,字节偏移是0x200,根目录区的起始扇区是扇区33,字节偏移是0x4200。
从上面的截图和数据可以看到:

  1. 在根目录区的第一个目录项就是我们要读的文件,文件名已是全大写。
  2. 从目录项中可以看到文件的起始簇号是0x0003。
  3. 在FAT表中第3个FAT表项的值是0x0004,表示该文件的第二个簇号是0x0004。
  4. 在FAT表中第4个FAT表项的值是0x0005,表示该文件的第三个簇号是0x0005。
  5. 在FAT表中第5个FAT表项的值是0xffff,表示该文件没有下一个簇了,到此结束。
  6. 这个文件的内容共占用3个簇的空间,依次是簇3、簇4、簇5,读取该文件就是依次将这3个簇中的数据读取出来。

四、程序演示

在cmd命令行中启动QEMU的调试模式:

C:\Users\CYJ>qemu-system-i386 d:\GrapeOS\VMShare\GrapeOS.img -S -s

在Linux命令行中启动GDB:

[root@CentOS7 Lesson23]# gdb
(gdb) target remote 你的Windows的IP地址:1234
(gdb) b *0x7c00
(gdb) c
(gdb) x /32xb 0x1000 #在读文件前查看此时0x1000处的内存数据,可以看到都是0。
0x1000: 0x00    0x00    0x00    0x00    0x00    0x00    0x00    0x00
0x1008: 0x00    0x00    0x00    0x00    0x00    0x00    0x00    0x00
0x1010: 0x00    0x00    0x00    0x00    0x00    0x00    0x00    0x00
0x1018: 0x00    0x00    0x00    0x00    0x00    0x00    0x00    0x00
(gdb) c
运行几秒,然后Ctrl键+C键暂停运行,此时读取文件的程序已运行完毕。
(gdb) x /32xb 0x1000 #在读完文件后查看此时0x1000处的内存数据,可以看到已经不是0了。
0x1000: 0x78    0x38    0x36    0x20    0x28    0x61    0x6c    0x73
0x1008: 0x6f    0x20    0x6b    0x6e    0x6f    0x77    0x6e    0x20
0x1010: 0x61    0x73    0x20    0x38    0x30    0x78    0x38    0x36
0x1018: 0x20    0x6f    0x72    0x20    0x74    0x68    0x65    0x20
(gdb) x /32c 0x1000 #为了方便观察可以以字符形式展示数据。通过对比,下面的32个字符的确和data.txt中前32个字符相同。
0x1000: 120 'x' 56 '8'  54 '6'  32 ' '  40 '('  97 'a'  108 'l' 115 's'
0x1008: 111 'o' 32 ' '  107 'k' 110 'n' 111 'o' 119 'w' 110 'n' 32 ' '
0x1010: 97 'a'  115 's' 32 ' '  56 '8'  48 '0'  120 'x' 56 '8'  54 '6'
0x1018: 32 ' '  111 'o' 114 'r' 32 ' '  116 't' 104 'h' 101 'e' 32 ' '
(gdb) x /32c 0x1440 #查看文件的最后二十多个字符。通过对比可以看到和data.txt中的相同。
0x1440: 105 'i' 116 't' 115 's' 32 ' '  90 'Z'  105 'i' 108 'l' 111 'o'
0x1448: 103 'g' 32 ' '  90 'Z'  45 '-'  56 '8'  48 '0'  32 ' '  118 'v'
0x1450: 97 'a'  114 'r' 105 'i' 97 'a'  110 'n' 116 't' 41 ')'  46 '.'
0x1458: 0 '\000' 0 '\000' 0 '\000' 0 '\000' 0 '\000' 0 '\000' 0 '\000' 0 '\000'

通过上述演示说明读取文件成功。


视频版地址:https://www.bilibili.com/video/BV1xN411K7Lc/
配套的代码与资料在:https://gitee.com/jackchengyujia/grapeos-course
GrapeOS操作系统交流QQ群:643474045

有关自己动手从零写桌面操作系统GrapeOS系列教程——23.从硬盘读取文件的更多相关文章

  1. ruby - 使用 RubyZip 生成 ZIP 文件时设置压缩级别 - 2

    我有一个Ruby程序,它使用rubyzip压缩XML文件的目录树。gem。我的问题是文件开始变得很重,我想提高压缩级别,因为压缩时间不是问题。我在rubyzipdocumentation中找不到一种为创建的ZIP文件指定压缩级别的方法。有人知道如何更改此设置吗?是否有另一个允许指定压缩级别的Ruby库? 最佳答案 这是我通过查看ruby​​zip内部创建的代码。level=Zlib::BEST_COMPRESSIONZip::ZipOutputStream.open(zip_file)do|zip|Dir.glob("**/*")d

  2. ruby - 其他文件中的 Rake 任务 - 2

    我试图在一个项目中使用rake,如果我把所有东西都放到Rakefile中,它会很大并且很难读取/找到东西,所以我试着将每个命名空间放在lib/rake中它自己的文件中,我添加了这个到我的rake文件的顶部:Dir['#{File.dirname(__FILE__)}/lib/rake/*.rake'].map{|f|requiref}它加载文件没问题,但没有任务。我现在只有一个.rake文件作为测试,名为“servers.rake”,它看起来像这样:namespace:serverdotask:testdoputs"test"endend所以当我运行rakeserver:testid时

  3. ruby-on-rails - 在 Rails 中将文件大小字符串转换为等效千字节 - 2

    我的目标是转换表单输入,例如“100兆字节”或“1GB”,并将其转换为我可以存储在数据库中的文件大小(以千字节为单位)。目前,我有这个:defquota_convert@regex=/([0-9]+)(.*)s/@sizes=%w{kilobytemegabytegigabyte}m=self.quota.match(@regex)if@sizes.include?m[2]eval("self.quota=#{m[1]}.#{m[2]}")endend这有效,但前提是输入是倍数(“gigabytes”,而不是“gigabyte”)并且由于使用了eval看起来疯狂不安全。所以,功能正常,

  4. ruby-on-rails - Rails 3 中的多个路由文件 - 2

    Rails2.3可以选择随时使用RouteSet#add_configuration_file添加更多路由。是否可以在Rails3项目中做同样的事情? 最佳答案 在config/application.rb中:config.paths.config.routes在Rails3.2(也可能是Rails3.1)中,使用:config.paths["config/routes"] 关于ruby-on-rails-Rails3中的多个路由文件,我们在StackOverflow上找到一个类似的问题

  5. ruby - 将差异补丁应用于字符串/文件 - 2

    对于具有离线功能的智能手机应用程序,我正在为Xml文件创建单向文本同步。我希望我的服务器将增量/差异(例如GNU差异补丁)发送到目标设备。这是计划:Time=0Server:hasversion_1ofXmlfile(~800kiB)Client:hasversion_1ofXmlfile(~800kiB)Time=1Server:hasversion_1andversion_2ofXmlfile(each~800kiB)computesdeltaoftheseversions(=patch)(~10kiB)sendspatchtoClient(~10kiBtransferred)Cl

  6. ruby - 如何将脚本文件的末尾读取为数据文件(Perl 或任何其他语言) - 2

    我正在寻找执行以下操作的正确语法(在Perl、Shell或Ruby中):#variabletoaccessthedatalinesappendedasafileEND_OF_SCRIPT_MARKERrawdatastartshereanditcontinues. 最佳答案 Perl用__DATA__做这个:#!/usr/bin/perlusestrict;usewarnings;while(){print;}__DATA__Texttoprintgoeshere 关于ruby-如何将脚

  7. ruby - 使用 Vim Rails,您可以创建一个新的迁移文件并一次性打开它吗? - 2

    使用带有Rails插件的vim,您可以创建一个迁移文件,然后一次性打开该文件吗?textmate也可以这样吗? 最佳答案 你可以使用rails.vim然后做类似的事情::Rgeneratemigratonadd_foo_to_bar插件将打开迁移生成的文件,这正是您想要的。我不能代表textmate。 关于ruby-使用VimRails,您可以创建一个新的迁移文件并一次性打开它吗?,我们在StackOverflow上找到一个类似的问题: https://sta

  8. Ruby 写入和读取对象到文件 - 2

    好的,所以我的目标是轻松地将一些数据保存到磁盘以备后用。您如何简单地写入然后读取一个对象?所以如果我有一个简单的类classCattr_accessor:a,:bdefinitialize(a,b)@a,@b=a,bendend所以如果我从中非常快地制作一个objobj=C.new("foo","bar")#justgaveitsomerandomvalues然后我可以把它变成一个kindaidstring=obj.to_s#whichreturns""我终于可以将此字符串打印到文件或其他内容中。我的问题是,我该如何再次将这个id变回一个对象?我知道我可以自己挑选信息并制作一个接受该信

  9. ruby - 如何使用 Ruby aws/s3 Gem 生成安全 URL 以从 s3 下载文件 - 2

    我正在编写一个小脚本来定位aws存储桶中的特定文件,并创建一个临时验证的url以发送给同事。(理想情况下,这将创建类似于在控制台上右键单击存储桶中的文件并复制链接地址的结果)。我研究过回形针,它似乎不符合这个标准,但我可能只是不知道它的全部功能。我尝试了以下方法:defauthenticated_url(file_name,bucket)AWS::S3::S3Object.url_for(file_name,bucket,:secure=>true,:expires=>20*60)end产生这种类型的结果:...-1.amazonaws.com/file_path/file.zip.A

  10. ruby - rspec 需要 .rspec 文件中的 spec_helper - 2

    我注意到像bundler这样的项目在每个specfile中执行requirespec_helper我还注意到rspec使用选项--require,它允许您在引导rspec时要求一个文件。您还可以将其添加到.rspec文件中,因此只要您运行不带参数的rspec就会添加它。使用上述方法有什么缺点可以解释为什么像bundler这样的项目选择在每个规范文件中都需要spec_helper吗? 最佳答案 我不在Bundler上工作,所以我不能直接谈论他们的做法。并非所有项目都checkin.rspec文件。原因是这个文件,通常按照当前的惯例,只

随机推荐