逻辑地址/转线性地址/物理地址

一、 段寄存器的产生

段寄存器的产生源于Intel 8086 CPU体系结构中数据总线与地址总线的宽度不一致。

 

数据总线的宽度,也即是ALU(算数逻辑单元)的宽度,平常说一个CPU是“16位”或者“32位”指的就是这个。8086CPU的数据总线是16位。

 

地址总线的宽度不一定要与ALU的宽度相同。因为ALU的宽度是固定的,它受限于当时的工艺水平,当时只能制造出16位的ALU;但地址总线不一样,它可以设计得更宽。地址总线的宽度如果与ALU相同当然是不错的办法,这样CPU的结构比较均衡,寻址可以在单个指令周期内完成,效率最高;而且从软件的解决来看,一个变量地址的长度可以用整型或者长整型来表示会比较方便。

 

但是,地址总线的宽度还要受制于需求,因为地址总线的宽度决定了系统可寻址的范围,即可以支持多少内存。如果地址总线太窄的话,可寻址范围会很小。如果地址总线设计为16位的话,可寻址空间是2^16=64KB,这在当时被认为是不够的;Intel最终决定要让8086的地址空间为1M,也就是20位地址总线。

 

地址总线宽度大于数据总线会带来一些麻烦,ALU无法在单个指令周期里完成对地址数据的运算。有一些容易想到的可行的办法,比如定义一个新的寄存器专门用于存放地址的高4位,但这样增加了计算的复杂性,程序员要增加成倍的汇编代码来操作地址数据而且无法保持兼容性。

 

Intel想到了一个折中的办法:把内存分段,并设计了4个段寄存器,CS,DS,ES和SS,分别用于指令、数据、其它和堆栈。把内存分为很多段,每一段有一个段基址,当然段基址也是一个20位的内存地址。不过段寄存器仍然是16位的,它的内容代表了段基址的高16位,这个16位的地址后面再加上4个0就构成20位的段基址。而原来的16位地址只是段内的偏移量。这样,一个完整的物理内存地址就由两部分组成,高16位的段基址和低16位的段内偏移量,当然它们有12位是重叠的,它们两部分相加在一起,才构成完整的物理地址。

 

Baseb15 ~ b12b11 ~ b0 
Offset o15 ~ o4o3 ~ o0
Addressa19 ~ a0

 

这种寻址模式也就是“实地址模式”。在8086中,段寄存器还只是一个单纯的16位寄存器,而且操作寄存器的指令也不是特权指令。通过设置段寄存器和段内偏移,程序就可以访问整个物理内存,无安全性可言。

 

总之一句话,段寄存器的设计是一个权宜之计,现在看来可以说是一个临时性的解决方案,设计它的目的是为了把地址空间从64KB扩展为1MB,仅此而已。但是它的加入却为日后Intel系列芯片的发展带来诸多不便,也为理解i386体系带来困扰。

二、 实现保护模式

到了80386问世的时候,工艺已经有了很大的进步,386的ALU有已经从16位跃升为32位,也就是说,38086是32位的CPU,而且结构也已经比较成熟,接下来的80486一直到Pentium系列虽然速度提高了几个数量级,但并没有质的变化,所以被统称为i386结构。

 

对于32位的CPU来说,只要地址总线宽度与数据总线宽度相同,就可以寻址2^32=4GB的内存空间,这已经足够用,已经不再需要段寄存器来帮助扩展。但这时Intel已经无法把段寄存器从产品中去掉,因为新的CPU也是产品系列中的一员,根据兼容性的需要,段寄存器必须保留下来。

 

这时,技术的发展需求Intel在其CPU中实现“保护模式”,用户程序的可访问内存范围必须受到限制,不能再任意地访问内存所有地址。Intel决定利用段寄存器来实现他们的保护模式,把保护模式建立在段寄存器的基础之上。

 

对于段的描述不再只是一个20位的起始地址,而是全新地定义了“段描述项”。段描述项的结构如下:

 

B31 ~ B24DES1 (4 bit)L19 ~ L16
DES2 (8 bit)B23 ~ B16
B15 ~ B0
L15 ~ L0

 

每一行是两个字节,总共8个字节,64位。

 

DES1和DES2分别是一些描述信息,用于描述本段是数据段还是代码段,以及读写权限等等。B0~B31是段的基地址,L0~L19是段的长度。

 

