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

本节我们在之前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大致就说到这,大家若是不理解,也不要糊弄自己,还是建议大家一行一行地看,直到弄清楚为止。代码写的不美,请大家多多包含。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/439841.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

11.深度学习练习:Keras tutorial - the Happy House(推荐)

本文节选自吴恩达老师《深度学习专项课程》编程作业&#xff0c;在此表示感谢。 课程链接&#xff1a;https://www.deeplearning.ai/deep-learning-specialization/ Welcome to the first assignment of week 2. In this assignment, you will: Learn to use Keras, a high-lev…

【HDU - 5014】Number Sequence(贪心构造)

题干&#xff1a; There is a special number sequence which has n1 integers. For each number in sequence, we have two rules: ● a i ∈ [0,n] ● a i ≠ a j( i ≠ j ) For sequence a and sequence b, the integrating degree t is defined as follows(“♁” deno…

一步步编写操作系统 24 编写内核加载器

这一节的内容并不长&#xff0c;因为在进入保护模式之前&#xff0c;我们能做的不多&#xff0c;loader是要经过实模式到保护模式的过渡&#xff0c;并最终在保护模式下加载内核。本节只实现一个简单的loader&#xff0c;本loader只在实模式下工作&#xff0c;等学习了保护模式…

12.深度学习练习:Residual Networks(注定成为经典)

本文节选自吴恩达老师《深度学习专项课程》编程作业&#xff0c;在此表示感谢。 课程链接&#xff1a;https://www.deeplearning.ai/deep-learning-specialization/ 目录 1 - The problem of very deep neural networks 2 - Building a Residual Network 2.1 - The identity…

【HDU - 5869】Different GCD Subarray Query(思维,数学,gcd,离线处理,查询区间不同数,树状数组 或 二分RMQ)

题干&#xff1a; This is a simple problem. The teacher gives Bob a list of problems about GCD (Greatest Common Divisor). After studying some of them, Bob thinks that GCD is so interesting. One day, he comes up with a new problem about GCD. Easy as it look…

13.深度学习练习:Autonomous driving - Car detection(YOLO实战)

本文节选自吴恩达老师《深度学习专项课程》编程作业&#xff0c;在此表示感谢。 课程链接&#xff1a;https://www.deeplearning.ai/deep-learning-specialization/ Welcome to your week 3 programming assignment. You will learn about object detection using the very pow…

一步步编写操作系统 25 cpu的保护模式

在保护模式下&#xff0c;我们将见到很多在实模式下没有的新概念&#xff0c;很多都是cpu硬件原生提供&#xff0c;并且要求的东西&#xff0c;也就是说按照cpu的设计&#xff0c;必须有这些东西cpu才能运行。咱们只要了解它们是什么并且怎么用就行了&#xff0c;不用深入到硬件…

【HDU - 5876】Sparse Graph(补图bfs,STLset)

题干&#xff1a; In graph theory, the complementcomplement of a graph GG is a graph HH on the same vertices such that two distinct vertices of HH are adjacent if and only if they are notnotadjacent in GG. Now you are given an undirected graph GG of NN no…

14.深度学习练习:Face Recognition for the Happy House

本文节选自吴恩达老师《深度学习专项课程》编程作业&#xff0c;在此表示感谢。 课程链接&#xff1a;https://www.deeplearning.ai/deep-learning-specialization/ Welcome to the first assignment of week 4! Here you will build a face recognition system. Many of the i…

java Integer 源码学习

转载自http://www.hollischuang.com/archives/1058 Integer 类在对象中包装了一个基本类型 int 的值。Integer 类型的对象包含一个 int 类型的字段。 此外&#xff0c;该类提供了多个方法&#xff0c;能在 int 类型和 String 类型之间互相转换&#xff0c;还提供了处理 int 类…

【HDU - 5875】Function(线段树,区间第一个小于某个数的数 或 RMQ二分)

