Chinaunix首页 | 论坛 | 博客
  • 博客访问: 1019222
  • 博文数量: 326
  • 博客积分: 10135
  • 博客等级: 上将
  • 技术积分: 2490
  • 用 户 组: 普通用户
  • 注册时间: 2006-04-22 23:53
文章分类

全部博文(326)

文章存档

2014年(1)

2012年(4)

2011年(1)

2010年(4)

2009年(41)

2008年(44)

2007年(63)

2006年(168)

我的朋友

分类: LINUX

2006-09-27 17:33:56

操作系统学习常见疑惑问与答(最后更新于2006-4-15 不断添加中...)

操作系统学习常见疑惑问与答(不断添加中...)



作者:yxin1322
blog:http://blog.csdn.net/yxin1322  转载请注明出处


         大三的时候已经学过了《操作系统原理》这门课,虽然学习过程中做过一些实验,但对操作系统的认识仍然停留在理论的层面上,对于如何才能编程实现一个真正的操作系统缺乏可操作的方案,于是有了我现在进行的学习计划。按照计划,近期我读了一些有关操作系统编写的书籍,也参考了网上很多的文章,接触了不少国内这方面的论坛,总算对编写操作系统有了一个大概的认识。在这期间,我曾经被许多小的问题困扰过,并且我认为许多初学操作系统的同学也会跟我一样有相同的困惑,所以我将自己找到的答案和心得以问答的形式记录在此,希望能给后来者提供参考,同时也当作自己的学习备忘。

         学习还在继续,问题也在不断增加,所以我会不断丰富问答的内容。其中部分回答摘自网上的文章,我会注明,而大部分回答则是我自己的见解。如果大家读后认为回答有不对或理解偏差的地方,还请留言指正。
         需要说明的是我这里讨论的操作系统是指基于IA构架的操作系统,并不包括其他计算机构架之上的操作系统或嵌入式操作系统。


1、问:操作系统如何能够开始控制一台计算机,操作系统的起点是什么?



      答:操作系统的“开端”一般被认为是引导程序(Boot),它是操作系统程序中最早被计算机硬件系统加载入内存并执行的部分,引导程序一般规定长度为512个字节,就是从这512字节开始,操作系统被一步步装载入计算机内存,进而最终控制整台计算机。那么引导程序是如何被找到并加载入计算机内存的呢?这里涉及到硬件厂商和软件厂商的协调。协调的结果如下:当计算机加电启后,首先转去执行BIOS中的程序进行硬件自检,如果自检成功,则开始尝试在可引导介质中依次寻找引导程序,可引导介质就是我们计算机的软盘驱动器,光盘驱动器以及硬盘等存储设备,搜索的顺序是按照BIOS里设置的引导顺序进行的。
          对于软盘来说,计算机会检查软盘的0面0磁道0扇区(被称为boot sector引导扇区),由于每个扇区512字节,因此刚好容纳下引导程序。如果该扇区最后两个字节依次是55H和AAH,那么就表明该扇区存储的是一段引导程序,进而由BIOS程序将这512个字节依次复制到0X7C00开始的内存单元,然后计算机会跳转到地址0X7C00处执行。至此,计算机真正开始执行我们自己编写的代码了。因为512字节很少,无法利用它做一些复杂的事,所以通常将这512字节的代码用来从存储器载入另一个程序,这个程序不受512字节的限制,因此可以用它来做一些载入系统内核,初始化操作系统的工作,通常这个程序被称为loader。
        与此相类似,硬盘Boot Sector也就是硬盘的第一个扇区,它由MBR(Master Boot Record), DPT(Disk Partition Table) 和 Boot Record ID三部分组成。 MBR又称为主引导记录,占用Boot Sector的前446个字节(0~0x1BD),存放系统主引导程序(它负责从活动分区中装载并且运行系统引导程序)。 DPT即主分区表占用64个字节(0x1BE~0x1FD),记录磁盘的基本分区信息。主分区表分为四个分区项,每项16个字节,分别记录每个主分区的信息(因此最多可以有四个主分区)。 Boot Record ID即引导区标记占用两个字节(0x1FE~0x1FF),对于合法引导区,它等于0xaa55,这是判别引导区是否合法的标志),合法引导区将会被计算机加载到0x7c00处,并执行之。
         由此可见,引导程序有以下特点:软盘中的引导程序应该少于510字节(除去引导标志0x55aa的两个字节),事实上有些时候会更少,因为某些文件系统需要在引导扇区添加若干信息,将占去一些字节;硬盘上的引导程序应少于446字节;引导程序有最大字节限制,但没有最小字节限制,引导程序和其他数据(如ox55aa标志,硬盘分区表等数据)加起来不足512字节的空间需要用数据填充,但应保证程序不会执行到这些数据。
         光盘和USB盘的启动过程没有研究过,希望有研究的朋友指点^_^.


