摘要:主要记录一下linux中内存中逻辑地址、线性地址、物理地址之间转换的映射关系。本文大量使用前辈代码,无意抄袭。
切记本文讨论的是近现代的linux系统,一些古老的比如Linux 0.11会有所差别。

理论基础

逻辑地址、线性地址、物理地址

(1)逻辑地址(Logical Address)
程序内部使用的地址。应用程序员仅需与逻辑地址打交道,而分段和分页机制对他来说是完全透明的,仅由系统编程人员涉及。更简单的来说,就是C语言中用(&)进行取地址操作的时候,获得的变量地址。

#include <stdio.h> 
int main() 
{ 
unsigned long tmp; 
tmp = 0x12345678; 
//打印出来的就是tmp变量的逻辑地址
printf("tmp variable address:0x%08lX\n", &tmp); 
return 0;
}

(2)线性地址(Linear Address)
逻辑地址到物理地址变换之间的中间层。程式代码会产生逻辑地址,加上相应的段基地址就生成了一个线性地址。如果启用了分页机制,那么线性地址能再经变换以产生一个物理地址。若没有启用分页机制,那么线性地址直接就是物理地址。

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

分段、分页

分段、分页是一个很有趣的问题。这个要翻一下cpu的发展史。

实地址模式

实模式(real mode),也称为实地址模式(real address mode)。实模式的特点是对所有可寻址内存,I/O地址和外设硬件的无限制直接软件访问。最早期的8086 cpu只有一种工作方式,那就是实模式。8086 cpu数据总线为16位,地址总线为20位。8086 cpu提供了对代码、栈、数据三种内存段访问的段寄存器,分别是代码段寄存器CS、栈段寄存器SS、数据段寄存器DS以及附加段寄存器ES,都是16位寄存器。

接下来看8086 cpu是如何得到物理地址的?

先上一幅图:

linux内存中关于物理地址和逻辑地址的探索1.png

首先cpu的地址总线决定了所能使用的内存条的大小,8086 cpu的地址总线为20位,2的20次方,是1MB。也就是说8086 cpu最多能够使用的内存条大小就是1MB(PS:不管你是linux、windows操作系统,硬件层面限制,后面随着cpu的进步,地址总线的位数也不断增加。linux下使用cpuid可以在maximum physical address bits一项看到cpu最大能支持多大的内存)。

8086 cpu的得到物理地址的方式也极其简单。参考上面的图片:

//段寄存器为16位,乘以16,相当于左移4位,刚好是2的20次方。充分利用了8086 cpu最大支持1MB内存的上限。
物理地址 = 段寄存器值 * 16 + 逻辑地址 = 段寄存器值 * 16 + 逻辑地址

简单粗暴,物理地址= 段寄存器值*16 + 逻辑地址 。进程之间的内存没有隔离,进程可以直接操作其他进程的物理地址

非PAE下保护模式

时间来到了80386 cpu身上。从80386开始cpu的数据总线和地址总线均为32位,但是代码段寄存器CS、栈段寄存器SS、数据段寄存器DS以及附加段寄存器ES这四个段寄存器依然是16位寄存器。保护模式登场,分段分页随之到来。这个时候怎么由逻辑地址到物理地址呢?

需要明确的是保护模式和实体地址模式的主要区别是:保护模式在逻辑地址和物理地址之间放置了一个线性地址。这样程序想要访问内存就必须经过逻辑地址-》线性地址-》物理地址的转换。

先从逻辑地址-》线性地址说起(分段):

保护模式下,段寄存器有了新的用途,叫做段选择符。16位段寄存器的格式规定如下:

linux内存中关于物理地址和逻辑地址的探索2.png

0到1位:特权等级
2位:Table indicator。值为0,使用GDT。值为1,使用LDT。
3位到15位:段描述表索引。基本上可以把GDT(全局描述符表)、LDT(局部描述符表)理解为一个数组,数组中的项叫做段描述符。19位到31位就是GDT、LDT的数组下标。

接下来根据段描述表索引和TI,决定使用GDT(全局描述符表)、还是LDT(局部描述符表)。

(1)假设TI=0。

