怎么重建mbr引导记录重写mbr的方法重建mbr主引导记录




怎么重建mbr引导记录重写mbr的方法重建mbr主引导记录

2022-07-20 21:52:51 网络知识 官方管理员

怎么重建mbr引导记录(重写mbr的方法)(1)

本节我们在之前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"4SECTIONMBRvstart=0x7c005movax,cs6movds,ax7moves,ax8movss,ax9movfs,ax10movsp,0x7c0011movax,0xb80012movgs,ax1314;清屏15;利用0x06号功能,上卷全部行,则可清屏。16;-----------------------------------------------------------17;INT0x10功能号:0x06功能描述:上卷窗口18;------------------------------------------------------19;输入:20;AH功能号=0x0621;AL=上卷的行数(如果为0,表示全部)22;BH=上卷行属性23;(CL,CH)=窗口左上角的(X,Y)位置24;(DL,DH)=窗口右下角的(X,Y)位置25;无返回值:26movax,0600h27movbx,0700h28movcx,0;左上角:(0,0)29movdx,184fh;右下角:(80,25),30;因为VGA文本模式中,一行只能容纳80个字符,共25行。31;下标从0开始,所以0x18=24,0x4f=7932int10h;int10h3334;输出字符串:MBR35movbyte[gs:0x00],'1'36movbyte[gs:0x01],0xA43738movbyte[gs:0x02],''39movbyte[gs:0x03],0xA44041movbyte[gs:0x04],'M'42movbyte[gs:0x05],0xA4;A表示绿色背景闪烁,4表示前景色为红色4344movbyte[gs:0x06],'B'45movbyte[gs:0x07],0xA44647movbyte[gs:0x08],'R'48movbyte[gs:0x09],0xA44950moveax,LOADER_START_SECTOR;起始扇区lba地址51movbx,LOADER_BASE_ADDR;写入的地址52movcx,1;待读入的扇区数53callrd_disk_m_16;以下读取程序的起始部分(一个扇区)5455jmpLOADER_BASE_ADDR5657;-------------------------------------------------------------------------------58;功能:读取硬盘n个扇区59rd_disk_m_16:60;-------------------------------------------------------------------------------61;eax=LBA扇区号62;bx=将数据写入的内存地址63;cx=读入的扇区数64movesi,eax;备份eax65movdi,cx;备份cx66;读写硬盘:67;第1步:设置要读取的扇区数68movdx,0x1f269moval,cl70outdx,al;读取的扇区数7172moveax,esi;恢复ax7374;第2步:将LBA地址存入0x1f3~0x1f67576;LBA地址7~0位写入端口0x1f377movdx,0x1f378outdx,al7980;LBA地址15~8位写入端口0x1f481movcl,882shreax,cl83movdx,0x1f484outdx,al8586;LBA地址23~16位写入端口0x1f587shreax,cl88movdx,0x1f589outdx,al9091shreax,cl92andal,0x0f;lba第24~27位93oral,0xe0;设置7~4位为1110,表示lba模式94movdx,0x1f695outdx,al9697;第3步:向0x1f7端口写入读命令,0x2098movdx,0x1f799moval,0x20100outdx,al101102;第4步:检测硬盘状态103.not_ready:104;同一端口,写时表示写入命令字,读时表示读入硬盘状态105nop106inal,dx107andal,0x88;第4位为1表示硬盘控制器已准备好数据传输,;第7位为1表示硬盘忙108cmpal,0x08109jnz.not_ready;若未准备好,继续等。110111;第5步:从0x1f0端口读数据112movax,di113movdx,256114muldx115movcx,ax;di为要读取的扇区数,一个扇区有512字节,每次读入一个字,116;共需di*512/2次,所以di*256117movdx,0x1f0118.go_on_read:119inax,dx120mov[bx],ax121addbx,2122loop.go_on_read123ret124125times510-($-$$)db0126db0x55,0xaa

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

1;-------------loader和kernel----------2LOADER_BASE_ADDRequ0x9003LOADER_START_SECTORequ0x2

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行的“movesi,eax”是把eax中的值先备份到esi中。因为al在out指令中会被用到,这会影响到eax的低8位。

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

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

怎么重建mbr引导记录(重写mbr的方法)(2)

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

第74~95行是将LBA地址写入三个LBA寄存器和device寄存器的低4位。端口0x1f3是寄存器LBAlow,端口0x1f4是寄存器LBAmid,端口0x1f5是寄存器LBAhigh。shr指令是逻辑右移指令,这里主要是通过此指令置换出地址的相应部分,写入相应的LBA寄存器。第93行的“oral,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行的“movcx,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>addsapathnametotheincludefilepath”,

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

nasm-Iinclude/-ombr.binmbr.S回车

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

记录了1+0的读入

记录了1+0的写出

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

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

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

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

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

发表评论:

最近发表
网站分类
标签列表