分享

X86分段机制

 guitarhua 2014-10-10
x86 CPU使用分段机制来管理和访问内存。但是为什么会有要使用分段机制呢?因为早期的x86 CPU内存总线宽度是20 bits,而寄存器的宽度只有16 bits。那么为了访问全部的20 bits内存空间,x86就引入了分段机制。将段寄存器的地址左移4位,作为段的基地址,然后在加上一个16位的偏移。这样就形成了一个20位的地址。这在当时,应该是一个权宜之计。但是没有想到x86一下子就占据了CPU市场的大部分份额。那么,作为应用如此广泛的CPU。x86之后的cpu不得不兼容以前的机制,继续使用分段机制——船大不好调头啊。之后的80286在分段机制的基础上,添加了保护模式——即对某些内存地址进行权限检查的支持等。

cs,ss,ds,es等段寄存器,保存的其实只是segment selector。而segment的表示是依赖于一个8字节的segment descriptor,它表示了该segment的所有特性。而这些segment descriptors是保存在GDT或LDT中。GDT既然是被称为Global Descriptor Table,一般只有一个。而LDT是用于保存除GDT中的以外的segments。GDT的地址和大小是保存在gdtr,而LDT的地址和大小是保存在ldtr。

segment descriptor主要保存了segment的基地址,大小,权限,类型等等。其中类型包括:Code Segment Descriptor,Data Segment Descriptor,Task State Segment Descriptor,Local Descriptor Table Descriptor。

那么完整的逻辑地址转换为线性地址的过程:cpu通过segment selector,得到其保存的位置GDT还是LDT,以及索引。然后利用这个索引,找到segment descriptor。再利用segment descriptor所保存的segment 信息加上偏移,生成最终的线性地址。而在真正的x86 CPU中,有一种不可编程的寄存器。当把segment selector加载到对应的segment register中时,CPU会自动将正确的segment descriptro加载到这个寄存器中。这样就加速了地址转换的性能。

内存分段机制有很多好处,不仅方便程序的重定位以及方便内存管理等等的好处,我们就从内存的重定位来分析分段机制的作用。
指令和指令集:
简单地说,处理器的设计者用某些数来指示处理器所进行的操作,这成为指令,或者叫机器指令,因为只有处理器才认得他们。比如,指令F4H表示让处理器停机,当处理器取到并执行这条指令后,就停止工作。指令是集中存放在内存里的,一条接着一条,处理器的工作是自动按顺序取出并加以执行。如下所示,从内存地址0000H开始(也就是内存地址的最低端)连续存放了一些指令,同时,假定执行这些指令的是一个16位处理器,拥有两个16位的寄存器RA和RB
一般来说,指令由操作码和操作数构成,但也有小部分指令仅有操作码,而不含操作数,如图所示,停机指令仅包含1字节的操作码F4,而没有操作数,指令的长度不定,短的指令仅有1字节,而长的指令则有可能大道15字节。
对处理器来说,指令的操作码隐含了如何执行该指令的信息,比如它是做什么的,以及怎么去做,第一条指令的操作码是B8,这表明,该指令是一条传送指令,第一个操作数是寄存器,第二个操作数是直接包含在指令中的,紧跟在操作码之后,可以立即从指令中取得,所以叫做立即数。同时,操作码还直接指出该寄存器是RA。RA是16位寄存器,这条指令将按字进行操作。所以,当这条指令执行之后,该指令的操作数(立即数)005DH就被传送到RA中。
对于复杂一些的指令来说,1个字节的操作码可能不会够用,所以,第2跳指令的操作码为8B1E,它隐含的意思是,这是一条传送指令,第一个操作数是寄存器,而且是RB寄存器,第二个操作数是内存地址,要传送到RB寄存器中的数存放在该地址中,同时,这是一个字操作指令,应当从第二个操作数指定的地址中取出一个字。
该指令的操作数部分是3F00,指定了一个内存地址003FH。它相当于高级语言里的指针,当处理器执行这条指令时,会再次用003FH作为地址去访问内存,从那里取出一个字(1002H),然后将它传送到寄存器RB。注意,“传送”这个词带有误导性,其实,传送的意思更像是“复制",传送之后,003FH单元里的数据还保持原样。
通过这两条指令的比较,很容易分清指令中的“立即数”是什么意思。指令执行和操作的对象是数。如果这个数已经在指令中给出,不需要再次访问内存,那这个数就是立即数,比如第一条指令中的005DH;相反,如果指令中给出的是地址,真正的数还需要用这个地址访问内存才能得到,那么它就不能称为立即数,比如第二条指令中的003FH。
 