注意,规定段的长度是非常必要的,如果不限定段长度,“保护”就无从谈起,用户程序的访问至少不能超过段的范围。另外,段长度只有20位,所代表的最大可能长度为2^20=1M,而整个地址空间是2^32=4GB,这样来看,段的长度是不是太短了?其实,在DES1中,有一位用于表示段长度的单位,当它被置1时(一般情况下都是如此),表示长度单位为4KB,这样,一个段的最大可能尺寸就成了1M*4K=4G,与地址空间相稳合。4KB也正是一个内存页的大小,说明段的大小也是向页对齐的。

 

另外,注意到一个有趣的现象吗?段描述项的结构被设计得不连续,不论是段基地址还是段长度,都被分成了两节表示。这样的设计与80286的过渡有关。上面的段描述项结构去掉第一行后剩下的三行正是286的段描述项。286被设计为24位地址总线,所以段基址是24位,相应地段长是16位。在386的地址总线扩展为32位之后,还必须兼容286产品的设计,所以只好在段描述项上“打补丁”。

 

在386中,段寄存器还是16位,那么16位的段寄存器如何存放得下64位的段描述项? 段描述项不再由段寄存器直接持有。段描述项存放在内存里,系统中可以有很多个段描述项,这些项连续存放,共同构成一张表,16位的段寄存器里只是含有这张表里的一个索引,但也并不仅是一个简单的序号,而是存储了一种数据结构,这种结构的定义如下:

 

index (b15 ~ b3)TI (b2)RPL (b1 ~ b0)

 

其中index是段描述表的索引,它指向其中的某一个段描述项。RPL表示权限,00最高,11最低。

 

还有一个关键的问题,内存中的段描述表的起始地址在哪里?显然光有索引是有不够的。为此,Intel又设计了两个新的寄存器:GDTR(global descriptor table register)和LDTR(local descriptor table register),分别用来存储段描述表的地址。段寄存器中的TI位正是用于指示使用GDTR还是LDTR。

 

当用户程序要求访问内存时,CPU根据指令的性质确定使用哪个段寄存器,转移指令中的地址在代码段,取数指令中的地址在数据段;根据段寄存器中的索引值,找到段描述项,取得段基址;指令中的地址是段内偏移,与段长比较,确保没有越界;检查权限;把段基址和偏移相加,构成物理地址,取得数据。

 

新的设计中处处有权限与范围的限制,用户程序只能访问被授权的内存空间,从而实现了保护机制。就这样,在段寄存器的基础上,Intel实现了自己的“保护模式”。

三、 与页式存管并存

现代操作系统的发展要求CPU支持页式存储管理。

 

页式存管本身是与段式存管分立的,两者没有什么关系。但对于Intel来说,同样是由于“段寄存器”这个历史的原因,它必须把页式存管建立在段式存管的基础之上,尽管这从设计的角度来说这是没有道理,也根本没有必要的。

 

在段式存管中,由程序发出的变量地址经映射(段基址+段内偏移)之后,得到的32位地址就是一个物理地址,是可以直接放到地址总线是去取数的。

 

在页式存管中,过程也是相似的,由程序发出的变量地址并不是实际的物理地址,而是一个三层的索引结构,这个地址经过一系统的映射之后才可以得到物理地址。

 

现在对于Intel CPU来说,以上两个映射过程就要先后各做一次。由程序发出的变量地址称为“逻辑地址”,先经过段式映射成为“线性地址”,线性地址再做为页式映射的输入,最后得到“物理地址”。

 

Linux内核实现了页式存储管理,而且并没有因为两层存管的映射而变得更复杂。Linux更关注页式内存管理,对于段式映射,采用了特殊的方式把它简化。让每个段寄存器都指向同一个段描述项,即只设了一个段,而这个段的基地址为0,段长度设为最大值4G,这个段就与整个物理内存相重合,逻辑地址经映射之后就与线性地址相同,从而把段式存管变成“透明”的。



1、80386的段寄存器

修改寄存器已近成为系统指令,所以必须有权限修改。
      8086是分段寻址的,而80386所有的通用寄存器都是32位的,2^32相当于4G,任何一个通用寄存器不必分段就已经可以访问到所有内存地址。
      在保护模式下,段寄存器就不在有用了呢?答案是否定的。虽然寻址上不再有分段的限制问题,但在保护模式下,一个地址空间是否可以被写入,可以被多少优先级的代码写入,是不是允许执行等涉及保护的问题就出来了,要解决这些问题,必须对一个地址空间定义一些安全上的属性。段寄存器就派上了用途。

