本节我们在之前MBR的基础上,做个稍微大一点的改进,经过这个改进后,我们的MBR可以读取硬盘。听上去这可是个大“手术”呢,我们要将之前学过的知识都用上啦。其实没那么大啦,就是加了个读写磁盘的函数而已,哈哈。怀着兴奋与忐忑的心情,咱们开始吧。

改造不是乱改的,在改之前要有个计划,对将来的程序布局要有个规划,心里有数才行。先说说目前的想法。

我们的MBR是受限于512字节大小的,在那么小的空间中,没法为内核准备好环境,更没法将内核成功加载到内存并运行。所以我们要在另一个程序中完成初始化环境及加载内核的任务,这个程序我们称之为loader,即加载器。Loader会在下一节中实现。问题来了,loader在哪里?如何跳过去执行?这就是新款MBR的使命,简而言之就是,负责从硬盘上把loader加载到内存,并将接力棒交给它。

由于MBR是占据了硬盘的第0扇区(以逻辑LBA方式,扇区从0开始编号,若是以物理CHS方式,扇区则从1开始编号),第1扇区是空闲的,可以用,但离得太近总感觉不如隔开一点心里踏实,所以把loader放到第2扇区。MBR从第2扇区中把它读出来。读出来放到哪里呢?原则上是找个空闲地方就行了,0x500~0x7BFF和0x7E00~9FBFF这两段内存区域都可以。

是这样的,容小弟分析一下:

首先,loader中要定义一些数据结构(如GDT全局描述符表,不懂没关系,以后会说),这些数据结构将来的内核还是要用的,所以loader加载到内存后不能被覆盖。

其次,随着咱们不断添加功能,内核必然是越来越大,其所在的内存地址也会向越来越高的地方发展,难免会超过可用区域的上限,咱们尽量把loader放在低处,多留出一些空间给内核。

所以,我将loader的加载地址选为0x900。为什么不是0x500,这个多省空间。还是预留出一定空间吧,彼此隔开远一点心里才踏实,不差这点空间了,哈哈,完全是个人偏好,大家随意啦。

按照上面所说的规划,下面代码就是改头换面的新款MBR。代码量增长到126行,下面给大家说说细节:

 1 ;主引导程序2 ;------------------------------------------------------------3 %include "boot.inc"4 SECTION MBR vstart=0x7c005 mov ax,cs6 mov ds,ax7 mov es,ax8 mov ss,ax9 mov fs,ax10 mov sp,0x7c0011 mov ax,0xb80012 mov gs,ax1314 ; 清屏15 ;利用0x06号功能,上卷全部行,则可清屏。16 ; -----------------------------------------------------------17 ;INT 0x10 功能号:0x06 功能描述:上卷窗口18 ;------------------------------------------------------19 ;输入:20 ;AH 功能号= 0x0621 ;AL = 上卷的行数(如果为0,表示全部)22 ;BH = 上卷行属性23 ;(CL,CH) = 窗口左上角的(X,Y)位置24 ;(DL,DH) = 窗口右下角的(X,Y)位置25 ;无返回值:26 mov ax, 0600h27 mov bx, 0700h28 mov cx, 0 ; 左上角: (0, 0)29 mov dx, 184fh ; 右下角: (80,25),30 ; 因为VGA文本模式中,一行只能容纳80个字符,共25行。31 ; 下标从0开始,所以0x18=24,0x4f=7932 int 10h ; int 10h3334 ; 输出字符串:MBR35 mov byte [gs:0x00],'1'36 mov byte [gs:0x01],0xA43738 mov byte [gs:0x02],' '39 mov byte [gs:0x03],0xA44041 mov byte [gs:0x04],'M'42 mov byte [gs:0x05],0xA4 ;A表示绿色背景闪烁,4表示前景色为红色4344 mov byte [gs:0x06],'B'45 mov byte [gs:0x07],0xA44647 mov byte [gs:0x08],'R'48 mov byte [gs:0x09],0xA44950 mov eax,LOADER_START_SECTOR ; 起始扇区lba地址51 mov bx,LOADER_BASE_ADDR ; 写入的地址52 mov cx,1 ; 待读入的扇区数53 call rd_disk_m_16 ; 以下读取程序的起始部分(一个扇区)5455 jmp LOADER_BASE_ADDR5657 ;-------------------------------------------------------------------------------58 ;功能:读取硬盘n个扇区59 rd_disk_m_16:60 ;-------------------------------------------------------------------------------61 ; eax=LBA扇区号62 ; bx=将数据写入的内存地址63 ; cx=读入的扇区数64 mov esi,eax ;备份eax65 mov di,cx ;备份cx66 ;读写硬盘:67 ;第1步:设置要读取的扇区数68 mov dx,0x1f269 mov al,cl70 out dx,al ;读取的扇区数7172 mov eax,esi ;恢复ax7374 ;第2步:将LBA地址存入0x1f3 ~ 0x1f67576 ;LBA地址7~0位写入端口0x1f377 mov dx,0x1f378 out dx,al7980 ;LBA地址15~8位写入端口0x1f481 mov cl,882 shr eax,cl83 mov dx,0x1f484 out dx,al8586 ;LBA地址23~16位写入端口0x1f587 shr eax,cl88 mov dx,0x1f589 out dx,al9091 shr eax,cl92 and al,0x0f ;lba第24~27位93 or al,0xe0 ; 设置7~4位为1110,表示lba模式94 mov dx,0x1f695 out dx,al9697 ;第3步:向0x1f7端口写入读命令,0x2098 mov dx,0x1f799 mov al,0x20
