吾爱破解 - LCG - LSG |安卓破解|病毒分析|www.52pojie.cn

 找回密码
 注册[Register]

QQ登录

只需一步,快速开始

查看: 3944|回复: 20
收起左侧

[系统底层] 从0到-1写一个操作系统-0x03-MBR操作硬盘以及Loader

  [复制链接]
peiwithhao 发表于 2022-12-30 15:03
本帖最后由 peiwithhao 于 2023-3-2 14:03 编辑

这里写个往期推荐,这样可以来回跳跃(狗头
0x00-环境准备
0x01-BIOS以及MBR
0x02-MBR支持显卡
0x03-MBR操作硬盘以及Loader
0x04-进入保护模式

0x00 基础知识们

今天我们来继续精进咱们的改进MBR,上次我们所做的事是使用显卡显存来实现屏幕输出,今天我们来了解了解磁盘相关知识.

1. 硬盘

首先我们来大概给出磁盘的基础知识,可能大伙学过机组或者说操作系统这两门课程的同学对此会比较熟悉,但我在这里还是简单说几句,首先就是磁盘的结构(注意下面解释的是机械键盘,而不是咱们目前比较常见的固态硬盘)。下面先给出图片:
一个磁盘由多个盘面组成,可以理解成下面途中光盘的A面B面

每个盘片被划分为一个个磁道,而这每个磁道也被称为柱面,你可以运用立体思维来想想,这么多个盘组合在一起那不就成了一个圆柱体了吗,然后圆柱体中我任取一个磁道,他们所有盘面的相对磁道恰好就构成了一个圆柱面,每个磁道又划分为一个个扇区。如下图:

由上,可用(柱面号,盘面号,扇区号)来定位任意一个“磁盘块”。这也被成为物理磁盘地址(由于磁盘属于块设备,所以一个地址对应了一个块,或者说一个簇)
可根据该地址读取一个“块”,操作如下:

  1. 根据“柱面号”移动磁臂,让磁头指向指定柱面;

  2. 激活指定盘面对应的磁头;

  3. 磁盘旋转的过程中,指定的扇区会从磁头下面划过,这样就完成了对指定扇区的读/写。


而硬盘也如同外设,其中也必定要有IO接口来实现与CPU通信,就像对于显卡之于显示器,磁盘也有属于自己的IO接口,那就是硬盘控制器。

2. 硬盘控制端口

对于硬盘控制器而言,其IO模式与显卡不同,在使用显卡时,我们是使用了统一编址,但是在硬盘控制器下,其使用的是独立编制,这里有不懂的地方可以参考上一节。以下我给出硬盘控制器主要端口寄存器信息:

由上表可知端口分为两组,即为Commend Block registers和Control Block registers,其中Commend Block registers用来向硬盘驱动器写入命令字或从硬盘控制器中读出硬盘状态(因为硬盘会通过发送自己的状态到硬盘控制器,这种状态有且不限于正在忙,空闲等),而Control Block registers用来控制硬盘工作状态,这有点类似于咱们机组中学到的控制/状态寄存器,在应试教育中他俩是和到一起的,而事实上他俩之中Control Block registers也确实精简了许多,他们俩的功能越来越揉和,所以一下主要介绍Commend Block registers

3. 主盘、从盘、通道

这里首先给出几个基本概念解释:
在电脑中我们通常会碰到安装多个硬盘的情况,可能还得安装光驱什么的,这样就必须分出主从关系,这样就有了硬盘中的Master和Slave,Master就是主盘的意思,Slave就是从盘的意思,那么要这个主盘从盘干什么呢,这里就得涉及到一个系统启动的问题,因为我们大多数启动会设置为主盘来启动。在很久之前,主从盘的分工很明确,很多工作以主盘为中心,就比如系统一般安装在主盘上,可是在后来计算机发展兼容性的过程中,这俩之间的区别就越来越小了。

然后就是通道,一般主板上面会给出两个插槽,这两个插槽被称作通道,这儿我自己还有一点不清楚,但是就之前考研复习408得出的知识来说,这个通道是比DMA模式更加高级的存在,也就是负责IO的数据直接传到内存,且此过程不需CPU控制,我们也可以把它看作一个沟渠,也就是负责硬盘控制器传输到内存的一个管道。说回来,这两个通道分别被称为Primary通道和Secondary通道(由上面的表也可以分别看到),而每个通道上面才分主盘和从盘。

3. Command Block registers

咱们来按照上面的表来逐一解释一下各端口的作用(实际上就是硬盘控制器上的寄存器)

  • 首先我们可以看到第一行的0x1F0所对应的端口,在读和写的时候分别作为Data寄存器,且唯独%只有这个寄存器是16位的,其他寄存器一般都是8位%。而这里他的作用就和其名字一致,接受磁盘传送来的数据或CPU传送的数据。

  • 然后就是0x1F1,在读操作时他被用作Error寄存器,只有在读取硬盘失败的时候,里面才会有读取失败的信息。而在写操作时,他充当Features寄存器,他的作用是CPU传来指令的时候有时需要夹带一些额外参数,,这些参数就存放在Features寄存器中。

  • 0x1F2在读写操作时都充当Sector count寄存器,他是用来指定待读取或待写入的扇区数。

  • 0x1F3、0x1F4、0x1F5分别代表了LBA地址的low、mid、high位,这里我们再科普一下LBA是什么

    CHS大家可能知道,在上面我们讲硬盘基础知识的时候设计到的,他是代表了硬盘的地址,跟我们所对应的内存地址是一致的,只不过硬盘地址代表一个块,而主存地址代表一个地址单位(一般是1字节),而且CHS的盘地址是从1开始的,即0盘0道1扇区,这在之前的篇章中也说过BIOS会加载位于0盘0道1扇区的MBR来交接。这里我们所说的LBA其实就是另外一种编址标准,他更适合程序员的认识,盘快地址从0开始。
    而LBA分为两种,一种是LBA28,即用28位来描述一个扇区的地址,即2的28次方个扇区,总共寻址大约128G,还有一种是LBA48,即用48位来描述,和LBA28大同小异,这里我们选个简单的那就是LBA28.
    现在我们来继续介绍上面三个端口,可知这三个端口都是8位,总共有24bit,但他如何表示28位呢,别慌,他把这后面4位放到别的寄存器了,之后碰到该寄存器我会继续解释。

  • 0x1F6在读写操作时都作为devie寄存器,他充当一个杂项寄存器,可以发现他的宽度是8位,其中低4位用来存储LBA28模式下剩余的4位,而第四位指定通道上的主盘和从盘,第6位来设置启用LBA还是CHS模式,其他位固定为1.这里给出他的结构示意

  • 0x1F7端口在读操作时用作Status寄存器,他用来给出硬盘的状态信息,第0位为ERR位,代表命令出错,具体原因存放在上面的Error寄存器中,地3位为data request位,若为1则表示硬盘将数据已经存放好,第6位为DRDY,表示硬盘就绪,这位是表示硬盘正常,可以继续执行一些指令。第七位为BSY位,为1则表示硬盘正忙。而在写硬盘时,该端口充当command寄存器,存放需要硬盘执行的命令,在咱们的系统中,主要使用三个命令:

    1. identify:0xEC,硬盘识别
    2. read sector: 0x20,读扇区
    3. write sector: 0x30,写扇区
      这里给出0x1F7端口的结构:

4. 硬盘操作方法

我们要读取硬盘,大家可能也会想到就是使用in out的IO命令来对端口进行操作,就比如往data寄存器写或读数据,往command寄存器写命令,没错就是这样,但是总得有个先后顺序把,其中我们的顺序必须遵循一个大前提,那就是command寄存器一定要最后写,因为一旦command寄存器被写入那就开始执行命令了。这里给出一个基本顺序供大家参考。

  1. 选择通道,往通道的sector count寄存器写入我们即将操作的扇区数目
  2. 向0x1F3、0x1F4、0x1F5分别写入LBA地址的低24位
  3. 往device寄存器的低4字节写入LBA地址的高4位,设置第6位为1,即表明我们需要使用LBA寻址模式,再设置第四位来选择操作的硬盘是主盘还是从盘
  4. 往通道的command寄存器写入操作命令
  5. 读取通道上的status寄存器,判断硬盘工作是否完成
  6. 若上述命令选择了读硬盘,则进入下一个步骤,若为写或者其他,则完工
  7. 将硬盘数据读出

这里我们继续说,当我们使用读硬盘命令时,也就是说执行完1-5步,此时数据已经缓存到Data寄存器中,我们该如何从获取寄存器中的数据呢,一般方式如下:

  1. 无条件传送
  2. 查询传送
  3. 中断传送
  4. 直接存储器存取(DMA)
  5. I/O处理器传送

这里我们使用第2、3种方式,其他方式各位感兴趣可以自行搜索相关资料,这里我就不过多赘述。

0x01 使用MBR来操作硬盘

写到这儿,其实我们的MBR还没干啥本职工作,之前咱们只是简单的实现了打印字符,但是MBR本来是要加载Loader的,然后Loader来加载我们的操作系统(在这里的Loader你可以看作又是一段程序),而Loader也存放在硬盘上,所以我们额MBR就需要读取硬盘上的Loader了,所以也就需要了今天的知识。

1. Loader放哪儿呢和该去哪儿呢

Loader当然存放在硬盘上啦( 。我们现在来回忆一下,我们知道MBR是存放在地一个扇区,也就是0扇区(若采用CHS寻址,则为地一个扇区,此处我们使用LBA寻址,所以为第0个扇区),所以说1扇区是空闲的,我们理论上也可以把Loader放入1扇区,但是这里考虑到两者隔太近有可能会出现问题,不妨咱们就浪费一个扇区算了,将它放入2号扇区。
而这里我们MBR就是要将Loader从2号扇区读出来,然后再存入内存,但是内存该放哪儿了,在以下内存布局图中,只要是标注为可用的区域都可以使用,如下

开始地址 结束地址 大小 用途
FFFF0 FFFFF 16B BIOS入口,这么小的一个位置实际上仅仅是一个跳转指令
F0000 FFFEF 64KB-16B 系统BIOS
C8000 EFFFF 160KB 映射硬件适配器的ROM或内存映射式I/O
C0000 C7FFF 32KB 显示适配器BIOS
B8000 BFFFF 32KB 文本显示适配器
B0000 B7FFF 32KB 黑白显示适配器
A0000 AFFFF 64KB 彩色显示适配器
9FC00 9FFFF 1KB EBDA
7E00 9FBFF 约608KB 可用
7C00 7DFF 512B MBR加载地址
500 7BFF 约30KB 可用
400 4ff 256B BIOS数据区域
000 3FF 1KB 中断向量表

也就是说在0x500~0x7BFF,0x7E00~0x9FBFF都可以使用,考虑到我们以后的程序越来越多,这里我们就将Loader尽量放到低地址,这里我们选择放到0x900这里,我们不直接放在0x500的愿意跟上述硬盘地址一样,距离产生美。

2. MBR操作磁盘实现

首先为了让Loader跟MBR分离开来,我们另外写个文件,文件名为 “boot.inc”,

;---------- loader 和 kernel ------------
LOADER_BASE_ADDER equ 0x900     ;内存首址
LOADER_STAER_SECTOR equ 0x2     ;硬盘扇区

这里我也给出关键读硬盘代码,需要完整代码还是去github上面吧,因为代码完整贴出来太长了

  mov eax,LOADER_START_SECTOR   ;起始扇区LBA地址,注意这里使用32位寄存器是可行的,虽然说实模式下咱们只能用16位地址,但不是说用不了32位寄存器
  mov bx,LOADER_BASE_ADDR       ;写入的内存地址
  mov cx,1                      ;待读入的扇区数
  call rd_disk_m_16             ;以下读取程序的其实部分(一个扇区),rd_disk_m_16表示在16位模式下读硬盘的section函数,我们在下面实现

  jmp LOADER_BASE_ADDR          ;代码运行至此说明Loader已经加载完毕

;------------------------------------
;功能:读取硬盘n个扇区
rd_disk_m_16:
;-----------------------------------
                                ;eax = 扇区LBA地址
                                ;bx = 将数据写入的内存地址
                                ;cx = 读入的扇区数
  mov esi,eax                   ;备份eax
  mov di,cx                     ;备份cx

;读写硬盘:
;第1步:设置要读取的扇区数
  mov dx,0x1F2
  mov al,cl
  out dx,al         ;读取的扇区数

  mov eax,esi       ;恢复ax

;第2布:将LBA地址送入0x1F3~0x1F6
  ;LBA地址0~7位存入0x1F3
  mov dx,0x1F3
  out dx,al

  ;LBA地址8~15位写入0x1F4
  mov cl,8
  shr eax,cl        ;将eax中数据右移8位,这样就可以接着使用al来取中间8位了
  mov dx,0x1F4
  out dx,al

  ;LBA地址16~23位写入0x1F5
  shr eax,cl
  mov dx,0x1F5
  out dx,al

  shr eax,cl
  and al,0x0f       ;取LBR第24~27位
  or al,0xe0        ;设置7~4位为1110,指明LBA寻址模式
  mov dx,0x1F6
  out dx,al

;第3步:向0x1F7写入读命令,即为0x20
  mov dx,0x1f7
  mov al,0x20
  out dx,al

;第4步:检测硬盘状态
  .not_ready:
  ;同一端口,写时表示写入命令字,读时表示读入硬盘状态
  nop   ;空操作,相当与sleep一下
  in al,dx      ;此时我们读取0x1F7端口,此时它充当Status寄存器
  and al,0x88   ;其中第3位为1表示硬盘控制器已经准备好,第7位为1表示硬盘忙,这里即为取对应位的值

  cmp al,0x08   ;判断符号位是否与顺利执行时的符号一致
  jnz .not_ready ;若未准备好,则继续回跳

;第5步:从0x1F0端口读取数据
  mov ax,di     ;di为之前备份的读入扇区数
  mov dx, 256
  mul dx
  mov cx,ax     ;这里cx来存放循环次数
;一个字为两字节,而我们需要读入一个扇区,即为512字节,每次读入一个字(这是因为data寄存器有16位),所以共需要di×512/2 = di*256次
  mov dx,0x1F0
  .go_on_read:
  in ax,dx
  mov [bx],ax   ;bx存放的是加载的内存地址
  add bx,2      ;因为每次存2字节,所以内存地址每次加2,然后继续读
  loop .go_on_read ;这里cx作为循环控制次数
  ret

由于咱们的程序逐渐多了起来,我们该适当整理整理文件了,我们先创建一个名为boot的文件夹,然后将mbr.S放进去,在boot目录下我们再创建一个include文件夹,再将我们刚写的boot.inc放入,具体操作如下:

mkdir boot
mv mbr.S boot.inc ./boot
cd boot
mkdir include
mv ./boot.inc ./include

然后我们就在boot目录下进行操作了,首先依然是编译,今天的编译同一往不同,需要加上I参数,具体解释可以参见nasm帮助手册,nasm -h 回车。其大概意思就是添加一个库目录,这样我们在寻找包含文件的时候就会不仅在本目录下找,也会在我们-I指定的目录下寻找
接下来我们执行下面的命令:

nasm -I include/ -o mbr.bin mbr.S
dd if=./mbr.bin of=你的安装路/bochs/hd60M.img bs=512 count=1 conv=notrunc

注意此时还不能执行,先编译了再说,具体情况我在下面讲

3. Loader编写

但是此时我们还不能执行,因为我们的内核加载器,也就是说Loader还没编写,咱们的MBR刚刚实现的工作就是加载位于2号扇区的Loader,但是此时2号扇区啥也没有。
我们这儿再编写一个简单Loader,用显卡输出到屏幕上就行,这里我推荐大家可以自行先尝试,跟前几次的显卡输出一样的(注意到boot目录下写奥):

%include "boot.inc"
section loader vstart=LOADER_BASE_ADDR  ;起始地址按照之前约定一样
;按照显卡输出
;输出背景色为蓝色,钱景色为红色且跳动的字符串“I am loader”,注意为小端序
  mov byte [gs:0x00], 'I'
  mov byte [gs:0x01], 0x94
  mov byte [gs:0x02], ' '
  mov byte [gs:0x03], 0x94
  mov byte [gs:0x04], 'a'
  mov byte [gs:0x05], 0x94
  mov byte [gs:0x06], 'm'
  mov byte [gs:0x07], 0x94
  mov byte [gs:0x08], ' '
  mov byte [gs:0x09], 0x94
  mov byte [gs:0x0a], 'l'
  mov byte [gs:0x0b], 0x94
  mov byte [gs:0x0c], 'o'
  mov byte [gs:0x0d], 0x94
  mov byte [gs:0x0e], 'a'
  mov byte [gs:0x0f], 0x94
  mov byte [gs:0x10], 'd'
  mov byte [gs:0x11], 0x94
  mov byte [gs:0x12], 'e'
  mov byte [gs:0x13], 0x94
  mov byte [gs:0x14], 'r'
  mov byte [gs:0x15], 0x94

jmp $       ;循环等待

然后我们依次执行命令(就跟我们将mbr.S打入硬盘类似)

nams -I include -o loader.bin loader.S
dd if=./loader.bin of=你的路径/bochs/hd60M.img bs=512 count=1 seek=2 conv=notrunc

我们此刻就可以来运行虚拟机了,结果如下图:

这里加入两张图依然是作为对照,证明他在闪烁(狗头

0x02 总结

今天操作磁盘的工作或许有点多,但他仍然处于简单的水平,本次的所有源代码依旧在github上面同步更新,分支名为UUMBR

传送门

免费评分

参与人数 15吾爱币 +20 热心值 +15 收起 理由
孟坤软件 + 2 + 1 我很赞同!
willJ + 7 + 1 欢迎分析讨论交流,吾爱破解论坛有你更精彩!
xieyu0 + 1 + 1 谢谢@Thanks!
lingyun011 + 1 + 1 用心讨论,共获提升!
宅友 + 1 + 1 我很赞同!
童子晴 + 1 用心讨论,共获提升!
Cxq766334539 + 1 + 1 我很赞同!
pycah + 1 + 1 谢谢@Thanks!
mymoyi + 1 + 1 用心讨论,共获提升!
安好明天 + 1 + 1 用心讨论,共获提升!
blackgrape + 1 + 1 谢谢@Thanks!
yibuquliu + 1 + 1 very good
paperbox + 1 我很赞同!
860384505 + 1 + 1 nb
haiou327 + 1 + 1 用心讨论,共获提升!

查看全部评分

本帖被以下淘专辑推荐:

发帖前要善用论坛搜索功能,那里可能会有你要找的答案或者已经有人发布过相同内容了,请勿重复发帖。

pj_soup 发表于 2023-2-26 13:35
这里用bochs来调试读取文件的时候想看下各个寄存器及内存的值,发现如果是单步调试到数据准备哪里会一直循环,一直以为是代码写的有问题,后来调试的时候跳过读取的方法,顺利执行加载文件到内存了。
jffwoo 发表于 2023-2-14 23:15
这个案例其实早就成功了,效果也已经模拟出来了,今天再看的时候有了一个疑问:
在0x00扇区加载的mbr.S文件的时候,也是有打印东西的,那个为何没有显示,如何显示呢?

再次成功

再次成功
lvves 发表于 2022-12-30 15:52
liangang 发表于 2022-12-30 15:59
这个真是强,佩服大神啊
chinalihao 发表于 2022-12-30 16:13
厉害,佩服大神
506874511 发表于 2022-12-30 16:35
这个就厉害了。值得学习学习!
Freedom_XY_ 发表于 2022-12-30 16:36
对我来说,有点太硬核了~
yumoo 发表于 2022-12-30 16:57
佩服大佬,强,学习学习
111wdw 发表于 2022-12-30 18:35
学习了,感谢分享
qq882011 发表于 2022-12-30 19:36
学到老活到老
onemores 发表于 2022-12-30 20:26
大佬级!
您需要登录后才可以回帖 登录 | 注册[Register]

本版积分规则 警告:本版块禁止灌水或回复与主题无关内容,违者重罚!

快速回复 收藏帖子 返回列表 搜索

RSS订阅|小黑屋|处罚记录|联系我们|吾爱破解 - LCG - LSG ( 京ICP备16042023号 | 京公网安备 11010502030087号 )

GMT+8, 2024-3-29 22:25

Powered by Discuz!

Copyright © 2001-2020, Tencent Cloud.

快速回复 返回顶部 返回列表