2、关于段描述符(Segment Desciptor)
      涉及属性和保护模式下段的其他参数,要表示的信息太多,要用64位长的数据才能表示,我们把这64位的属性数据叫做段描述符(Segment Desciptor)
      在保护模式下,每个段由以下三个参数进行定义
          1、段基地址(Base Address)
                段基地址规定线性地址空间中段的开始地址。80386保护方式下,段基地址长32为与寻址地址的长度相同,所以任何一个段都可以从32位线性地址空间中的任何一个字节开始,而不像实方式下规定的边界必须被16整除。
          2、段界限( Limit )
                段界限规定段的大小。在80386保护模式下,段界限用20位表示,而且段界限可以是以字节为单位或以4K字节为单位。段属性中有一位对此进行定义,把该位称为粒度位( granularity ),用符号G标记。
                G=0表示段界限以字节为单位,表示范围1字节~1M字节,增量为1字节。
                G=1表示段界限以4K字节为单位,表示范围4K字节~4G字节,增量为4K字节。
                堆栈段情况有些特殊。堆栈底在高地址,随着压栈操作的进行,堆栈向低地址方向扩展。
                为了适应普通数据段和堆栈数据段在两个相反方向上扩展,数据段的段属性中安排了一个扩展方向位,标记为ED。
                ED=0,表示向高端扩展(向上扩展)
                ED=1,表示向低端扩展(向下扩展)
                注意:只有数据段的段属性才有扩展方向属性位ED,其他段都是自然向上扩展。
                如果一个段是向下扩展,则所有的偏移必须大于限长,因为其限长指下限 ,其基地址从高地址开始。反之,若一个段是向上扩展的,则所有偏移必须小于等于限长,因为其限长是指上限,基地址从低地址处开始。
 
          3、段属性( Attributes )
               段属性规定段的主要特征。例如上面的粒度G就是段属性一部分。在对段进行各种访问时,将对访问是否合法进行检查,主要依据段属性
    以上三个参数的数据结构称为描述符。每个描述符长8个字节(64位)。在保护模式下,每个段都有一个相应的描述符来描述。按对象来分,可分为如下3类:
    存储段描述符
    系统段描述符
    门描述符(控制描述符)。

    存储段描述符的格式
    存储段描述符是存放可由程序直接访问的代码 和数据段 ,所以也称为代码和数据段描述符。
 
存储段描述符
    从上表可知
    1、长32位的段基地址(段开始地址)被安排在描述符的两个域中:位0-位23,安排在描述符的第2-第4字节中,其位24-位31被安排在描述符的第7字节中。
    2、长20位的段界限也被安排在描述符的两个域中:位0-位15被安排在描述符的第0-第1字节中,其位16-位19被安排在描述符内的第6字节的第4位中
    注:使用两个域存放段基地址和段界限的原因与80286有关。在80286保护方式下,段基地址只有24位长,而段界限只有16位长。80286存储段描述符尽管也是8字节长,但实际只使用低 6字节,高2字节必须置为0。80386存储段描述符这样的安排,可使得80286的存储段描述符的格式在80386下继续有效。
    下面对其定义及意义作说明 :
    

存储段
描述符
属  性
Byte m+6Byte m+5
BIT7BIT6BIT5BIT4BIT3BIT2BIT1BIT0BIT7BIT6BIT5BIT4BIT3BIT2BIT1BIT0
GD0AVLLimit(19...16)PDPLDT1TYPE

 (1)P位称为存在(Present)位。P=1表示描述符对地址转换是有效的,或者说该描述符所描述的段存在,即在内存中;P=0表示描述符对地址转换无效,即该段不存在。使用该描述符进行内存访问时会引起异常。
(2)DPL表示描述符特权级(Descriptor Privilege level),共2位。它规定了所描述段的特权级,用于特权检查,以决定对该段能否访问。 
(3)DT位说明描述符的类型:
        DT=1,
 存储段描述符
        DT=0,
 系统段描述符和门描述符
(4)TYPE说明存储段描述符所描述的存储段的具体属性。

