购买
下载掌阅APP,畅读海量书库
立即打开
畅读海量书库
扫码下载掌阅APP

2.1 主引导记录(MBR)

MBR在Windows启动之前就已经被填充好,它并不专属于任何一个操作系统。MBR包含代码和数据两部分,前半部分是启动引导代码,后半部分是一张磁盘分区表,记录每个分区在磁盘上的位置、大小及分区类型。MBR只有512B,通常我们会定义一个标准的结构来描述,它具有以下一些共同的特征:

整体上看,这段MBR代码并没有多少行。设置好栈框架后,我们直接调用LoadSector加载NTLDR。鉴于我们系统的整体设计,NTLDR文件在编译链接阶段就放在磁盘第8扇区开始。不管NTLDR真实大小是多大,NTLDR占据的扇区总数设定为(80000h/512-8)。如果设定的扇区数太小,NASM链接器会给出错误提示“error:TIMES value-xxxx is negative”,因此,我们设定NTLDR占用的扇区数只可以多而不可以少。加载NTLDR到0x20000物理内存后,我们不做是否加载成功这一步检查,直接跳到0x2000:0000运行。如果加载失败,那么跳到0x2000:0000运行的代码肯定不是我们想要的,后续运行肯定会出错,所以对于加载来说,成功就是成功,失败了就失败了。

为了观察MBR代码的运行状态,我们使用Bochs进行调试。bochsrc.bxrc文件内容设置如下。我们先注释掉Bochs和WinDbg通信端口,因为在MBR阶段,编写COM1通信的代码是不现实的,而且MBR没有任何目的需要和WinDbg建立连接,WinDbg调试系统的初始化需要等到osloader运行之后。

双击运行run.bat,将立即进入Bochs调试状态。图2.2所示部分是Bochs自己中断下来的,现在我们需要调试MBR代码。我们知道MBR代码被加载在0x7c00位置,随后CPU肯定会跳到MBR执行,因此我们预先下断点b 0x7c00,再输入c,执行流程就来到了MBR。

图2.2

MBR的开始地址是0000:7c00。cli是我们所写代码的第一条指令。Bochs命令n表示单步步过执行,s表示单步步入执行,c则表示连续执行(除非遇到断点)。综合运用这几个简单的指令,我们就可以一步一步地调试MBR,调试过程和我们写代码时预期的设计应该是一致的。读者在学习这一部分的时候可以同步进行,一边看本章讲述,一边看代码,一边调试观察。

MBR分成三个部分,第一部分就是前面446B代码区,第二部分就是接下来的64B磁盘分区表,磁盘分区表又分为4个16B的分区表项。硬盘最多可以有4个主分区,或者3个主分区和1个扩展分区。最后一部分就是2B的分区标志AA 55。NASM汇编允许在代码中直接定义数据,但是不作为代码执行。每个分区表项占据16B,我们只使用第一个分区,下面描述的就是第一个活动分区(C:)。

上面从数据80h开始的16B是硬盘的第一个分区表项,描述了一个完整的分区信息,标示了分区的活动状态、起始位置、结束位置及分区类型。分区状态标志由MBR来识别,这里我们的MBR代码根本不需要识别。对于系统的设计来说,分区的起始位置和结束位置是最重要的,分区类型显得并不那么重要,但是在后续的设计中,比如文件系统利用分区类型来识别分区的数据格式,以确定需要什么文件系统来解析时,它又显得非常重要,因此这一点需要在安装系统时就指定好。这个分区表在我们的启动阶段并不需要,但我们必须定义好,主要是为I/O系统做准备,详见第8章。

MBR找到NTLDR文件在磁盘上的地址之后,需要将其加载到物理地址0x20000,那么NTLDR文件是如何被加载的呢?事实上,只需调用一个简单的BIOS中断13h/AH=42h进行读取就可以了。由于BIOS中断13h/AH=42h一次最多只能读取127扇区,如果按一次127扇区进行读取,显然不符合计算机的运算习惯,通常计算机更倾向于按2的n次方进行运算。为了提高运行效率,我们不按127扇区的倍数读取,而是退而求其次,每次按64(2的6次方)扇区进行读取。由于NTLDR文件并不大,最多循环几次就能完成对整个文件的读取。

BIOS中断13H/42H在读取不是2的n次方扇区时是否真的会变慢,笔者没有进行相关测试,只是根据理论这么认为。如果读者需要进行测试,测试的代码应该放在Loader模块中实现。因为Loader模块是使用C语言编写的,更容易理解,同时它能够切换回实模式,那时WinDbg也已经连接上了,可以近距离观察。

根据以上原理,下面是使用C语言实现的读取扇区函数代码。参数des存放所读取数据的目的地址(物理内存地址),src是扇区号(0~n),count是需要读取的扇区数。LoadSector函数没有返回值,倘若读取失败就完全失败了。

