逻辑地址、线性地址和物理地址的转换

一、逻辑地址

        逻辑地址是指在计算机体系结构中是指应用程序角度看到的内存单元、存储单元、网络主机的地址。 逻辑地址往往不同于物理地址,通过地址翻译器或映射函数可以把逻辑地址转化为物理地址。

        一个逻辑地址由两部份组成,段标识符段内偏移量

        段标识符是由一个16位长的字段组成,称为段选择符。其中前13位是一个索引号。引号,可以理解为数组的下标——而它将会对应一个数组,它又是什么的索引呢?这就是“段描述符(segment descriptor)”,段描述符具体地址描述了一个段(对于“段”这个字眼的理解:我们可以理解为把虚拟内存分为一个一个的段。比如一个存储器有1024个字节,可以把它分成4段,每段有256个字节)。这样,很多个段描述符,就组了一个数组,叫“段描述符表”,这样,可以通过段标识符的前13位,直接在段描述符表中找到一个具体的段描述符,这个描述符就描述了一个段,我刚才对段的抽像不太准确,因为看看描述符里面究竟有什么东东——也就是它究竟是如何描述的,就理解段究竟有什么东东了,每一个段描述符由8个字节组成,如图:

        Intel设计的本意是,一些全局的段描述符,就放在“全局段描述符表(GDT)”中,一些局部的,例如每个进程自己的,就放在所谓的“局部段描述符表(LDT)”中。那究竟什么时候该用GDT,什么时候该用LDT呢?这是由段选择符中的T1字段表示的,=0,表示用GDT,=1表示用LDT。

        GDT在内存中的地址和大小存放在CPU的gdtr控制寄存器中,而LDT则在ldtr寄存器中。

         首先,给定一个完整的逻辑地址   [段选择符段内偏移地址],

1、看段选择符的T1=0还是1,知道当前要转换是GDT中的段,还是LDT中的段,再根据相应寄存器,得到其地址和大小。我们就有了一个数组了。

2、拿出段选择符中前13位,可以在这个数组中,查找到对应的段描述符,这样,它了Base,即基地址就知道了。

3、把Base + offset,就是要转换的线性地址了。

        逻辑地址(Logical Address) 是指由程序产生的与段相关的偏移地址部分。例如,进行C语言指针编程中,可以读取指针变量本身值(&操作),实际上这个值就是逻辑地址,它是相对于你当前进程数据段的地址,不和绝对物理地址相干。只有在Intel实模式下,逻辑地址才和物理地址相等(因为实模式没有分段或分页机制,Cpu不进行自动地址转换);逻辑也就是在Intel 保护模式下程序执行代码段限长内的偏移地址(假定代码段、数据段如果完全一样)。应用程序员仅需与逻辑地址打交道,而分段和分页机制对您来说是完全透明的,仅由系统编程人员涉及。应用程序员虽然自己可以直接操作内存,那也只能在操作系统给你分配的内存段操作。

        线性地址(Linear Address) 是逻辑地址到物理地址变换之间的中间层。程序代码会产生逻辑地址,或者说是段中的偏移地址,加上相应段的基地址就生成了一个线性地址。如果启用了分页机制,那么线性地址可以再经变换以产生一个物理地址。若没有启用分页机制,那么线性地址直接就是物理地址。Intel 80386的线性地址空间容量为4G(2的32次方即32根地址总线寻址)。

        物理地址(Physical Address) 是指出现CPU外部地址总线上的寻址物理内存的地址信号,是地址变换的最终结果地址。如果启用了分页机制,那么线性地址会使用页目录和页表中的项变换成物理地址。如果没有启用分页机制,那么线性地址就直接成为物理地址了。

        虚拟内存(Virtual Memory) 是指计算机呈现出要比实际拥有的内存大得多的内存量。因此它允许程序员编制并运行比实际系统拥有的内存大得多的程序。这使得许多大型项目也能够在具有有限内存资源的系统上实现。在Linux 0.11内核中,给每个程序(进程)都划分了总容量为64MB的虚拟内存空间。因此程序的逻辑地址范围是0x0000000到0x4000000。

        逻辑地址与物理地址的“差距”是0xC0000000,是由于虚拟地址->线性地址->物理地址映射正好差这个值。这个值是由操作系统指定的。

参考: 逻辑地址_百度百科