段描述符属性意义 段描述符属性 
    1)其中的BIT0指示描述符是否被访问过(Accessed),用符号A标记:
        
 A=0表示尚未被访问
        A=1 表示段已被访问。
        当把描述符的相应选择子装入到段寄存器时,80386把该位置为1,表明描述符已被访问。操作系统可测试访问位,已确定描述符是否被访问过。
    2)其中的BIT3指示所描述的段是代码段还是数据段,用符号E标记。
        2.1)E=0表示段为数据段,相应的描述符也就是数据段(包括堆栈段)描述符。数据段是不可执行的,但总是可读的。 
            2.1.1)此时(E=0)TYPE中的BIT1指示所描述的数据段是否可写,用W标记:
                            W=0表示对应的数据段不可写。
                            W=1表示数据段是可写的。(注意,数据段总是可读的)
            2.1.2)此时(E=0)TYPE中的BIT2是ED位,指示所描述的数据段的扩展方向:
                            ED=0表示数据段向高端扩展,也即段内偏移必须小于等于段界限
                            ED=1表示数据段向低扩展,段内偏移必须大于段界限。

        2.2)E=1表示段是可执行段,即代码段,相应的描述符就是代码段描述符。代码段总是不可写的,若需要对代码段进行写入操作,则必须使用别名技术,即用一个可写的数据段描述符来描述该代码段,然后对此数据段进行写入。 
            2.2.1)此时(E=1)TYPE中的BIT1指示所描述的代码段是否可读,用符号R标记:
                            R=0表示对应的代码段不可读,只能执行
                            R=1表示对应的代码段可读可执行。
                            注意:代码段总是不可写的,若需要对代码段进行写入操作,则必须使用别名技术。
            2.2.2)此时(E=1)TYPE中的BIT2指示所描述的代码段是否是一致代码段,用C标记:
                            C=0表示对应的代码段不是一致代码段(普通代码段)
                            C=1表示对应的代码段是一致代码段。
    存储段描述符中的TYPE字段所说明的属性可归纳为下表: 

数据段
类  型
类型值说      明
0(0 000h)只读
1(0 001h)只读、已访问
2(0 010h)读/写
3(0 011h)读/写、已访问
4(0 100h)只读、向下扩展(BIT3)
5(0 101h)只读、向下扩展、已访问
6(0 110h)读/写、向下扩展
7(0 111h)读/写、向下扩展、已访问
 
代码段
类  型
类型值说      明
8(1 000h)只执行
9(1 001h)只执行、已访问
A(1 010h)执行/读
B(1 011h)执行/读、已访问
C(1 100h)只执行、一致码段(BIT3)
D(1 101h)只执行、一致码段、已访问
E(1 110h)执行/读、一致码段
F(1 111h)执行/读、一致码段、已访问
(5)G为就是段界限粒度(Granularity)位:
          段描述符属性bit4-7 
          G=0表示界限粒度为字节
          G=1表示界限粒度为4K字节
          注意,界限粒度只对段界限有效,对段基地址无效,段基地址总是以字节为单位
(6)D位是一个很特殊的位,在描述可执行段、向下扩展数据段或由SS寄存器寻址的段(通常是堆栈段)的三种描述符中的意义各不相同
    可执行段的描述符:
        D位决定了指令使用的地址及操作数所默认的大小:
            D=1表示默认情况下指令使用32位地址及32位或8位操作数,这样的代码段也称为32位代码段;
            D=0 表示默认情况下,使用16位地址及16位或8位操作数,这样的代码段也称为16位代码段,它与80286兼容。

    在向下扩展数据段的描述符中:
        D位决定段的上部边界
            D=1表示段的上部界限为4G
            D=0表示段的上部界限为64K,这是为了与80286兼容。
    在描述由SS寄存器寻址的段描述符中:
        D位决定隐式的堆栈访问指令(如PUSH和POP指令)使用何种堆栈指针寄存器
            D=1表示使用32位堆栈指针寄存器ESP
            D=0表示使用16位堆栈指针寄存器SP,这与80286兼容 
(7)AVL位是软件可利用位。80386对该位的使用未左规定,Intel公司也保证今后开发生产的处理器只要与80386兼容,就不会对该位的使用做任何定义或规定。
此外,描述符内第6字节中的位5必须置为0,可以理解成是为以后的处理器保留的。
 
