内存映射与访问机制

通过参考内存布局及访问机制的相关文章,本文试着整合一下相关知识点,希望能对有需要的朋友提供一点参考。但由于所参考文章皆为网友所作,相关知识并没有形成系统的认识,所以有些知识点仍然不够清楚,也难免有谬误之处。如果大家发现错误,敬请指出,另外倘若有关于“系统启动及内存布局”方面的可靠的资料或书籍,恳请大神留言告知。

参考文章(只列出几个重要的):http://www.cnblogs.com/clover-toeic/p/3754433.html

                   http://blog.chinaunix.net/uid-26126915-id-2981205.html

                   http://blog.csdn.net/yeruby/article/details/39718119

                   http://www.cnblogs.com/qintangtao/p/3325985.html

                   http://www.cnblogs.com/wangccc/p/5342300.html

一、内存空间布局

1.1 虚拟地址

  随着图形界面的兴起和用户需求的增大,内存空间变得容纳不下程序了。因此人们将程序分割成许多片段,并由操作系统将这些片段调入内存运行,于是便产生一个虚拟地址的概念,其思想是:操作系统把程序当前使用的部分保留在内存中,而把其他未被使用的部分保存在磁盘上,并在需要切换时将外设中的数据调入内存空间。而程序运行在虚拟地址空间中,并通过相关机制将虚拟地址映射到物理内存。因此内存空间就分为物理存储空间和虚拟存储空间。

  引入虚拟地址的好处在于:

  1、程序可以使用一系列相邻的虚拟地址来访问物理内存中不相邻的大内存缓冲区。

  2、程序可以使用一系列虚拟地址来访问大于可用物理内存的内存缓冲区。当物理内存的供应量变小时,内存管理器会将物理内存页(通常大小为 4 KB)保存到磁盘文件。数据或代码页会根据需要在物理内存与磁盘之间移动。

  3、不同进程使用的物理地址由系统进行映射管理,从而避免直接操作内存,防止恶意程序或bug破坏其它内存空间。

1.2 物理空间布局

         物理存储空间布局与处理器相关,详细情况可以从处理器用户手册的存储空间分布表(memory map)相关章节中查到。

1.3 虚拟空间布局

         在多任务操作系统中,每个进程都运行在虚拟地址空间(Virtual Address Space),在32位模式下它是一个4GB的内存地址块。在Linux系统中, 内核进程和用户进程所占的虚拟内存比例是1:3,而Windows系统为2:2(通过设置Large-Address-Aware Executables标志也可为1:3)。

         虚拟地址通过页表(Page Table)映射到物理内存,页表由操作系统维护并被处理器引用。内核空间在页表中拥有较高特权级,因此用户态程序试图访问这些页时会导致一个页错误(page fault)。在Linux中,内核空间是持续存在的,并且在所有进程中都映射到同样的物理内存。内核代码和数据总是可寻址,随时准备处理中断和系统调用。与此相反,用户模式地址空间的映射随进程切换的发生而不断变化。

 Linux进程在虚拟内存中的标准内存段布局如下图所示:

 

  其中,用户地址空间中的蓝色条带对应于映射到物理内存的不同内存段,灰白区域表示未映射的部分。这些段只是简单的内存地址范围,与Intel处理器的段没有关系。

  上图中Random stack offset和Random mmap offset等随机值意在防止恶意程序。Linux通过对栈、内存映射段、堆的起始地址加上随机偏移量来打乱布局,以免恶意程序通过计算访问栈、库函数等 地址。execve(2)负责为进程代码段和数据段建立映射,真正将代码段和数据段的内容读入内存是由系统的缺页异常处理程序按需完成的。另外,execve(2)还会将BSS段清零。

二、内存访问机制

         在80X86CPU的发展过程中,存储器的管理机制发生了较大的变化。8086/8088CPU对存储器的管理采用分段的实方式;80286CPU除了可在实方式下工作外,还可以在保护模式下工作;而80386CPU之后的处理器则具有三种工作方式:实方式、保护方式和虚拟8086方式。