100 out dx,al
101
102 ;第4步:检测硬盘状态
103 .not_ready:
104 ;同一端口,写时表示写入命令字,读时表示读入硬盘状态
105 nop
106 in al,dx
107 and al,0x88 ;第4位为1表示硬盘控制器已准备好数据传输,;第7位为1表示硬盘忙
108 cmp al,0x08
109 jnz .not_ready ;若未准备好,继续等。
110
111 ;第5步:从0x1f0端口读数据
112 mov ax, di
113 mov dx, 256
114 mul dx
115 mov cx, ax ; di为要读取的扇区数,一个扇区有512字节,每次读入一个字,
116 ; 共需di*512/2次,所以di*256
117 mov dx, 0x1f0
118 .go_on_read:
119 in ax,dx
120 mov [bx],ax
121 add bx,2
122 loop .go_on_read
123 ret
124
125 times 510-($-$$) db 0
126 db 0x55,0xaa

程序最开始的%include "boot.inc",这个%include是nasm编译器中的预处理指令,意思是让编译器在编译之前把boot.inc文件包含了进来。任何编译器都应该有include之类的能够包含其它文件的预处理指令,不要认为底层的汇编语言就应该简陋到一穷二白,哈哈,这和语言是没关系的,是编译器为了开发人员方便管理代码,应该加的。boot.inc的内容很简单,目前就两句话,文件内容如下:

1 ;-------------    loader和kernel ----------
2 LOADER_BASE_ADDR equ 0x900
3 LOADER_START_SECTOR equ 0x2

boot.inc是我们的配置文件,我们目前关于加载器的配置信息就写在里面,今后还会在此添加更多的配置信息。大家看到的这两句也是预处理命令,是nasm提供的宏,和c语言中的宏是一回事。只不过nasm中的语法是:宏名 equ 值,而c语言中的宏是由#define指令来实现的。所以LOADER_BASE_ADDR和LOADER_START_SECTOR是两个宏名。

LOADER_BASE_ADDR是定义了loader在内存中的位置,MBR要把loader从硬盘读入后放到此处。如前所述,它的值是0x900,说明将来loader会在内存地址0x900处。

LOADER_START_SECTOR是定义了loader在硬盘上的逻辑扇区地址,即LBA地址。前面和大家交待过啦,它等于0x2,说明loader是放在了第2块扇区。

接下来的第4~48行和上一版本没区别,不用多说啦。

第50~52行是为函数rd_disk_m_16传递参数。在此说明一下,汇编语言中定义的函数(或者称为例程,proc),由于汇编语言能够直接操作寄存器,所以其传递参数可以用寄存器,也可以用栈。由于c语言中不能直接操作寄存器,所以咱们这里体验一回用寄存器来传递参数的函数是怎样实现的。另外再说明一下,用寄存器传参数,没有固定的形式,原则上用哪个寄存器都行,只要根据实际应用,别把还有用的寄存器值给覆盖就行,如果真需要用到某个正在使用中的寄存器,只要提前把该寄存器备份好就行了,如备份到其它寄存器或夺入栈中。此函数需要三个参数,我们选择用eax,bx,cx寄存器来传递参数。

在寄存器eax中的是待读入的扇区起始地址,赋值后eax为定义的宏LOADER_START_SECTOR,即0x2。

