精华内容
下载资源
问答
  • 内存寻址

    千次阅读 2011-07-07 14:39:55
    <!-- @page { margin: 2cm } P { margin-bottom: 0.21cm ... 内存寻址内存地址使用用80x86微处理器时,必須区分以下三种不同的地址逻辑地址包含在机器语言指令中用来指定一个操作数或一条指令的地址。这种寻址有80x

    内存寻址


    内存地址


    使用用80x86微处理器时,必須区分以下三种不同的地址

    1. 逻辑地址包含在机器语言指令中用来指定一个操作数或一条指令的地址。这种寻址有80x86著名的分段结构中得到表现,它促使MS-DOSwindows程序员把程序分成若干段。每个逻辑地址都是由一个段和偏移量组成,偏移量指明了从段开始的地方到实际地址之间的距离。

    2. 线性地址是一个32位的无符号整数,可以用来表示高达4GB的地址,也就是,高达4294967296个内存单元。线性地址通常用十六进制表示,值的范围从0x000000000xffffffff

    3. 物理地址用于内存芯片级内存单元寻址。它们与从微处理器的地址引脚发送到内存总线上的电信号相对应。物理地址同32位或36位无符号整数表示。


    内存控制单元通过一种为分段单元的硬件电路把一个逻辑地址转换成线性地址;接着,第二个称为分页单元的硬件电路把线性地址转换成物理地址。(见图21)


    在多处理器系统中,所有CPU都共享同一内存;这意味着RAM芯片可以由独立的CPU并发地访问。因为在RAM芯片上读或写操作必须串行地执行,因此一种所谓内存仲裁器的硬件电路插在总线和每个RAM芯片之间。其作用是如果其中一个RAM芯片空闲,就准予一个CPU访问,如果该芯片忙于为另一个处理器提出的请求服务,就廷迟这个CPU访问。即使在单处理器上也使用内存仲裁器,因为单处理器系统中包括一个叫做DMA控制器的特殊处理器,而DMA控制器与CPU并发操作。在多处理器系统的情况下,因为仲裁器有多个输入端口,所以其结构更加复杂。例如,双pentium在每个芯片的入口维持一个两端口仲裁器,并在试图使用公用总线前请求两个CPU交换同步信息。从编程观点看,因为仲裁器由硬件电路管理,因此它是隐藏的。


    硬件中的分段


    80286模型开始,Intel微处理器以两种不同的方式执行地址转换,这两种方式分别称为实模式和保护模式。实模式存在主要原因是要维持处理器与早期模型兼容,并让操作系统自举。


    段选择符和段寄存器


    一个逻辑地址由两部分组成:一个段标识符和一个指定段内相对地址的偏移量。段标识符是一个16位长的字段,称为段选择符如图2-2所示,而偏移量是一个32位长的字段。


    为了快速方便地找到段选择符,处理器提供段寄存器,段寄存器的唯一目的是存放段选择符。这些段寄存器称为cs,ss,ds,es,fsgs。尽管只有6个段寄存器,但程序可以把同一个段寄存器用于不同的目的,方法是先将其值保存在内存中,用完后再恢复


    6个寄存器中3个有专门的用途:

    cs代码段寄存器,指向包含程序指令的段。

    Ss栈段寄存器,指向包含当前程序栈的段。

    Ds数据段寄存器,指向包含静态数据或者全局数据段。


    其它3个段寄存器作一般用途,可以指向任意的数据段。


    cs寄存器还有一个很重要的功能:它含有一个两位的字段,用以指明CPU的当前特权级。值为0代表最高优先级,而值为3代表最低优先级。Linux只有0级和3级,分别称之为内核态和用户态。


    段描述符


    每个段由一个8字节的段段描述符表示,它描述了段的特征。段描述符放在全局描述符表或局部描述符表中。


    通常只定义一个GDT,而每个进程除了存放在GDT中的段之外如果还需要创建附加的段,就可以有自己的LDTGDT在主存中的地址和大小存放在gdtr控制寄存器中,当前正被使用的LDT地址和大小放在ldtr控制寄存器中。


    2-3阐明了段描述符的格式:表2-1解释了图中各个字段的含义


    有几种不同类型的段以及和它们对应的段描述符。下面列出了Linux中被广泛使用的类型:


    代码段描述符

    表示这个段描述符代表一个代码段,它可以放在GDTLDT中。该描述符置S标志为1

    数据段描述符

    表示这个段描述符代表一个数据段,它可以放在GDTLDT中,该描述符置S标志为1

    任务状态段描述符

    表示这个段描述符代表一个任务状态段,也就是说这个段用于保存处理器寄存器的内容。它只能出现在GDT中,根据相应的进程是否正CPU上运行,其Type字段的值分别为119。这个描述符的S标志置为0

    局部描述符表描述符

    表示这个段描述符代表一个包含LDT的段,它只出现在GDT中。相应的Type字段的值加2S标志置为0


    快速访问段描述符


    逻辑地址由16位段选择符和32位偏移量组成,段寄存器仅仅存放段选择符。


    为了加速逻辑地址到线性地址的转换,80x86处理提供一种附加的非编程的寄存器,供6个可编程的段寄存器使用。每一个非编程的寄存器含有8个字节的段描述符,由相应的段寄存器中的段选择符来指定。每当一个段选择符被装入段寄存器时,相应的段描述符由内存到对应的非编程CPU寄存器。从那时起,针对那个寄存器的逻辑地址转换就可以不访问主存中的GDTLDT,处理器只需直接引用存放段描述符中CPU寄存器即可。仅当段寄存器的内容改变时,才有必要访问GDTLDT(2-4)


    由于一个段描述符是8字节长,因此它在GDTLDT内的相对地址是由段选择符的最高13位值乘以8得到的。例如:如果GDT0x00020000且由段选择符所指定的索引号为2,那么相应的段描述符地址是0x00020000+ (2 * 8),


    GDT第一项总是设为0。这就确保空段选择符的逻辑地址会被认为是无效的,因此引起一个处理器异常。能够保存在GDT中的段描述符的最大数目是8191


    分段单元


    2-5详细显示了一个逻辑地址是怎样转换成相应的线性地址的。分段单元执行以下操作:

    1. 先检查段选择符的TI字段,以决定段描述符保存在哪一个描述表中。TI字段指明描述符是在GDT中还是在激活的LDT中。

    2. 从段选择符的index字段计算段描述符的地址,index字段的值乘以8,这个结果与gdtrldtr寄存器中的内容相加。

    3. 把逻辑地址的偏移量与段描述符Base字段的值相加就得了线性地址。


    请注意,有了与段寄存器相关的不可编程寄存器,只有当段寄存器的内容被改变时才需要执行前两个操作。


    Linux中的分段


    80x86微处理器中的分段鼓励程序员把他们的程序化成逻辑上相关的实体,例如子程序或者全局与局部数据区。然而,Linux以很有限的方式使用分段。实际上,分段和分页在某种程度上有点多余,因为它们都可以划分进程的物理空间:分段可以给每一个进程分配不同的线性地址空间,而分页可以把同一线性地址空间映射到不同的物理空间。与分段相比,Linux更喜欢使用分页方式,因为:


    1. 当所有进程使用相同的段寄存器值时,内存管理变得更简单,也就是说它们能共享同样的一组线性地址。

    2. Linux设计目标之一是可以把它移植到绝大多数流行的处理器平台上。然而,RISC体系结构对分段的支持很有限。


    2.6版的Linux只有在80x86结构下才需要使用分段。


    运行在用户态的所有Linux进程都使用一对相同的段来对指令和数据寻址。这个两个段就是所谓的用户代码段和用户数据段。类似地,运行在内核态的所有Linux进程都使用一个相同的段对指令和数据寻址:它们分别叫做内核代码段和内核数据段。表2-3显示了这四个重要段的段描述字段的值。


    相应的段选择符由宏__USER_CS,__USER_DS,__KERNEL_CS,__KERNEL_DS分别定义。例如,为了对内核代码段寻址,内核只需要把__KERNEL_CS宏产生的值装进cs段寄存器即可。


    注意,与段相关的线性地址从0开始,达到2321的寻址限长。这就意味着在用户态或内核态下的所有进程可以使用相同的逻辑地址。


    所有段都从0x00000000开始,这可以得出另一个重要结论,那就是在Linux下逻辑地址与线性地址是一致的,即逻辑地址的偏移量字段的值与相应的线性地址的值总是一致的。


    如前所述,CPU的当前特权级反映了进程是在用户态还是内核态,并由存放在cs寄存器中的段选择符的RPL字段指定。只要当前特权级被改变,一些段寄存器必须相应地更新。例如,当CPL3时,ds寄存器必须含有用户数据段的段描述符,而当CPL=0时,ds寄存器必须含有内核数据段的段选择符。


    类似的情况也出现在ss寄存器中。当CPL3时,它必须指向一个用户数据段中用户栈,当CPL0时,它必须指向内核段中的一个内核栈。当从用户态切换到内核态时,Linux总是确保ss寄存器装有内核数据段的段的段选择符。


    当对指向指令或者数据结构的指针进行保存时,内核根本不需要为其设置逻辑地址的段选择符,因为cs寄存器就含有当前的段选择符。例如,当内核调用一个函数时,它执行一条call汇编语言指令,该指令仅指定其逻辑地址的偏移部分,而段选择符不用设置,它已经隐含在cs寄存器中了,因为“在内核态执行”的段只有一种,叫做代码段,由宏__KERNEL_CS定义,所以只要当CPU切换到内核态时将__KERNEL_CS装载入cs就足够了。同样的首也适用于指向内核数据结构的指针。


    除了刚才描述的4个段以外,Linux还使用了其它几个专门的段。


    LinuxGDT


    在单处理器系统中只有一个GDT,而在多处理器系统中每个CPU对应一个GDT。所有的GDT都存放在cpu_gdt_table数组中,而所有GDT的地址和它们的大小被存放在cpu_gdt_descr数组中。


    2-6GDT的布局示意图。每个GDT包含18个段描述符和14个空的,未使用的,或保留的项,插入未使用的项的目的是为了使经常一起访问的描述符能够处于同一个32字节的硬件高速缓存行中。


    每一个GDT中包含的18个段描述符指向下列的段:

    1. 用户态和内核态下的代码段和数据段共4

    2. 任务状态段,每个处理器有1。每个TSS相应的线性地址空间都是内核数据段相应线性地址空间的一个小子集。所有的任务状态段都顺序地存放在init_tss数组中;值得特别说明的是,第NCPUTSS描述符的Base字段指向init_tssN个元素。G标志被清0,而Limit字段置为0xeb,因为TSS段是236字节长。Type字段置为911,且DPL置为0,因为不允许用户态下的进程访问TSS段。

    3. 1个包括缺省局部描述符表的段,这个段通常是被所有进程共享的段

    4. 3个局部线程存储段:这种机制允许多线程应用程序使用最多3个局部于线程的数据段。系统调用set_thread_area()get_thread_area()分别下在执行的进程创建和撤销一个TLS段。

    5. 与高级电源管理相关的3个段:由于BIOS代码使用段,所以当LinuxAPM驱动程序调用BIOS函数来获取或者设置APM设备的状态时,就可以使用自定义的代码段和数据段。

    6. 与支持即敀插即用功能的BIOS服务程序相关的5个段:在前一种情况下,就像前述与AMP相关的3个段的情况一样,由于BIOS例程使用段,所以当LinuxPnP设备驱动程序调用BIOS函数来检测PnP设备使用的资源时,就可以使用自定义的代码段和数据段。

    7. 被内核用来处理“双重错误”异常的特殊TSS段。


    如前所述,系统中每个处理器都有一个GDT副本。除了少数几种情况以外,所有GDT的副本都存放相同的表项。首先,每个处理器都有它自己的TSS段,因此其对应的GDT项不同。其次,GDT中只有少数项可能依赖于CPU正在执行的进程。最后,在一些情况下,处理器可能临时修改GDT副本里的某个项。


    LinuxLDT


    大多数用户态下的Linux程序不使用局部描述符表,这样内核就定义了一个缺省的LDT供大多数进程共享。缺省的局部描述符表存放在default_ldt数组中。它包含5个项,但内核仅仅有效地使用了其中的两个项:用于iBCS执行文件的调用门和Solaris/x86可执行文件的调用门。调用门是80x86微处理器提供的一种机制,用于在调用预定义函数时改变CPU的特权段。


    在一些情况下,进程仍然需要创建自己的局部描述符表。这对有些应用程序很有用,像wine那样的程序,它们执行面向段的微软windows应用程序。modify_ldt()系统调用允许进程创建自己的局部描述符。


    任何被modify_ldt()创建的自定义局部描述符表仍然需要它自己的段。当处理器开始执行拥有自定义局部描述符表的进程时,该CPUGDT副本中的LDT表项相应地就被修改了。


    用户态下的程序同样也利用modify_ldt()来分配新的段,但内核从不使用这些段,它也不需要了解相应的段描述符,因为这些段描述符被包含在进程自定义的局部描述符表中了。


    硬件中的分页


    分页单元把线性地址转换成物理地址。其中的一个关键会务是把所请求的访问类型与线性地址的访问权限相比较,如果这次内存访问是无效的,就产生一个缺页异常。


    为了效率起见,线性地址被分成以固定长度为单位的组,称为页。页内部连续的线性地址被映射到连续的物理地址中。这样,内核可以指定一个页的物理地址和其存取权限,而不用指定页所包含的全部线性地址的存取权限。


    分页单元把所有的RAM分成固定长度的页框。每一个页框包含一个页,也就是一个页框的长度与一个页的长度一致。页框是主存的一部分,因此也是一个存储区域。区分一页和一个页框是很重要的,前者是一个数据块,可以存放任何页框或磁盘中。


    把线性地址映射到物理地址的数据结构称为页表。页表存放在主存中,并在启用分页单元之前由内核对页表进行适当的初始化。


    80386开始,所有的80x86处理器都支持分页,它通过设置cr0寄存器的PG标志启用。当PG0,线性地址就被解释成物理地址。


    常规分页


    80386起,Intel处理器的分页单元处理器4KB的页。


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


    Directory(目录)

    最高10

    Table(页表)

    中间10

    Offset(偏移量)

    最低12


    线性地址的转换分两步完成,每一步都基于一种转换表,第一种转换表称为页目录表,第二种转换表称为页表。


    使用这种二级模式的目的在于减少每个进程页表所需RAM的数量。如果使用简单的一级页表,那将需要高达220次方个表页来表示每个进程的页表,即使一个进程并不使用那个范围内的所有地址。二级模式通过只为进程实际使用的那些虚拟内存区请求页表来减少内存容量。


    每个活动进程必须有一个分配给它的页目录。不过,没有必要很快为进程的所有页表都分配RAM。只有在进程实际需要一个页表时才给该页表分配RAM会更为有效率。


    正在使用的页目录的物理地址存放在控制寄存器cr3中。线性地址内的Directory字段决定页目录中的目录项,而目录项指向适当的页表。地址的Table字段依次又决定页表中的表项,而表项含有页所在页框的物理地址。Offset字段决定页框的相对位置(如图2-7)。由于它是12位长,帮每一页含有4096字节的数据。


    Directory字段和Table字段都是10位长,因此页目录和页表都可以多达1024项。那么一个页目录可以寻址到高达102410242092232次方个存储单元,这和你对32位地址所期望的一样。


    页目录项和页表项有同样的结构,每项都包含下面的字段:


    Present标志

    如果被置为1,所指的页就在主存中;如果为0,则这一页不在主存中,些时这个表项剩余的位可由操作系统用于自己的目的。如果执行一个地址转换所需要页表项目录项中present标志被清0,那么分页单元就把该线性地址存放在cr2中,并产生14号异常:缺页异常。


    包含页框物理地址的最高20位的字段

    由于每一个页框有4KB的容量,它的物理地址必须是4096的倍数,因此物理地址的最低12总是为0。如果这个字段指向一个页目录,相应的页框就包含有一个页表;如果它指向一个页表。相应的页框就含有一页数据。


    Accessed标志

    每当分布单元对相应页框进行寻址时就设置这个标志。当选中的页被交换出去时,这一标志就可以由操作系统使用。分页单元从来不重置这个标志,而是必须由操作系统去做。


    Dirty标志

    只应用于页表项中,每当对一个页框进行写操作时就设置这个标志。与Accessed标志一样,当选中的页被交换出去时,这一标志就可以由操作系统使用。分页单元从来不智重置这个标志。


    Read/Write标志

    含有页或页表的存取权限。

    User/Supervisor标志

    含有访问页或页表所需的特权级。

    PCDPWT标志

    控制硬件高速缓存处理页或页表的方式。

    PageSize标志

    只应用于页目录项。如果设置为1,则页目录项指的是2MB4MB的页框。


    扩展分页

    pentium模型开始,80x86微处理器引入了扩展分页,它允许页框大小为4MB而不是4KB。扩展分页用于把大段连续的线性地址转换成相应的物理地址,在这种情况下,内核可以不用中间页表进行地址转换。


    正如前面所述,通过设置页目录项的PageSize标志启用扩展分页功能。在这种情况下,分页单元把32位线性地址分成两个字段:

    Directory

    最高10

    offset

    剩下22


    扩展分页与正常分页的页目录基本相同,除了:

    1. PageSize标志必須被设置

    2. 20位物理地址字段只有最高10位是有意义的。这是因为每一个物理地址都是在以4MB为边界的地方开始的,故这个地址的最低22位为0

    通过设置cr4处理器寄存器的PSE标志能使扩展分页和正常分页共存。


    硬件保护方案


    分页单元和分段单元的保护方案不同。尽管80x86处理器允许一个段使用4种可能的特权级别,但与页和页表相关的特权级只有两个,因为特权由前面“常规分页”一节中所提到的User/Ssupervisor标志所控制。若这个标志为0,只有当CPL小于3时才能对页寻址;若该标志为1,则总能对页寻址。


    此外,与段的3种存取权限不同的是,页的存取只有两种(读写)。如果页目录或页表的Read/Write标志等于0,说明相应的页表或页是只读的,否则是可读写的。


    常规分页举例


    假定内核已给一个正在运行的进程分配的线性地址空间范围是0x200000000x2003ffff。这个空间正好是64页组成。我们不必关心包含这些页的页框的物理地址,事实上,其中的一些页甚至可能不在主存中。只关注页表项中剩余的字段。


    让我们从分配给进程的线性地址的最高10开始。这两个地址都以2开头后面跟着0,因此高10有相同的值即0x080或十进制128。因此,这两个地址的directory字段都指向进程页目录的第129项。相应的目录项中必须包含分配给该进程的页表的物理地址(2-9)。如果没有给这个进程分配其它的线性地址,则页目录的其它1023项都填为0


    中间的10位的值范围从00x03f。因而只有页表的前64个表项是有意义的,其它960个表项都填0


    假设进程需要读线性地址0x20021406中的字节。这个地址由分页单元按下面的方法处理:

    1. Directory字段的0x80用于选择页目录的第0x80项,此目录项指向和该进程的页相关的页表。

    2. Table字段0x21用于选择页表的第0x21表项,此表项指向包含所需页的页框。

    3. 最后,offset字段0x406用于在目标页框中读偏移量为0x406中的字节。


    如果页表第0x21表项的present标志为0,则此页不在主存中,在这种情况下,分页单元在线性地址转换的同时产生一缺页异常。无论何时,当进程试图访问限定在0x200000000x2003ffff范围之外的线性地址是,都将产生一个缺页异常,因为这些页表项都填充了0,特别是它们的present标志都被清0


    物理地址扩展分页机制


    处理器所支持的RAM容量受连接到地址总线上的地址管脚数限制。早期Intel处理器从80386Pentium使用32位物理地址。从理论上讲,这样的的系统上可以安装高达4GBRAM,而实际上,由于用户进程线性地址空间的需要,内核不能直接对1GB以上的RAM进行寻址。


    然而,大型服务器需要大于4GBRAM来同时运行数以千计的进程,近几年这对Intel造成了压力,所以必須扩展3280x86结构所支持的RAM容量。


    Intel通过在它的处理器上把管脚数从32增加到36已经满足了这些需求。从PentiumPro开始。Intel所有处理器现在寻址能力达64GB。不过,只有引入一种的分页机制把32位线性地址转换到36位物理地址才能使用所增加的物理地址。


    pentiumPro处理器开始,Intel引入一种叫做物理地址扩展的机制。另外一种叫做页大小扩展的机制在Pentium3处理器中引入,但是Linux并没有使用这种机制。


    通过设置cr4控制寄存器中的物理地址扩展标志激活PAE。页目录中的页大小标志ps启用大尺寸页。


    Intel为了支持PAE已经改变了分页机制。


    1. 64GBRAM被分为了224次方个页框,页表项的物理地址字段从20位扩展到24位。因为PAE页表项必须包含12个标志位和24个物理地址。总数之和为36。页表项大小从32位变成64们增加了一倍。结果,一个4KB的页表包含512个表页不是1024个表项。

    2. 引入一个叫做页目录指针表的页表新级别,它由464位表项组成。

    3. cr3控制寄存器包含一个27位的页目录指针表基地址字段。因为PDPT存放在RAM的前4GB中,并在32字节的倍数上对齐,因此27位足以表示这种表的基地址。

    4. 当把线性地址映射到4KB的页时,32位线性地址按下列方式解释:

      cr3

      指向一个PDPT

      3130

      指向PDPT4个项中的一个

      2921

      指向嶡目录中512个项中的一个

      2012

      1. 指向页表中512项中一个

    110

    4KB页中的偏移量

    总之,一旦cr3被设置,就可能寻址高达4GBRAM,如果我们希望对更多的RAM寻址,就必须在cr3中放置一个新值,或改变PDPT的内容。然而,使用PAE的主要问题是线必地址仍然是32位长。


    硬件高速缓存


    当今的微处理器时钟频率接近几个GHZ,而动态RAM芯片的存取时间是时钟周期的数百倍。这意味着,当从RAM中取操作数或向RAM中存放结果这样的指令执行是,CPU可能等待很长时间。


    为了缩小CPURAM之间的速度不匹配,引入了硬件高速缓存内存。硬件高速缓存基于著名的局部性原理,该原理既适用程序结构和适用数据结构。这表明由于程序的循环结构及相关数组可以组织成线性数组,最近最常用的相邻在最近的将来又被用到的可能性极大。因此,引入小而快的内存来存放最近最常使用的代码和数据变得很有意义。为此,80x86体系结构中引入了一个叫行的新单元。行由几十个连续的字节组成,它们以脉冲突发模式在慢速DRAM和快速的用来实现高速缓存的片上静态RAM之间传送,用来实现高速缓存。


    高速缓存再被细分为行的子集。在一种极端的情况下,高速缓存可以是直接映射的,这时主存中的一个行总是存放在高速缓存中完全相同的位置。在另一种极端情况下,高速缓存是充分关联的,这意味着主存中的任意一个行可以存放在高速缓存中的任意位置。但是大多数高速缓存在某种程序上是N路相关联的,意味着主存中的任意一个行可以存放在高速缓存N行中的任意一行中。例如,内存中的一个行可以存放到一个2路组关联高速缓存两个不同的行中。


    转换后援缓冲器


    除了通过硬件高速缓存之外,80x86处理器还包含了另一个称为转换后援缓冲器的高速缓存用于加快线性地址的转换。当一个线性地址第一次使用时,通过慢速访问RAM中的页表计算出相应的物理地址。同时,物理地址被存放在一个TLB表项中,以便以后对同一个线性地址的引用可以快速地行到转换。


    Linux中的分页


    Linux采用了一种同时适用于32位和64位系统的普通分页模型。正像前面“64位系统中的分页”一节所解释的那样,两级页表对32位系统来说已经足够了,但64位系统需要更多数量的分页级别。直到2.6.10版本,Linux采用三级分页模型。从2.6.11版本开始,采用了四级分页模型。图2-12中提示的4种页表分别被为:

    1. 页全局目录

    2. 页上级目录

    3. 页中间目录

    4. 页表


    页全局目录包含若干页上级目录的地址,页上级目录又依次包含若干页中间目录的地址,而页中间目录又包含若干页表的地址。每一个页表项指向一个页框。线性地址因此被分成五个部分。图2-12没有显示位数,因为每一部分的大小与具体的计算机体系结构有关。


    对于没有启用物理地址扩展的32位系统。两级页表已经足够了。Linux通过使“页上级目录”位和“页中间目录”位全为0,从根本上取消了页上级目录和页中间目录字段。不过,页上级目录和页中间在指针序列中的位置被保留,以便同样的代码在32位系统和64位系统下都能使用。内核为页上级目录和同中间目录保留一个位置,这是通过把它们的页目录数设置为1,并把这两个目录项映射到页全局目录的一个适当的目录项而实现的。


    启用了物理地址扩展的32位系统使用了三级页表。Linux的页全局目录对应80x86的页目录指针,取消了页上级目录,页中间目录对应80x86的页目录,Linux的页对应80x86的页表。


    最后,64位系统使用三级还是四级分页取决于硬件对线性地址的位的划分(见表24)


    Linux的进程处理很大程序上依赖于分页。事实上,线性地址到物理地址的自动转换使下面的设计目录就得可行:

    1. 给每一个进程分配一块不同的物理地址空间,这确保了可以有效地防止寻址错误。

    2. 区别页和页框之不同。这就允许放在某个页框中的一个页,然后保存到磁盘上,以后重新装入这同一页时又可以被装在不同的页框中。这就是虚拟内存机制的基本要素。


    每一个进程有它自己的页全局目录和自己的页表集。当发生进程切换时,Linuxcr3控制寄存器的内容保存在前一个执行进程的描述符中,然后把下一个要执行进程的描述符装入cr3寄存器中。因此,当新进程重新开始在CPU上执行时,分页单元指向一组正确的页表。


    把线性地址映射到物理地址虽然有点复杂,但现在已经成了一种机械的任务。


    线性地址字段


    下列宏简化了页表处理:


    PAGE_SHIFT

    指定Offset字段的位数;当用于80x86处理时,它产生的值为12。由于页内所有地址都必须能放到Offset字段中。因此80x86系统的页的大小是4096个字节。PAGE_SHIFT的值为12,可以看作以2为低的页大小的对数。这个宏由PAGE_SIZE使用以返回页的大小。最后,PAGE_MASK宏产生的值为0xfffff000,用以屏蔽offset字段的所有位。


    PMD_SHIFT


    指定线性地址的Offset字段和Table字段的总位数;换句话说,是页中间目录可以映射的区域大小的对数。PMD_SIZE宏用于计算页中间目录的一个单独表项所映射的区域大小,也就是一个页表的大小。PMD_MASK宏用于屏蔽OFFSET字段与TABLE字段的所有位。

    PAE被禁用时,PDM_SHIFT产生的什为22PMD_SIZE产生的值为4MBPMD_MASK产生的值为0xffc00000。相反,当PAE被激活时,PMD_SIZE产生的值为2MPMD_MASK产生的值为0xffe00000


    大型页不使用最后一级页表,所以产生大型尺寸的LARGE_PAGE_SIZE宏等于PMD_SIZE,而在大型页地址中用于屏蔽Offset字段和Table字段的所有位的LARGE_PAGE_MASK宏,就等于PMD_MASK


    PUD_SHIFT


    指定页上级目录项能映射的区域大小的对数。PUD_SIZE宏用于计算页全局目录中的一个单独项所能映射的区域大小。PUD_MASK宏用于屏蔽Offset字段、Table字段。MiddleAir字段等。

    80x86处理上,PUD_SHIFT总是等价于PMD_SHIFT,而PUD_SIZE则等于4M2M


    PGDIR_SHIFT


    确定页全局目录能映射的区域大小的对象。PGDIR_SIZE宏用于计算页全局目录中一个单独表项能映射区域的大小。PGDIR_MASK宏用于屏蔽OffsetTable,等一些字段。

    PAE被禁止时,PGDIR_SHIFT产生的值为22PGDIR_SIZE产生的值为4M,以及PGDIR_MASK产生的值为0xffc00000。相反,当PAE被激活时,PGDIR_SHIFT产生的值为30PGDIR_SIZE产生的值为1GB


    PTRS_PER_PTEPTRS_PER_PMDPTRS_PER_PUDPTRS_PER_PGD


    用于计算页表,页中间目录,页上级目录和页全局目录表中表项的个数。当PAE被禁止时,它们产生的值为别为1024111024。当PAE被激活时,产生的值分别为51251214


    页表处理


    pte_t,pmd_t,pud_tpgd_t分别描述页表项、页中间目录项、页上级目录和页全局目录项的格式。当PAE被激活时它们都是64位的数据类型。否则都是32位数据类型,它表示与一个单独表项相关的保护标志。


    五个类型转换宏(__pte,__pmd,__pud,__pgd__pgprot)把一个无符号整数转换成所需要类型。另外的五个类型转换成(pte_val,pmd_val,pud_val,pgd_valpgprot_val)执行相反的转换,即把上面提到的四种特殊的类型转换成一个无符号整数。


    内核还提供了许多宏和函数用于读或修改页表表项:

    1. 如果相应的表项值为0,那么,宏pte_none,pmd_none,pud_nonepgd_none产生的值为1,否则产生的值为0

    2. pte_clear,pmd_clear,pud_clear,pgd_clear清除相应页表的一个表项,由此禁止进程使用由该页表项映射的线性地址。ptep_get_and_clear()函数清除一个页表项并返回前一个值。

    3. set_pte,set_pmd,set_pudset_pgd向一个页表项中写入指定的值。set_pte_atomicset_pte的作用相同,但是当PAE被激活时它同样能保证64位的值被原子地写入。

    4. 如果AB两个页表项指向同一个页并且指定相同的访问优先级,那么pte_same(A,B)返回1,否则返回0

    5. 如果页中间目录项e指向一个大型页,那么pmd_large返回1,否则返回0


    pmd_bad由函数使用并通过输入参数传递来检查页中间目录项。如果目录项指向一个不能使用的页表,也就是说,如果至少出现以下条件中的一个,则这宏产生的值为1

    1. 页不在主存中

    2. 页只允许读访问

    3. Acessed或者Dirty被清除。


    pud_bad宏和pgd_bad宏总是产生0。没有定义pte_bad宏,因为页表项引用一个不在主存中的页,一个不可写的页或一个根本无法访问的页都是合法的。


    如果一个页表项的Present标志或者pagesize标志等于1,则pte_present宏产生的值为1,否则为0。前面讲过页表项的Pagesize标志对微处理器的分页单元来讲没有意义,然而,对于当前在主存中却又没有读、写或执行权限的页,内核将其PresentPageSize分别标记为01。这样,任何试图对此类页的访问都会引起一个缺页异常。因为页的present标志清0,而内核可能通过检查Pagesize的值来检测到产生异常并不是因为缺页。


    如果相应表项的present标志等于1,也就是说,如果对应的页或页表被载入主存,pmd_present宏产生的值为1pud_presetn宏和pgd_present宏产生的值总为1


    25中列出的函数用来查询页表项中任意一个标志的当前值;除了pte_file()外,其它函数只有在pte_present返回1的时候。才能正常返回页表项中任意一个标志。


    26列出的另一组函数用于设置页表项中和标志的值。


    27对页表项进行操作,它们把一个页地址和一组保护标志组合成页表项,或者执行相反的操作,从一个页表项中提出页地址。


    当使用两级页表时,创建或删除一个页中间目录项是不重要的。页中间目录仅含有一个指向下属页表的目录项。所以,页中间目录项只是页全局目录中的一项而已。然而当处理页表时,创建一个页表可能很复杂,因为包含页表项的那个页表可能就不存在。在这样的情况下,有必要分配一个新页框,把它填写为0,并把这个表项加入。


    如果PAE被激活,内核使用三级页表。当内核创建一个新的页全局目录时,同时也分配四个相应的页中间目录;只有当父页全局目录被释放时,这四个页中间目录才以释放。


    当使用两级或三级分页时,页上级目录项总是被映射为页全局目录中的一个单独项。


    28中列出的函数描述是针对80x86体系结构的。


    物理内存布局


    在初始化阶段,内核必須建立一个物理地址映射来指定哪些物理地址范围对内核可用而哪些不可用。

    内核将下列页框记为保留:

    1. 在不可用的物理地址范围内的页框

    2. 含有内核代码和已初始化的数据结构的页框


    保留页框的页绝不能被动态分配或交换到磁盘上。


    一般来说,Linux内核安装在RAM中物理地址0x00100000开始地方,也就是说,从第二个MB开始。所需页框总数依赖于内核的配置方案:典型的配置所得到的内核可以被安装在小于3MRAM中。


    为什么内核没有安装在RAM第一个MB开始的地方呢?因为PC体系结构有几个独特的地方必须考虑到。劎例如:

    1. 页框0BIOS使用,存放加电自检期间检查到的系统硬件配置。

    2. 物理地址从0x000a00000x000fffff的范围通常留给BIOS例程,并且映射ISA图形卡上的内部内存。这个区域就是所有IMB兼容PC上从640KB1MB之间著名的洞

    3. 第一个MB内的其它页框可能由特定计算机模型保留。


    在启动过程的早期阶段,内核询问BIOS并了解物理内存的大小。在新近的教育处机中,内核也调用BIOS过程建立一组物理地址范围和其对应的内存类型。


    随后,内核执行machine_specific_memory_setup()函数,该函数建立物理地址映(见表2-9)射。当然,如果这张表是可获取的,那是内核在BIOS列表的基础上构建的;否则,内核按保守的缺省设置构建这张表:从0x9f0x100号的所有页框都标记为保留。


    2-9显示了具有128MRAM计算机的典型配置。从0x07ff00000x07ff2fff的物理地址范围中存有加电自检阶段由BIOS写入的系统硬件设备信息;在初始化阶段,内核把这些信息拷贝到一个合适的内核数据结构中,然后认为这些页框是可用的。相反,从0x07ff30000x07ffffff的物理地址范围被映射到硬件设备的ROM芯片。从0xffff0000开始的物理地址范围标记为保留,因为它由硬件映射到BIOSROM芯片。注意BIOS也许不提供一些物理地址范围的信息。


    内核可能不会见到BIOS报告的所有物理内存:例如,如果未使用PAE支持来编译,即使有更大的物理内存可供使用,内核也只能寻址4GB大小的RAMsetup_memory()函数在machine_specific_memory_setup()执行后被调用:它分析物理内存区域表并初始化一些变量业描述内核的物理内存布局,这些变量如表2-10所示。


    为了避免把内核装入一组不连续的页框里面,Linux更愿意跳过RAM的第一个MB。明确地说,LinuxPC体系结构未保留的页框来动态存放所分配的页。


    2-13显示Linux怎样填充前3MBRAM。假定内核需要小于3MBRAM


    符号_text对应于物理地址0x00100000,表示内核代码第一个字节的地址。内核代码的结束位置由另外一个类似的符号_etext表示。内核数据分为两组:初始化过的数据和和没有初始化的数据。初始化过的数据在_etext后开始,在_edata处结束。紧接着是未初始化的数据并以_end结束。


    进程的页表


    进程的线性地址空间分成两部分:


    1. 0x000000000xbfffffff的线性地址,无论进程运行在用户态还是在内核态都可以寻址。

    2. 0xc00000000xffffffff的线性地址,只有内核态的进程才能寻址。


    当进程运行在用户态时,它产生的线性地址小于0xc0000000;当进程运行在内核态时,它执行内核代码,所产生的地址大于等于0xc0000000。但是,在一些情况下,内核为了检索或存放数据必须访问用户态线性地址空间。


    PAGE_OFFSET产生的值为0xc0000000,这就是进程在线性地址空间中的偏移量,也是内核生存空间的开始之处。


    页全局目录的第一部分表项映射的线性地址小于0xc0000000,具体大小依赖于特定进程。


    内核页表


    内核维持着一组自己使用的页表,驻留在所谓的主内核页全局目录中。系统初始化后,这组页表还从未被任何进程或任何内核线程直接使用;更确切地说,主内核页全局目录的最高目录项部分作为参考模型,为系统中每个普通进程对应的页全局目录项提供参考模型。


    内核初始化自己的页表。这个过程分为两个阶段。事实上,内核映像刚刚被装入内存后,CPU仍然运行于实模式,所以分页功能没有被启用。


    第一个阶段,内核创建一个有限的地址空间,包括内核的代码段和数据段、初始页表和用于存放动态数据结构的共128KB大小的空间。这个最小限度的地址空间仅够内核状入RAM和对初始化的核心数据结构。


    第二个阶段,内核充分利用剩余的RAM并适当地建立分页表。


    临时内核页表


    临时页全局目录是在内核编译过程中静态地初始化的,而临时页表是由startup_32()汇编语言函数初始化的。


    临时页全局目录放在swapper_pg_dir变量中。临时页表在pg0变量处开始存放,紧接在内核未初始化的数据段后面。为简单起见,我们假定内核使用的段、临时页表和128KB的内存范围能容纳于RAM8MB空间里。为了映射RAM8MB的空间,需要用到两个页表。


    分页第一个阶段的目录是允许在实模式下和保护模式下都能很容易地对这8MB寻址。因些,内核必須创建一个映射,把从0x000000000x007fffff的线性地址和从0xc00000000xc07fffff的线性地址映射到从0x000000000x007fffff的物理地址。换句话说,内核在初始化的第一个阶段,可以通过与物理地址相同的线性地址或者通过从0xc0000000开始的8MB线性地址对RAM的前8MB进行寻址


    内核通过把swapper_pg_dir所有项都填充为0来创建期望的映射,不过,010x3000x301这四项除外;后两项包含了从0xc00000000xc07fffff间的所有线性地址。010x3000x301按以下方式初始化:

    1. 0项和0x300项的地址字段置为pg0的物理地址,而1项和0x301项的地址字段置为紧随pg0后的页框的物理地址。

    2. 把这四个项中的PresentReand/WriteUser/Supervisor标志置位。

    3. 把这四个项中的AccessedDirytPCDPWDPageSize标志清0

    汇编语言函数startup_32()也启用分页单元,通过向cr3控制寄存器装入swpper_pg_dir的地址及设置cr0控制寄存器的PG标志来达到这一目的。下面是等价的代码片段:


    RAM小于896MB时的最终内核页表


    由内核页表所提供的最终映射必须把从0xc0000000开始的线性地址转化为从0开始的物理地址。


    __pa用于把从PAGE_OFFSET开始的线性地址转换成相应的物理地址,而宏__va做相反的转化。


    主内核页全局目录仍然保存在swapper_pg_dir变量中。它由page_init()函数初始化。该函数进行如下操作:

    1. 调用pagetable_init()适当地建立页表项。

    2. swapper_pg_dir的物理地址写cr3控制寄存器中。

    3. 如果CPU支持PAE并且如果内核编译时支持PAE,则将cr4控制寄存器的PAE标志置位。

    4. 调用__flush_tlb_all()使TLB的所有项无效。

    pagetable_init()执行的操作既依赖于现有RAM的容量,也依赖于CPU模型。计算机有小于896MBRAM32位物理地址足以对所有可用RAM进行寻址,因而没有必要激活PAE机制。


    我们假定CPU是支持4MB页和“全局”TLB表项的最新80x86微处理器。注意如果页全局目录项对应的是0xc0000000之上的线性地址,则把所有这些项的User/Supervisor标志清0。由此拒绝用户态进程访问内核地址空间。还要注意PageSize被置位使得内核可能通过使用大型而对RAM进行寻址。


    startup_32()函数创建的物理内存前8MB的恒等映射用来完成内核的初始化阶段。当这种映射不再必要是,内核调用zap_low_mappings()函数清除对应的页表项。


    RAM大小在896MB4096MB之间时的最终内核页表


    在这种情况下,并把RAM全部映射到内核地址空间。Linux在初始化阶段可以做的最好的事把一个具有896MBRAM窗口映射到内核线性空间。如果一个程序需要对现在RAM的其余部分寻址,寻就必须把某些其它的线性地址间隔映射到所需的RAM。这意味着修改一些页表的值。


    RAM大于4096MB时的最终内核页表


    如果RAM大于4GB计算机的内核页表初始化;更确切地说,要处理以下发生的情况:

    1. CPU模型支持物理地址扩展

    2. RAM容量大于4GB

    3. 内核以PAE支持来编译

    尽管PAE处理36位物理地址,但是线性地址依然是32位地址,如前所述,Linux映射一个896MBRAM窗口到内核线性地址空间,剩余RAM留着不映射,并由动态重映射来处理。


    而全局目录中的前三项与用户线性地址空间相对应,内核用一个空页的地址对这三项进行初始化。第四项用页中间目录中的前448项用RAM896MB的物理地址填充。


    注意,支持PAE的所有CPU模型也支持大型2MB页和全局页。正如前一种情况一样,只要可能,Linux使用大型页来减少页表数。


    然后页全局目录的第四项被拷贝到第一项中,这样好为线性地址空间的前896MB中的低物理内存映射做镜像。为了完成对SMP系统的初始化,这个映射是必需的:当这个映射不再必要时,内核通过调用zap_low_mapping()函数来清除对应的页表项。


    固定映射的线性地址


    我们看到内核线性地址第四个GB的初始部分映射系统的物理内存。但是,至少128M的线性地址总是留作他用,因为内核使用这些线性地址实现非连续内存分配和固定映射的线性地址。


    非连续内存分配仅仅是动态分配和释放内存页的一种特殊方式。


    固定映射的线性地址基本上是一种类似于0xffffc000这样的常量线性地址,其对应的物理地址不必等于线性地址送去0xc0000000,而是可以以任意方式建立。因此,每个固定映射的线性地址都映射一个物理内存的页框。其实主是使用固定映射的线性地址来代替指针变量,因为这些变量的值从不改变。


    固定映射的线性地址概念上类似于对RAM896MB映射的线性地址。不过,固定映射的线性地址可以映射任何物理地址,而由第4GB初始化的线性地址所建立的映射是线性的。


    就指针变量而言,固定映射的线性地址更有效。事实上间接引用一个立即常量地址要多一次内存访问。此外,在间接引用一个指针变量之前对其值进行检查是一个良好的编程习惯。


    每个固定映射的线性地址都定义于enumfixed_address数据结构中的整型索引来表示:


    每个固定映射的线性地址都存放在线性地址第四个GB的低端。fix_to_virt()函数计算从给定索引开始的常量线性地址:


    假定某个内核函数调用fix_to_virt(FIX_IO_APIC_BASE_0).因为该函数声明为“inline”,所以C编译程序不调用fix_to_virt(),而是仅仅把它的代码插入到调用函数中。此外,运行时从不对这个索引值执行检查。事实上,FIX_IO_APIC_BASE_0是个等于3的常量,因此编译程序可以去掉if语句,因为它的条件在编译时为假。相反,如果条件为址,或者fix_to_virt()参数不是一个常量,则编译程序在连接阶段产生一个错误。


    为了把一个物理地址与固定映射的线性地址关联起来,内核使用set_fixma(idx,phys)set_fixmap_nocache(idx,phys)宏。这两个函数都把fix_to_virt(idx)线性地址对应一个页表项初始化为物理地址phys;不过,第二个函数也把页表项的PCD标志置位,因此,当访问这个页框中的数据时禁用硬件高速缓存。反过来,clear_fixmap(idx)用来撤消固定映射线性地址idx和物理地址之间的连接。


    处理硬件高速缓存和TLB


    内存寻址的最后一个主题是关于内核如果使用硬件高速缓存来达到最佳效果。硬件高速缓存和转换后援缓冲器在提高现代计算机体系结构的性能上扮演着重要的角色。采用一些技术来减少高速缓存和TLB的未命中次数。


    处理硬件高速缓存


    如前所述,硬件高速缓存是通过高速缓存行寻址的。L1_CACHE_BYTES宏产生以字节为单位的高速缓存行的大小。


    为了使高速缓存的命中率达到最优化,内核在下列决策中考虑体系结构:

    1. 一个数据结构中最常使用的字段放在该数据结构内的低偏移部分,以便它们能够处于高速缓存的同一行中。

    2. 当为一大组数据结构分配空间时,内核试图把它们都存放在内存中,以便所有高速缓存行按同一方式使用。

    80x86微处理器自动处理高速缓存的同步,所以应用于这种处理器的Linux内核并不处理任何硬件高速缓存的刷新。不过内核却为不能同步高速缓存的处理器提供了高速缓存刷新接口。


    处理TLB


    处理器不能自动同步它们自己的TLB高速缓存,因为决定线性地址和物理地址之间映射何时不再有效的是内核,而不是硬件。


    Linux2.6提供了几种在合适时机应当运用的TLB刷新方法,这取决于页表更换的类型。


    尽管普通Linux内核提供了丰富的TLB方法,但通常每个微处理器都提供了更受限制的一组使TLB无效的汇编语言指令。在这个方面,一个更为灵活的硬件平台就是SunUltraSPARC.与之相比,Intel微处理器只提供了两种使TLB无效的技术:

    1. 在向cr3寄存器写入值时所有Pentium处理器自动刷新相对于非全局页的TLB表项。

    2. pentiumPro及以后的处理器中,invlpg汇编语言指令使映射指定线性地址的单个TLB表项无效。

    2-12列出了采用这种硬件技术的 Linux宏;这些宏是实现独立于系统的方法的基本要点。


    注意表2-12中没有flush_tlb_pgtables方法:在80x86系统中,当页表与父页表解除链接时什么也不需要做,所以实现这个方法的函数为空。


    独立于体系结构的使TLB无效的方法非常简单地扩展到了多处理器系统,在一个CPU上运行的函数发送一个处理器间中断给其它的CPU来强制它们执行适当的函数使TLB无效。


    一般来说,任何进程切换都会暗示着更换活动页表集。相对于过期页表,本地TLB表项必须被刷新:这个过程在内核把新页全局目录的地址写入cr3控制寄存器时会自动完成。不过内核在下列情况下将避免TLB被刷新:

    1. 当两个使用相同页表集的普通进程之间执行进程切换时。

    2. 当在一个普通进程一个内核线程执行进程切换时。事实上,内核线程并不拥用自己的页表集;更确切地说,它们使用刚在CPU上执行过的普通进程的页表集。

    除了进程切换以外,还有其它几种情况下内核需要刷新TLB中的一些表项。例如,当内核为某个用户态进程分配页框并将它的物理地址存入页表项,它必须刷新与相应线性地址对应的任何本地TLB表项。在多处理器系统中,如果有多个CPU在使用相同的页表集,那么内核还必须刷新这些CPU上使用相同页表的TLB表项。


    为了避免多处理器系统上无用的TLB刷新,内核使用一种叫做懒惰TLB模式的技术。其基本思想是,如果几个CPU正在使用相同的页表,而且必须对这些CPU上的一个TLB表项刷新,那么,在一些情况下,正在运行内核线程的那些CPU上的刷新就可以廷迟。


    事实上,每个内核线程并不拥有自己的页表集;更确切的说,它使用一个普通进程的页表集。不过,没有必要使用一个用户态线性地址对应的TLB表项无效,因为内核线程不访问内核态地址空间。


    当某个CPU开始运行一个内核线程时,内核把它置为懒惰TLB模式。当发出清除TLB表项的请求时,处理懒惰TLB模式的每个CPU都不刷新相应的表项;但是,CPU记住它的当前进程正运行在一组页表上,而这组页表的TLB表项对用户态地址是无效的。只要处于懒惰TLB模式的CPU用一个不同的页表集切换到一个普通进程,硬件就自动刷新TLB表项,同时内核把CPU设置为非懒惰TLB模式。然而,如果处理懒惰TLB模式的CPU切换到进程与刚才运行的内核拥有相同的页表集,那么,任何使TLB无效的廷迟操作必须由内核有效地实施;这种使TLB无效的“懒惰”操作可以通过刷新CPU的所有非全局TLB项来有效地获取。


    为了实现懒惰TLB模式,需要一些额外的数据结构。cpu_tlbstate变量是一个具有NR_CPUS个结构的静态数组,这个结构有两个字段,一个是指向当前进程内存描述符的active_mm字段,一个是具有两个状态值的state字段:TLBSTATE_OKTLBSTATE_LYZY。此外,每个内存描述符中包含一个cpu_vm_mask字段,该字段存放的是CPU下标;只有当内存描述符属于当前运行的一个进程时这个字段才有意义。


    当一个CPU开始执行内核线程是,内核把该CPUcpu_tlbstate元素的state字段置为TLBSTATE_LAZY;此外,活动内存描述符的cpu_vm_mask字段存放系统中所有CPU的下标。对于与给定页表集相关的所有CPUTLB表项,当另外一个CPU想使这些表项我效时,该CPU就把一个处理器间中断发送给下标处于对应内存描述符的cpu_vm_mask字段中的那些CPU


    CPU接受到一个与TLB刷新相关的处理器中断,并验证它影响了当前进程的页表集时,它就检查它的cpu_tlbstate元素的state字段是否等于TLBSTATE_LAZY。如果等于,内核就拒绝使TLB表项无效,并从内存描述符的cpu_vm_mask字段删除该CPU下标。这有两种结果:


    1. 只要CPU还处于懒惰TLB模式,它将不接受其它与TLB刷新相关的处理器间中断。

    2. 如果CPU切换到另一个进程,而这个进程与刚被替换的内核线程使用相同的页表集。那么内核调用__flush_tlb()使该CPU的所有非全局TLB表项有效。


    展开全文
  • Linux 内存寻址

    2014-03-06 14:00:57
    80x86微处理器下的三种不同的...物理地址:芯片内存单元寻址 MMU 通过分段单元将 逻辑地址转换为线性地址; 分页单元将线性地址转换为物理地址; 分段的过程: 段选择符有三个字段: 1)Index字段,表示

    80x86微处理器下的三种不同的地址:

    逻辑地址:16位段选择符+32位offset, 段选择符存放在段寄存器中
    线性地址:也称为虚拟地址,32bit 体系结构可以表达4GB的地址空间
    物理地址:芯片内存单元寻址

    MMU 通过分段单元将 逻辑地址转换为线性地址;
    分页单元将线性地址转换为物理地址;

    分段的过程:

    段选择符有三个字段:
    1)Index字段,表示在GDT或者LDT中相应的段描述符的索引
    2)TI,两个描述符表的标志,用于区分
    3)RPL,特权级

    首先从TI字段中决定段描述符保存在哪一个描述符表中,从相应的gdtr或ldtr及寄存器中得到相应的描述符表的基地址
    然后将index字段乘以8(一个描述符的大小)加上基地址得到段描述符的地址

    段描述符中有一个字段是Base字段,将逻辑地址中的offset与这个Base字段相加就得到了线性地址。这就完成了从逻辑地址到线性地址的转换。
    在Linux中,段描述符中的Base字段全部是0x00000000,所以逻辑地址中的offset与线性地址在这个意义上是相等的。
    Linux只是象征性地使用了分段机制,目的可能是为了满足可移植性和内存管理的简便。

    分页单元把线性地址转换成物理地址:

    page:一组线性地址,在内存管理中它是最小的单位,它可以指这组地址中的数据块
    page frame:物理页,是RAM的一个区域,表示内存的一部分
    page table: 页表存放在主存中,是一种将线性地址隐射到物理地址的数据结构



    以上是比较常规的分页,Linux采用三级页表或者四级页表,也就是32为的线性地址多划分两个域出来而已
    其中:CR3寄存器存放的是当前活动进程的页目录的物理地址,这是进程上下文的一部分,也就是说当发生进程切换的时候,CR3寄存器的值会被重写,当然,如果两个 进程使用相同的working page set,也就是共享页表结构,那么该寄存器不会被重置(不太确定?有路过的大神求解释下)
    线性地址的高10位表示在页目录中的offset,找到页目录项之后,就有了页表的地址,中间10位有是在页表中的offset,这样找到了页表,得到页框的物理地址,最后12位是数据在页框中的offset,12 bit, 4KB大小的页
    总共就是1024*1024*4096=2^32

    其中页表项中有一个present标志,如果该标志是0,说明这页数据没有在main memory中,产生一个异常page fault
    还有一个dirty标志,用于write-back。

    Linux中的分页模式一般是3级页表,线性地址被划分为5个域:
    Page global directory
    Page Upper dir.
    Page Middle dir.
    Page table 页表
    offset

    最后提一个TLB缓存,目的是为了加速从虚拟地址到物理地址的隐射,每一个entry会缓存一个VA到一个PA直接的隐射关系。 



    展开全文
  • 深入x86的内存寻址

    千次阅读 2014-04-21 12:53:49
    深入x86的内存寻址本文旨在全面解决寻址方面的疑问,解决一些教材对寻址问题解说不够全面的问题。包含以下主要内容: 4个数据寄存器: EAX,Extended Accumulator Register 累加寄存器; EBX,Extended Base Register...

    注意:以下这黑带为上图的文字内容,复制到写字板就可以看到。完整的文档无法附加到文章,就不上传了。

    深入x86的内存寻址
    本文旨在全面解决寻址方面的疑问,解决一些教材对寻址问题解说不够全面的问题。包含以下主要内容:
    
        4个数据寄存器:
        EAX,Extended Accumulator Register 累加寄存器;
        EBX,Extended Base Register 基址寄存器;
        ECX,Extended Counter Register 计数寄存器;
        EDX,Extended Data Register 数据寄存器;
        2个变址寄存器:
        ESI,Extended Source Index Register 源索引寄存器;
        EDI,Extended Destination Index Register 目标索引寄存器;
        2个指针寄存器:
        ESP,Extended Stack Pointer Register 堆栈指针寄存器;
        EBP,Extended Stack-frame Base Pointer 堆栈基址指针寄存器;
        4个段寄存器+段寄存器与内存模型:
        CS,Code Segment Register 代码段寄存器;
        DS,Data Segment Register 数据段寄存器;
        ES,Extra Segment Register 额外数据段寄存器;
        SS,Stack Segment Register 堆栈寄存器;
        1个指令指针寄存器:
        EIP,Execution Instruction Pointer Register
        1个标志寄存器:
        EFlags,Executioin Status Flags Register
        5种寻址方式+内存寻址的组合:
        Immediate Addressing 立即数寻址
        Register Addressing 寄存器寻址
        Direct Addressing 直接寻址
        Register Indirect Addressing 寄存器间接寻址
        I/O Port Addressing I/O 端口寻址
        保护模式+内存模型+特权等级等
    
    
    一、数据寄存器
    数据寄存器主要用来保存操作数和运算结果等信息,从而节省读取操作数所需占用总线和访问 存储器的时间。32位CPU有4个32位的通用寄存器EAX、EBX、ECX和EDX。对低16位数据的存取,不会影响高16位的数据。这些低16位寄存 器分别命名为:AX、BX、CX和DX,它和 8086 的寄存器相一致。
    
    4个16位寄存器又可分割成8个独立的8位寄存器(AX:AH-AL、BX:BH-BL、CX:CH-CL、DX:DH-DL),每个寄存器都有自己的名称,可独立存取。AX和AL通常称为累加器(Accumulator),用累加器进行的操作可能需要更少时间。累加器可用于乘、 除、输入/输出等操作,它们的使用频率很高; 寄存器BX称为基地址寄存器(Base Register),它可作为存储器指针来使用,此时将使用堆栈段来寻址数据; CX称为计数寄存器(Count Register),在循环和字符串操作时,要用它来控制循环次数;在位操作 中,当移多位时,要用CL来指明移位的位数;DX称为数据寄存器(Data Register),在进行乘、除运算时,它可作为默认的操作数参与运算,也 可用于存放I/O的端口地址。在16位CPU中,AX、BX、CX和DX不能作为基址和变址寄存器来存放存储单元的地址,但在32位CPU中,其32位寄 存器EAX、EBX、ECX和EDX不仅可传送数据、暂存数据保存算术逻辑运算结果,而且也可作为指针寄存器,所以,这些32位寄存器更具有通用性。
    
    二、变址寄存器
    2个32位通用寄存器ESI和EDI为变址寄存器,又称为索引寄存器。其低16位对应先前CPU中的SI和DI,对低16位数据的存取,不影响高16位的数据。(E)SI、(E)DI 统称为变址寄存器(Index Register),它们主要用于存放存储单元在段内的偏移量,使用(E)SI时表示相对堆栈段的偏移。用它们可实现多种存储器操作数的寻址方式,为以不同的地址形式访问存储单元提供方便。变址寄存器不可分割成8位寄存器。作为通用寄存器,也可存储算术逻辑运算的操作数和运算结果。它们可作一般的存储器指针使用。在字符串操作指令的执行过程中, 对它们有特定的要求,而且还具有特殊的功能。
    三、指针寄存器
    
    2个 32-bit 通用指针寄存器 EBP 和 ESP 分别用作基地指针和堆栈指针。其低16位对应 8086 中的BP和SP,对低16位数据的存取,不影响高16位的数据。(E)BP、(E)SP 统称为指针寄存器(Pointer Register),主要用于存放堆栈内存储单元的偏移量,用它们可实现多种存储器操作数的寻址方式,为以不同的地址形式访问存储单元提供方便。指针寄存器不可分割成8位寄存器。作为通用寄存器,也可存储算术逻辑运算的操作数和运算结果。它们主要用于访问堆栈内的存储单元,并且规定:
    
    BP为基指针(Base Pointer)寄存器,通过它减去一定的偏移值,来访问栈中的元素;
    
    SP为堆栈指针(Stack Pointer)寄存器,它始终指向栈顶。
    
    虽然说始终指向栈顶,但并不是指它始终保持不变。因堆栈的增长方向是从高地址向低地址的,所以 PUSH 进栈时,数据就存入栈顶,然后 SP 按数据长度自减;POP 出栈时,反向操作,先按数据长度自增,再读出数据。
    四、段寄存器与寻址
    
        Intel 8086/8088 CPU
    
    File:KL Intel D8086.jpg
    
     
    分段内存模型
    
    1978年,Intel 8086 CPU 刚研制出来,这种16位CPU芯片使用40针的DIP封装。地址线只有20条,设计时认为地址线只要20条就够了,这样可以寻址 220=1MB 的内存。对当时,1MB内存就像现在了1TB的概念一样,也就是这个设计导致后来CUP升级过程中,为了兼容而带来软件开发的各种问题,其中就有 A20 Gate。
    
    这时就引入了内存分段的概念了,因为1个16位的寄存器只能访问到216=64KB 的内存,为此就要使用一个额外的偏移值,这样就引入了16位的 CS、DS、ES、SS 段寄存器组合一个偏移值来寻址1MB内存的概念,偏移值也称为偏移地址 Offset Address。使用 CS 来访问代码段,DS 来访问数据段,ES 留给程序来访问额外数据段,SS 来访问堆栈。内存从第一个字节到最后一个字节都有一个唯一的号码连续的地址,称作内存的物理地址 Physical Address、有效地址 Effective Address。
    
    引入分段概念后,CS、DS、ES、SS 就存储各个段的首地址的高16位,也称作段基址,偏移值则作为低16位值相加。偏移值是16-bit数据,最大值也只有65535,因此也可以认为每个段为64KB。用冒号来表示拼接偏移量, SEGMEMT:OFFSET 这样就表示了一个20位的有效地址,也就是1MB的寻址空间。计算方法,段基址左移4位+16位偏移=20位地址,如下:
    
    SEGMENT<<4 + OFFSET
    
     注意,这种方式能够表示的最大内存为:
    
    FFFF:FFFF=FFFF0+FFFF=10FFEF=1MB+64KB-16Bytes
    
    
    8086 时代的内存分区
    
    因此,这种方法不只能寻址1M,还多余出近64KB,这部分被称做高端内存区High Memory Area (HMA),也就有了右边所示的内存分段机制模型 Segmentation Memory Model (SMM)。但8086/8088只有20位地址线,如果访问 100000~10FFEF 这部分内存,则必须有第21根地址线。CPU寻址时,系统并不认为其访问越界而产生异常,而是根据20根地址线来进行寻址,因此系统计算有效地址的时候对1M求模的方式进行的,这种技术被称为折回 Wrap-around。由于开来,对不同的内存区就形成了不同称谓,0~640KB 这部分内存就称为传统内存 Conventional Memory,用一个16-bit的寄存器就可以完成寻址;而640KB~1MB这部分内存就称为上位内存 Upper Memory Area (UMA);而高于HMA的部分就总称为扩展内存 Extended Memory。在较新版本的DOS系统是可以通过软件控制,将HMA当作传统内存使用的,以扩大程序的内存空间。
    
        ntel 80286 CPU
    
    1982年,Intel 80286 CPU被研制出来,共有24条地址线,寻址16MB。这种CPU引入了虚拟内存及内存保护技术,并对内存的分段概念进行了修改,而 8086 的运行方式被称为实地址模式 Real Address Mode。80286 兼容了实模式,同时引入了一种称为保护模式 Protected Mode,全称 Protected Virtual Address Mode。保护模式下,CPU运行情况要复杂得多,它装入段寄存器的不再是段值,而是称为“选择子”Selector 的某个值。我觉得“选择子”叫法挺怪异的,更情愿称之为“选择器”。保护模式下的内存寻址比较复杂,在后面的保护模式进行讲述。保持兼容本是好事,这样8086的程序可以直接在80286上运行。为了兼容 8086 内存分段机制的实模式,80286 可以进入实模式运行。问题是 80286 拥有24根地址线而不是20根,因此 80286 的指针就可以指向 100000 至 10FFEF 处的内存地址,这就是将近 64KB 的高位内存 HMA。也可以认为这是芯片的一个BUG:如果程序访问HMA内存,因为有24条地址线,系统将真实地访问这块内存,而不是像8086一样进行折回访问1MB的区域。
    
    话说当其时,IBM作为全球PC生产厂商的龙头,它的PC市场份额占据了全球50%以上。作为利益的受损方,IBM想到一个解决方法。使用键盘控制器 8042 芯片来处理A20 Gate,利用控制器上剩余的一些输出线来管理第 A20 根地址线,即第21根地址线,这个功能就被称为A20 Gate:开启A20 Gate,A20 根地址线有效,则程序可以访问HMA内存区;关闭A20Gate,则程序访问HMA时,系统遵循8086的折回方式访问1MB寻址空间。IBM PC兼容机默认的 A20 Gate 是被禁止的。在当时似乎只能这样的方法来解决A20 Gate,这就是硬件 bug 的 Hack 行为,毕竟 A20 Gate 和键盘操作是没没有任何关系的。新型PC上引入了 BIOS 芯片,这样 A20 Gate 功能就集成到了 BIOS上面。因此,在保护模式下,尽管系统有24条地址线,但开启 A20 Gate 就意味着损失了一条,就只能访问内存的奇数段(2N+1)MB,寻址空间也只有16MB的一半。在BIOS中,这个功能就是Fast A20中断,INT 15h,AX=2401h。
    
        Intel 80386 CPU
    
    1985年10月,Intel 80386 32-bit CPU正式上市,全功能的386芯片到1986年上市。它更强大了,在硬件上有了一定的虚拟能力,此时兼容 8086 实模式的运行状态就是通过硬件虚拟技术实现的,因此又称为 Virtual 8086 Mode、Virtual real mode, V86-mode 或 VM86 等等。还引进了两个通用段寄存器 FS 和 GS,并没有限定功能,在不同的操作系统上具有功能差异。到了2003年,x86-64 CPU研发出来后,内存分段的概念已经大大削弱了。 
    
        再谈堆栈
    
    堆栈是一种数据结构定义,在CPU内部,它就是以硬件实现的数据结构,并在 SS 指定堆栈的起始地址,ESP 指向栈顶地址。使用 PUSH 和 POP 指令来向堆栈存入、取出数据。PUSH 存入数据时,会修改 ESP,使其向内存的低位地址移动,长度为数据需要的字节数。POP 则从栈顶取出数据,并使用 ESP 向内存的高位地址还原。
    
    堆栈还被用来实现函数的上下文保存与还原。在 CALL 指令执行前,通过 PUSH 指令来将数据或特定的寄存器入栈,这样既可以向被调用的代码传入参数,又可以保护寄存器的数据。CALL 指令执行时,它会保存当前代码执行的地址,即 EIP 的数据会被 PUSH 到堆栈,待调用过程通过 RET 返回时,EIP 就会被还原回来。跳转指令和 CALL、RET 指令一样会转移控制到另外一个代码区,只是跳转指令不会主动去处理 EIP。
    
    五、指令指针寄存器
    指令指针 EIP 用来存放下次将要执行的指令在代码段的偏移量,即 CS:EIP 指向下一条指令。它也常称为程序计数器 Program Counter (PC),JMP、CALL、RET 指令就是通过修改它来实现转移控制的。在具有预取指令功能的系统中,下次要执行的指令通常已被预取到指令队列中,除非发生转移情况。所以,在理解它们的功能时,不考虑存在指令队列的情况。在实方式下,由于每个段的最大范围为64K,所以,EIP中的高16位肯定都为0,此时,相当于只用其低16位的IP来反映程序中指令的执行次序。32位CPU把指令指针扩展到32位,并记作 EIP,EIP 的低16位与先前 8086 CPU 中的 IP 相同。
    六、标志寄存器
    运算结果标志位
    
        1、进位标志CF(Carry Flag)
    
    进位标志CF主要用来反映运算是否产生进位或借位。如果运算结果的最高位产生了一个进位或借位,那么,其值为1,否则其值为0。使用该标志位的情况有:多字(字节)数的加减运算,无符号数的大小比较运算,移位操作,字(字节)之间移位,专门改变CF值的指令 Set Carry Flag (STC)、Clear Carry Flag (CLC)。
    
        2、奇偶标志PF(Parity Flag)
    
    奇偶标志PF用于反映运算结果中”1″的个数的奇偶性。如果”1″的个数为偶数,则PF的值为1,否则其值为0。
    利用PF可进行奇偶校验检查,或产生奇偶校验位。在数据传送过程中,为了提供传送的可靠性,如果采用奇偶校验的方法,就可使用该标志位。
    
        3、辅助进位标志AF(Auxiliary Carry Flag)
    
    在发生下列情况时,辅助进位标志AF的值被置为1,否则其值为0:
    (1)、在字操作时,发生低字节向高字节进位或借位时;
    (2)、在字节操作时,发生低4位向高4位进位或借位时。
    对以上6个运算结果标志位,在一般编程情况下,标志位CF、ZF、SF和OF的使用频率较高,而标志位PF和AF的使用频率较低。
    
        4、零标志ZF(Zero Flag)
    
    零标志ZF用来反映运算结果是否为0。如果运算结果为0,则其值为1,否则其值为0。在判断运算结果是否为0时,可使用此标志位。
    
        5、符号标志SF(Sign Flag)
    
    符号标志SF用来反映运算结果的符号位,它与运算结果的最高位相同。在微机系统中,有符号数采用补码表示法,所以,SF也就反映运算结果的正负号。运算结果为正数时,SF的值为0,否则其值为1。
    
        6、溢出标志OF(Overflow Flag)
    
    溢出标志OF用于反映有符号数加减运算所得结果是否溢出。如果运算结果超过当前运算位数所能表示的范围,则称为溢出,OF的值被置为1,否则,OF的值被清为0。溢出和进位是两个不同含义的概念,前者意味结果是不准确的,不要混淆。
    
    通过判断标志位可以确定运算结果,如指令 CMP ax,bx,逻辑含义是比较ax和bx中的值,不影响上目标操作数 ax,在此基础上就相当 SUB ax, bx:
    
    ZF=1,说明(ax)=(bx),JZ/JE 指令的判断条件。
    ZF=0,说明(ax)≠(bx),JNZ/JNE 指令的判断条件。
    CF=1,说明(ax)<(bx),JC/JB/JNAE 指令的判断条件。
    CF=0,说明(ax)≥(bx),JNC/JNB/JAE 指令的判断条件。
    CF=0 并且 ZF=0,说明(ax)>(bx),JNLE/JG 指令判断条件。
    CF=1 或 ZF=1,说明(ax)≤(bx),JBE/JNA 指令判断条件。
    SF=OF,说明(ax)≥(bx),JNL/JGE 指令判断条件。
    
    TEST 指令则常用来检测内容而不是运算内容的,如 TEST eax, eax,它不会存储结果,因此 eax 内容不变,在此基础上 TEST 就相当 AND 运算,经常用于测试寄存器 ZF 是否为0,结合  JZ 指令来实现跳转。
    
    以上指令的缩写 J 表示 Jump 跳转指令,E 表示 Equal 相等;B 表示 Below,L 表示 Less,都是小于的意义;A 表示 Above,G 表示 Greater,都是大于的意义;N 表示 Not 否定。这样就很容易还原指令的全称,如 JNLE 表示 Jump if Not Less Or Equal 即不小于或等于时跳转。C、Z 则表示对应的 ZF、CF 标志位。
    
    状态控制标志位
    状态控制标志位是用来控制CPU操作的,它们要通过专门的指令才能使之发生改变。
    
        1、追踪标志TF(Trap Flag)
    
    当追踪标志TF被置为1时,CPU进入单步执行方式,即每执行一条指令,产生一个单步中断请求。这种方式主要用于程序的调试。指令系统中没有专门的指令来改变标志位TF的值,但程序员可用其它办法来改变其值。
    
        2、中断允许标志IF(Interrupt-enable Flag)
    
    中断允许标志IF是用来决定CPU是否响应CPU外部的可屏蔽中断发出的中断请求。但不管该标志为何值,CPU都必须响应CPU外部的不可屏蔽中断所发出的中断请求,以及CPU内部产生的中断请求。具体规定如下:
    (1)、当IF=1时,CPU可以响应CPU外部的可屏蔽中断发出的中断请求;
    (2)、当IF=0时,CPU不响应CPU外部的可屏蔽中断发出的中断请求。
    CPU的指令系统中也有专门的 CLI、STI 指令来改变标志位IF的值。
    
        3、方向标志DF(Direction Flag)
    
    方向标志DF用来决定串操作指令执行时有关指针寄存器指向地址的自增或自减,对应 DF=0、DF=1。指令系统提供了专门的指令来置DF的值为0、1,对应指令为 Clear Direction Flag (CLD) 和 Set Direction Flag (STD)。
    32位标志寄存器增加的标志位
    
        1、I/O特权标志IOPL(I/O Privilege Level)
    
    I/O特权标志用两位二进制位来表示,也称为I/O特权级字段。该字段指定了要求执行I/O指令的特权级。如果当前的特权级别在数值上小于等于IOPL的值,那么,该I/O指令可执行,否则将发生一个保护异常。
    
        2、嵌套任务标志NT(Nested Task)
    
    嵌套任务标志NT用来控制中断返回指令IRET的执行。具体规定如下:
    (1)、当NT=0,用堆栈中保存的值恢复EFLAGS、CS和EIP,执行常规的中断返回操作;
    (2)、当NT=1,通过任务转换实现中断返回。
    
        3、重启动标志RF(Restart Flag)
    
    重启动标志RF用来控制是否接受调试故障。规定:RF=0时,表示”接受”调试故障,否则拒绝之。在成功执行完一条指令后,处理机把RF置为0,当接受到一个非调试故障时,处理机就把它置为1。
    
        4、虚拟8086方式标志VM(Virtual 8086 Mode)
    
    如果该标志的值为1,则表示处理机处于虚拟的8086方式下的工作状态,否则,处理机处于一般保护方式下的工作状态。
    
     七、寻址
    
    寻址是指CPU寻找数据的过程,怎样定位到数据,并读取到数据的过程。要完整理解寻址,还需要从CPU的定义的内存模型着手,典型的内存模型就是 8086 的内存分段模型,它一直沿用到现代的CPU上,如引入分页内存模型的 80386 依然将分段内存作为基础。只在理解不同的内存模型,才能全面的理解和掌握指令执行时的寻址问题。这也就是为什么将保护模式和相关的一些CPU特性组织到本文来讲述,因为掌握技术的最好方法,就是从这种技术的源头出发。只有站在一个广阔视角看问题,才理解得更透彻。关于寻址,先来了解处理器是如何计算有效地址的,下面这个有效地址算式图基本说明了除立即数寻址和寄存器寻址以外的寻址方式:
    
        立即数寻址
    
    这是最简单了一种寻址方式,或者说这是一种不用寻找的寻址方式,因为数据就是指令当中,如:
    
    MOV ax, 0
    
        寄存器寻址
    
    数据存放在寄存器时,CPU只需要访问寄存器就可取得数据,这就是寄存器寻址方式:
    
    MOV ax, bx
    
        直接寻址
    
    数据存放在内存某处时,CPU就需要访问指定的内存地址来取得数据,这就是直接寻址方式,如下示例 [0] 就指定了数据所在内存地址:
    
    MOV ax, [0]
    
        寄存器间接寻址
    
    数据存放在内存某处,而其地址存入寄存器时,CPU只需要访问寄存器取得地址,然后再取得数据,这就是寄存器间接寻址方式,它比寄存器寻址多了取数据地址这个过程:
    
    MOV ax, [bx]
    
        内存寻址的组合
    
    通过组合和引入额外的数据,可以重新组合内存的寻址形成特别用途的数据寻址方式。通过使用位移量加基址或变址寄存器(BP、BX、DI或SI)的内容寻址存储器段中的数据,这样的寻址方式称为寄存器相对寻址。示例如下:
    
    MOV AX, [DI+2H]
    MOV ARRAY[SI], BL
    MOV LIST[SI+2H], CL
    MOV DI, [EAX+2H]
    
    又如比例变址寻址,这种方式使用一个总数因子用来访问内存的对齐地址,这种方式就像使用高级语言的数组下标来获取数据一样。示例如下,假设 ARRAY 是定义好的变量,其中的数字4就是比例因子:
    
    MOV EAX, [EBX+4*ECX]
    MOV [EAX+4*EDI+2], CX
    MOV AL, [EBP+4*EDI+2]
    MOV EAX, ARRAY[4*ECX]
    
    如果使用用一个基址寄存器(BP或BX)和一个变址寄存器(DI或SI)间接寻址存储器,此时数据的起始地址通常保存在基址寄存器,而变址寄存器保存数组元素的相对位置。80386+ 这种寻址方式允许除了ESP以外的任意两个32位扩展寄存器组合使用。注意,如果使用了 EBP 寄存器来寻址,则数据在堆栈段中而不在数据段中。这种差异由 CUP 使用段寄存器的隐含约定引起,指令的操作数默认使用数据段来寻址,但是显式使用 (E)BP 后将引起对其隐含约定的 SS 指定的堆栈段来寻址,后面在保护模式中有讲及。这种寻址方式就称作基址加变址寻址,示例如下,注意第2、4条指令使用的数据、目标来自堆栈段。注意按 Intel 80386 编程手册要求这种寻址方式以 [BX] [DI] 形式出现,如前面展示的有效地址算式图,而不是 [BX+DI] 形式:
    
    MOV CX, [BX+DI] // Intel ASM: MOV CX,[BX][DI]
    MOV CH, [BP+SI]
    MOV [BX+SI], SP
    MOV [BP+DI], AH
    MOV CL, [EDX+EDI]
    MOV [EAX+EBX], ECX
    MOV [RSI+RBX], RAX // X64 instructions
    
    在基址加变址寻址方式的基础上添加一个偏移量就可以形成另一种常用来寻址存储器的二维数组的方式,称作相对基址加变址寻址。此时,基址寄存器就相当数组起点地址,变址寄存器就相当数据的第一维度,偏移量则为数组的第二维度。示例如下:
    
    MOV DH, [BX+DI+20H]
    MOV AX, FILE[BX+DI]
    MOV LIST[BP+DI], CL
    MOV LIST[BP+SI+4],DH
    MOV EAX, FILE[EBX+ECX+2]
    
    前面提到,使用 (E)BP 会隐含地使用 SS 作为寻址的段,这样的也就可以名副其实地称为堆栈存储器寻址,按这样的分类思想,PUSH、POP 这样的指令也可以归纳为堆栈存储器寻址的行列,因此它们隐含地使用堆栈段来进行数据寻址。而对执行代码的指令如 CALL、JMP、RET,因其使用的是代码段寻址,又可以称之为程序存储器寻址,它又分直接寻址、相对寻址和间接寻址形式。这类指令和其它操作数,即将要被执行的指令地址存储在一起时,就是直接程序存储器寻址。还有相对程序存储器寻址,这里相对(relative)意味着相对于指令指针(IP)。例如,如果JMP指令跳过后面两个存储器字节,则相对于指令指针的地址是2,将2与指令指针相加,就得到程序下一条指令的地址,这个2就是相对 IP 的偏移量。注意,JMP指令的格式是1字节操作码加1个或2个字节的位移量,位移量将与指令指针相加。1个字节位移量用于短(short)转移;2个字节位移量用于近(near)转移和调用。这两种类型都为段内转移(intrasegment jump),段内转移是转移到当前代码段内的任何位置。在80386及更高型号的微处理器中,位移量还可以是32位数,允许用相对寻址转移到4GB代码段内的任何位置。最后,程序的跳转地址还可以通过其它寄存器来给出,这样就有了间接程序存储器寻址。对于CALL和JMP指令,可以用任何16位寄存器(AX、BX、CX、DX、SP、BP、DI或SI),任何相对寄存器([BP]、[BX]、[DI]或[SI]),或任何带有位移量的相对寄存器。80386和更高型号的微处理器中,可以用扩展寄存器存放相对JMP 或CALL的地址或间接地址。
    
    位寻址是一种常用来访问比特位的寻址,就读写比特位的含意。有单片机的基础,就很容易理解,比特位也可以像寄存器一样被访问,只是它只有一个bit位。例如 8051 通过 P1.0 就可以访问一个输出端的第一个比特位,这时将 P1.0 理解为一个寄存器是正确的思维!而 x86 CPU 中最常见的位寻址就是 JECXZ、JA、JB、JG、JL、JE、JZ、JS、JC、JO、JP 等等系列跳转指令,它位通过访问标志寄存器的比特位来进行有条件跳转。关于位寻址,可以参考芯片级 Datasheet 手册,通过阅读芯片手册,你会形成一个从顶往下的观点。即操作系统就是对芯片的编程,应用程序是对操作系统的编程,而脚本则是对应用程序的编程!因此,掌握一种知识,从它的源头着手一直是我最为认可的方法。
    
    总结一下,这里介绍了这么多的寻址方式,总的来说,基本的还只有那么几个。衍生出和各种寻址方式,只因和特定的寄存器、立即数有关系,才形成了各式各样的特例。
    
        I/O寻址
    
    在计算机内部,通过数据总线等等硬件连接,可以给整个系统添加各种各样的外设。主机与外设的通信或数据的互通就是I/O寻址,80386 允许两种方式:
    
        独立于内存的 I/O 寻址空间
        映射到内存 I/O 寻址
    
    通过使用不同的指令形式就可以确立是何种 I/O 寻址方式。独立的 I/O 寻址空间共有216=64KB,如果按 8-bit 、16-bit、 32-bit 划分成端口则对应为 64K、32K、 16K 个端口数量,端口的起始地址就作为端口的编号使用。端口 Port 是 I/O 寻址空间的组织单元,要求地址是连续的。以端口组织 I/O 寻址的原因,是因为 I/O 指令使用 AL、AX 或 EAX 来进行 I/O 操作,因为这几个通用寄存器就是 8-bit 、16-bit、 32-bit 的。端口的 bit 位数决定了端口可以接收和获取的数据 bit 数量。使用 IN、OUT 指令来获取、输出数据,如果是一连串的数据则使用 INS、OUTS 指令来自执行。像后者这些传输多个数据的指令又称为 串 I/O 指令 String I/O Instruction 或者 块 I/O 指令 Block I/O Instruction。给指令指定端口时,可以通过立即数给出,也可以使用 DX 作为间接寄存器的 I/O 寻址。对于块 I/O 指令只能通过 DX 寄存器来指定端口,至于端口的 bit 长度则是通过操作数来确认的。这些块操作指令包括 OUTS、OUTSB、OUTSW、OUTSD、INS、INSB、INSW、INSD,后缀 B 表示 Byte、W 表示 Word、D 表示 Double Word。因为这些指令只能接受 8-bit 的立即数,所以通过立即数来指定端口时,只能访问 8-bit 端口的前 28=256个,或 16-bit 端口的前 28/2=128个,又或者 32-bit 端口的前 28/4=64个。为了使用 I/O 指令能在单个总线周期完成数据的传输需要按端口的 bit 长度来对齐数据在内存的地址,如 32-bit 的端口传输的数据要求对齐 4Byte 的边界,即内存地址的最低两位保持为0。
    
    块操作指令的原理是 CPU 自动按标志寄存器的 Direction Flag (DF) 位的指示自动增减指定数据地址的 ESI 或 EDI 寄存器值,DF=0 自动按操作端口的字节数增值,DF=1 则自动端口的字节数减值。因此可以通过给 INS 或 OUTS 前缀 REP 指令来重复执行指令以处理块数据,或都使用 LOOP 指令来循环执行代码块来处理块数据,这两条指令都通过 (E)CX 来指定重复次数。对于 LOOP 指令则还要指定一个偏移值来执行程序跳转。另外,对于 INS 指令,它只能使用 ES 段作为目标地址,输出类型的块 I/O 指令和大多数指令一样默认使用 DS 段作为数据源。
    
    如果设备拥有自己的内部寄存器,那么系统就可以将其映射到系统内存上。这样就可以使用映射内存的 I/O 寻址,它的便利之处就是通过映射的内存,所有指令对映射区的内存操作都直接反映到外设的端口上,因此使用起来更灵活。外设的端口映射是在计算机启动时完成的,BIOS 在自检时会对所有安装的设置进行配置,端口分配就是其中一项主要的工作。操作系统启动后,如 Windows 可以在设备管理器中打开设备属性页的资源选项卡来检视设备的端口号。例如本系统的蜂呜器 System Speaker 的端口为 0061,System Time 的端口号为 0040 - 0043。这里有个有趣的现象,通常 BIOS 的编程者是来自生成 PC 的厂商,所以作为内部人员,就知道如何访问外设,因此会直接给集成的外设固定的端口。久而久之,形成了一种习惯,一系列常见的设备就按约定给它分配端口了。但是实际使用中,这种固定分配端口的做是很不方便的,当系统更改硬件时就会出现外设的配置问题,因此旧有的设备就有很多跳线开关 Jumper 用来更改配置。后来,Intel 联合几家公司推出了即插即用 Plug-and-Play (PnP) 的概念,PnP 的作用是自动配置低层计算机中的板卡和其他设备,然后告诉对应设备都做了什么。PnP的任务是配对物理设备和软件设备驱动程序,建立通信信道。简而言之,就是给设备配置 I/O地址、IRQ、DMA通道和映射内存地址。PnP 就是一种硬件规范,符合这一规范的设备就可以实现自动配置。。
    
    最后,来讲一下 80386 的保护模式对 I/O 寻址的影响部分。在标志寄存器中有部分为 I/O privilege level (IOPL),这是特权指令约束标记,只有满足条件的指令才许可执行:
    
    CPL ≤ IOPL
    
    CPL 可以从段描述符中得到,它指示了当前进程所拥有的特权。只有当前进程特权不比约束特权小时,以下 I/O 指令才会被执行:
    
    IN ── Input
    INS ── Input String
    OUT ── Output
    OUTS ── Output String
    CLI ── Clear Interrupt-Enable Flag
    STI ── Set Interrupt-Enable
    
    每个进程都会带有自身的一套标志寄存器副本,所以不是所有进程都可以使用这些特权指令的。越权执行指令,系统将发出通用的保护异常。一般来讲,设备驱动是在 Ring 0 运行的,拥有这些特权指令的使用权。
    
    IBM  PC/XT系统中8253的计数器是一种 Programmable Interval Timer (PIT) 芯片,改进型号为8254。它使用5v电源,有3个16位的独立计数器,有6种工作方式,已经作为PC标准的外设集成在系统中,即称为系统定时器 System Timer。它支持二进制或十进制 BCD 计数,计数频率为2MHz,改进型可高达10MHz,所有引脚电平和晶体管-晶体管集成电路 Transistor-Transistor Logic (TTL) 兼容。
    
    以下是扬声器接口电路的驱动发声程序,42H为扬声器端口地址,43H为控制寄存器地址,阅读程序,回答以下问题
     
    DEEP   PROC
     
                MOV  AL,0B6H  ①                  OUT   61H,AL    ⑩
     
                OUT   43H,AL    ②                  SUB   CX,CX
     
                MOV   AX,0533H③                  GO:LOOP   GO
     
                OUT   42H,AL    ④                      DEC    BL
     
                MOV   AL,AH     ⑤                      JNE   GO
     
                OUT   42H,AL   ⑥                       MOV   AL,AH  ⑾
     
                IN   AL,61H       ⑦                       OUT   61H,AL⑿
     
                MOV   AH,AL    ⑧                       RET
     
                OR    AL,03H    ⑨                      BEEF    ENDP
     
    1,指令①--②的作用是什么
     
    2,指令③--⑥的作用是什么
     
    3,指令⑦-- ⑩的作用是什么
     
    4,指令 ⑾--⑿的作用是什么
     
    参考答案:一,选择题:CDDCCBADBDDC      二,简答:DCDAA
     
                    三,综合题:1,指令①--②的作用是要8253/54工作在方式3,计数器2输出方波 ,控制字
     
                                                为0B6H,写到43H端口
                                          2,指令③--⑥的作用是将发声频率送入端口42h,计数器2
                                          3,指令⑦-- ⑩的作用是读61h端口,然后将第0、1位置1,写回61h,使PB0、PB1
     
                                                为高,使能扬声器
                                          4,指令 ⑾--⑿的作用是恢复61h初值,停止发声
    八、保护模式与寻址
    
        80286保护模式
    
    正如前面提到,保护模式全称就是保护虚拟寻址模式。CPU一通电工作就处于实模式下,通过设置几个描述符表,就可以通过控制寄存器的PE Protection Enable 位 RC0 来激活保护模式。80286 的保护模式主要实现了内存分段的保护机制,防止进程间互访问对方的内存带来冲突,这种机制在 80286+ 的CPU都有效。它给CPU带来了24-bit的内存寻址,可以访问16MB内存空间。但这个空间不是通过移位产生的,而是通过增强的分段内存管理机制来实现的。此时,16位的段寄存器不再直接存放基址,而是存放一个段描述符表的索引值 ,段描述符又是一种在CPU内部实现的数据结构,它包含了一个24-bit的段基址。这样,另外再加一个16-bit的偏移值就实现了16MB的内存寻址。2086 的保护模式也是最不受欢迎的保护模式,并没有得到广泛应用。有几个原因,首先它不支持不重置CPU的情况下从保护模式切换回到实模式,这使用 BIOS 和 DOS 调用无法正常使用;另外,让人不可接受的是它通过段寄存器访问的段只允许16-bit长度的段,这问意味着每个段最大才为64KB,同时只能访问到 4*216=256KB内存。这是因为在保护模式下修改段寄存器会引起CPU从内存某处加载一个6-Byte的段描述符数据,这会消耗几十个时钟周期,这使用得 80286 比 8086 运行还要慢!因此,运算一个超过128KB、处于相邻段的数据结构这样的策略也变得不切实际。因此,我们需要的是 80386+ 的保护模式!
    
        80386+保护模式
    
    80386+,即80386或以上的CPU,这种CPU的保护模式带来了32-bit寻址空间、32-bit段地址偏移、RM-PM两种模式自由切换、强劲的内存分页机制、虚拟内存、虚拟8086模式及多任务特性,因此又称为 IA32 处理器架构,成为 x86 处理器的一支强大的分支,与 x86-64 即 AMD64 相呼应。AMD64 与 x86 属同系,可以兼容 x86 架构,而 Intel 的 IA 64 架构和 AMD64 架构不是兼容架构。80386 还引入了两个通用的数据段寄存器 FS 和 GS,有人把它们称作 File Segment Register 和 Graphics Segment Register, 只是这两个寄存器并没有指定用途,在不同的系统可以用于不同的目的。
    
    段描述符概念与符表
    
    通过在CPU的地址译码电路添加一层分页单元,使得可访问的内存空间可以超过4GB。尽管如此,80386+ 继续保持了 80286的内存分段机制。地址偏移也使用32-bit而不是16-bit值,段描述符中的段基址也使用 32-bit 而不是 80286 实模式使用的 24-bit 段基址,而段寄存器则完全可以不理它。这里正式引入了段描述符 Segment Descriptor 的概念,它就是用来解析内存段的一个数据结构。试想一下,在 8086 的内存分段机制,每个段基址只含有内存物理地址的高16-bit,通过16-bit的偏移形成一个物理地址,这个约束使得每个段最大也只能是64KB。内存分段模型提供了一个基础,这里的一系列数据其实就是一个隐含的段描述符!
    
    保护模式引入的一个重要特性就是内存的保护机制,又为段模式提供了保护机制。这要求段描述符规定对自身的访问权限。因而段描述符的数据结构将不可避免地包含 Base Address, Limit, Access 三个方面的内容,它们组成了一个 64-bit 的数据结构。进而通过段描述符来寻址一个段,就要求使用 64-bit 的段寄存器来装入这个段描述符。但Intel为了保持向后兼容,将段寄存器仍然规定为16-bit。尽管事实上,每个段寄存器有隐藏不可见的部分,有足够长的 64-bit,但对于程序员来说,段寄存器就是16-bit的,隐藏部分只 CPU 才能从内部访问。
    
    如何使用 64-bit 的段描述符就存在问题了。从寄存器的宽度来讲,32-bit 的段寄存器放不下 64-bit 的段描述符数据结构。解决方法就是使用数组,用数据来管理段描述符,这个全局的数组就是全局描述符表 Global Descriptor Table (GDT),每个元素就是 64-bit 的段描述符。前面提到的,在段寄存器中的索引值就是选择器 Segment Selector 就是用来找到 GDT 中对应的段描述符的。选择器 Selector 是一个 16-bit 的数据,它高 13-bit 是一个索引值,用于定位 GDT 或 LDT 上的段描述符,也有教材怪怪地称之为“选择子”。因为段选择器是指向 GDT 中的某个段描述符,因此得名,如果一个选择器指向的是 GDT 中不同类型的描述符如 LDT 的描述符,那么此选择器就称为 LDT 选择器了。和 GDT 一样,局部描述符表 Local Descriptor Table (LDT) 就是用来管理局部段描述符的数组。不同的是LDT 属于进程的,只有当前引用它的进程可以使用。同时,作为进程级的段描述符表,它自然而然就可以在系统中存在多个不同的副本,在每个进程最多只能有一个 LDT。它是可选的,使用它需要在程序复杂度及便利性之间平衡。
    
    正如前面所述,GDT 不仅存放段描述符而已,还有用于多任务的 Task State Segment (TSS) 的描述符、用于进程的 LDT 的描述符,即描述局部符表的描述符,有点绕,其实它就是用来定位系统存在的 LDT 的。也就是说,段描述符之所以称作段描述符,是因为它对内存的段起到描述的作用。还有 Call Gate 的描述符。最后一个,Call Gates 表示调用门,在 x86 的特权级间转移控制的重要调用,尽管现代操作系统很少使用。这些描述符都是 64-bit 的数据,同时 GDT 的第一个元素是置 0 的。
    
    为了使用 GDT,Intel设计了一个寄存器 GDTR 用来存放 GDT 的入口地址,程序确定 GDT 数据后,就可以使用 LGDT 指令来装入其所在内存的位置。这样,CPU就根据此寄存器中的地址来访问 GDT。尽管可以通过 GDT 来获取 LDT 的地址,IA-32 还是为 LDT 提供了一个入口地址寄存器 LDTR。因为在任何时刻只能有一个任务在运行,所以LDT寄存器全局也只需要有一个。通过 LLDT 将其 LDT 的地址装入此寄存器。不同的是,LGDT 指令的操作数是一个 32-bit 的内存地址,这个内存地址就是GDT的入口地址,而LLDT指令的操作数是一个的选择器,利用这个选择子就就可以在 GDT 找到 LDT 的描述符。这里涉及的两个指令都是 Ring 0 权限等级的,后面作解析。
    
    其实段描述符就是一个 64-bit 的数据结构,结构如下:
    
        32-bit的段基址 Segment Base
        20-bit的段极值 Segment Limit
        访问权限字节 Access Rights Byte
        控制位 Control Bits
    
    B 	Bits 	80286 	80386 	B
    0 	00..07, 0..7 	limit 	bits 0..15 of limit 	0
    1 	08..15, 0..7 	1
    2 	16..23, 0..7 	base address 	bits 0..23 of base address 	2
    3 	24..31, 0..7 	3
    4 	32..39, 0..7 	4
    5 	40..47, 0..7 	attribute flags #1 	5
    6 	48..51, 0..3 	unused 	bits 16..19 of limit 	6
    52..55, 4..7 	attribute flags #2
    7 	56..63, 0..7 	bits 24..31 of base address 	7
    	
    Attribute flags #2 52 	4 	unused, available for operating system
    53 	5 	reserved, should be zero
    54 	6 	default flag / D-bit
    55 	7 	granularity flag / G-bit
    
    段极值与粒度控制位 G-bit 配合,当 G-bit 为 0,表示段极值可以为 1-Byte 至 220-Byte 任意字节长度的值,即粒度为1-Byte,即此时每段最大可以为220=1MB;当 G-bit 为 1 时,粒度为 212-Byte,即段极植最大可以为220*212=2GB!当每页机制关闭时,base address + limit * G 得到的线性地址就是物理地址,否就将得到的线性地址作为分页机制的输入参数。此时将起用虚拟内存地址,段寄存器基址、偏移地址、还有CPU内部的分段单元推断得到的32-bit线性地址都是虚拟地址 Virtual Address,或称作逻辑地址 Logical Address!通过 CR0 寄存器的 PG 位来关闭分页机制,80386 就像 80286 一样使用内存分段模型,不通过分页,直接通过段描述符提供的基址和偏移值组成的物理地址访问内存。
    
    分页机制与段选择器
    
    从逻辑地址到物理地址,中间需要做的工作是复杂的,其中重要的一步就是从逻辑地址到线性地址 Linear Address 的转换。这个过程也称之为段转换 Segment Translation,从线性地址得到物理地址的过程也称之为页转换 Page Translation,后者是可以选的,可以通过 CR0 的 PG 位关闭,由操作系统来实现。线性地址就是需要通过分页表格来间接表达物理地址的一种虚拟地址表达,它的低 12-bit 作为偏移值,接下 10-bit 作为页表格项的索引值,剩下高端的 10-bit 作为页目录表项的索引值,分页表格这一概念后面细讲。如果关闭分页,由逻辑地址转换得到的线性地址其实就是物理地址,通过它就可以直接访问内存了。
    
    分段单元 Segmentation Unit (SU),是CPU内部专门用来处理分段内存模型的线性地址的推导及运算的部件。一个逻辑地址可以表达为 SELECTOR:OFFSET,即虚拟地址由一个16-bit的段选择器,也就是一个段的“选择子”和一个 32-bit (80286 使用 16-bit) 的偏移地址组成,这里的段选择器提供了 13+1 个地址位。段选择器也必需位于段寄存器中,最低两位含有 2-bit 的请求者特权等级 Requestor's Privilege Level (RPL)用来做访问约束,还有 1-bit 的符表指示器 Table Indicator (TI),和高端的 13-bit 索引值用于定位 GDT 或 LDT 上的段描述符,亦即后二者将提供 13+1 个作为寻址用的地址位。这里的索引值就是前面提到,存放于段寄存器中的索引值,因此,在80386 的保护模式中,段寄存器也就可以等价段选择器来使用。这一点就是段寄存器在分段内存模型与分页内存模型使用中最大的区别,在分段内存模型中,段寄存器就是段基址,简单至极。更多内容可以参考 80386 的程序员参考手册,虽然此书是 Intel 1986 年出版的,但一点也不落后,右侧逻辑地址与线性地址关系图就可以从此书的第5章内存管理看到。
    
    
    GDT/LDT与段选择器的关系
    
    由前一节段描述符可以了解到 80386 的实模式下,内存的分段不再是由段寄存器简单地指出段所在的基址。而是通过段描述符来详细记录段的信息,配合分页机制来实现虚拟内存与物理内存地址的映射。取得段描述符就意味着取得了段所在地址,即虚拟地址。而获取段描述符的关键就是段选择器,它指向 GDT 或 LDT 中的段描述符记录了段的详细信息。例如图 Segment 2,它是通过选择器 0x0027 选中的段,这个选择器可以存放在任意的段寄存器中。0x0027 的高13-bit可以通过右称 3-bit 运算得到,就是 0x04,即 LDT 中的第5个描述符,图中对应绿色的部分。这里提个问题,为什么 0x0027 这个选择器会得到 LDT 上的段描述符呢?答案就是后面。
    
    在逻辑地址输入到 SU 进行转换时,处理器就根据 TI 即选择器的第3位 bit2 来确认从 GDT 或 LDT 入一个64-bit 的段描述符,对应 TI=0、TI=1。这里同时引入了两个概念,GDT 和 LDT,一个称之全局描述符表 Global Descriptor Table (GDT),另一个称之局部描述符表 Local Descriptor Table (LDT)。这里简单理解它们为用来存储段描述符的数组就可以了,后面补充说明。取得段描述符后,处理器进行特权检查:
    
        max(CPL, RPL) ≤ DPL
    
    式中 CPL 表示当前特权等级 Current Privilege Level,即 CS 寄存器的最低 2-bit。RPL 在前面提到了,DPL 就是段描述符特权等级 Descriptor Privilege Level,位于段描述符中。这三个特权等级值取 0~3,值越小表示权越高。当不等式不成立,即 CPL、DPL、RPL中,DPL的特权最高,因此进程特权不足以调用段描述符,则处理器产生一个常规保护错误 General Protection (GP),终止系统。否则,SU 继续执行,取出 32-bit 或 16-bit 的偏移值与段极值比较,如果大于段极值,同样产生 GP 宕机。SU 还会用一个 46-bit 的程序逻辑地址对运算得到的逻辑地址进行验证。这个程序逻辑地址由16-bit 段寄存器中的 14-bit,除余下两位用于标记特权等级,外加 32-bit 的偏移值组成。通过后,处理器的分页单元将段描述符内 32-bit (80286 是 24-bit) 的段基址与偏移值相加得到一个物理地址,80386 处理器通过分页机制得到的物理地址是 32-bit 的,而在新的支持物理内存扩展 Physical Address Extension 的处理器上会更大。
    
    这里可以发现,段寄存器存储的索引不仅用来在 GDT 或 LDT 查找段描述符,还用在分页机制中生成物理地址。特权级检查需要完全加载段寄存器,因为段描述符会在段寄存器的隐藏位缓存,这些隐藏位对于开发者是透明的。
    
    
     
    逻辑地址、线性地址与物理地址的转换
    分页机制的使用有效地避免了内存在长时间使用过程中变得散乱的问题,因为通过分页,程序直接使用的是虚拟地块,通过分页机制的映射,可以有效地利用散乱的物理内存,而虚拟地址总是常用常新,永不散乱。它也是虚拟内存实现的重要环节,通过映射可以将外部存储器如硬盘当作内存来使用,这样即在物理内存很小的系统上,程序也能正常使用完整的寻址空间。x86 内存页通过页目录 Page Directories 和页表格 Page Table 这两个数组来管理。初始时,页目录表为一个页的大小即 4KB,含有 1024 个页目录项 Page Directory Entity (PDE),每项 32-bit。后续增强的处理器可以使用更大的页,而不仅是 4KB。每个 PDE 指向一个页表格,每个页表格初始时也是 4KB,包含了 1024 个页表格项 Page Table Entity (PTE),而每个 PTE 则指向一个物理页的起始地址。而且只有页表格的项都填满时才会被系统使用。其实每一个 PTE 和 PDE 的数据结构是一致的,因此称为页容器 Page Frame (PF)更为恰当。就是一条虚拟地址到物理地址的映射,虚拟内存映射关系就是通过页表格完成的页表格就是映射地址的集合。在寻址时,PF 只有高 20-bit 作为地址数据,低 12-bit 则作为状态控制位使用的。其中有一个状态位 Present (P),当 P=1 时表示 PF 指向的地址有效。另外在每一时刻,只能有一个页目录处于激活状态,由 32-bit 的 CR3 寄存器指明地址,因此 CR3 就是这些表的总入口。两个表合起来就可以寻址 220=1M 个页,而每页为 212=4KB,合共至少可以寻址 220 * 212=4GB。
    
    操作系统实现分页机制时的一个主要功能就是处理 CPU 在进行地址转换时产生的异常,当程序访问未准备好的页即 P=0 的页,CPU 就产生的一个 page-not-present 异常。此时操作系统实现的分页机制就起到以下作用:
    
        确认数据所在二级存储器的地址;
        在物理内存中取得一个空页作为页数据容器 Page Frame;
        装入受求的数据到空页;
        更新页表格以呈现新的数据;
        返回控制到引发异常的程序,隐式地执行引发异常的那条代码。
    
    
    Page Table 与内存映射
    
    查找页表格时,通常会有两种失败的可能。一是使用了无效的虚拟地址,通常是软件原因引起的,系统必需进行处理。现代的操作系统会发出一个称为分段异常 Segmentation Fault 的信息。另一种情况就是查找的页并未装入物理内存,通常是因为物理不足时引起页的卸载以腾出空间给正在请求内存的程序。这种情况下,未装入的页数据通常在二级存储器的交换文件、页文件或交换分区上,因此只需要将数据装回到物理内存中即可。在物理内存不足的情况下,卸载页以腾出空间是很常有的事。在装入页数据后,就需要及时更新 TLB 的缓存,以保证映射正确无误。
    
    这个过程可以用以下的流程图说明,CPU 内部的内存管理单元 Memory Management Unit (MMU)暂存着就近使用的页表格映射, 当接收到需要转换的虚拟地址时,转换备用缓冲 Translation Lookaside Buffer (TLB) 就从 MMU 暂存的页表格映射中查找,如果找到对应的物理地址就直接返回给程序,表示命中缓存 Cache Hit。如果没有找到,则表示缓存错失 Cache Miss,这时就到系统的页表格中查找,如果找到物理地址的映射,则回写到 TLB。因为硬件访问内存时是要通过 TLB 的,所以这个操作是必需的。如果在页表格也没找到对应的物理地址映射,那么就引发异常。
    
    Intel 内存模型与编程
    
    正如前面所讲,寻址涉及的知识点是非常广泛的,它不仅和指令的用法,还和 CPU 内部对内存的处理方式紧密相关。由此,在实际的编程工作中就产生了一系列的在内存编程模型,及指针约定:
    
    模型 	数据指针 	代码指针 	约定
    Tiny 	near 	CS=DS=SS=ES
    Small 	near 	near 	DS=SS
    Medium 	near 	far 	DS=SS,多个代码段
    Compact 	far 	near 	单一代码段,多个数据段
    Large 	far 	far 	多个代码段及数据段
    Huge 	huge 	huge 	多个代码段及数据段,单个数组可以超过 64 KB
    
    编译器将会根据程序设定的内存模型进行处理源代码的编译,由上表可知,near 指针是较快的,因为它不用修改段寄存器或很少修改,节省了 CPU 执行指令的时钟周期。而 far 指针则会包含 DS 或 CS 值,访问内存时会修改段寄存器,完成后再恢复,因此会比 near 指针要慢。但 far 在性能上的牺牲带来了寻址容量上补偿,它可以寻址超过 1MiB 的空间。MiB 是 IEC 推荐的通用单位,因为 MB 在硬件厂商方面可以解析为 106,在用户方面又可以解析为220。在本文中 MB=MiB,不再进行更改。而 huge 指针则是最慢的,但它可以跨跃多个段,因此可以执行精确的指针比较。如果在平滑的内存模型中,huge 指针可以指向的完整内存空间的任一地址,因此,两个指向同一物理内存地址的 huge 指针一定是相等的,不像其它的指针一样可以将物理地址分解成不同的段值和偏移值。
    
    平滑虚拟寻址
    
    系统还可以使用一种简易的内存模型——平滑虚拟寻址 Flat Virtual Address。平滑就是这种模型的特色,通过设置段寄存器指向一个特定的段描述符就可以实现,这就是简易之处。这个特定的段描述符包含的值为 offset=0、limit=232。这样所有程序在无需处理段寄存器的情况下就可以使用232=4GB的寻址空间了!平滑虚拟寻址模型的实现,得益于 80386 CPU 对通用寄存器的扩展,即在 8086 上的 AX、BX、CX、DX 寄存器被扩展成了32-bit的 EAX、EBX、ECX、EDX,通常段描述符中的段基址也一样会被扩展成32-bit。这样通过寄存器寻址就可以完成4GB内存的访问,完全可以不管段寄存器。
    
    段寄存器的隐含功能
    
    段寄存器除了在分页机制上的重要作用外,还有一系列隐含的功能。
    
    如 CPU 取指令时隐含地使用 CS 作为段选择器。
    
    大多数使用数据的指令的寻址隐含地使用 DS 作为段选择器。当然可以使用 ES 作为段选择器,如果以 ES 前缀在指令的寻址处。此外还可以使用 CS、SS 作为段选择器,而不论是目标操作数还是源操作数。例句,将使用 ES 作为目标操作数的段选择器,而不是默认的数据段选择器 DS,这种显式指定段选择器的方式也称为段超越:
    
    MOV ES:[BX], AX
    
    处理器的堆栈可以通过 PUSH、POP 等等指令来隐含使用 SS 作为段选择器。而或者通过 (E)SP 或 (E)BP 显式使用SS 作为段选择器,这样的寻址方式亦可以称之为堆栈存储器寻址。
    字符串指令如 STOS、MOVS等等,可以使用 DS 作为段选择器来访问数据段或额外数据段 ES 作为选择器。
    
     特权等级
    
    
    保护模式下的保护环
    
    为什么讲寻址的章节会谈及特权等级!这不是一个让人前着惊的内容编排,80386 作为首个32-bit CPU,它引入的保护特性是一个重要的部分,这里的保护直接体现在内存的保护行为上。前面讲到分页机制时,出现了三个概念 CPL、DPL、RPL。用于标记特权等级的标志取值 0~3,值越小表示权越高,约束越少,对应称为 Ring 0、Ring 1、Ring 2、Ring 3。然而,特权等级远不只在分页机制上应用,它还用来约束软件的数据访问、调用门、执行指令行为,大多数据情况下,系统内核及驱动程序运行在最高特权等级,即最小的约束或无约束的 Ring 0 等级,而应用程序则运行在 Ring 3 即完全受约束的特权等级, 这样一来系统的稳定性就得到了大的增强。如此一来,可以明显知道特权对内存访问的影响是巨大的,对指令的寻址也同样影响深远。右图,在计算科学上称之为保护环 Protection Rings。
    
    80386 提供了内存段级别和分页级别的保护,通过段描述符和页表项指定的保护机制的信息来约束进程,并确认特定的进程是否拥有一些特权指令的使用权。进而增强了系统的强壮性,也为系统的调试带来了便利。
    
    附录、参考资料
    
        IA-32 Intel Architecture Software Developer's Manual, Volumes 1, 2A, 2B, and 3
        http://www.intel.com/content/www/us/en/processors/architectures-software-developer-manuals.html
        IA-32 Intel Architecture Optimization Reference Manual
        Intel 80386 Reference Programmer's Manual  http://pdos.csail.mit.edu/6.828/2005/readings/i386/toc.htm
        X86 Memory Segmentation http://en.wikipedia.org/wiki/X86_memory_segmentation
        Hight Memory Area http://en.wikipedia.org/wiki/High_Memory_Area
        Intel Memory Model http://en.wikipedia.org/wiki/Intel_Memory_Model
        Protected Mode http://en.wikipedia.org/wiki/Protected_mode
        Intel x86 Microprocessors http://en.wikipedia.org/wiki/Category:Intel_x86_microprocessors
        《Intel微处理器》第8版 Barry B. Brey 著 http://book.51cto.com/art/201006/208446.htm
        Intel x86 CPU Datasheet http://datasheets.chipdb.org/Intel/x86/
    
      
    展开全文
  • p++, 要看p 的所指类型占用的 byte 数
    p++, 要看p 的所指类型占用的 byte 数
    展开全文
  • 内存寻址一(分段)

    千次阅读 2014-05-01 00:21:40
    内存地址 逻辑地址
  • 从书中可见,可寻址最小内存空间和存储的基本单元是不同的。 存储的基本单元,被称为存储字。 存储字的位数与存储器中的数据线根数是相同的。 也就是说,存储器在取数据的时候,是按一个存储字一个存储字取的。 即...
  • 第二章--内存寻址

    2014-03-09 20:39:07
    本文作为第二章--内存寻址,主要讲述: 1、内存地址。 2、硬件中的分段。 3、Linux中的分段。 4、硬件中的分页。 5、Linux中的分页。
  • 最近看内存寻址部分的内容,这篇真的不错。 分享转载一下: https://www.cnblogs.com/felixfang/p/3420462.html 物理地址和逻辑地址 物理地址: 加载到内存地址寄存器中的地址,内存单元的真正地址。在前端总线上...
  • 寻址的本质是CPU给出一个虚拟地址,经过寻址能够找到它的物理地址,我们称之为映射。上一节我们说到实模式寻址,在实模式下段寄存器中存放的是段首地址,加上PC程序寄存器中存放段内偏移值,两者相加即可得到物理...
  • 深入理解Linux内核day01--内存寻址

    千次阅读 2016-04-26 18:43:17
    内存寻址 内存地址:  逻辑地址: 段+偏移量 组成  线性地址: 可用来表达4GB的地址 (也称虚拟地址)  物理地址: 用于内存芯片级内存单元寻址。他们与微处理器地址引脚发送到内存总线上的电信号相对应  内存...
  • 内存地址与寻址

    2017-10-23 13:07:47
    ①位(bit):计算机存储信息的最小单位。 ②字节(Byte):计算机存储信息的基本单元,1Byte = 8 bit。   bit在大多数情况都以“了解硬件的工作原理”的方式出现的,Byte才是我们真正用于存储数据的单元,...
  • 在基于Intel 80x86微处理器的平台上,内存寻址是内存管理最重要的一部分内容。而关于内存地址在实际的软件及硬件实现上,也出现了不同的表示方式:逻辑地址(logical address)、线性地址/虚拟地址(linear address/...
  • 正如之前所说,虽然计算机的存储最小单位是 bit/位。 但是 基本数据结构 都是以 byte 作为单位。 比如 bool, 虽然理论上 我们可以用 1 bit来表示, 但计算机里,我们至少要用 1 byte(8 bit)来表示它。 最小...
  • 1.3.内存编址和寻址以及内存对齐

    千次阅读 2016-04-25 10:45:28
    1.3.内存编址和寻址以及内存对齐
  • 位表示的是二进制位,一般称为比特,即0或1,是计算机存储的最小单位; 什么是字节? 字节是计算机中数据处理的基本单位;计算机中以字节为单位存储和解 释信息,规定一个字节由八个二进制位构成,即1个字节等于8...
  • 一、8080CPU 1、8位CPU,16位地址线 2、寻址范围4K 3、寻址方案: 16位地址的地址线,取出所在的8位内容(一个字节) ...一个最小的8位CP / M系统需要以下硬件组件: 一个使用ASCII字符集的计算机终端 英特尔8
  • ing 逻辑---(分段单元)----->线性-...计划在这篇笔记中写内存寻址和内存管理的,实际上简单一点说,内存管理模块包括内存寻址、内核如何给自己分配主存以及怎样给进程分配线性地址。这几天的兴致不是很高,有点将就应
  • 我们先从一道简单的问题说起!... 设有一个1MB容量的存储器,字长32位,问:按字节编址,字编址的寻址范围以及各自的寻址范围大小? 如果按字节编址,则 1MB = 2^20B 1字节=1B=8bit 2^20B/1B = 2^20 ...
  • NAND FLASH 内存详解与读写寻址方式

    千次阅读 2012-09-02 20:33:28
    一、内存详解 NAND闪存阵列分为一系列128kB的区块(block),这些区块是 NAND器件中最小的可擦除实体。擦除一个区块就是把所有的位(bit)设置为"1"(而所有字节(byte)设置为FFh)。有必要通过编程,将已擦除 的位从"1...
  • 关于地址总线,字长,内存容量,寻址范围的计算

    万次阅读 多人点赞 2015-10-25 21:49:51
    地址总线,字长,内存容量,寻址范围 之间的计算  处理机字长是指处理机能同时处理(或运算)的位数,即同时处理多少位(bit)数据。比如Intel Pentium 4处理器字长为32位,它能同时处理32位的数据,也即它的数据...
  • 字节寻址和按字寻址

    2020-12-14 19:55:45
    位:数据存储的最小单位。计算机中最小的数据单位,一个位的取值只能是0或1; 字节:由八位二进制数组成,是计算机中最基本的计量单位,也是最重要的计量单位(个人理解)。 字长:计算机中对CPU在单位时间内能处理...
  • 存储单元是CPU访问存储器的基本单位。变量的名字对应着存储单元的地址,变量内容对应着单元所存储的数据。存储地址一般用十六进制数表示,而每一个存储器地址中又存放着一组二进制(或十六进制)表示的...
  • 在前端总线上传输的内存地址都是物理内存地址,编号从0开始一直到可用物理内存的最高端。这些数字被北桥(Nortbridge chip)映射到实际的内存条上。物理地址是明确的、最终用在总线上的编号,不必转换,不必分页,也...
  • 寻址存储器

    2017-02-13 22:39:53
    寻址存储器 这一课直接建立在“1.3:变量的第一次看”部分的材料上。...内存最小单位是二进制数字(位),它可以保持0或1的值。你可以认为有点像一个传统的光开关-要么灯关闭(0),或它是在(1)。中间没
  • 寻址空间一般指的是CPU对于内存寻址的能力。通俗地说,就是能最多用到多少内存的一个问题。数据在存储器(RAM)中存放是有规律的 ,CPU在运算的时候需要把数据提取出来就需要知道数据在那里 ,这时候就需要挨家挨户的...
  • 内存每一个存储位置的最小单元都可以储存0或1,即一个位的内容,而内存将8个位设定为一个存储空间的基本单位。而在地址线上每一个地址的编号便对应的是一个存储位置的最小的基本单位。 如何在地址线上用信号表示这个...
  • 对于按字寻址和按字节寻址的理解

    万次阅读 多人点赞 2018-05-14 17:33:18
    想要搞清按字寻址和按字节寻址就要先搞清位、字节、字长、字的定义 :位:数据存储的最小单位。计算机中最小的数据单位,一个位的取值只能是0或1;字节:由八位二进制数组成,是计算机中最基本的计量单位,也是最...
  • 按字寻址与按字节寻址

    千次阅读 多人点赞 2019-04-23 17:24:07
    内存的容量有多少,就是用多少个二进制数表示,地址线的条数就是多少根。 16K是其容量大小,16K=2^14,故地址线14根; 数据线指一次传输的数据的宽度,32位的宽度需要用32根数据线。 故总...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 18,383
精华内容 7,353
热门标签
关键字:

内存寻址最小单位