二、线性地址

        线性地址(Linear Address)是逻辑地址到物理地址变换之间的中间层。在分段部件中逻辑地址是段中的偏移地址,然后加上基地址就是线性地址。

        线性地址是一个32位无符号整数,可以用来表示高达4GB的地址,也就是,高达4294967296个内存单元。线性地址通常用十六进制数字表示,值的范围从0x00000000到0xffffffff)。 程序代码会产生逻辑地址,通过逻辑地址变换就可以生成一个线性地址。如果启用了分页机制,那么线性地址可以再经过变换以产生一个物理地址。当采用4KB分页大小的时候,线性地址的高10位为页目录项在页目录表中的编号,中间10位为页表中的页号,其低12位则为偏移地址。如果是使用4MB分页机制,则高10位页号,低22位为偏移地址。如果没有启用分页机制,那么线性地址直接就是物理地址。 

        CPU的页式内存管理单元,负责把一个线性地址,最终翻译为一个物理地址。从管理和效率的角度出发,线性地址被分为以固定长度为单位的组,称为页(page),例如一个32位的机器,线性地址最大可为4G,可以用4KB为一个页来划分,这页,整个线性地址就被划分为一个tatol_page[2^20]的大数组,共有2的20个次方个页。这个大数组我们称之为页目录。目录中的每一个目录项,就是一个地址——对应的页的地址。

如上图:

1、分页单元中,页目录是唯一的,它的地址放在CPU的cr3寄存器中,是进行地址转换的开始点。

2、每一个进程,因为都有其独立的对应的虚似内存(页目录也是唯一的),那么它也对应了一个独立的页目录地址。——运行一个进程,需要将它的页目录地址放到cr3寄存器中。

3、每一个32位的线性地址被划分为三部份,面目录索引(10位):页表索引(10位):偏移(12位)

      依据以下步骤进行转换:

1、从cr3中取出进程的页目录地址(操作系统负责在调度进程的时候,把这个地址装入对应寄存器);

2、根据线性地址前十位,在数组中,找到对应的索引项,因为引入了二级管理模式,页目录中的项,不再是页的地址,而是一个页表的地址。(又引入了一个数组),页的地址被放到页表中去了。

3、根据线性地址的中间十位,在页表(也是数组)中找到页的起始地址;

4、将页的起始地址与线性地址中最后12位相加,得到最终我们想要的物理地址;

参考:线性地址_百度百科

三、物理地址

        在存储器里以字节为单位存储信息,为正确地存放或取得信息,每一个字节单元给以一个唯一的存储器地址,称为物理地址,又叫实际地址或绝对地址。

        地址从0开始编号,顺序地每次加1,因此存储器的物理地址空间是呈线性增长的。它是用二进制数来表示的,是无符号整数,书写格式为十六进制数。它是出现在CPU外部地址总线上的寻址物理内存的地址信号,是地址变换的最终结果。用于内存芯片级的单元寻址,与处理器和CPU连接的地址总线相对应。

        针对不对齐的寻址,对内存的不对齐的访问对计算机的性能可能会有所损害。例如,像Intel 8086这种数据总线为16位的计算机,对偶数地址的访问会更有效率。在那种情况下,获取一个16位的值只要读一次内存以及在数据总线上传送一次数据。显然,如果那16位的值储存在奇数地址上,处理器实际上要读两次内存,即,一次用于读存储在低地址的部分,另一次读存储在高地址的部分;两次都要把读到的数据丢弃一半。

四、逻辑地址转换线性地址

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

       在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。

        内核段描述符和用户段描述符虽然起始线性地址和长度都一样,但DPL(描述符特权级)是不一样的。__KERNEL_CS 和__KERNEL_DS 的DPL值为0(最高特权),__USER_CS 和__USER_DS的DPL值为3。

4.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 数据段寄存器:指向保存着静态数据和全局数据的段(静态区)。

4.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:忽略。

4.2.1、数据段描述符

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

4.2.2、代码段描述符

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

4.3、全局描述符表与局部描述符表

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

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

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

4.4、分段机制将逻辑地址转化为线性地址的步骤

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

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

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

4.5、调试 

       用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主要以分页的方式实现内存管理

五、线性地址转换物理地址

        逻辑地址:是相对于段而言的,需要段描述符和段内偏移来组成。所有段都从0x00000000开始,只需关注段内偏移即可。而段内偏移的值恰好等于线性地址的值。

        线性地址:是进程使用的地址,虚拟的地址。人为抽象出一大片地址空间给进程使用,为了方便32位地址总线存取,linux内核定义为了4G。

       物理地址:是采用32位总线存取物理内存某个字节时,地址总线上电位的高低。

        分段单元将逻辑地址转换成线性地址,分页单元将线性地址转换成物理地址。第四节分析前者,第五节分析后者。

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

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

        Linux采用了分页的方式来记录对应关系。所谓的分页,就是以更大尺寸的单位页来管理内存。在Linux中,通常每页大小为4KB。

转换图在这里再放一次。

依据以下步骤进行转换:

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

前面说了二级页管理架构,不过有些CPU,还有三级,甚至四级架构。

猜你喜欢

转载自blog.csdn.net/hfut_zhanghu/article/details/122340261