2.1 分段机制

         段的引入:8086为了用16位寄存器实现1MB(20位)的寻址内存空间,引入了段的概念。在没有采用分页管理时,逻辑地址计算而得的线性地址是直接映射物理地址(Physical Address)的,于是可以直接用线性地址访问内存;否则,还要通过X86的分页转换,将线性地址转换为物理地址。

         2.1.1 实模式

         在Real Mode下,我们对一个内存地址的访问是通过Segment:Offset的方式来进行的,其中Segment是一个段的Base Address,一个Segment的最大长度是64 KB,这是16-bit系统所能表示的最大长度。而Offset则是相对于此Segment Base Address的偏移量。Base Address+Offset就是一个内存绝对地址。在实际编程的时候,使用16-bit段寄存器CS(Code Segment),DS(Data Segment),SS(Stack Segment)来指定Segment,CPU将段寄存器中的数值向左偏移4-bit,放到20-bit的地址线上就成为20-bit的Base Address。

         2.1.2 保护模式

         到了Protected Mode,内存的管理模式分为两种,段模式和页模式。

         由于 Protected Mode运行在32-bit系统上,那么Segment的两个因素:Base Address和Limit也都是32位的。IA-32允许将一个段的Base Address设为32-bit所能表示的任何值(Limit则可以被设为32-bit所能表示的,以2^12为倍数的任何指),而不像Real Mode下,一个段的Base Address只能是16的倍数(因为其低4-bit是通过左移运算得来的,只能为0,从而达到使用16-bit段寄存器表示20-bit Base Address的目的),而一个段的Limit只能为固定值64 KB。另外,Protected Mode,顾名思义,又为段模式提供了保护机制,也就说一个段的描述符需要规定对自身的访问权限(Access)。所以,在Protected Mode下,对一个段的描述则包括3方面因素:【Base Address, Limit, Access】,它们加在一起被放在一个64-bit长的数据结构中,被称为段描述符。

  (1)GDT

  如果我们直接通过一个64-bit段描述符来引用一个段的时候,就必须使用一个64-bit长的段寄存器装入这个段描述符。但Intel为了保持向后兼容,将段寄存器仍然规定为16-bit(尽管每个段寄存器事实上有一个64-bit长的不可见部分,但对于程序员来说,段寄存器就是16-bit的),那么很明显,我们无法通过16-bit长度的段寄存器来直接引用64-bit的段描述符。解决的方法就是把这些长度为64-bit的段描述符放入一个数组中,而将段寄存器中的值作为下标索引来间接引用(事实上,是将段寄存器中的高13 -bit的内容作为索引)。这个全局的数组就是GDT。事实上,在GDT中存放的不仅仅是段描述符,还有其它描述符,它们都是64-bit长。GDT是Protected Mode所必须的数据结构,也是唯一的。

  全局描述符表GDT含有每一个任务都可能或可以访问的段的描述符,通常包含操作系统所使用的代码段,数据段和堆栈段的描述符,也包含多种特殊数据段的描述符,如各个LDT的描述符等.每个GDT最多含有8192个描述符.注意,GDT的第0个描述符总不被处理,通常它置成全0.

GDT结构图如下:

 

说明如下:

G:

(1)、G=0时,段限长的20位为实际段限长,最大限长为2^20=1MB

(2)、G=1时,则实际段限长为20位段限长乘以2^12=4KB,最大限长达到4GB

D/B:

当描述符指向的是可执行代码段时,这一位叫做D位,D=1使用32位地址和32/8位操作数,D=0使用16位地址和16/8位操作数。如果指向的是向下扩展的数据段,这一位叫做B位,B=1时段的上界为4GB,B=0时段的上界为64KB。如果指向的是堆栈段,这一位叫做B位,B=1使用32位操作数,堆栈指针用ESP,B=0时使用16位操作数,堆栈指针用SP。

DPL:特权级,0为最高特权级,3为最低,表示访问该段时CPU所需处于的最低特权级

type : 类型

