[关闭]
@mwumli 2015-01-03T17:24:53.000000Z 字数 5005 阅读 2781

内存寻址

Linux OS Memory CPU


内存寻址

Intel x86下CPU的演变

cpu 位数 特点与变化
4004 4bit
8080 8bit 几个8bit寄存器配合可以访问16bit内存地址,访问内存为真实物理地址
8086 16bit 寻址目标为1MB,故AB为20bit(扩展的);由于DB为16bit,只能表示64K的内存,所以出现了段的概念:真实地址=段地址<<4+偏移地址
80286 24bit 出现了新的寻址方式:保护模式,之前传统的寻址方式:实模式,依旧被使用;在系统启动的时候,处理器处于实模式,只能访问1M空间,之后会进入保护模式,访问空间大大提高;每个段的大小依然64k(DB=16bit)
80386 32bit DB=AB=32bit,理论上,DB与AB一致,CPU的结构应该简洁,如最初8080一样,但是为了考虑兼容性问题,Intel选择了在段寄存器上构筑保护模式,并且保留段寄存器为16bit;保护模式中,寻址能力达到4G(段描述符表的使用)
80x86 ... 80386之后的CPU被称为80x86,因为架构没什么变化

上表的位数基本代表地址总线(AB),AB决定了CPU的寻址能力,故整个表从上到下是寻址能力的演进,只有8086有点特别:最初为16bit,后来扩展为20bit

80386之前随着bit的变化,CPU的体系结构也发生着变化(实模式-->保护模式)
80386之后的处理器,尽管位数发生变化,但因为体系结构并未发生改变,故也统称为IA32(32 bit Intel Architecture)

80x86寄存器简介

CPU的主要组成成分之一是寄存器

CPU的设计者在设计CPU的时候,首要考虑的问题是兼容性问题--向后兼容,既要支持16bit还要支持32bit,甚至支持8bit,这也让之前设计的软件得以移植
这不仅仅是CPU设计要考虑的问题,也应该是我们自己在程序设计中考虑的问题

80x86的寄存器:

通用寄存器

32bit 低16bit 低16bit的高8bit 低16bit的低8bit 用途
EAX AX AH AL 一般用作累加器
EBX BX BH BL 一般用做基址寄存器
ECX CX CH CL 一般用作计数
EDX DX DH DL 一般用作存放数据
EBP BP ... ... 一般用作堆栈指针
ESP SP ... ... 一般用做基址指针
ESI SI ... ... 一般用作源变址
EDI DI ... ... 一般用作目标变址

从上表可以看出,为了兼容16bit,8个32bit的寄存器的低16位当作8086的8个16bit寄存器,而为了兼容8bit, AX、BX、CX、DX可以分为8个8bit的寄存器...

因此,这8个通用寄存器既可以支持1位、8位、16位和32位数据运算,也支持16位和32位存储器寻址。

段寄存器

在80x86中,段寄存器依然为16bit
我们熟知的段寄存器有4个: CS,DS,SS,ES,意义也未曾发生变化
另外还另加2个段寄存器

在过去,这些段寄存器分别存储各个段的基地址
而现在,这些段寄存器分别存储的是各个段的选择符
因为16bit的段寄存器无法存储32位的地址,然后所有段的基地址被存放在一个被称为段描述符表的地方,而表的表项存储了段的基地址等有关信息,每个表项的索引则是每个段的选择符

指令指针寄存器和标志寄存器

指令指针寄存器(EIP)存放下一条将要执行的指令的偏移量,这个偏移量是相对于当前代码段寄存器CS而言的

标志寄存器(EFLAGS)存放了有关处理器的控制标志,很多标志和16bit的FLAGS中标志含义一样

控制寄存器

控制寄存器(CR0,CR1,CR2,CR3)主要用于操作系统的分页机制

虚拟地址->线性地址->物理地址

将编写程序时看到的内存空间定义为虚拟内存空间,其中的地址就叫虚拟地址,一般用段:偏移量来描述,而我们在编程时所取的地址就是偏移量

线性地址空间是一段连续的,范围为0--4G的地址空间, 一个线性地址就是线性地址空间的绝对地址

我们把主板上连接物理内存条所提供的内存空间定义为物理内存空间,其中每个存储单元地址被称作物理地址