如果要读取的扇区总数大于64,我们可以通过一个for循环读取。每次调用ReadWrite-Sector函数读取64扇区并调整源地址和目的地址,扇区数小于64的部分,单独做一次读取就行,这样就完成了扇区的读取。

通过C语言代码把握LoadSector函数读取扇区的大致思路后,现在我们转入相应汇编代码的学习,根据第1章介绍的方法整理出LoadSector函数的反汇编代码。因为我们的MBR是使用NASM汇编的。我们将沿用在C语言代码中定义的参数,给出下面这样一个参数位置,表明各个参数在栈中的相应存放位置,读者可以通过查看参数位置,知道每次从栈中读取的数据代表什么,代码在进行什么操作,从而更好地理解代码的含义。

首先来看一下其C语言函数原型,有3个参数,每个参数占据4个字节:

第3个参数count=0x3f8入栈后,[0x7c00-4]=0x3f8,sp=0x7bfc,注意栈是反方向递减的。第2个参数src=8入栈后,[0x7c00-4-4]=8,每次入栈都减4,sp=0x7bf8。第1个参数des=0x20000入栈后,[0x7c00-4-4-4]=0x20000,sp=0x7bf4。

调用call LoadSector时,还需要把指向下一条指令的地址入栈。由于MBR运行在实模式下,指令地址占2B,sp=0x7bf4-2=0x7bf2。

下面是执行流程进入LoadSector函数后的一些基本情况:esp=0x7bf2,ebp=0,这两个值的变化是要注意的。LoadSector内首先有两行代码:

把ebp入栈,esp=0x7bf2-4=0x7bee,再把esp值给ebp,于是ebp=0x7bee。现在我们应该想想,参数des存储在栈的哪个地址里。由上可知,des放在栈地址0x7bf4,而当前ebp=0x7bee,0x7bf4-0x7bee=6,如果我们取参数des,从栈[ebp+6]取就可以了。同理,src在栈[ebp+0Ah],count在栈[ebp+0Eh]。栈参数des=[ebp+6],src=[ebp+0Ah],count=[ebp+0Eh]。然后对照阅读汇编代码。

LoadSector函数的作用我们通过C函数明白了,我们看汇编代码也能看到是怎样编写的。汇编代码有点长,读者可以通过Bochs调试,结合代码中的注释,很快弄懂代码的含义。在上述代码中,读取数据时需要调用一个叫作ReadWriteSector的函数,这才是真正读取数据的函数,在Loader模块中进入实模式时也是调用它来读取数据的,下面我们需要认真理解它的设计过程。ReadWriteSector只是简单地根据传入的参数设置好磁盘地址包(Disk Address Packet,DAP),然后调用BIOS中断13H/AH=42H来完成读取。通常用SectorFrame结构来传递参数,也就是取参数的偏移。下面我们先看一下DAP和SecotrFrame结构是如何定义的。

磁盘地址包(DAP)约定使用BIOS中断13H/AH=42H。这个约定是设计BIOS中断时便决定的,我们没法去改变BIOS的设计,所以既然使用了BIOS中断,就按照BIOS约定的方式使用。如果不使用BIOS中断,我们需要自已设计类似读取磁盘地址包DAP的方法,但因为和外设的通信都是IN/OUT指令,设计的复杂度比较高。有兴趣的读者可以参看uniata工程项目,它是微端口驱动,通过访问端口方式来读取数据。但我们在MBR做这样的工作是不现实的。考虑到操作系统的设计,BIOS也提供了辅助中断方式来供操作系统启动阶段使用。

参数传递SecotrFrame框架,定义了栈的偏移地址。比如,我们需要使用[BP+4]的栈地址,只要使用[BP+SecotrFrame.LBNLow]就可以了,因为LBNLow相对SectorFrame基址偏移了4B。这样的设计提高了NASM汇编代码的可读性。

每进行一次ReadWriteSector调用,首先需要填充DAP结构。填充数据来自SectorFrame中的对应参数值,而SectorFrame中的参数值是从之前已在栈中存放好的相应地址中取出来的。填充完DAP结构之后,直接调用BIOS中断13H/AH=42H就能完成一次数据的读取。

ReadWriteSector函数原型如下(参数对照SectorFrame结构):

代码如下:

DAP结构是开辟函数栈空间来存放的(sub sp,16)。si寄存器指向DAP地址,也就是栈顶地址(mov si,sp),接下来的几行汇编代码都是填充DAP包结构。ah设置读(42h)或写(43h)。dl设置读哪个盘(bit 7代表硬盘,第一块硬盘为80h,第二块硬盘为81h)。设置好之后调用BIOS中断13H完成一次读取。在BIOS代码内部自动把数据放在DAP指向的Buffer物理内存中。 4N8MMdkMdf/w5xtAGk8qxnEt2MlufoLGoiXSx8/6JaHWgm7/YHs6Is89dMFHtQfu

点击中间区域
呼出菜单
上一章
目录
下一章
×