(1)、type<8时:数据段

 

(2)、type>=8时:代码段

 

         GDT可以被放在内存的任何位置,那么当程序员通过段寄存器来引用一个段描述符时,CPU必须知道GDT的入口,也就是基地址放在哪里,所以Intel的设计者门提供了一个寄存器GDTR(GDTR是一个48位的全局描述符寄存器,高32位存放GDT的基址,低16位存放GDT限长。)用来存放GDT的入口地址,程序员将GDT设定在内存中某个位置之后,可以通过LGDT指令将GDT的入口地址装入此寄存器,从此以后,CPU就根据此寄存器中的内容作为GDT的入口来访问GDT了。

         (2)LDT

         除了GDT之外,IA-32还允许程序员构建与GDT类似的数据结构,它们被称作LDT(Local Descriptor Table,局部描述符表),但与GDT不同的是,LDT在系统中可以存在多个,并且从LDT的名字可以得知,LDT不是全局可见的,它们只对引用它们的任务可见,保护模式支持多任务,每个任务都有自己的局部描述符表LDT,且每个任务最多只有一个LDT,每个任务的LDT含有该任务自己的代码段,数据段和堆栈段的描述符。另外,每一个LDT自身作为一个段存在,它们的段描述符被放在GDT中。每个LDT最多含有8192个描述符。

         IA-32为LDT的入口地址也提供了一个寄存器LDTR(LDTR是一个16位的局部描述符寄存器,高13位存放LDT在GDT中的索引值。),因为在任何时刻只能有一个任务在运行,所以LDT寄存器全局也只需要有一个。如果一个任务拥有自身的LDT,那么当它需要引用自身的LDT时,它需要通过lldt指令将其LDT的段描述符装入此寄存器。lldt指令与lgdt指令不同的时,lgdt指令的操作数是一个32-bit的内存地址,这个内存地址处存放的是一个32-bit GDT的入口地址,以及16-bit的GDT Limit。而lldt指令的操作数是一个16-bit的选择子,这个选择子主要内容是:被装入的LDT的段描述符在GDT中的索引值。

         所以我们可以这样理解GDT和LDT:GDT为一级描述符表,LDT为二级描述符表。如图:

 

         其中,selector选择器(一个16位的数据结构)被装入段寄存器,它的高13位作为被引用的段描述符在GDT/LDT中的下标索引,bit 2用来指定被引用段描述符被放在GDT中还是到LDT中,bit 0和bit 1是RPL——请求特权等级,被用来做保护目的。如图所示:

 

         (3)IDT

         中断描述符表(Interrupt Descriptor Table,IDT)将每个异常或中断向量分别与它们的处理过程联系起来。与GDT和LDT表类似,IDT也是由8字节长描述符组成的一个数组。

         IDT表可以驻留在线性地址空间的任何地方,处理器使用IDTR寄存器来定位IDT表的位置。这个寄存器中含有IDT表32位的基地址和16位的长度(限长)值。

         在实地址模式中,CPU把内存中从0开始的1K字节作为一个中断向量表。表中的每个表项占四个字节,由两个字节的段地址和两个字节的偏移量组成,这样构成的地址便是相应中断处理程序的入口地址。但是,在保护模式下,由四字节的表项构成的中断向量表显然满足不了要求。这是因为,除了两个字节的段描述符,偏移量必用四字节来表示;要有反映模式切换的信息。在保护模式下,中断向量表中的表项由8个字节组成,中断向量表也改叫做中断描述符表IDT(InterruptDescriptor Table)。其中的每个表项叫做一个门描述符(gate descriptor),“门”的含义是当中断发生时必须先通过这些门,然后才能进入相应的处理程序。

主要门描述符是:

· 中断门(Interrupt gate)

其类型码为110,中断门包含了一个中断或异常处理程序所在段的选择符和段内偏移量。当控制权通过中断门进入中断处理程序时,处理器清IF标志,即关中断,以避免嵌套中断的发生。中断门中的DPL(Descriptor Privilege Level)为0,因此,用户态的进程不能访问Intel的中断门。所有的中断处理程序都由中断门激活,并全部限制在内核态。