寄存器cx是读入的扇区数,cx其值为1。到底读入几个扇区,是由实际文件大小来决定的。由于将来会写一个简单的loader,其大小肯定不会超过512字节,所以此处读入的扇区数置为1即可。

数据从硬盘读进来后放在内存中哪里呢,这就要用寄存器bx来指定。在这里,bx寄存器值为LOADER_BASE_ADDR,即0x900。函数名rd_disk_m_16的意思是“在16位模式下读硬盘”。此函数是咱们本节的重点,大伙儿一定要拿下。

第64行的“mov esi,eax”是把eax中的值先备份到esi中。因为al在out指令中会被用到,这会影响到eax的低8位。

第65行是备份读取的扇区数到di寄存器,di寄存器是16位的,和cx大小一致。cx的值会在读取数据时用到,所以在此提前备份。

第67~70行,按照咱们操作硬盘的约定,先选定一个通道,再往sector count寄存器中写扇区数。往端口中写入数据是用out指令,注意out指令中dx寄存器是用来存储端口号。

咱们的虚拟硬盘属于ata0,是Primary通道,所以其sector count寄存器是由0x1f2端口来访问的。顺便再看第二行的ata0-master,path=”hd60M.img”,这说明hd60M.img是主盘。

第74~95行是将LBA地址写入三个LBA 寄存器和device寄存器的低4位。端口0x1f3是寄存器LBA low,端口0x1f4是寄存器LBA mid,端口0x1f5是寄存器LBA high。shr指令是逻辑右移指令,这里主要是通过此指令置换出地址的相应部分,写入相应的LBA寄存器。第93行的“or al,0xe0”,用了or“或”指令和0xe0做或运算,拼出device寄存器的值。高4位为e,即高4位的2进制表示为1110,其第5位和第7位固定为1,第6位为1表示启用LBA。大家可以参考注释。

第97~100行便是写入命令啦,因为我们这里是读操作,所以读扇区的命令是0x20。通过out指令写入command端口0x1f7后,硬盘就开始工作了。

第102~109行是检测status寄存器的BSY位。由于status寄存器依然是0x1f7端口,所以不需要再为dx重新赋值。105行的nop表示空操作,即什么了也不做,只是为了增加延迟,相当于sleep了一小下,目的是减少打扰硬盘的工作。对同一端口在读写两种操作时有不同的用途,在读硬盘时,此端口中的值是硬盘的工作状态。第106行是将Status寄存器的值读入到al寄存器,通过第107行的and“与”操作,保留第4位和第7位,第4位若为1,表示数据已经准备好,可以传输了。若第7位为1,表示硬盘现在正忙着。只要判断第4位是否为1就好了,用第108行的cmp指令和0x08做减法运算,判断第4位是否为1。cmp指令并不改变操作数的值,只是根据结果去设置标志位,从而咱们根据标志位反着去判断结果。cmp指令会影响的标志位有ZF,CF,PF等,这里咱们借助ZF位来判断cmp的结果。于是用第109行的jnz .not_ready来判断结果是否不等于0,即若等于0,则status寄存器的第4位为1,这表示只可以读数据了。若不等于0,说明status寄存器的第4位为0,表示硬盘正忙(此时status寄存器第7位肯定为1)。.not_ready是个标号,于是跳回去继续判断硬盘状态,直到硬盘把数据准备好才跳出这个循环。

第111行~122行是从硬盘取数据的过程。由于data寄存器是16位,即每次in操作只读入2字节,根据读入的数据总量(扇区数*512字节)来求得执行in指令的次数。这里的乘法是用mul指令,在实模式下,mul指令可以做8位乘法和16位乘法,格式是:mul 操作数。操作数可以是寄存器或内存。乘法运算至少要有两个数参与才行,这里的操作数只是一个乘数,被乘数隐含在al或ax寄存器中(mul指令被设计成这样的,由于历史原因产生很多奇怪的用法,习惯就好啦)。如果操作数是8位,被乘数就是al寄存器的值,乘积就是16位,位于ax寄存器。如果操作数是16位,被乘数就是ax寄存器的值,乘积就是32位,积的高16位在dx寄存器,积的低16位在ax寄存器。