我们知道,处理器是自动化的器件,在给出了起始地址之后,它将从这个地址开始,自动地取出每条指令并加以执行,只要每条指令都正确无误,它就能准确地知道下一条指令的地址。这就意味着,完成某个工作的所有指令,必须集中在一起,处于内存的某个位置,形成一个段,叫做代码段,事情是明摆着的,要是指令并没有一条挨着一条存放,中间夹杂了其他非指令的数据,处理器就因为不能识别而出错。
为了做某件事而编写的指令,它们一起形成了我们平时所说的程序,程序总要操作大量的数据,这些数据也应该集中在一起,位于内存中的某个地方,形成一个段,叫做数据段。
段在内存中的位置并不重要,因为处理器是可控的,我们可以让它从内存的任何位置开始取指令并加以执行。这里有一个例子,如图所示,我们有一大堆数字,现在想把它们加起来求出一个总和。假定我们有16个数要相加,这些数都是16位的二进制,分别是0005H,00A0H,00FFH,...。为了让处理器把它们加起来,我们应该先在内存中定义一个数据段,将这些数字写进去,数据段可以起始于内存中的任何位置,既然如此,我们将它定义在0100H处,这样一来,第一个要加的数位于地址0100H,第二个要加的数位于地址0102H,最后一个数的地址是011EH。
一旦定义了数据段,我们就知道了每个数的内存地址,然后,紧挨着数据段,我们从内存地址0120H处定义代码段,严格来说,数据段和代码段是不需要连续的,但这里我们把它们挨在一起更自然一些,为了区别数据段和代码段,我们使用了不同的底色。
代码段是从内存地址0120H开始的,第一条指令是A10001,其功能是将内存单元0100H里的字传送到AX寄存器,指令执行后,AX的内容为0005H。
第二条指令是03060201,功能是将AX中的内容和内存单元0102H里的字相加,结果在AX中。由于AX的内容为0005H,而内存地址0102H里的数是00A0H,这条指令执行后,AX的内容是00A5H。
第三条指令是03060401,功能是将AX中的内容和内存单元0104H里的字相加,结果在AX中,此时,由于AX里的内容是00A5H,内存地址0104H里的数是00FFH,本指令执行后,AX的内容为01A4H。
后面的指令跟前2条相似,依次用AX的内容和下一个内存单元里的字相加,一直到最后,在AX中得到总的累加和,在这个例子中,我们没有考虑AX寄存器容纳不下结果的情况。当累加的总和超过了AX所能表示的数的范围(最大为FFFFH,即十进制的65535)时,就会产生进位,但这个进位被丢弃。
在内存中定义了数据段和代码段之后,我们就可以指令处理器从内存地址0120H处开始执行,当所有的指令执行完后,就能在AX寄存器中得到最后的结果。


 
看起来好像没有问题,很完美。但是仔细想一想,还是能感觉到会有问题存在。
在前面的例子中,所有在执行时需要访问内存单元的指令,使用的都是真实的内存地址。比如A10001,这条指令的意思是从地址为0100H的内存单元里取出一个字,并传送到寄存器AX。在这里,0100H是一个真实的内存地址,又称物理地址。
整个程序(包括代码段和数据段)在内存中的位置,是由我们自己定的。我们把数据段定在0100H,把代码段定在0120H。
问题是,大多数时候,整个程序(包括代码段和数据段)在内存中的位置并不是我们能够决定的。请想一想你平时是怎么使用计算机的,你所用的程序,包括那些用来调整计算机性能的工具、小游戏、音乐和视频播放器等,都是从网上下载的,位于你的硬盘、盘或光盘中。即使有些程序是你自己编写的,那又如何?当你双击它们的图标,使它们在里启动之前,内存已经被塞了很多东西,就算你是刚刚打开计算机,自己已经占用了很多内存空间,不然的话,你怎么可能在它上面操作呢?
在这种情况下,你所运行的程序,在内存中被加载的位置完全是随机的,哪里有空闲的地方,它就会被加载到哪里,并从那里开始被处理器执行。所以,前面那段程序不可能恰好如你所愿,被加载到内存地址,它完全可能被加载到另一个不同的位置,比如。但是,同样是那个程序,一旦它在内存中的位置发生了改变,灾难就出现了。
如图所示,因为程序现在是从内存地址处被加载的,所以,数据段的起始地址为。这就是说,第一个要加的数,其地址为,第二个则为,其他依次类推。代码段依然紧挨着数据段之后,起始地址相应地是1020H。
只要所有的指令都是连续存放的,代码段位于内存中的什么地方都可以正常执行。所以,处理器可以按你的要求,从内存地址处连续执行,但结果完全不是你想要的。
请看第一条指令,它的意思是从内存地址处取得一个字,将其传送到寄存器。但是,由于程序刚刚改变了位置,它要取的那个数,现在实际上位于,它取的是别人地盘里的数!
这能怪谁呢?发生这样的事情,是因为我们在指令中使用了绝对内存地址(物理地址),这样的程序是无法重定位的。为了让你写的程序在卖给别人之后,可以在内存中的任何地方正确执行,就只能在编写程序的时候使用相对地址或者逻辑地址了,而不能使用真实的物理地址。当程序加载时,这些相对地址还要根据程序实际被加载的位置重新计算。   


 
那么这种问题如何解决呢?
而此时,内存分段机制就应运而生了。如图所示,整个内存间就像长长的纸条,在内存中分段,就像从长纸条中裁下一小段来。根据需要,段可以开始于内存中的任何位置,比如图中的内存地址A532H处。 在这个例子中,分段开始于地址为A532H的内存单元处,这个起始地址就是段地址。这个分段包含了6个存储单元。在分段之前,它们在整个内存空间里的物理地址分别是A532H、A533H、A534H、A535H、A536H、A537H。但是,在分段之后,它们的地址可以只相对于自己所在的段。这样,它们相对于段开始处的距离分别为0、1、2、3、4、5,这叫做偏移地址。于是,当采用分段策略之后,一个内存单元的地址实际上就可以用“段:偏移”或者 “段地址:偏移地址”来表示,这就是通常所说的逻辑地址。比如,在图2-10中,段内第1个存储单元的地址为A532H:0000H,第3个存储单元的地址为A532H:0002H,而本段最后一个存储单元的地址则是A532H:0005H。为了在硬件一级提供对“段地址:偏移地址”内存访问模式的支持,处理器至少要提供两个段寄存器,分别是代码段(Code Segment,CS)寄存器和数据段(Data Segment,DS)寄存器。对CS内容的改变将导致处理器从新的代码段开始执行。同样,在开始访问内存中的数据之前,也必须首先设置好DS寄存器,使之指向数据段。除此之外,最重要的是,当处理器访问内存时,它把指令中指定的内存地址看成是段内的偏移地址,而不是物理地址。这样,一旦处理器遇到一条访问内存的指令,它将把DS中的数据段起始地址和指令中提供的段内偏相加,来得到访问内存所需要的物理地址。
  