这时说明程序使用的段位于GDT(全局描述符表)。操作系统首先从GDTR寄存器的47位至16位获取GDT的线性地址,然后减去0xc0000000得到GDT首地址的物理地址(PS:注意一下 这里GDTR存放的是线性地址,linux内核源码规定:线性地址空间中0xc0000000--0xffffffff为内核空间,对应物理地址为0x0000000--0x40000000。如果不规定的的话,就会陷入鸡生蛋蛋生鸡问题。)。然后从物理地址为GDT首地址 + 段描述表索引*8处得到全局段描述符。

linux内存中关于物理地址和逻辑地址的探索3.png

全局段描述符结构如下:

linux内存中关于物理地址和逻辑地址的探索4.png

全局段描述符中的 Base Addres 31:24、Base Addres 23:16、Base Address 15:0 刚好组成一个32bit的段基地址。

linux内存中关于物理地址和逻辑地址的探索5.png

线性地址 = 段基地址 + 逻辑地址。这样就得到线性地址了。

(2)假设TI=1。

这时说明程序使用的段位于LDT(局部描述符表)。

操作系统首先从GDTR寄存器的16位至47位获取GDT(全局描述符表)的线性地址,然后减去0xc0000000得到GDT首地址。然后从LDTR寄存器中获取全局描述符表索引,根据全局描述符表索引从GDT(全局描述符表)中获取全局段描述符(PS:注意一下 这里GDTR存放的是线性地址,linux内核源码规定:线性地址空间中0xc0000000--0xffffffff为内核空间,对应物理地址为0x0000000--0x40000000。如果不规定的的话,就会陷入鸡生蛋蛋生鸡问题。)。再根据全局段描述符获取到LDT(局部描述符表)的首地址。

获取到LDT(局部描述符表)的首地址后,再从LDT首地址 + 段描述表索引*8处得到局部段描述符。局部段描述符和全局段描述符结构一样,按照前面的方法从局部段描述符中取出32bit的段基地址。最后线性地址 = 段基地址 + 逻辑地址

linux内存中关于物理地址和逻辑地址的探索6.png

所以逻辑地址-》线性地址的转换可以总结为下图:

linux内存中关于物理地址和逻辑地址的探索7.png

线性地址-》物理地址(分页):

从线性地址到物理地址的映射过程为:

(1)线性地址被划分为以下格式:

linux内存中关于物理地址和逻辑地址的探索8.png

(2)从CR3寄存器中获取页目录表(Page Directory)的物理地址;

CR3寄存器(32bit)结构如下:

linux内存中关于物理地址和逻辑地址的探索9.png

31位到12位为页目录表首地址的31位到12位高位地址,将其向左偏移12bit,得到页目录表首地址的物理地址。为啥向左偏移12个bit呢?因为一个页目录表刚好是4KB大小,4K刚好就是2的12次方。

页目录表(Page Directory)类似一个数组。数组中每一项为四个字节,称为页表描述项,结构如下:

linux内存中关于物理地址和逻辑地址的探索10.png

(3)以线性地址中Directory部分为下标,在页目录表首地址 + Directory*4处取得对应的页表描述项。页表描述项中的前20bit高位地址为页表首地址的31位到12位高位地址。将20位高位地址向左偏移12bit,得到页表首地址(32bit)。为啥向左偏移12个bit呢?因为一个页表是4KB大小,4KB刚好就是2的12次方。

页表(Page Table)类似一个数组。数组中每一项为四个字节,称为页面描述项,结构如下:

linux内存中关于物理地址和逻辑地址的探索11.png

(4)以线性地址中的Table部分为下标,在页表首地址 + Table*4处获得相应的页面描述项;

(5)将页面描述项中31位至12位向左偏移12bit,得到页面基地址(32bit),然后与线性地址中的offset位段(12bit)相加得到物理地址。

所以 线性地址-》物理地址 的转换可以总结为下图:

linux内存中关于物理地址和逻辑地址的探索12.png

以上就是非PAE下保护模式逻辑地址-》线性地址-》物理地址的全部过程。

PAE下保护模式

PAE是指物理地址扩展。简单来说就是内存不够用了,32位的物理地址最大支持2的32次方即4GB内存,就引入了PAE技术。

PAE下保护模式的逻辑地址-》线性地址与非PAE下一致,主要在分页上面有所不同,所以直接介绍线性地址-》物理地址

从线性地址到物理地址的映射过程为:

(1)线性地址被划分为以下格式:

linux内存中关于物理地址和逻辑地址的探索13.png