CPU使用MMU把虚拟地址转换成物理地址,从而进行数据的存取

MMU是一种硬件电路,分为两个部分:分段部件和分页部件
我们往往把其称为分段机制和分页机制,以便于从逻辑角度去理解硬件的实现机制

分段机制把一个虚拟地址转换成线性地址
分页机制把一个线性地址转换为物理地址

段机制

段是描述虚拟地址空间的基本单位
段机制可以把虚拟地址转换为线性地址

为什么分段呢?
1. 段机制的出现是因为8086的AB>DB,尽管AB决定了可以访问地址空间大小,但是由于DB太小不能表示相应的地址,限制访问内存的大小,为了摆脱这种局限性,采用把内存分段,通过段地址:偏移地址 的方式访问内存,这样就解决不能访问足够内存的问题
2. 程序的地址不再进行硬编码,调试错误变的容易
3. 为代码段共享提供了可能

实现段机制,就需要一个段描述符表,这个表的每一个表项存储了每个段基地址、段的界限、段的保护属性,因此表项被称作段描述符,8个字节

在保护模式下,有三种类型的描述符表:全局描述附表(GDT),中段描述符表(IDT),局部描述符表(LDT)
为了加快对这些表的访问,Intel专门设计了专门的寄存器GDTR,IDTR,LDTR,以便存放这些表的基地址以及表的长度界限

因此,可以推断,我们在段寄存器中不再存储段的基地址,而是存储相应段在段描述符表的索引,这个索引表示了段描述符在描述符表中的位置,因此段寄存器也被称作选择符

选择器的结构:

15 ~ 3 2 1 ~ 0
索引 TL RPL

TL为选择域,决定从全局描述符表中(TL=0)还是从局部描述符表(TL=1)中选择相应的段描述符
RPL域为请求这的特权级
保护模式提供了4个特权级,用0~3表示,但是通常只用了0(内核态)和3(用户态)
内核态为最好特权级别,而用户态为最低特权级
保护模式规定:高特权级可以访问低特权级,而低特权级不能随便访问高特权级

Linux绕过段机制

因为很多硬件平台并不支持段机制,为了实现系统更多移植到其他平台上,那么就不能使用段机制
但是80x86规定段机制是不可以禁止的,因此不可能直接给出线性地址

Linux为了提高系统的可移植性,采用了一种巧妙的方式使用了段机制却又绕过它: Linux的设计人员让段的基地址为0,而段的界限为4GB,这样任意给出一个偏移值,则等式为"0+偏移量=线性地址",即”偏移量=线性地址“。此外,由于段机制规定”偏移量<4GB“,所以偏移量为0H ~ FFFFFFFFH,这恰好是线性地址空间的范围,也就是说我们把虚拟地址直接映射到线性地址,即虚拟地址和线性地址就是同一地址

80x86段机制规定:代码段和数据段必须创建不同的段
所以Linux又单独为代码段和数据段分别创建了一个基地址为0,段界限为4G的段描述符
80x86段机制还规定特权级为3的程序无法访问特权级为0段,而Linux的内核运行在特权级0,用户运行在特权级3
所以Linux为内核和用户分别创建了代码段和数据段
最终Linux必须创建4个段描述符:特权级为0的数据段和代码段、特权级为3的数据段和代码段

Linux这样绕过段机制,所以段机制所提供的保护作用(通过”段基地址:偏移地址“方式将线性地址空间分割,让段与段之间完全隔离)消失了,因为这些段使用完全相同的线性地址空间(0 ~ 4GB),它们互相覆盖。
如果继续不使用分页的话,线性地址空间将被直接映射到物理空间,那么修改一个段的数据,都会同时影响到其他段的数据。

那么是否意味着用户可以随意修改内核数据
当然不是
1. 用户段和内核段具有不同的特权级别
2. Linux使用分页机制,分页机制会提供给我们所需的保护

分页机制

80x86规定:页机制是可选的
如果分页机制并不被允许(CR0的最高位为0),那么经过段机制转化的32位线性地址就是物理地址
如果分页机制被允许(CR)的最高位为1),那么经过段机制转化的32位线性地址需要通过分页机制把其转化成物理地址

页与页表