如图所示,代码段的段地址为1020H,数据段的段地址为1000H。在代码段中有一条指令A1 02 00,它的功能是将地址0002H处的一个字传送到寄存器AX。在这里,处理器将0002H看成是段内的偏移地址,段地址在DS中,应该在执行这条指令之前就已经用别的指令传送到DS中了。当执行指令A1 02 00时,处理器将把DS中的内容和指令中指定的地址0002H相加,得到1002H。这是一个物理地址,处理器用它来访问内存,就可以得到所需要的数00A0H。如果一下次执行这个程序时,代码段和数据段在内存中的位置发生了变化,只要把它们的段地址分别传送到CS和DS,它也能够正确执行。   

8086内部有4个段寄存器。其中,CS是代码段寄存器,DS是数据段寄存器,ES是附加段(Extra Segment)寄存器。附加段的意思是,它是额外赠送的礼物,当需要在程序中同时使用两个数据段时,DS指向一个,ES指向另一个。可以在指令中指定使用DS和ES中的哪一个,如果没有指定,则默认是使用DS。SS是栈段寄存器,以后会讲到,而且非常重要。IP是指令指针(Instruction Pointer)寄存器,它只和CS一起使用,而且只有处理器才能直接改变它的内容。当一段代码开始执行时,CS指向代码段的起始地址,IP则指向段内偏移。这样,由CS和IP共同形成逻辑地址,并由总线接口部件变换成物理地址来取得指令。然后,处理器会自动根据当前指令的长度来改变IP的值,使它指向下一条指令。当然,如果在指令的执行过程中需要访问内存单元,那么,处理器将用DS的值和指令中提供的偏移地址相加,来形成访问内存所需的物理地址。
8086的段寄存器和IP寄存器都是16位的,如果按照原先的方式,把段寄存器的内容和偏移地址直接相加来形成物理地址的话,也只能得到16位的物理地址。麻烦的是,8086却提供了20根地址线。换句话说,它提供的是20位的物理地址。提供20位地址线的原因很简单,16位的物理地址只能访问64KB的内存,地址范围是0000H~FFFFH,共65536个字节。这样的容量,即使是在那个年代,也显得捉襟见衬。注意,这里提到了一个表示内存容量的单位“KB”。为了方便,我们通常使用更大的单位来描述存容量,比如千字节(KB)、兆字节(MB)和吉字节(GB),它们之间的换算关系如下:
1 KB = 1024 Byte
1 MB = 1024 KB 
1 GB = 1024 MB
所以,65536 个字节就是64KB,而20 位的物理地址则可以访问多达1MB 的内存,地址范围从00000H 到FFFFFH。问题是,16 位的段地址和16 位的偏移地址相加,只能形成16 位的物理地址,怎么得到这20 位的物理地址呢? 为了解决这个问题,8086 处理器在形成物理地址时,先将段寄存器的内容左移4 位(相当于乘以十六进制的10,或者十进制的16),形成20 位的段地址,然后再同16 位的偏移地址相加,得到20 位的物理地址。比如,对于逻辑地址F000H:052DH,处理器在形成物理地址时, 将地址F000H 左移4 位,变成F0000H,再加上偏移地址052DH,就形成了20 位的物理地址F052DH。这样,因为段寄存器是16 位的,在段不重叠的情况下,最多可以将1MB 的内存分成65536 个段,段地址分别是0000H、0001H、0002H、0003H,……,一直到FFFFH。
在这种情况下, 如图2-13 所示,每个段正好16 个字节,偏移地址从0000H 到000FH。 同样在不允许段之间重叠的情况下,每个段的最大长度是64KB,因为偏移地址也是16 位的,从0000H 到FFFFH。在这种情况下,1MB 的内存,最多只能划分成16 个段,每段长64KB, 段地址分别是0000H、1000H、2000H、3000H,…,一直到F000H。 以上所说的只是两种最典型的情况。通常情况下,段地址的选择取决于内存中哪些区域是空闲的。
举个例子来说,假如从物理地址00000H开始,一直到82251H处都被其他程序占用着,而后面一直到FFFFFH的地址空间都是自由的,那么,你可以从物理内存地址82251H之后的地方加载你的程序。 接着,你的任务是定义段地址并设置处理器的段寄存器,其中最重要的是段地址的选取。因为偏移地址总是要求从0000H开始,而82260H是第一个符合该条件的物理地址,因为它恰好对应着逻辑地址8226H:0000H,符合偏移地址的条件,所以完全可以将段地址定为8226H。
举个例子来说,如果你从物理内存地址82255H处加载程序,由于它根本无法表示成一个偏移地址为0000H的逻辑地址,所以不符合要求,段不能从这里开始划分。这里面的区别在于,82260H可以被十进制数16(或者十六进制数10H)整除,而82255H不能。通过这个例子可以看出,8086处理器的逻辑分段,起始地址都是16的倍数,这称为是按16字节对齐的。段的划分是自由的,它可以起始于任何16字节对齐的位置,也可以是任意长度,只要不超过64KB。比如,段地址可以是82260H,段的长度可以是64KB。在这种情况下,该段所对应的逻辑地址范围是8226H:0000H~8226H:FFFFH,其所对应的物理地址范围是82260~9225FH。同时,正是由于段的划分非常自由,使得8086的内存访问也非常随意。同一个物理地址,或者同一片内存区域,根据需要,可以随意指定一个段来访问它,前提是那个物理地址位于该段的64KB范围内。也就是说,同一个物理地址,实际上对应着多个逻辑地址。

 上面的内容是这次分段机制的全部内容,大部分来自<<x86汇编-从实模式到保护模式一书>>,后面要开始学习<<30天自制操作系统>>一书中的第一个例子了。

    本站是提供个人知识管理的网络存储空间,所有内容均由用户发布,不代表本站观点。请注意甄别内容中的联系方式、诱导购买等信息,谨防诈骗。如发现有害或侵权内容,请点击一键举报。
    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多