(2)从CR3寄存器中获取PDPT表(Page Directory Pointer Table)的物理地址;

CR3寄存器(32bit)结构如下:

linux内存中关于物理地址和逻辑地址的探索14.png

31位到5位为PDPT表首地址的31位到5位高位地址,将其向左偏移5bit,得到PDPT表首地址的物理地址。为啥向左偏移5个bit呢?因为一个PDPT表刚好是32B大小,32刚好就是2的5次方。

PDPT表(Page Directory Pointer Table)类似一个数组。数组中每一项为8个字节,叫做页目录表描述项,结构如下:

linux内存中关于物理地址和逻辑地址的探索15.png

(3)以线性地址中 Directory Pointer 部分为下标,在PDPT表首地址+ Directory Pointer*8处取得对应的页目录表描述项(64bit)。页目录表描述项中的51位到12位为页目录表首地址的51位到12位高位地址。将40位高位地址向左偏移12bit,得到页目录表首地址(52bit)。为啥向左偏移12个bit呢?因为一个页目录表是4KB大小,4KB刚好就是2的12次方。

页目录表(Page Directory)类似一个数组。数组中每一项为8个字节,称为页表描述项,结构如下:

linux内存中关于物理地址和逻辑地址的探索16.png

(4)以线性地址中 Directory 部分为下标,在页目录表首地址+ Directory*8处取得对应的页表描述项。页表描述项中的51位到12位为页表首地址的51位到12位高位地址。将40位高位地址向左偏移12bit,得到页表首地址(52bit)。为啥向左偏移12个bit呢?因为一个页表是4KB大小,4KB刚好就是2的12次方。

(5)页表(Page Table)类似一个数组。数组中每一项为8个字节,称为页面描述项。以线性地址中的Table部分为下标,在页表首地址+Table*8处获得相应的页面描述项;

(6)页面描述项中51位至12位向左偏移12bit得到页面基地址(52bit),然后与线性地址中的offset位段(12bit)相加得到物理地址。

所以 线性地址-》物理地址 的转换可以总结为下图:

linux内存中关于物理地址和逻辑地址的探索17.png

保护模式有何好处?

如下图:实模式下程序1和程序2中的段寄存器值 * 16 + 逻辑地址*不能相同,如果相同,物理地址就可能冲突。保护模式下就相当于给每个程序的配备一个0-4GB的线性地址空间,由操作系统负责线性地址空间-》物理地址的转换。

linux内存中关于物理地址和逻辑地址的探索18.png

代码实验

非PAE模式下的保护模式

环境配置

用户权限:
本次实验全部在root权限下运行。

系统镜像为:
xubuntu-12.04.3

系统配置:
1核1GB内存15GB硬盘。注意,如果配置内存过大,fileview程序会提示cannot locate 'end-of-file' ,不知道为什么。

系统版本:

root@ubuntu:~# cat /etc/*release
DISTRIB_ID=Ubuntu
DISTRIB_RELEASE=12.04
DISTRIB_CODENAME=precise
DISTRIB_DESCRIPTION="Ubuntu 12.04.3 LTS"
NAME="Ubuntu"
VERSION="12.04.3 LTS, Precise Pangolin"
ID=ubuntu
ID_LIKE=debian
PRETTY_NAME="Ubuntu precise (12.04.3 LTS)"
VERSION_ID="12.04"

root@ubuntu:~# uname -a
Linux ubuntu 3.2.0-52-generic #78-Ubuntu SMP Fri Jul 26 16:23:24 UTC 2013 i686 i686 i386 GNU/Linux

源码编译:

// 编译源码
make 

//加载dram.ko驱动,访问所有物理内存
insmod dram.ko

//加载sys_reg.ko驱动,读取系统寄存器 
insmod sys_reg.ko

//创建字符设备
mknod /dev/dram c 85 0

//编译生成mem_map程序
gcc mem_map.c -o mem_map

//运行mem_map程序,并打印该程序用到的寄存器的值
./mem_map

//打印出来物理内存内容
./fileview

实验记录

第一步:

//注意运行后,不能退出这个程序。
root@ubuntu:~/mm_addr-master# ./mem_map
%ebp:0xBFFA6CA8
tmp address:0xBFFA6C9C
cr4=001406D0  PSE=1  PAE=0  
cr3=358CE000 cr0=8005003B
pgd:0xF58CE000
gdtr address:F778E000, limit:FF

