zoukankan      html  css  js  c++  java
  • 【自制操作系统03】读取硬盘中的数据

    通过 【自制操作系统01】硬核讲解计算机的启动过程【自制操作系统02】环境准备与启动区实现 的讲解,我们已经实现了一个最简单的操作系统(仅仅一条机器指令)。

    今天我们要再往前进一步,逐渐将这个最简单的操作系统完善起来。之前最简单的操作系统是写在启动区的 512 字节里,这么小的空间以后肯定不能全部用来写操作系统的代码,所以它的主要任务就是将硬盘中更多的数据读取到内存里,并跳转到内存的那个位置开始运行。

    这里不得不回顾一下每节课都说到的四次跳跃:

    1. 一跳:按下开机键,CPU 将 PC 寄存器的值强制初始化为 0xffff0,这个位置是 BIOS 程序的入口地址
    2. 二跳:该入口地址处是一个跳转指令,跳转到 0xfe05b 位置,开始执行
    3. 三跳:执行了一些硬件检测工作后,最后一步将启动区内容加载(复制)到内存 0x7c00,并跳转到这里
    4. 四跳:启动区代码主要是加载操作系统内核,并跳转到加载处

    其实我们可以无限跳跃下去,只要感觉某一个环节的任务复杂了,就可以分成两步来走。但也完全可以从第三跳开始就再也不跳转了,把所有操作系统需要的指令和数据都从硬盘中加载到内存,然后执行,但这样显然不好。

    一、代码总览

    先不说别的,先发上来一份本章内容的全部代码

    mbr.asm

    ;----BIOS把启动区加载到内存的该位置,所以需设置地址偏移量
    section mbr vstart=0x7c00
    
    ;----设置堆栈地址
    mov sp,0x7c00
    
    ;----卷屏中断,目的是清屏
    mov ax,0x0600
    mov bx,0x0700
    mov cx,0
    mov dx,0x184f
    int 0x10
    
    ;----直接往显存中写数据
    mov ax,0xb800
    mov gs,ax
    mov byte [gs:0x00],'m'
    mov byte [gs:0x02],'b'
    mov byte [gs:0x04],'r'
    
    ;----读取硬盘(第2扇区)并加载到内存(0x900)
    mov eax,0x02	;起始扇区lba地址,LBA=(柱面号*磁头数+磁头号)*扇区数+扇区编号-1
    mov bx,0x900    ;写入的内存地址,之后用
    mov cx,4        ;待读入的扇区数
    call read_disk
    jmp 0x900
    
    ;----读硬盘方法,eax为lba扇区号,bx为待写入内存地址,cx为读入的扇区数
    read_disk:
    	mov esi,eax	;备份
    	mov di,cx	;备份
    	
    ;第一步,设置要读取的扇区数
    	mov dx,0x1f2
    	mov al,cl
    	out dx,al
    	mov eax,esi	;恢复
    	
    ;第二步,设置LBA地址
    	mov cl,8
    	;0-7位写入0x1f3
    	mov dx,0x1f3
    	out dx,al
    	;8-15位写入0x1f4
    	mov dx,0x1f4
    	shr eax,cl
    	out dx,al
    	;16-23位写入0x1f5
    	mov dx,0x1f5
    	shr eax,cl
    	out dx,al
    	;24-27位写入0x1f6
    	mov dx,0x1f6
    	shr eax,cl
    	and al,0x0f	;lba的24-27位
    	or al,0xe0	;另外4位为1110,表示lba模式
    	out dx,al
    	
    ;第三步,写入读命令
    	mov dx,0x1f7
    	mov al,0x20
    	out dx,al
    
    ;第四步,检测硬盘状态
    .not_ready:
    	nop
    	in al,dx
    	and al,0x88	;第4位为1表示准备好,第7位为1表示忙
    	cmp al,0x08
    	jnz .not_ready
    	
    ;第五步,读数据
    	mov ax,di
    	mov dx,256
    	mul dx
    	mov cx,ax
    	
    	mov dx,0x1f0
    	.go_on_read:
    		in ax,dx
    		mov [bx],ax
    		add bx,2
    		loop .go_on_read
    		ret
    	
    ;----512字节的最后两字节是启动区标识
    times 510-($-$$) db 0
    db 0x55,0xaa
    

    loader.asm

    section loader vstart=0x900
    mov byte [gs:0xa0],'l'
    mov byte [gs:0xa2],'o'
    mov byte [gs:0xa4],'a'
    mov byte [gs:0xa6],'d'
    mov byte [gs:0xa8],'e'
    mov byte [gs:0xaa],'r'
    

    Makefile

    mbr.bin: mbr.asm
    	nasm -I include/ -o out/mbr.bin mbr.asm -l out/mbr.lst
    	
    loader.bin: loader.asm
    	nasm -I include/ -o out/loader.bin loader.asm -l out/loader.lst
    	
    os.raw: mbr.bin loader.bin
    	../bochs/bin/bximage -hd -mode="flat" -size=60 -q target/os.raw
    	dd if=out/mbr.bin of=target/os.raw bs=512 count=1
    	dd if=out/loader.bin of=target/os.raw bs=512 count=4 seek=2
    	
    brun:
    	make install
    	make only-bochs-run
    	
    only-bochs-run:
    	../bochs/bin/bochs -f ../bochs/bochsrc.disk -q
    	
    install:
    	make clean
    	make -r os.raw
    	
    clean:
    	rm -rf target/*
    	rm -rf out/*
    

    二、磁盘

    如果你粗略地读了一下代码,起码可以知道 mbr.asm 中的代码,前半部分是在屏幕上输出一个 mbr 字符串,这是上节课为了做最小操作系统而用直观方式写的代码,可有可无。后半部分仅仅是读取了几个扇区的硬盘数据,加载到内存中的某个位置,然后跳转到此位置,这部分是关键,也是 mbr 的职责所在。

    那怎么读取硬盘中的数据呢,这就要从磁盘的结构说起。硬件的东西并不是很懂,所以也只能说个大概。硬盘属于磁盘的一种,磁盘分为硬盘和软盘。但他们的逻辑结构是一样的:

    盘片(platter)
    磁头(head)
    磁道(track)
    扇区(sector)
    柱面(cylinder)

    机械式硬盘示意图

    我不想管它怎么动的,我只需要想明白,确定一个磁头、柱面、扇区,就确定了一个 512 字节大小的区域,这就够了。这也就是硬盘的 CHS 表示法,即 Cylinder(柱面)、Head(磁头)、Sector(扇区),只要知道了硬盘的 CHS 的数目,即可确定硬盘的容量,硬盘的容量 = 柱面数 × 磁头数 × 扇区数 × 512B

    如果不考虑这个物理结构,其实硬盘就是 n 多个 512 字节的区域构成的,我们完全可以从 0 开始编号,每 512 字节加一,这样就可以完全不用考虑什么扇区啦,柱面啦,这种是我比较喜欢的(看来还是软件工程师思想呀),这种方式叫做 LBA 表示法

    LBA = (柱面号 * 磁头数 + 磁头号) * 扇区数 + 扇区编号 - 1
    

    所以 CPU 要和硬盘打交道,要么用这个 CHS 表示法,就至少要告诉硬盘柱面、磁头、扇区号是多少,要么用 LBA 表示法告诉硬盘一个 LBA 号码,然后再给硬盘一个是读还是写的信号。硬盘制作厂商千千万,CPU制作厂商也是各不相同,自然就会想到一定有一个硬盘接口标准,这个标准就叫做 ATA 标准,也可以俗称为 IDE 硬盘接口技术标准。这个标准可以下载 AT_Attachment_with_Packet_Interface 共三册的内容,但我们用不到那么多,我这里找到了一个还算原汁原味的中文版的论文 《IDE接口硬盘读写技术》 ,看这个基本就够用了。

    三、IDE硬盘接口技术

    CPU 与外设是通过 IO 接口交互的,所以最核心的就是这个技术标准定义的 IO 接口都有哪些,分别有什么作用

    I/O地址 读(主机从硬盘读数据) 写(主机数据写入硬盘)
    1F0H 数据寄存器 数据寄存器
    1F1H 错误寄存器(只读寄存器) 特征寄存器
    1F2H 扇区计数寄存器 扇区计数寄存器
    1F3H 扇区号寄存器或 LBA 块地址 0~7 扇区号或 LBA 块地址 0~7
    1F4H 磁道数低 8 位或 LBA 块地址 8~15 磁道数低 8 位或 LBA 块地址 8~15
    1F5H 磁道数高 8 位或 LBA 块地址 16~23 磁道数高 8 位或 LBA 块地址 16~23
    1F6H 驱动器/磁头或 LBA 块地址 24~27 驱动器/磁头或 LBA 块地址 24~27
    1F7H 命令寄存器或状态寄存器 命令寄存器

    所以如果要写一个程序来读文件的话,不难分析出整个过程就是:

    1. 在 1F2H 写入要读取的扇区数
    2. 在 1F3H ~ 1F6H 这四个端口写入计算好的起始 LBA 地址
    3. 在 1F7H 处写入读命令的指令号
    4. 不断检测 1F7H (此时已成为状态寄存器的含义)的忙位
    5. 如果第四步骤为不忙,则开始不断从 1F0H 出读取数据到内存指定位置,知道读完

    这五步刚刚好对应着上面的代码

    最后,别忘了我们这些代码仍然是要加载到启动区的,所以最后两个字节依然要是启动区标识符 0x55 0xaa

    四、运行代码

    写好了 mbr.asm,我们再写一个 loader.asm,设置其起始地址为 0x900(因为读写磁盘后存入的内存位置就是这个,这是我们自己定义的),并把它放在磁盘的第二扇区(这也是我们自己定的,只要和读盘的代码保持一致就行)

    loader.asm

    section loader vstart=0x900
    mov byte [gs:0xa0],'l'
    mov byte [gs:0xa2],'o'
    mov byte [gs:0xa4],'a'
    mov byte [gs:0xa6],'d'
    mov byte [gs:0xa8],'e'
    mov byte [gs:0xaa],'r'
    

    剩下的精华就在于我们的 Makefile 文件了,可以参考下上面的代码

    执行 make brun,可以看到如下效果,说明加载磁盘中的 loader 代码到内存这个过程生效了。

    五、开源项目和课程规划

    如果你对自制一个操作系统感兴趣,不妨跟随这个系列课程看下去,甚至加入我们(下方有公众号和小助手微信),一起来开发。

    参考书籍

    《操作系统真相还原》这本书真的赞!强烈推荐

    项目开源

    项目开源地址:https://gitee.com/sunym1993/flashos

    当你看到该文章时,代码可能已经比文章中的又多写了一些部分了。你可以通过提交记录历史来查看历史的代码,我会慢慢梳理提交历史以及项目说明文档,争取给每一课都准备一个可执行的代码。当然文章中的代码也是全的,采用复制粘贴的方式也是完全可以的。

    如果你有兴趣加入这个自制操作系统的大军,也可以在留言区留下您的联系方式,或者在 gitee 私信我您的联系方式。

    课程规划

    本课程打算出系列课程,我写到哪觉得可以写成一篇文章了就写出来分享给大家,最终会完成一个功能全面的操作系统,我觉得这是最好的学习操作系统的方式了。所以中间遇到的各种坎也会写进去,如果你能持续跟进,跟着我一块写,必然会有很好的收货。即使没有,交个朋友也是好的哈哈。

    目前的系列包括

  • 相关阅读:
    AliSQL的编译使用
    linux下编译gcc6.2.0
    TransmitFile函数的简单使用
    C++11的简单线程池代码阅读
    TJpgDec使用说明
    TJpgDec—轻量级JPEG解码器
    PPM图片格式及其C读写代码
    linux下vmware的安装、物理分区使用及卸载
    visual stuido 跨解决方案调试
    Proj.4坐标系统创建参数
  • 原文地址:https://www.cnblogs.com/flashsun/p/12232630.html
Copyright © 2011-2022 走看看