一、逻辑地址转线性地址

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

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

mov    0x80495b0, %eax

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

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

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

这样的情况下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             0x73     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主要以分页的方式实现内存管理

架构师 逻辑地址转物理地址 逻辑地址转成物理地址_寄存器

架构师 逻辑地址转物理地址 逻辑地址转成物理地址_描述符_02

1.--CPU的段寄存器:

  在CPU中,跟段有关的CPU寄存器一共有6个:cs,ss,ds,es,fs,gs,它们保存的是段选择符(或者叫段描述符)。而同时这六个寄存器每个都有一个对应的非编程寄存器,它们对应的非编程寄存器中保存的是段描述符。系统可以把同一个寄存器用于不同的目的,方法是先将其寄存器中的值保存到内存中,之后恢复。而在系统中最主要的是cs,ds,ss这三个寄存器。

  • CS 代码段寄存器:指向包含程序指令的段,在CS寄存器中RPL用于表示当前CPU的特权级(CPL),CPL为0是最高权限(内核态使用),CPL为3是用户态使用。
  • SS栈段寄存器:指向当前程序的栈的段。
  • DS 数据段寄存器:指向保存着静态数据和全局数据的段(静态区)。

2.--段描述符

  段描述符就是保存在全局描述符表或者局部描述符表中,当某个段寄存器试图通过自己的段选择符获取对于的段描述符时,会将获取到的段描述符放到自己的非编程寄存器中,这样就不用每次访问段都要跑到内存中的段描述符表中获取。

 

  • BASE(32位):段首地址的线性地址。
  • G:为0代表此段长度以字节为单位,为1代表此段长度以4K为单位。
  • LIMIT(20位):此最后一个地址的偏移量,也相当于长度,G=0,段大小在1~1MB,G=1,段大小为4KB~4GB。
  • S:为0表示是系统段,否则为代码段或数据段。
  • Type:描述段的类型和存取权限。
  • DPL:描述符特权级,表示访问这个段CPU要求的最小优先级(保存在cs寄存器的CPL特权级),当DPL为0时,只有CPL为0才能访问,DPL为3时,CPL为0为3都可以访问这个段。
  • P:表示此段是否被交换到磁盘,总是置为1,因为linux不会把一个段都交换到磁盘中。
  • D或B:如果段的LIMIT是32位长,则置1,如果是16位长,置0。(详见intel手册)
  • AVL:忽略。

2.1--数据段描述符:

  表示这个段描述符代表一个数据段,这种描述符可以放在GDT或者LDT。该描述符的S标志位为1,也就是非系统段。需要注意内核数据段属于数据段描述符,并不属于系统段描述符。

2.2--代码段描述符:

  表示这个段描述符代表一个数据段,这种描述符可以放在GDT或者LDT。该描述符的S标志位为1,也就是非系统段。需要注意内核代码段属于代码段描述符,并不属于系统段描述符

3.--全局描述符表与局部描述符表

  全局描述符表和局部描述符表保存的都是段描述符,记住要把段描述符和段选择符区别开来,保存在寄存器中的是段选择符,这个段选择符会到描述符表中获取对于的段描述符,然后将段描述符保存到对应寄存器的非编程寄存器中。

  系统中每个CPU有属于自己的一个全局描述符表(GDT),其所在内存的基地址和其大小一起保存在CPU的gdtr寄存器中。其大小为64K,一共可保存8192个段描述符,不过第一个一般都会置空,也就是能保存8191个段描述符。第一个置空的原因是防止加电后段寄存器未经初始化就进入保护模式而使用GDT。

  而对于局部描述符表,CPU设定是每个进程可以创建属于自己的局部描述符表(LDT),当前被使用的LDT的基地址和大小一起保存在ldtr寄存器中。不过大多数用户态的liunx程序都不使用局部描述符表,所以linux内核只定义了一个缺省的LDT供大多数进程共享。描述这个局部描述符表的局部描述符表描述符保存在GDT中

4.--分段机制将逻辑地址转化为线性地址的步骤:

1)使用段选择符中的偏移值(段索引)在GDT或LDT表中定位相应的段描述符.(仅当一个新的段选择符加载到段寄存器中是才需要这一步)

2)利用段选择符检验段的访问权限和范围,以确保该段可访问。

3)把段描述符中取到的段基地址加到偏移量(也就是上述汇编语言汇中直接出现的操作地址)上,最后形成一个线性地址。

架构师 逻辑地址转物理地址 逻辑地址转成物理地址_寄存器_03