4、全局和局部描述符表
    一个任务会涉及多个段,每个任务需要一个描述符来描述,为了便于组织管理,80386把描述符组织成线性表。由描述符组成的线性表称为描述符表。在80386中有三种类型的描述符表:
        全局描述符表GDT(Global Descriptor Table)
        局部描述符表LDT(Local Descriptor Table)
        中断描述符表IDT(Interrupt Descriptor Table)
        在整个系统中,全局描述符表GDT和中断描述符表IDT只有一张,局部描述符表可以有若干张,每个任务可以有一张。 
    每个描述符表本身形成一个特殊的数据段。这样的特殊数据段最多可包含有8K(8192)个描述符. 
    每个任务的局部描述符表LDT含有该任务自己的代码段、数据段和堆栈段的描述符,也包含该任务所使用的一些门描述符,如任务门和调用门描述符等。随着任务的切换,系统当前的局部描述符表LDT也随之切换。 
    全局描述符表GDT含有每一个任务都可能或可以访问的段描述符,通常包含操作系统所使用的代码段、数据段和堆栈段的描述符,也包含多种特殊数据段描述符,如各个用于描述任务LDT的特殊数据段 等。在任务切换时,并不切换GDT
    通过LDT可以使各个任务私有的各个段与其它任务相隔离,从而达到受保护的目的。通过GDT可以使各任务都需要使用的段能够被共享。下图给出了任务A和任务B所涉及的有关段既隔离受保护,又合用共享的情况。通过任务A的局部描述符表LDTA和任务B的局部描述符表LDTB,把任务A所私有的代码段CodeA及数据段DataA与任务B所私有的代码段CodeB和数据段DataB及DataB2隔离,但任务A和任务B通过全局描述符表GDT共享代码段CodeK及CodeOS和数据段DataK及DataOS。
LDT与GDT对进程的保护 
一个任务可使用的整个虚拟地址空间分为相等的两半,一半空间的描述符在全局描述符表中,另一半空间的描述符在局部描述符表中。由于全局和局部描述符表都可以包含多达8192个描述符,而每个描述符所描述的段的最大值可达4G字节,因此最大的虚拟地址空间可为:
4GB*8192*2=64MMB=64TB
 
5、段选择子(也叫段选择器,Segment Selector)
    在保护方式下,虚拟地址空间(相当于逻辑地址空间)中存储单元的地址由段选择子和段内偏移两部分组成。与实模式相比,段选择子代替了段值
    段选择子长16位,其格式如下表所示。从表中可见,段选择子的高13位是描述符索引(Index)。所谓描述符索引是指描述符在描述符表中的序号。
    段选择子的第2位是引用描述符表指示位,标记为TI(Table Indicator):
        TI=0指示从全局描述符表GDT中读取描述符
        TI=1指示从局部描述符表LDT中读取描述符。 
选择子
结  构
BIT15BIT14BIT13BIT12BIT11BIT10BIT9BIT8BIT7BIT6BIT5BIT4BIT3BIT2BIT1BIT0
描述符索引TIRPL
    选择子确定描述符,描述符确定段基地址,段基地址与偏移之和就是线性地址。所以,虚拟地址空间中的由选择子和偏移两部分构成的二维虚拟地址,就是这样确定了线性地址空间中的一维线性地址。 
    由于选择子中的描述符索引字段用13位表示,所以可区分8192个描述符。这也就是描述符表最多包含8192个描述符的原因。由于每个描述符长8字节,根据上表所示选择子的格式,屏蔽选择子低3位后所得的值就是选择子所指定的描述符在描述符表中的偏移,这可认为是安排选择子高13位作为描述符索引的原因。
    有一个特殊的选择子称为空(Null)选择子,它的Index=0,TI=0,而RPL字段可以为任意值。空选择子有特定的用途,当用空选择子进行存储访问时会引起异常。空选择子是特别定义的,它不对应于全局描述符表GDT中的第0个描述符,因此处理器中的第0个描述符总不被处理器访问 ,一般把它置成全0。但当TI=1时,Index为0的选择子不是空选择子,它指定了当前任务局部描述符表LDT中的第0个描述符。
        段选择子的RPL与描述符的DPL的关系:
                当改变一个段寄存器的内容时,CPU会加以检查,以确保该段程序的当前执行权限 和段寄存器所指定要求的权限(RPL) 均不低于所要访问的那个段内存的权限DPL