虽然我们进行的是16位的乘法,其结果是32位,但由于我知道这两个乘数ax的值和dx的值都不大,ax的实际的值其实是1,乘出来的这个结果,其高位是0,所以在第115行的“mov cx, ax”我们只将这个结果的低16位移入cx做为循环读取的次数。此处用8位乘法不合适,因为256超过了8位寄存器表示的范围。在第118~122行通过循环来将数据写入bx寄存器指向的内存,每读入2个字节,bx所指的地址便+2。值得注意的是,由于在实模式下偏移地址为16位,所以用bx只会访问到0~FFFFh的偏移。待写入的地址超过bx的范围时,从硬盘上读出的数据会把0x0000~0xffff的覆盖,所以此处加载的程序不能超过64k,即2的16次方等于65536。由于本mbr是用来加载loader的,所以loader.bin要小于64k才行。这一点大可以放心,我们最终的loader不超过2k,将来的内核也不会超过70k。

也许有同学会说,把bx改为ebx行吗?也不行,在实模式下,cpu依然会用16位偏移地址。这是实模式下访问内存的规定与缺陷,还记得那个“段基址+段内偏移地址”吗。段内偏移地址正因为是16位,只能访问64k的段空间,所以才将段基址乘以16来突破这64k,从而实现访问低调1M空间的。

第123行是返回指令ret,它是用来从函数中返回。如果我们没有定义函数,就不需要它了。函数和一般代码相比,就是在被调用时,cpu会将返回地址压到栈中,所以在函数体中,要用ret指令将栈中的返回地址重新加载到程序计数器中,如cs:ip,这样程序便恢复到之前的执行顺序了。

执行完第123行后,程序便 回到了第55行,这是个跳转的指令。个人觉得,jmp指令和call指令是必不可少的,jmp表示一去不回头,call表示去了还回来。各有各的用途。这里是MBR交出接力棒的一刻,采用jmp是唯一合适的选择。Jmp的操作数是LOADER_BASE_ADDR,即0x900,这是要跳到内核加载器的节奏。MBR到此结束了使命,顺序完成了第二棒的拼接。复习一下,第一棒是谁来着?是bios交给了MBR。

接下来的工作是编译,本次的编译较之前相比,多加了一个参数 -I。此参数的意思还是先见nasm帮助,nasm –h回车,找到-I的说明:

“-I<path> adds a pathname to the include file path”,

大概意思是添加一个包含文件的路径,其实就是添加个库目录。为了目录整洁一些,我在boot目录下建立了个子目录include,并把boot.inc放到了include目录下。所以nasm的编译参数是,在boot目录下输入:

nasm -I include/ -o mbr.bin mbr.S回车

接下来用dd命令将mbr.bin写入虚拟硬盘:dd if=./mbr.bin of=/此处替换成你的安装目录/bochs/hd60M.img bs=512 count=1 conv=notrunc 回车,下面是dd命令的三行输出:

记录了1+0 的读入

记录了1+0 的写出

512字节(512 B)已复制,0.0265972 秒,19.3 kB/秒

dd命令输出的第三行显示了实际写入硬盘的数据大小,是512字节。

现在还没有准备好loader,所以目前不宜执行。如果好奇心实在太大了,可以运行一下试试,反正只是虚拟机,对物理机不会有伤害,也许会cpu使用率过高。记得用ctrl+c在bochs控制台中断运行就好了。

说了半天咱们还没有loader呢,若此时执行此MBR,cpu会直接跳到0x900的地方,非乱了不可,程序的运行不可预测。难为大家一直跟我在这假想这个虚幻的loader,下一节我们要实现个真的loader啦。

MBR大致就说到这,大家若是不理解,也不要糊弄自己,还是建议大家一行一行地看,直到弄清楚为止。代码写的不美,请大家多多包含。