2、问:显卡有自己的显存,内存ram中也有一块叫显存,比如A0000开始的那一段,我想问一下这两个显存是一码事吗?


      答:要弄清楚这个问题,首先要知道什么是地址映射,为什么平时我们说内存地址空间,而不是说内存空间。因为实模式下,20根地址线,最多可以寻址1M内存,也就是我们有1M的地址资源,这1M的地址我们除了用来寻址物理内存外,还要用它来寻址访问bios rom,还有各种外设的rom(因为计算机体系中并没有提供其他的手段来访问这些rom).这样,1M的地址并不是所有都对应到了物理内存上,有的地址对应到了BIOS的rom,有的则对应到了显卡的显存里。比如A0000开始的一段地址范围被用来寻址图形模式的显存,而B8000开始的一段地址范围用来寻址字符模式的显存。当你用这些范围的地址进行读写“内存”的时候,实际上并没有访问到物理内存,而是被定位到显卡的显存上去了,也就是说在当前这种状况下,这些地址对应的物理内存单元就没用了,因为访问被重定向了,所以永远不可能被访问到。由此也解释了为什么要用“地址空间”这个术语,而不是用内存空间,在这里,我们能用连续的地址,但这些地址并不是都到物理内存上去寻址,而是被各个外设的rom和物理内存瓜分了。所以问题中“内存ram中也有一块叫显存”本身就是错误的说法,这个时候的地址已经不对应着内存了。以下附上PC系统启动时的地址映射安排:



: (2006-4-7 10:38, 21.81 K)



3、DPL,RPL,CPL 之间的联系和区别是什么?RPL和CPL是必须相同吗?如果相同,为什么要釆用两个而不改用一个呢?



      答:特权级是保护模式下一个重要的概念,CPL,RPL和DPL是其中的核心概念,查阅资料无数,总结如下:
         简单解释:

--------------------------------------------------------------------------------

         CPL是当前进程的权限级别(Current Privilege Level),是当前正在执行的代码所在的段的特权级,存在于cs寄存器的低两位。
         RPL说明的是进程对段访问的请求权限(Request Privilege Level),是对于段选择子而言的,每个段选择子有自己的RPL,它说明的是进程对段访问的请求权限,有点像函数参数。而且RPL对每个段来说不是固定的,两次访问同一段时的RPL可以不同。RPL可能会削弱CPL的作用,例如当前CPL=0的进程要访问一个数据段,它把段选择符中的RPL设为3,这样虽然它对该段仍然只有特权为3的访问权限。
        DPL存储在段描述符中,规定访问该段的权限级别(Descriptor Privilege Level),每个段的DPL固定。
当进程访问一个段时,需要进程特权级检查,一般要求DPL >= max {CPL, RPL}
        下面打一个比方,中国官员分为6级国家主席1、总理2、省长3、市长4、县长5、乡长6,假设我是当前进程,级别总理(CPL=2),我去聊城市(DPL=4)考察(呵呵),我用省长的级别(RPL=3 这样也能吓死他们:-))去访问,可以吧,如果我用县长的级别,人家就不理咱了(你看看电视上的微服私访,呵呵),明白了吧!为什么采用RPL,是考虑到安全的问题,就好像你明明对一个文件用有写权限,为什么用只读打开它呢,还不是为了安全!


         全面解释:
--------------------------------------------------------------------------------

         RPL是段选择子里面的bit 0和bit 1位组合所得的值,但这里要首先搞清楚什么是段选择子,根据Intel 的文件(IA-32 IntelR Architecture Software Developer's Manual, Volume 3System Programming Guide)它是一个16Bit identifier (原文:A segment selector is a 16-bit identifier for a segment). 但 identifier 又是什么. identifier 可以是一个变数的名字( An identifier is a name for variables), 简单的说它可以就是一般意义的变数. 这里 16-bit identifier for a segment 可以就是一个一般意义的16bit变数但同时要求对它的值解释的时候必须跟据Intel定下的规则---也就是bit 0和bit 1位的组合值就是RPL等等… 因此在程序里如果有需要的话你可以声明一个或者多个变数来代表这些段选择子,这样的话你的程序在某一时刻就可以有很多段选择子,当然有那么多段选择子就有那么多RPL.可以这样说程序有多少个是RPL是你怎样看待你自己声明的变数. |
         程序的CPL(CS.RPL)是CS register 里bit 0和bit 1 位组合所得的值.在某一时刻就只有这个值唯一的代表程序的CPL.
   
         而DPL是段描述符中的特权级, 它的本意是用来代表它所描述的段的特权级. 一个程序可以使用很多段(Data,Code,Stack)也可以只用一个code段等.在正常的情况下当程序的环境建立好后,段描述符都不需要改变-----当然DPL也不需要改变.


     一、对数据段和堆栈段访问时的特权级控制:

          要求访问数据段或堆栈段的程序的CPL≤待访问的数据段或堆栈段的DPL,同时选择子的RPL≤待访问的数据段或堆栈段的DPL,即程序访问数据段或堆栈段要遵循一个准则:只有相同或更高特权级的代码才能访问相应的数据段。这里,RPL可能会削弱CPL的作用,访问数据段或堆栈段时,默认用CPU和RPL中的最小特权级去访问数据段,所以max {CPL, RPL} ≤ DPL,否则访问失败。


     二、对代码段访问的特权级控制(代码执行权的特权转移):

         让我们先来记一些“定律”:
        所有的程序转跳,CPU都不会把段选择子的RPL赋给转跳后程序的CS.RPL. .

         转跳后程序的CPL(CS.RPL)只会有下面的俩种可能
         转跳后程序的CPL(CS.RPL) = 转跳前程序的CPL(CS.RPL)
         或
         转跳后程序的CPL(CS.RPL) = 转跳后程序的CodeDescriptor.DPL

         以 Call 为例(只能跳到等于当前特权级或比当前特权级更高的段):
         怎样决定这两种选择,这就要首先知道转跳后程序的段是一致代码段还是非一致代码段.其实也很简单,规则如下:
         如果能成功转跳到一致代码段, 转跳后程序的CPL(CS.RPL) = 转跳前程序的CPL(CS.RPL),(转跳后程序的CPL继承了转跳前程序的CPL)
         如果能成功转跳到非一致代码段, 转跳后程序的CPL(CS.RPL) =转跳后程序的Descriptor.DPL。(转跳后程序的CPL变成了该代码段的特权级.我在前面提到DPL是段描述符中的特权级, 它的本意是用来代表它所描述的段的特权级)怎样才能成功转跳啦?


         这里有四个重要的概念:

         1).段的保护观念是高特权级不找低特权级办事,低特权级找高特权级帮忙,相同的一定没问题.(这样想逻辑是没错,事实对不对就不知道.)也就是县长不找乡长,乡长不求农民,反过来农民求乡长,乡长找县长.这个概念是最重要的。
         2) 一致代码段的意义: 让客人很方便的利用主人(一致代码段)的东西为自己办事.但客人这身份没有改变NewCS.RPL=OldCS.RPL所以只能帮自己办事。比方说乡长有一头牛,农民可以借来帮自己种田,但不能种别人的田.但是如果你是乡长当然可以种乡里所有的田。
         3) 非一致代码段的意义:主人(非一致代码段)可以帮客人但一定是用自己的身份NewCS.RPL= DestinationDescriptorCode.DPL这里可能有安全的问题, 搞不好很容易农民变县长。主人太顽固了一定要坚持自己的身份,有什么方法变通一下,来个妥协好不好。好的,它就是RPL的用处。
         4) RPL: 它让程序有需要的时候可以表示一个特权级更低的身份Max(RPL,CPL)而不会失去本身的特权级CPL(CS.RPL),有需要的时候是指要检查身份的时候。事实上RPL跟段本身的特权级DPL和当前特权级CPL没有什么关系,因为RPL的值在成功转跳后并不赋给转跳后的CS.RPL。
         还是要问怎样才能成功转跳啦?这里分两种情况:

         普通转跳(没有经过Gate 这东西):即JMP或Call后跟着48位全指针(16位段选择子+32位地址偏移),且其中的段选择子指向代码段描述符,这样的跳转称为直接(普通)跳转。普通跳转不能使特权级发生跃迁,即不会引起CPL的变化,看下面的详细描述:


                  目标是一致代码段:
                           要求:CPL(CS.RPL)>=DestinationDescriptorCode.DPL ,其他RPL是不检查的。
                           转跳后程序的CPL(NewCS.RPL) = 转跳前程序的CPL( OldCS.RPL)
                           上面的安排就是概念1,2的意思,此时,CPL没有发生变化,纵使它执行了特权级(DPL)较高的代码。若访问时不满足要求,则发生异常。
                  目标是非一致代码段:
                           要求:CPL(CS.RPL)=DestinationDescriptorCode.DPL AND  RPL≤CPL(CS.RPL)
                           转跳后程序的CPL(NewCS.RPL) = DestinationDescriptorCode.DPL
                           上面的安排就是概念3的意思和部分1的意思----主人(一致代码段)只帮相同特权级的帮客人做事。因为前提是CPL=DPL,所以转跳后程序的CPL(NewCS.RPL) = DestinationDescriptorCode.DPL不会改变CPL的值,特权级(CPL)也没有发生变化。如果访问时不满足前提CPL=DPL,则引发异常。


         通过调用门的跳转:当段间转移指令JMP和段间转移指令CALL后跟着的目标段选择子指向一个调用门描述符时,该跳转就是利用调用门的跳转。这时如果选择子后跟着32位的地址偏移,也不会被cpu使用,因为调用门描述符已经记录了目标代码的偏移。使用调门进行的跳转比普通跳转多一个步骤,即在访问调用门描述符时要将描述符当作一个数据段来检查访问权限,要求指示调用门的选择子的RPL≤门描述符DPL,同时当前代码段CPL≤门描述符DPL,就如同访问数据段一样,要求访问数据段的程序的CPL≤待访问的数据段的DPL,同时选择子的RPL≤待访问的数据段或堆栈段的DPL。只有满足了以上条件,CPU才会进一步从调用门描述符中读取目标代码段的选择子和地址偏移,进行下一步的操作。
         从调用门中读取到目标代码的段选择子和地址偏移后,我们当前掌握的信息又回到了先前,和普通跳转站在了同一条起跑线上(普通跳转一开始就得到了目标代码的段选择子和地址偏移),有所不同的是,此时,CPU会将读到的目标代码段选择子中的RPL清0,即忽略了调用门中代码段选择子的RPL的作用。完成这一步后,CPU开始对当前程序的CPL,目标代码段选择子的RPL(事实上它被清0后总能满足要求)以及由目标代码选择子指示的目标代码段描述符中的DPL进行特权级检查,并根据情况进行跳转,具体情况如下:


                  目标是一致代码段:
                           要求:CPL(CS.RPL)≥DestinationDescriptorCode.DPL ,RPL不检查,因为RPL被清0,所以事实上永远满足RPL≤DPL,这一点与普通跳转一致,适用于JMP和CALL。
                           转跳后程序的CPL(NewCS.RPL) = 转跳前程序的CPL( OldCS.RPL),因此特权级没有发生跃迁。
                           
                  目标是非一致代码段:
                                当用JMP指令跳转时:
                                         要求:CPL(CS.RPL)=DestinationDescriptorCode.DPL AND  RPL<= CPL(CS.RPL)(事实上因为RPL被清0,所以RPL≤CPL总能满足,因此RPL与CPL的关系在此不检查)。若不满足要求则程序引起异常。
                                         转跳后程序的CPL(NewCS.RPL) = DestinationDescriptorCode.DPL
                                         因为前提是CPL=DPL,所以转跳后程序的CPL(NewCS.RPL) = DestinationDescriptorCode.DPL不会改变CPL的值,特权级也没有发生变化。如果访问时不满足前提CPL=DPL,则引发异常。
                                当用CALL指令跳转时:
                                          要求:CPL(CS.RPL)≥DestinationDescriptorCode.DPL(RPL被清0,不检查),若不满足要求则程序引起异常。
                                          转跳后程序的CPL(NewCS.RPL) = DestinationDescriptorCode.DPL
                                          当条件CPL=DPL时,程序跳转后CPL=DPL,特权级不发生跃迁;当CPL>DPL时,程序跳转后CPL=DPL,特权级发生跃迁,这是我们当目前位置唯一见到的使程序当前执行忧先级(CPL)发生变化的跳转方法,即用CALL指令+调用门方式跳转,且目标代码段是非一致代码段。


         总结:以上介绍了两种情况的跳转,分别是普通跳转和使用调用门的跳转,其中又可细分为JMP跳转和CALL跳转,跳转成功已否是由CPL,RPL和DPL综合决定的。所有跳转都是从低特权级代码向同级或更高特权级(DPL)跳转,但保持当前执行特权级(CPL)不变,这里有点难于区别为什么说向高特权级跳转,又说特权级没变,这里“高特权级”是指目标代码段描述符的DPL,它规定了可以跳转到该段代码的最高特权级;而后面的CPL不变才真正说明了特权级未发生跃迁。我们可以看到,只有用CALL指令+调用门方式跳转,且目标代码段是非一致代码段时,才会引起CPL的变化,即引起代码执行特权级的跃迁,这是目前得知的改变执行特权级的唯一办法,如果各位读者还知道其他方法请留言告诉我。


         以上解释参考了OldLinux BBS的帖子