6、局部描述符表寄存器与全局描述符寄存器
    80386中引入了两个新的寄存器来管理段描述符表。一个是48位的全局描述符寄存器GDTR,一个是16位的局部描述符表寄存器LDTR。
GDTR 
    GDTR指向的描述符表位全局描述符表(Global Descriptor Table)。它包含系统中所有任务都可用的段描述符,通常包含描述操作系统所使用的代码段、数据段和堆栈段的描述符及各任务的LDT段 等;全局描述符只有一个。
    LDTR指向描述符表LDT(Local Descriptor Talbe)。80386处理器设计成每个任务都有一个独立的LDT。它包含每个任务私有的代码段,数据段和堆栈段描述符,也包含该任务所使用的一些门描述符。
    不同任务的局部描述符表分别组成不同的内存段,描述这些内存段的描述符当做系统描述符放在全局描述符表中, 图解如下:
    GDT与LDTR的关系 
    和GDTR直接指向内存地址不同,LDTR和CS,DS等段选择器一样只存放索引值,指向局部描述符表内存段对应的描述符在全局描述符表中的位置。 随着任务的切换,只要改变LDTR的值,系统当前的局部描述符表LDT也随之切换,这样便于各任务之间数据的隔离。
7、80386保护模式下,段选择子的寻址
段选择子的寻址
    如上图,在保护模式下,同样以xxxx:yyyyyyyy格式表示一个虚拟地址。凭段选择子中的数值xxxx无法反应出段的基址在哪里。对于这个地址首先看xxxx的TI位是否为0:
    TI=0:
        1)从GDTR寄存器中获取GDT的基址(图步骤1)
        2)然后在GDT中以段选择器xxxx的高13位当做位置索引得到段描述符(步骤2)
        3)段描述符包含段的基址、限长、优先级等各种属性,这就得到了段的起始地址(步骤3)
        4)得到段的基址后(图Result所示),再加上偏移地址yyyyyyyy才得到最后的线性地址
    TI=1:
        1)从GDTR寄存器中获取GDT的基址(图步骤1)
        2)并且从LDTR中获取LDT所在段的位置索引(步骤2')
        3)然后以这个位置索引在GDT中得到LDT段的位置(步骤3')
        4)然后才是用xxxx做索引从LDT段中获得段描述符(步骤4')
        5)再以这个段描述符得到段的基址信息(步骤5')
        6)得到段的基址后(图Result所示),再加上偏移地址yyyyyyyy才得到最后的线性地址


原文地址: http://blogs.ejb.cc/archives/5956/difference-between-the-logical-addresses-linear-addresses-and-physical-addresses

逻辑地址转线性地址

机器语言指令中出现的内存地址,都是逻辑地址,需要转换成线性地址,再经过 MMU (CPU中的内存管理单元)转换成物理地址才能够被访问到。

我们写个最简单的 hello world 程序,用 gcc 编译,再反汇编后会看到以下指令:

mov 0x80495b0 ,  % eax

这里的内存地址0x80495b0 就是一个逻辑地址,必须加上隐含的 DS 数据段的基地址,才能构成线性地址。也就是说0x80495b0是当前任务的DS数据段内的偏移。

在x86保护模式下,段的信息(段基线性地址、长度、权限等)即段描述符占8个字节,段信息无法直接存放在段寄存器中(段寄存器只有2字节)。Intel 的设计是段描述符集中存放在GDT或LDT中,而段寄存器存放的是段描述符在GDT或LDT内的索引值(index)。

Linux中逻辑地址等于线性地址。为什么这么说呢?因为Linux所有的段(用户代码段、用户数据段、内核代码段、内核数据段)的线性地址都是从0×00000000开始,长度4G,这样线性地址=逻辑地址+ 0×00000000,也就是说逻辑地址等于线性地址了。

这样的情况下Linux只用到了GDT,不论是用户任务还是内核任务,都没有用到LDT。GDT的第12和13项段描述符是__KERNEL_CS和__KERNEL_DS,第14和15项段描述符是__USER_CS和__USER_DS。内核任务使用__KERNEL_CS和__KERNEL_DS,所有的用户任务共用__USER_CS和__USER_DS,也就是说不需要给每个任务再单独分配段描述符。内核段描述符和用户段描述符虽然起始线性地址和长度都一样,但DPL(描述符特权级)是不一样的。__KERNEL_CS和__KERNEL_DS 的DPL值为0(最高特权),__USER_CS和__USER_DS的DPL值为3。