一步步编写操作系统 23 重写主引导记录mbr相关推荐

  1. 一步步编写操作系统 71 直接操作显卡,编写自己的打印函数71-74

    一直以来,我们在往屏幕上输出文本时,要么利用bios中断,要么利用系统调用,这些都是依赖别人的方法.咱们还用过一个稍微有点独立的方法,就是直接写显存,但这貌似又没什么含量.如今我们要写一个打印函数了, ...

  2. 一步步编写操作系统 69 汇编语言和c语言共同协作 70

    由于有了上一节的铺垫,本节的内容相对较少,这里给大家准备了两个小文件来实例演示汇编语言和c语言相互调用. 会两种不同语言的人,只是掌握了同一件事物的两种表达方式.人在学习一种新语言时,潜意识里是建立了 ...

  3. 一步步编写操作系统 62 函数调用约定

    由于我们要将c语言和汇编语言结合编程啦,所以一定会存在汇编代码和c代码相互调用的问题,有些事情还是要提前交待给大家的,本节就是要给大家说下函数调用规约中的那些事儿. 函数调用约定是什么? 调用约定,c ...

  4. 一步步编写操作系统 08 bios跳转到神奇的内存地址0x7c00

    为什么是0x7c00 计算机执行到这份上,bios也即将完成自己的历史使命了,完成之后,它又将睡去.想到这里,心中不免一丝忧伤,甚至有些许挽留它的想法.可是,这就是它的命,它生来被设计成这样,在它短暂 ...

  5. 硬盘结构,主引导记录MBR,硬盘分区表DPT,主分区、扩展分区和逻辑分区,电脑启动过程...

    filex的文件系统看的云里雾里,还是先总结下FAT的一些基本知识吧. 硬盘结构 硬盘有很多盘片组成,每个盘片的每个面都有一个读写磁头.如果有N个盘片.就有2N个面,对应2N个磁头(Heads),从0 ...

  6. 硬盘主引导记录MBR

    主引导记录:(MBR,Main Boot Record)是位于磁盘最前边的一段引导(Loader)代码.它负责磁盘操作系统(DOS)对磁盘进行读写时分区合法性的判别.分区引导信息的定位,它由磁盘操作系 ...

  7. 主引导扇区及主引导记录MBR的详细说明

    引导扇区在每个分区里都存在,但是我们常说的*主引导扇区*是硬盘的 第一物理扇区.它由两个部分组成:即主引导记录MBR和硬盘分区表DPT.在 总共512字节的主引导分区里其中MBR占446个字节(偏移0 ...

  8. 主引导记录MBR/硬盘分区表DPT/主分区、扩展分区和逻辑分区/电脑启动过程

    主引导扇区 主引导扇区位于整个硬盘的0柱面0磁头1扇区{(柱面,磁头,扇区)|(0,0,1)},bios在执行自己固有的程序以后就会jump到MBR中的第一 条指令.将系统的控制权交由mbr来执行.主 ...

  9. 硬盘结构,主引导记录MBR,硬盘分区表DPT,主分区、扩展分区和逻辑分区

    filex的文件系统看的云里雾里,还是先总结下FAT的一些基本知识吧. 硬盘结构 硬盘有很多盘片组成,每个盘片的每个面都有一个读写磁头.如果有N个盘片.就有2N个面,对应2N个磁头(Heads),从0 ...

最新文章

  1. 周志华:Boosting学习理论的探索 —— 一个跨越30年的故事
  2. 脑电分析系列[MNE-Python-1]| MNE-Python详细安装与使用(更新)
  3. 用typedef定义函数指针类型(转)
  4. 直播源码和短视频源码,相亲相爱的一家人
  5. 图灵71年前提出神经网络!《智能机器》再掀热议,却一生未发表
  6. MongoDB安装和启动
  7. html表单代码例子_关于React的这些细节,你知道吗?-表单
  8. 不肯嫁的几种男人(转)
  9. 伟大骡子的一生和性能测试
  10. RabbitMq消息序列化简述
  11. MATLAB数据标准化处理,mapminmax、zscore、mapstd对比
  12. maven跳过单元测试
  13. 苹果锁定计算机的快捷键,苹果电脑快捷键使用 Mac快捷键大全详细介绍
  14. logit回归模型假设_logistic回归模型分析
  15. firsthead学习笔记
  16. python加密安装方法_安装Python加密错误
  17. 英语背单词有用吗_从0学英语背单词有用吗,正确的方法是什么?
  18. 智慧交通,迎来产业谍战丨产业特稿
  19. 有道云笔记 - Markdown模板(文首附markdown源码,即.md文件)
  20. 上海市高校大学生程序设计邀请赛 C:小花梨判连通

热门文章

  1. Autodesk 360 Mobile不能显示图片?
  2. 记一次 IIS 7.0 身份验证相关的问题解决
  3. asp。net中常用的文件操作类
  4. [Leetcode][第35题][JAVA][搜索插入位置][二分法]
  5. 用python画六瓣雪花_python-turtle-画雪花-2种方法及效果的详解
  6. python列表统计每个元素出现次数_python 统计list中各个元素出现的次数的几种方法...
  7. linux修改挂载目录名字,linux下修改mount挂载目录名
  8. C语言作业不足之处,C语言和汇编语言的优缺点分析-控制器/处理器-与非网
  9. 联想服务器系统安装bios设置,Windows 8操作系统如何通过Legacy BIOS与UEFI两种模式安装...
  10. wpf的listbox循环数据滚动_滚动版 CentOS Stream 和 Fedora 的关系