题干&#xff1a; The shorter, the simpler. With this problem, you should be convinced of this truth. You are given an array AA of NN postive integers, and MM queries in the form (l,r)(l,r). A function F(l,r) (1≤l≤r≤N)F(l,r) (1≤l≤r≤N) is defin…

15.深度学习练习:Deep Learning Art: Neural Style Transfer

本文节选自吴恩达老师《深度学习专项课程》编程作业&#xff0c;在此表示感谢。 课程链接&#xff1a;https://www.deeplearning.ai/deep-learning-specialization/ 目录 1 - Problem Statement 2 - Transfer Learning 3 - Neural Style Transfer 3.1 - Computing the cont…

java中synchronized(同步代码块和同步方法)详解及区别

问题的由来&#xff1a; 看到这样一个面试题&#xff1a; ? 1 2 3 4 5 6 //下列两个方法有什么区别 public synchronized void method1(){} public void method2(){ synchronized (obj){} } synchronized用于解决同步问题&#xff0c;当有多条线程同时访问共享数据时&a…

【2018icpc宁夏邀请赛现场赛】【Gym - 102222F】Moving On(Floyd变形,思维,离线处理)

https://nanti.jisuanke.com/t/41290 题干&#xff1a; Firdaws and Fatinah are living in a country with nn cities, numbered from 11 to nn. Each city has a risk of kidnapping or robbery. Firdawss home locates in the city uu, and Fatinahs home locates in the…

动手学PaddlePaddle(1):线性回归

你将学会&#xff1a; 机器学习的基本概念&#xff1a;假设函数、损失函数、优化算法数据怎么进行归一化处理paddlepaddle深度学习框架的一些基本知识如何用paddlepaddle深度学习框架搭建全连接神经网络参考资料&#xff1a;https://www.paddlepaddle.org.cn/documentation/doc…

Apollo进阶课程㉒丨Apollo规划技术详解——Motion Planning with Autonomous Driving

原文链接&#xff1a;进阶课程㉒丨Apollo规划技术详解——Motion Planning with Autonomous Driving 自动驾驶车辆的规划决策模块负责生成车辆的行驶行为&#xff0c;是体现车辆智慧水平的关键。规划决策模块中的运动规划环节负责生成车辆的局部运动轨迹&#xff0c;是决定车辆…

JVM核心之JVM运行和类加载全过程

为什么研究类加载全过程&#xff1f; 有助于连接JVM运行过程更深入了解java动态性&#xff08;解热部署&#xff0c;动态加载&#xff09;&#xff0c;提高程序的灵活性类加载机制 JVM把class文件加载到内存&#xff0c;并对数据进行校验、解析和初始化&#xff0c;最终形成J…

【2018icpc宁夏邀请赛现场赛】【Gym - 102222A】Maximum Element In A Stack(动态的栈中查找最大元素)

https://nanti.jisuanke.com/t/41285 题干&#xff1a; As an ACM-ICPC newbie, Aishah is learning data structures in computer science. She has already known that a stack, as a data structure, can serve as a collection of elements with two operations: push, …

动手学PaddlePaddle(2):房价预测

通过这个练习可以了解到&#xff1a; 机器学习的典型过程&#xff1a; 获取数据 数据预处理 -训练模型 -应用模型 fluid训练模型的基本步骤: 配置网络结构&#xff1a; 定义成本函数avg_cost 定义优化器optimizer 获取训练数据 定义运算场所(place)和执行器(exe) 提供数…

JAVA 堆栈 堆 方法区 解析

基础数据类型直接在栈空间分配&#xff0c; 方法的形式参数&#xff0c;直接在栈空间分配&#xff0c;当方法调用完成后从栈空间回收。 引用数据类型&#xff0c;需要用new来创建&#xff0c;既在栈空间分配一个地址空间&#xff0c;又在堆空间分配对象的类变量 。 方法的引用…