注:CS.RPL代表CS寄存器中的Register Privilege Level,并不是一般所说的Request Privilege Level.



4、问:为什么全局描述符表GDT的第0项总是一个空描述符,而局部描述符表却不是这样?




       答:首先让我们先来熟悉一下概念。一个任务(Task )通常会涉及多个段,每个段需要一个描述符号来描述(当然不是绝对的一对一关系,一个段也可以由多个段描述符来对应,视具体应用而定),为了便于组织管理,80386把描述符组织成线性表。由描述符组成的线性表称为描述符表。在80386中有三种类型的描述符表(Descriptor Table),分别是全局描述符表GDT(Global Descriptor Table),局部描述符表(Local Descriptor Table)和中断描述符表IDT(Interrupt Descriptor Table)。在整个系统中,全局描述符表GDT和中断描述符表IDT只有一张,局部描述符表可以有若干张,每个任务可以有一张。
         在实模式下,逻辑地址是由段基址和段内偏移构成的。保护模式下,规则发生了很大变化,虚拟地址空间(相当于逻辑地址空间)中存储单元的地址由段选择子和段内偏移两部分组成,与实模式相比,段选择子代替了原来的段基址。从本质上来讲,段选择子最终还是要转化成段基址,那么选择子是如何转化为段基址的呢?让我们来看看选择子的结构和用法:



: (2006-4-8 17:46, 3.25 K)



         段选择子长16位,其格式如上图所示。从图中可见,段选择子的高13位是描述符的索引值。所谓描述符索引是指描述符在描述符表中的序号。由于描述符总是8个字节的,所以将描述符索引值逻辑左移3位即可得到对应描述符在描述符表中的偏移地址,再加上描述符表起始地址就可以确定描述符的位置,这算是一个小技巧。段段选择子的第2位是引用描述符表指示位,标记为TI(Table Indicator),TI=0表示该选择子指示的是全局描述符表GDT中的描述符,TI=1表示该选择子指示的是局部描述符表LDT中的描述符。第0和第1位称为RPL(Request Privilege Level请求特权级),用于特权级控制,在上一个问题中有详细描述。通过段选择子,我们可以从GDT或LDT中找到需要的段描述符,段描述符中存储着目标段的基址(起始地址),界限(段的范围)以及其他一些控制信息,由此,我们完成了段选择子到段基址的转化。
         到这里,我们似乎离开题目太远了,请不要急,我们惟有将基本的概念陈述清楚,才能将问题回答透彻,那么现在开始回答问题。
         如前所述,描述符的线性表构成了GDT,LDT,IDT,这些描述符并非都是用来描述数据段或代码段的,有的可能用来指示一个任务,比如任务门描述符,用于任务切换;有的用来指示子程序,比如调用门;还有的用来指示中断处理程序,如中断门、陷阱门等,当然,中断门和陷阱门只存在于IDT中。除此之外,由于CPU把局部描述符表LDT也当作数据段来管理,所以要求每一个LDT都必须有相应的描述符存在于GDT中,暂且称之为LDT描述符。由此可见,GDT、LDT和IDT是由各种不同种类的描述符排列构成的,他们的组成数据各不相同,作用也不同,唯一相同的是他们都是8字节的,都存于描述符表中,都用选择子来定位(中断门和陷阱门用INT 指令后的数字来做描述符索引)。
         前面说到GDT和IDT是整个系统一张,而LDT可以每个任务独占一长,用于存储每个任务私有的段的信息,所以当任务发生切换时,LDT也要随之切换,CPU中专门用一个16位的寄存器LDTR来存储当前任务的LDT在GDT中的描述符的选择子,以此来定位当前任务的LDT。同时也存在这么一种情况,那就是一个任务使用的所有段都是系统全局的,它不需要用LDT来存储私有段信息,因此,当系统切换到这种任务时,会将LDTR寄存器赋值成一个空(全局描述符)选择子,选择子的描述符索引值为0,TI指示位为0,RPL可以为任意值,用这种方式表明当前任务没有LDT。这里的空选择子因为TI为0,所以它实际上指向了GDT的第0项描述符,第0项的作用类似于C语言中NULL的用法,它虽然是一个描述符,但却只起到到了标志的作用,规定GDT的第0项描述符为空描述符,其8个字节全为0,就是这个原因。如果把前面的空描述符选择子的TI位改为1,使之指向LDT中的0号描述符,这样的选择子就不是空选择子,它指向的LDT中的0号描述符是可以正常使用的,也就是LDT中没有空描述符一说。


      5、问:为什么编写NASM语法的系统引导程序,汇编代码的开始总是使用“org 7c00h”?为什么有时候去掉org指令程序也能正常执行?


      答:对于这个问题,我首先在《NASM中文手册》中找到了org指令的解释:NASM汇编编译器为bin文件格式提供了额外的操作符org,它的功能是指定程序被载入内存时的起始地址。根据书中的解释,我们很容易想到,因为引导程序将会被加载到内存0x7c00处,而且引导程序一般都被编译成bin文件格式(bin文件格式没有文件头,它的文件映像与加载到内存运行时的内存映像是一致的),似乎在引导程序中用 org 7c00h是很符合规范的,可仔细一想,似乎又不对,按《NASM中文手册》的说法,使用org 7c00h将会指定程序加载入内存的起始地址为7c00h,但我们都知道,引导程序加载到内存的7c00h处是一项标准,并不是在编程时决定的,经过试验也验证了我的想法,将org后的数字改成其他值,bois程序一样将它加载到7c00处。那么是不是可以去掉org指令呢,因为程序被加载到哪里并不关它什么事。于是我将org指令去掉,重新编译,写入软盘引导扇区,用它引导系统。出乎预料,程序不能正常运行——没有正常打印出提示信息!到底怎么回事呢?我决定一探究竟。以下是我的引导代码:

boot.asm