从运行结果可以看出:PAE=0 代表没有开启PAE功能。tmp变量的逻辑地址:0xBFFA6C9C。gdtr寄存器的值为F778E000,即GDT(全局描述符表)的线性地址为F778E000,转化为物理地址 F778E000 - C0000000 即3778E000。cr3寄存器的值为358CE000 。

内核在建立一个进程时都要将其段寄存器设置好,有关代码在 include/asm/processor.h。(注意:内核源码版本为2.6.18-308.el5)

00531: 
00532: #define start_thread(regs, new_eip, new_esp) do { \ 
00533: __asm__("movl %0,%%fs ; movl %0,%%gs": :"r" (0)); \ 
00534: set_fs(USER_DS); \ 
00535: regs- >xds = __USER_DS; \ 
00536: regs- >xes = __USER_DS; \ 
00537: regs- >xss = __USER_DS; \ 
00538: regs- >xcs = __USER_CS; \ 
00539: regs- >eip = new_eip; \ 
00540: regs- >esp = new_esp; \ 
00541: preempt_disable(); \ 
00542: load_user_cs_desc(smp_processor_id(), current- >mm); \ 
00543: preempt_enable(); \ 
00544: } while (0)

因为tmp在栈中,那么就要从SS寄存器中读取段选择符。而SS寄存器的值为__USER_DS,其值定义在文件件include/asm/segment.h中。

00053: 00054: #define GDT_ENTRY_DEFAULT_USER_CS 14 
00055: #define __USER_CS (GDT_ENTRY_DEFAULT_USER_CS * 8 + 3) 
00056: 
00057: #define GDT_ENTRY_DEFAULT_USER_DS 15 00058: 
00057:#define __USER_DS (GDT_ENTRY_DEFAULT_USER_DS * 8 + 3)

__USER_DS(14*8 + 3 =115)的值展开二进制的结果为:0000000001111 011。高13位为段描述表索引。第三个bit为0,表示仅使用GDT(Global Descriptor Table),而没有使用LDT(Local Descriptor Table)。

然后去物理内存查看GDT(全局描述符表)的内容

./fileview

段描述表索引为15,对应的全局段描述符物理地址= GDT首地址 + 段描述表索引 * 8 = 0x3778E000 + 15 * 8 = 3778E078。物理内存中跳转到3778E078。

linux内存中关于物理地址和逻辑地址的探索19.png

可以看到全局段描述符值为00CFF300 0000FFFF(为什么是反过来的?因为内存中是小端序)。

全局段描述符值中段基地址的值为0x00000000 00000000。因为线性地址 = 段基地址 + 逻辑地址,所以线性地址为0xBFFA6C9C。
从这里也可以看出Linux巧妙地绕过了逻辑地址到线性地址的映射,段基地址为0,逻辑地址直接等于线性地址。

线性地址0xBFFA6C9C转换成二进制为:1011111111 1110100110 110010011100。CR3寄存器值为358CE000,对应的页目录表的物理地址为358CE000。

对应的页表描述项所在物理地址 = 358CE000 + (1011111111)b * 4 = 358CEBFC。

物理内存中跳转到358CEBFC处:

linux内存中关于物理地址和逻辑地址的探索20.png

可以看到页表描述项的值为 2C011067。取页表描述项的前20bit高位地址0x2C011,将20位高位地址向左偏移12bit,得到页表首地址0x2C011000。

对应的页面描述项所在物理地址 = 2C011000 + (1110100110)b * 4 = 2C011E98。

物理内存中跳转到2C011E98处:

linux内存中关于物理地址和逻辑地址的探索21.png

可以看到页面描述项的值为 1D12C067。取页面描述项的前20bit高位地址0x1D12C,将20位高位地址向左偏移12bit,得到页面基地址0x1D12C000。temp变量的物理地址 = 页面基地址 + (110010011100)b = 1D12CC9C。

物理内存中跳转到1D12CC9C处:

linux内存中关于物理地址和逻辑地址的探索22.png

可以看到就是temp变量的值(PS:这里显示0x12345678是因为我直接用来别人生成的mem_map程序,没有用自己生成编译的。正常情况下应该显示0x78563412)。

PAE模式下的保护模式

环境配置