架构师 逻辑地址转物理地址 逻辑地址转成物理地址_描述符_04

二, 线性地址转物理地址

逻辑地址:是相对于段而言的,需要段描述符和段内偏移来组成。所有段都从0x00000000开始,只需关注段内偏移即可。而段内偏移的值恰好等于线性地址的值。
       线性地址:是进程使用的地址,虚拟的地址。人为抽象出一大片地址空间给进程使用,为了方便32位地址总线存取,linux内核定义为了4G。
       物理地址:是采用32位总线存取物理内存某个字节时,地址总线上电位的高低。

       分段单元将逻辑地址转换成线性地址,分页单元将线性地址转换成物理地址。此处分析后者

CPU通过地址来访问内存中的单元,地址有虚拟地址和物理地址之分,如果CPU没有MMU(Memory Management Unit,内存管理单元),或者有MMU但没有启用,CPU核在取指令或访问内存时发出的地址将直接传到CPU芯片的外部地址引脚上,直接被内存芯片(以下称为物理内存,以便与虚拟内存区分)接收,这称为物理地址(Physical Address,以下简称PA),如下图所示。

架构师 逻辑地址转物理地址 逻辑地址转成物理地址_描述符_05

架构师 逻辑地址转物理地址 逻辑地址转成物理地址_描述符表_06

如果CPU启用了MMU,CPU核发出的地址将被MMU截获,从CPU到MMU的地址称为虚拟地址(Virtual Address,以下简称VA),而MMU将这个地址翻译成另一个地址发到CPU芯片的外部地址引脚上,也就是将虚拟地址映射成物理地址,如下图所示

 

架构师 逻辑地址转物理地址 逻辑地址转成物理地址_架构师 逻辑地址转物理地址_07

架构师 逻辑地址转物理地址 逻辑地址转成物理地址_架构师 逻辑地址转物理地址_08

虚拟内存地址和物理内存地址的分离,给进程带来便利性和安全性。虚拟地址必须和物理地址建立一一对应的关系,才可以正确的进行地址转换。

记录对应关系最简单的办法,就是把对应关系记录在一张表中。为了让翻译速度足够地快,这个表必须加载在内存中。不过,这种记录方式惊人地浪费。

因此,Linux采用了分页(paging)的方式来记录对应关系。所谓的分页,就是以更大尺寸的单位页(page)来管理内存。在Linux中,通常每页大小为4KB。如果想要获取当前树莓派的内存页大小,可以使用命令:

baiheng@baiheng-OptiPlex-5055-Ryzen-CPU:~$ getconf PAGE_SIZE
4096
地址转换过程
32bit 分页机制下虚拟地址是由32bit组成的,常规4KB分页,32位的虚拟地址被分成3个域。

架构师 逻辑地址转物理地址 逻辑地址转成物理地址_描述符_09

架构师 逻辑地址转物理地址 逻辑地址转成物理地址_描述符_10

具体的地址转换过程,文字描述太累,看图直观一些:

架构师 逻辑地址转物理地址 逻辑地址转成物理地址_描述符表_11

架构师 逻辑地址转物理地址 逻辑地址转成物理地址_描述符_12

依据以下步骤进行转换:

  1. 从cr3中取出进程的页目录地址(操作系统负责在调度进程的时候,把这个地址装入对应寄存器);
  2. 根据线性地址前十位,在数组中,找到对应的索引项,因为引入了二级管理模式,页目录中的项,不再是页的地址,而是一个页表的地址。(又引入了一个数组),页的地址被放到页表中去了。
  3. 根据线性地址的中间十位,在页表(也是数组)中找到页的起始地址;
  4. 将页的起始地址与线性地址中最后12位相加,得到最终我们想要的葫芦;

前面说了二级页管理架构,不过有些CPU,还有三级,甚至四级架构,Linux为了在更高层次提供抽像,为每个CPU提供统一的界面。提供了一个四层页管理架构,来兼容这些二级、三级、四级管理架构的CPU。这四级分别为:

  • 页全局目录PGD(对应刚才的页目录)
  • 页上级目录PUD(新引进的)
  • 页中间目录PMD(也就新引进的)
  • 页表PT(对应刚才的页表)。 

整个转换依据硬件转换原理,只是多了二次数组的索引罢了,如下图:

架构师 逻辑地址转物理地址 逻辑地址转成物理地址_描述符表_13

架构师 逻辑地址转物理地址 逻辑地址转成物理地址_寄存器_14