用gdb调试程序的时候,用info reg显示当前寄存器的值:

cs 0×73 115
 
ss 0x7b 123
 
ds 0x7b 123
 
es  0x7b  123

可以看到ds值为0x7b,转换成二进制为00000000 01111011,TI字段值为0,表示使用GDT,GDT索引值为01111,即十进制15,对应的就是GDT内的__USER_DS用户数据段描述符。

从上面可以看到,Linux在x86的分段机制上运行,却通过一个巧妙的方式绕开了分段。

Linux主要以分页的方式实现内存管理。

逻辑地址转线性地址

图1-2 逻辑地址转线性地址

二、线性地址转物理地址

前面说了Linux中逻辑地址等于线性地址,那么线性地址怎么对应到物理地址呢?这个大家都知道,那就是通过分页机制,具体的说,就是通过页表查找来对应物理地址。

准确的说分页是CPU提供的一种机制,Linux只是根据这种机制的规则,利用它实现了内存管理。

在保护模式下,控制寄存器CR0的最高位PG位控制着分页管理机制是否生效,如果PG=1,分页机制生效,需通过页表查找才能把线性地址转换物理地址。如果PG=0,则分页机制无效,线性地址就直接做为物理地址。

分页的基本原理是把内存划分成大小固定的若干单元,每个单元称为一页(page),每页包含4k字节的地址空间(为简化分析,我们不考虑扩展分页的情况)。这样每一页的起始地址都是4k字节对齐的。为了能转换成物理地址,我们需要给CPU提供当前任务的线性地址转物理地址的查找表,即页表(page table)。注意,为了实现每个任务的平坦的虚拟内存,每个任务都有自己的页目录表和页表。

为了节约页表占用的内存空间,x86将线性地址通过页目录表和页表两级查找转换成物理地址。

32位的线性地址被分成3个部分:

最高10位Directory页目录表偏移量,中间10位Table是页表偏移量,最低12位Offset是物理页内的字节偏移量。

页目录表的大小为4k(刚好是一个页的大小),包含1024项,每个项4字节(32位),项目里存储的内容就是页表的物理地址。如果页目录表中的页表尚未分配,则物理地址填0。

页表的大小也是4k,同样包含1024项,每个项4字节,内容为最终物理页的物理内存起始地址。

每个活动的任务,必须要先分配给它一个页目录表,并把页目录表的物理地址存入cr3寄存器。页表可以提前分配好,也可以在用到的时候再分配。

还是以mov 0x80495b0, %eax中的地址为例分析一下线性地址转物理地址的过程。

前面说到Linux中逻辑地址等于线性地址,那么我们要转换的线性地址就是0x80495b0。转换的过程是由CPU自动完成的,Linux所要做的就是准备好转换所需的页目录表和页表(假设已经准备好,给页目录表和页表分配物理内存的过程很复杂,后面再分析)。

内核先将当前任务的页目录表的物理地址填入cr3寄存器。

线性地址0x80495b0 转换成二进制后是0000 1000 0000 0100 1001 0101 1011 0000,最高10位0000 1000 00的十进制是32,CPU查看页目录表第32项,里面存放的是页表的物理地址。线性地址中间10位00 0100 1001 的十进制是73,页表的第73项存储的是最终物理页的物理起始地址。物理页基地址加上线性地址中最低12位的偏移量,CPU就找到了线性地址最终对应的物理内存单元。

我们知道Linux中用户进程线性地址能寻址的范围是0-3G,那么是不是需要提前先把这3G虚拟内存的页表都建立好呢?一般情况下,物理内存是远远小于3G的,加上同时有很多进程都在运行,根本无法给每个进程提前建立3G的线性地址页表。Linux利用CPU的一个机制解决了这个问题。进程创建后我们可以给页目录表的表项值都填0,CPU在查找页表时,如果表项的内容为0,则会引发一个缺页异常,进程暂停执行,Linux内核这时候可以通过一系列复杂的算法给分配一个物理页,并把物理页的地址填入表项中,进程再恢复执行。当然进程在这个过程中是被蒙蔽的,它自己的感觉还是正常访问到了物理内存。

分页机制

图1-2 分页机制


  • 3
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值