页、物理页及大小

将线性地址空间划分为若干个大小相等的片,称为页
和数组一样,给页按顺序加以编号,第0页,第1页, ..., 第n页
相应的,把物理空间划分为若干与页相等的块,称为物理块或页面(Page Frame),也同样加以编号:第0页面,第1页面, ..., 第n页面

第0页可能映射到第1页面,第1页可能映射到第4页面,第2也可能映射到第2页面...
这些映射是由MMU的分页部件决定的,可以对应映射,也可以不对应映射

页的大小在设计硬件分页机制时候就已经确定了,80x86支持的的标准页为4KB(也支持4MB)

页表

和段描述符表类似,页表存储的物理页面基地址和页属性

页面大小为4K,物理页面基地址为32位,所以表示的任意物理页面基地址必然为4K的倍数,因此其最低12位总是0,那么可以使用这12位来存放页的属性
这样32bit完全可以描述页的映射关系,即页表中的每一项为4个字节

4GB的线性空间被划分为1M个4KB大小的页,每个页表项占4个字节,那么整个页表占4M空间,而且要求是连续的,这个显然不现实,可以采用两级页表来解决

两级页表

两级页表就是的页表进行再分页

第一级为页目录,存放页表的信息
4MB的页表再次分页(4MB/4KB)可以存放1K个页,而每个页的描述为4个自己,那么页目录最多占用4KB,刚好是一个页
页目录共1K个表项,于是,线性地址的最高10位(31 ~ 22bit)用来存储第一级的索引

第二级称为页表,每个页表也刚号在4K的页中,并且每个页表包含1K个表项
线性地址的中间10位(21 ~ 12bit)存放索引

线性地址的低12位表示页内偏移量

具有两级页表的线性地址结构:

31 ~ 22 21 ~ 12 11 ~ 0
页目录 页内偏移量

线性地址到物理地址转换

  1. 用32位线性地址的高10位(31 ~ 22bit)作为页目录项的索引,将它乘以4,与CR3中页目录的起始地址相加,获得相应目录项在内存的地址

  2. 从这个地址开始读取32位页目录项,取出其高20位,再给低12位补0,形成32位地址就是页表在内存中的起始地址

  3. 用32位线性地址中的第21 ~ 12位作为页表中页表项的索引,将它乘以4,与页表的其实地址相加,获得相应页表项在内存中的地址

  4. 中这个地址开始读取32位页表项,取出其高20位,再给低12位不0,与线性地址的第11 ~ 0位相加,形成最终32位的页面物理地址

页面高速缓存

由于分页机制的存在,而页表被放在内存中,每次访问一个页面,是必要经过两次访问内存,从而大大降低了访问的速度

所以为了提高速度,在80x86中设置一个最近存取页的高速缓存硬件机制
它将自动保持3处理器最近使用过的32个页表项,因此,覆盖128KB的内存地址

所以当需要访问一个页面的时候,首先在页面高速缓存中查找有没有相同的页表项,如果有,那么就直接找到:如果没有, 则需要经过两级访问

每次经过两级查表访问页面,都会自动把最近访问过的页表项存入高速缓存

Linux中的分页机制

  1. 许多RISC处理器支持的段功能有限,Linux为了使自己可以移植到绝大多数当前流行的处理器平台:Linux的分段机制,使所有进程都使用相同的段寄存器值,即所有进程使用同样的线性地址空间(0~4GB)

  2. 绝大多数处理器都采用64位架构的处理器,而64位处理器使用两级分页不适合,为了保持良好的可移植性,Linux采用三级的分页模式而不是二级

三级分页:

Linux中进程与页表

每一个进程有它自己的页目录和自己的页表项,当进程切换的时候,Linux把CR3控制寄存器的内容保存到前一个执行进程的PCB中,然后把下一个要执行进程的PCB的值装入CR3寄存器中。因此,当新进程恢复在CPU上执行时,分页单元执行一组正确的页表

Linux中的可执行文件格式为ELF,链接器(ld)总是从0x08000000开始安排程序的“代码段”,对每个程序都这样,这是虚拟地址

至于程序执行时,在物理内存中的实际地址,则由内核为其建立内存映射时临时分配,具体地址取决于当时所分配的物理内存页面

添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注