用户权限:
本次实验全部在root权限下运行。

系统配置:
1核2GB内存15GB硬盘。

系统版本:

root@ubuntu:~# cat /etc/*release
DISTRIB_ID=Ubuntu
DISTRIB_RELEASE=12.04
DISTRIB_CODENAME=precise
DISTRIB_DESCRIPTION="Ubuntu 12.04.5 LTS"
NAME="Ubuntu"
VERSION="12.04.5 LTS, Precise Pangolin"
ID=ubuntu
ID_LIKE=debian
PRETTY_NAME="Ubuntu precise (12.04.5 LTS)"
VERSION_ID="12.04"

root@ubuntu:~# uname -a
Linux ubuntu 3.13.0-32-generic #57~precise1-Ubuntu SMP Tue Jul 15 03:50:54 UTC 2014 i686 i686 i386 GNU/Linux

源码编译:

// 编译源码
make 

//加载dram.ko驱动,访问所有物理内存
insmod dram.ko

//加载sys_reg.ko驱动,读取系统寄存器 
insmod sys_reg.ko

//创建字符设备
mknod /dev/dram c 85 0

//编译生成mem_map程序
gcc mem_map.c -o mem_map

//运行mem_map程序,并打印该程序用到的寄存器的值
./mem_map

//打印出来物理内存内容
./fileview

实验记录

第一步:

//注意运行后,不能退出这个程序。
root@ubuntu:~/Downloads/mm_addr-master# ./mem_map
%ebp:0xBFBA1128
tmp address:0xBFBA111C
cr4=003407F0  PSE=1  PAE=1  
cr3=2406F000 cr0=80050033
pgd:0xE406F000
gdtr address:F7B82000, limit:FF

从运行结果可以看出:PAE=1 代表开启PAE功能。tmp变量的逻辑地址:0xBFBA111C。gdtr寄存器的值为F7B82000,即GDT(全局描述符表)的线性地址为F7B82000,转化为物理地址 F7B82000 - C0000000 即37B82000。CR3寄存器的值为2406F000 。

分段和非PAE模式下的保护模式一样,不在重复。tmp变量的线性地址:0xBFBA111C。线性地址转换成二进制为:10 111111101 110100001 000100011100。

CR3寄存器的值为2406F000,取31位到5位001001000000011011110000000,将其向左偏移5bit,得到00100100000001101111000000000000。即PDPT表首地址的物理地址为2406F000。

temp的 Directory Pointer 为 (10)b,转化为十进制为2.

线性地址对应的页目录表描述项的物理地址 = PDPT表首地址 + Directory Pointer * 8 = 2406F000 + 2 * 8 = 2406F010。

物理内存中跳转到2406F010:

linux内存中关于物理地址和逻辑地址的探索23.png

可以看到页目录表描述项的值为 00000000 31749001。取页目录表描述项的51位到12位高位地址0x00000031749,将40位高位地址向左偏移12bit,得到页目录表首地址 0x00000 31749000。

对应的页表描述项所在物理地址 = 页目录表首地址 + Directory * 8 = 0x000000 31749000 + (111111101)b * 8 = 31749FE8。

物理内存中跳转到31749FE8:

linux内存中关于物理地址和逻辑地址的探索24.png

可以看到页表描述项的值为 0x0000000033138067。取页表描述项的51位至12位高位地址0x0000033138,将40位高位地址向左偏移12bit,得到页表首地址 0x00000 33138000。

对应的页面描述项所在物理地址 = 页表首地址 + Table * 8 = 0x00000 33138000 + (110100001)b * 8 = 33138D08。

物理内存中跳转到33138D08处:

linux内存中关于物理地址和逻辑地址的探索25.png

可以看到页面描述项的值为80000000 25912867。取页面描述符的51位至12位高位地址0x000000 25912,将40位高位地址向左偏移12bit,得到页面基地址0x000000 25912000。temp变量的物理地址 = 页面基地址 + (000100011100)b = 2591211C。

物理内存中跳转到2591211C:

linux内存中关于物理地址和逻辑地址的探索26.png

可以看到就是temp变量的值。

相关资料

实验源码

非PAE模式下的保护模式:实验源码
PAE模式下的保护模式:实验源码

相关参考

Linux内存地址映射
Linux 从虚拟地址到物理地址
本文编写时参考的一些pdf书籍