· 陷阱门(Trap gate)

其类型码为111,与中断门类似,其唯一的区别是,控制权通过陷阱门进入处理程序时维持IF标志位不变,也就是说,不关中断。

· 系统门(System gate)

这是Linux内核特别设置的,用来让用户态的进程访问Intel的陷阱门,因此,门描述符的DPL为3。通过系统门来激活4个Linux异常处理程序,它们的向量是3、4、5及128,也就是说,在用户态下,可以使用int3、into、bound 及int0x80四条汇编指令。

最后,在保护模式下,中断描述符表在内存的位置不再限于从地址0开始的地方,而是可以放在内存的任何地方。为此,CPU中增设了一个中断描述符表寄存器IDTR,用来存放中断描述符表在内存的起始地址。中断描述符表寄存器IDTR是一个48位的寄存器,其低16位保存中断描述符表的大小,高32位保存IDT的基址.

2.2 分页机制

         2.2.1 MMU

         在没有使用虚拟存储器的机器上,虚拟地址被直接送到内存总线上,使具有相同地址的物理存储器被读写。而在使用了虚拟存储器的情况下,虚拟地址不是被直接送到内存地址总线上,而是送到内存管理单元——MMU。他由一个或一组芯片组成,一般存在于协处理器中,其功能是把虚拟地址映射为物理地址,这是MMU的基本作用之一,除了硬件的支持外,软件上实际就是维护一张表,表中的内容是VA到PA的转换法则;另一作用是可以实现不同的访问权限。

         2.2.2 地址映射

         分页的最大作用就在于:使得进程的物理地址空间可以是非连续的。在分段的方法中,每次程序运行时总是把程序全部装入内存,而分页的方法按照程序运行的局部性原理只将部分程序装入内存。

         当创建一个进程时,操作系统会为该进程分配一个4GB(32位系统中)大小的虚拟进程地址空间。创建4GB虚拟地址空间其实并不是要真的创建空间,只是要创建那种映射机制所需要的数据结构而已,这种数据结构就是页目和页表。

         一个页表的大小为4K字节,放在一个物理页中。由1024个4字节的页表项组成。页表项的大小为4个字节(32bit),所以一个页表中有1024 个页表项。页表中的每一项的内容(每项4个字节,32bit)高20bit用来放一个物理页的物理地址,低12bit放着一些标志。

页目录,一个页目录大小为4K字节,放在一个物理页中。由1024个4字节的页目录项组成。页目录项的大小为4个字节(32bit),所以一个页目录中有 1024个页目录项。页目录中的每一项的内容(每项4个字节)高20bit用来放一个页表(页表放在一个物理页中)的物理地址,低12bit放着一些标志。

对于x86系统,页目录的物理地址放在CPU的CR3寄存器中。

页表中包含物理页面基地址和页的属性。对于页表有两级页表和三级页表之分。linux为了保证可移植行,采用了三级分页机制,当然其在某些情况下可以返回到二级分页。

 

三、系统启动过程的内存变化

         参考http://blog.csdn.net/huangzhipeng/article/details/6159169

四、小结

         1、在实模式逻辑地址是直接映射到物理地址的,所以可以直接使用逻辑地址访问物理存储单元;在分段模式下,逻辑地址对应线性地址(虚拟地址),需要转换为对应的物理地址;在分页模式下,内存被划分为一系列较段小的页,从而减少了内存交换的开销,提高了内存使用效率,其使用的虚拟地址与物理地址并非一一对应,所以需要进行转换。

         2、实模式下通过段寄存器:偏移量确定逻辑地址;段模式下通过GDT(R)、LDT(R)和selector(就是段寄存器)确定虚拟地址;分页模式下通过MMU和多级页表确定存储单元,而CPU使用的是虚拟地址。

猜你喜欢

转载自www.cnblogs.com/fengliu-/p/9261740.html
今日推荐