CODE:
1    %include "PrintLib.inc"
2   
3            org 07c00h
4            ;org 0100h
5            mov ax,cs
6            mov ds,ax
7            mov es,ax
8   
9            mov ah,10h
10            mov al,03h
11            mov bl,01h
12            int 10h
13   
14            PrintString BootMessage,LenOfBootMessage,display_mode_2,0h,(ATTR_BLACK<<4)|ATTR_GREEN,0000h
15   
16            hlt ;停机
17   
18   
19            BootMessage:        db "Dreamix Starting Please wait..."
20            LenOfBootMessage        equ $-BootMessage
21   
22            times 510-($-$) db 0
23            dw        0xaa55
boot.asm 里引用到了PrintLib.inc文件,PrintLib.inc文件中定义了一个向屏幕输出字符串信息的宏,封装了部分10h BISO子功能,文件内容如下:

PrintLib.inc

CODE:
1     %ifndef PrintLib
2         %define PrintLib
3   
4    ;此宏在实模式下使用,属于BIOS子功能调用
5   
6    ;显示模式
7    %define display_mode_1 00h ;字符串只包含字符码,显示之后不更新光标位置,属性值在BL中
8    %define display_mode_2 01h ;字符串只包含字符码,显示之后更新光标位置,属性值在BL中
9    %define display_mode_3 02h ;字符串包含字符码及其属性值,显示之后不更新光标位置
10   %define display_mode_4 03h ;字符串包含字符码及其属性值,显示之后更新光标位置
11   
12    ;背景及字体格式属性值
13    %define ATTR_BLACK                 0h
14    %define ATTR_BLUE                  01h
15    %define ATTR_GREEN                 02H
16    %define ATTR_PURPLE                 03h
17    %define ATTR_RED                   04h
18    %define ATTR_MAGENTA                 05h
19    %define ATTR_BROWN                 06h
20    %define ATTR_GREYISH                 07h
21    %define ATTR_GREY                  08h
22    %define ATTR_LIGHTBLUE        09h
23    %define ATTR_LIGHTGREEN        0Ah
24    %define ATTR_LIGHTPURPLE        0Bh
25    %define ATTR_LIGHTRED                0Ch
26    %define ATTR_LIGHTMAGENTA        0Dh
27    %define ATTR_YELLOW                 0Eh
28    %define ATTR_WHITE                0Fh
29   
30    ;参数: 1.要显示的字符串标号  2.要显示的字符串的长度值
31    ;        3.显示模式
32    ;        4.视频页号
33    ;        5.当显示模式选3和4时为0h,否则需要背景和字符的格式属性值
34    ;        6.显示的列和行
35    %macro PrintString 6
36   
37            push ax
38            push bp
39            push cx
40            push bx
41            push dx
42   
43            mov ax,%1
44            mov bp,ax
45            mov cx,%2
46            mov ax,01300h + %3
47            mov bx,%4 + %5
48            mov dx,%6
49            int 10h
50   
51            pop dx
52            pop bx
53            pop cx
54            pop bp
55            pop ax
56    %endmacro
57   
58    %endif  
以上两个文件中的代码都是正确的代码,其中的boot.asm文件使用了org 7c00h.当我把org 7c00h语句去掉后,编译的引导程序不能正常打印提示信息"Dreamix Starting Please wait...".


        用WinHex查看编译好的boot.bin文件,显示如下:

CODE:
Offset      0  1  2  3  4  5  6  7   8  9  A  B  C  D  E  F
00000000   8C C8 8E D8 8E C0 B4 10  B0 03 B3 01 CD 10 50 55   ŒÈŽØŽÀ´.°.³.Í.PU
00000010   51 53 52 B8 2C 7C 89 C5  B9 1F 00 B8 01 13 BB 02   QSR¸,|‰Å¹..¸..».
00000020   00 BA 00 00 CD 10 5A 5B  59 5D 58 F4 44 72 65 61   .º..Í.Z[Y]XôDrea
00000030   6D 69 78 20 53 74 61 72  74 69 6E 67 20 50 6C 65   mix Starting Ple
00000040   61 73 65 20 77 61 69 74  2E 2E 2E 00 00 00 00 00   ase wait........
00000050   00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00   ................
00000060   00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00   ................
00000070   00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00   ................
00000080   00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00   ................
00000090   00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00   ................
000000A0   00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00   ................
000000B0   00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00   ................
000000C0   00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00   ................
000000D0   00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00   ................
000000E0   00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00   ................
000000F0   00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00   ................
00000100   00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00   ................
00000110   00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00   ................
00000120   00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00   ................
00000130   00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00   ................
00000140   00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00   ................
00000150   00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00   ................
00000160   00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00   ................
00000170   00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00   ................
00000180   00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00   ................
00000190   00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00   ................
000001A0   00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00   ................
000001B0   00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00   ................
000001C0   00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00   ................
000001D0   00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00   ................
000001E0   00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00   ................
000001F0   00 00 00 00 00 00 00 00  00 00 00 00 00 00 55 AA   ..............Uª
可以看到字符串被编译到文件头偏移02Ch处,此时文件映像的内容也反映了它在内存中的映像内容,当boot.bin被加载到内存7c00h处时,字符串"Dreamix Starting Please wait..."应该位于内存7c2ch处。我们用Bochs调试未加org 7c00语句的引导程序,看看它到底是如何运行的。将boot写入软盘镜像文件,把该镜像文件挂载为Bochs虚拟机的软驱,启动Bochs调试器,先用pb命令在物理内存0x7c00处下断点,然后用c命令执行执行到断点(关于如何用Bochs调试操作系统请读者自己查阅相关资料,此处不再赘述),此时boot.bin已经被加载到内存0x7c00处。用disassemble命令反汇编这段内存,得到如下结果:

CODE:
disassemble 0x7c00 0x7c30

00007c00: (                    ): mov ax, cs                ; 8cc8
00007c02: (                    ): mov ds, ax                ; 8ed8
00007c04: (                    ): mov es, ax                ; 8ec0
00007c06: (                    ): mov ah, 0x10              ; b410
00007c08: (                    ): mov al, 0x3               ; b003
00007c0a: (                    ): mov bl, 0x1               ; b301
00007c0c: (                    ): int 0x10                  ; cd10
00007c0e: (                    ): push ax                   ; 50
00007c0f: (                    ): push bp                   ; 55
00007c10: (                    ): push cx                   ; 51
00007c11: (                    ): push bx                   ; 53
00007c12: (                    ): push dx                   ; 52
00007c13: (                    ): mov ax, 0x2c              ; b82c00   <-----
00007c16: (                    ): mov bp, ax                ; 89c5         <-----
00007c18: (                    ): mov cx, 0x1f              ; b91f00
00007c1b: (                    ): mov ax, 0x1301            ; b80113
00007c1e: (                    ): mov bx, 0x2               ; bb0200
00007c21: (                    ): mov dx, 0x0               ; ba0000
00007c24: (                    ): int 0x10                  ; cd10
00007c26: (                    ): pop dx                    ; 5a
00007c27: (                    ): pop bx                    ; 5b
00007c28: (                    ): pop cx                    ; 59
00007c29: (                    ): pop bp                    ; 5d
00007c2a: (                    ): pop ax                    ; 58
00007c2b: (                    ): hlt                       ; f4
00007c2c: (                    ): inc sp                    ; 44
00007c2d: (                    ): jb .+0x7c94               ; 7265
00007c2f: (                    ): popa                      ; 61
实际上可执行的代码只到00007c2b,后面的是数据和填充字符,我们看到,00007c13处的两条指令负责在调用10h中断之前将需要打印的字符串的首地址装入约定的寄存器bp,它装入的是字符串相对于7c00的偏移0x2c。我们知道,实模式下内存的寻址是通过段寄存器提供的段基址和偏移地址相组合的方式,这里的0x2c相当于偏移地址。那么段基址是多少呢?我们在Bochs下用info registers命令查看,得到以下结果:

CODE:
info registers
eax            0xfffaa55        268413525
ecx            0xa0001          655361
edx            0x0              0
ebx            0x0              0
esp            0xfffe           0xfffe
ebp            0x0              0x0
esi            0xa070           41072
edi            0xffde           65502
eip            0x7c00           0x7c00
eflags         0x82             130
cs             0x0              0
ss             0x0              0
ds             0x0              0
es             0x0              0
fs             0x0              0
gs             0x0              0
一般来说bp是进行堆栈寻址的,我不知道此时bp是和哪个段寄存器配合寻址,但我们发现所有6个段寄存器的值都是0,因此无论如何(即使引导程序一开始就将cs寄存器的内容复制到了es和ds),用bp里的偏移0x2c和段基址0x0只能访问到内存单元0x2c,而不是0x7c2c(字符串"Dreamix Starting Please wait..."被加载大内存0x7c2c处),这就是为什么程序不能正确打印提示字符的原因。受原来编程习惯的影响,我们想当然的认为段寄存器应该存着程序被加载到的段的基址,也就是0x7c00,这样和偏移地址0x2c结合刚好可以正确找到字符串,但事实是引导程序被加载后,所有的段寄存器都被清零了,不知道这是不是IBM PC兼容机启动时的规范动作?知道的朋友可以给我留言。此时,由于段寄存器的值为0x0,偏移地址就变成了绝对(物理)地址,要访问内存0x7c2c,偏移地址就必须是0x7c2c;或者编程时将段寄存器的值赋为0x7c00,偏移地址0x2c保持不变,也可以达到预期目的。


        我们来看看boot.asm中加入org 7c00h指令后的情况,按前述步骤在Bochs中反汇编代码,结果如下:

CODE:
disassemble 0x7c00 0x7c30
00007c00: (                    ): mov ax, cs                ; 8cc8
00007c02: (                    ): mov ds, ax                ; 8ed8
00007c04: (                    ): mov es, ax                ; 8ec0
00007c06: (                    ): mov ah, 0x10              ; b410
00007c08: (                    ): mov al, 0x3               ; b003
00007c0a: (                    ): mov bl, 0x1               ; b301
00007c0c: (                    ): int 0x10                  ; cd10
00007c0e: (                    ): push ax                   ; 50
00007c0f: (                    ): push bp                   ; 55
00007c10: (                    ): push cx                   ; 51
00007c11: (                    ): push bx                   ; 53
00007c12: (                    ): push dx                   ; 52
00007c13: (                    ):mov ax, 0x7c2c            ; b82c7c    <-----
00007c16: (                    ): mov bp, ax                ; 89c5          <-----
00007c18: (                    ): mov cx, 0x1f              ; b91f00
00007c1b: (                    ): mov ax, 0x1301            ; b80113
00007c1e: (                    ): mov bx, 0x2               ; bb0200
00007c21: (                    ): mov dx, 0x0               ; ba0000
00007c24: (                    ): int 0x10                  ; cd10
00007c26: (                    ): pop dx                    ; 5a
00007c27: (                    ): pop bx                    ; 5b
00007c28: (                    ): pop cx                    ; 59
00007c29: (                    ): pop bp                    ; 5d
00007c2a: (                    ): pop ax                    ; 58
00007c2b: (                    ): hlt                       ; f4
00007c2c: (                    ): inc sp                    ; 44
00007c2d: (                    ): jb .+0x7c94               ; 7265
00007c2f: (                    ): popa                      ; 61
可以看到,加入org 7c00h后,偏移被编译成了0x7c2c,再次用info registers命令查看寄存器内容,发现还是全为0x0,因此程序通过基址加偏移的方式能够正确访问到字符串。


        通过以上分析,我们可以看出org指令的作用确实是指示出程序将要被加载到内存的起始地址,这里用“指示”比用原来的“指定”更确切点。“指示”有被动的含义,org指令本身并不能决定程序将要加载到内存的什么位置,它只是告诉编译器,我的程序在编译好后需要加载到xxx地址,所以请你在编译时帮我调整好数据访问时的地址。用“指定”有种主动的含义,容易引起误解。另外我们看出,org指令只会在编译期影响到内存寻址指令的编译(编译器会把所有程序用到的段内偏移地址自动加上org后跟的数值),而其自身并不会被编译成机器码。


        到此我们已经初步了解到了org指令的作用,一句话,就是为程序中所有的内部地址引用增加一个段内偏移值(引导程序可以看做是被加载到以0为基址的段,偏移为0x7c00)。


        为了方便调试,我们常常用NASM将程序编译为windows能直接执行的com文件,这样能在windows下直接运行观看其效果。其实com文件格式和bin文件格式并没有本质的区别,它们都是纯二进制文件,即文件映像的内容和内存映像的内容相同。所不同的是,com文件总是被MS的操作系统加载到段偏移0100h处,因此com的汇编源文件中需要加入语句org 100h。


        下面我们来看看com文件的寻址情况,我们分别用windows自带的debug和boland的turbo debuger 调试运行com版本的boot程序,分析其中的异同:



: (2006-4-15 16:15, 645.69 K)


图1: Debug下调试执行boot.com





: (2006-4-15 16:15, 20.11 K)


图2: Turbo Debuger下调试执行boot.com




        从图1和图2中可以看出,访问字符串的偏移地址都是012ch,即org指令指定的100h与字符串文件内偏移相加而得到的值。而两种情况下段寄存器的值都不为0,一个是0BE2,一个是5328。为什么不同的调试器在加载同一个com文件时,加载到的段各不相同;而同一个调试器每次加载同一文件总在一个段,这其中有什么规则,我不得而知,请知道的朋友留言告诉我。我们看到,虽然两种情况下的段基址各不相同,但用基址+偏移的方式总能正确访问到内存数据,这归功于程序总被加载到段偏移0100h处,由此引出了以下结论:


        如果一个程序使用了org xxx指令,那么该程序只能被加载到段内偏移xxx处,否则将不能正常访问段内数据,这是本篇文章得出的最重要的结论。


        最后我们来解释剩下的问题,为什么有时候去掉org指令程序也能正常执行?实际上不加org就相当于没有指示出程序加载的段内偏移值,这时编译器会默认用0做为偏移(相当于org 0x0的情况)。这样的程序只要加载到段内偏移0x0处都能正常执行,还有一种情况就是程序中没有对内存的寻址操作,那么也不会出错,因为org指令就是调整段内地址引用值的。
阅读(1783) | 评论(0) | 转发(0) |
给主人留下些什么吧!~~