【总结】计算机中的地址的理解 2011年

计算机中的地址的理解

引言:写作本文档,是我在上课和写程序中遇到的困惑,我一直很希望能够很好的理解这些地址,而实际上另一方面我也认识到, 这些恰恰是计算机在底层如何构建,那些数据,那些指令是怎样是一个地方到另外一个地方的,文档中的有些地方是引用别的数据和文章的,里面用了一些图形,因为我觉得图形是一种很好的抽象,能够把难以描述的问题很好的表达出来。总之在以后的程序中更好地理解这些地址,写出更好的程序。

(一)物理地址,线性地址,逻辑地址,虚拟地址。

物理地址:

物理地址最好理解,我们可以简单的把内存比作一个大的数组(为了分析方便),每个数组都有其下标,这个下标标识了内存中的地址,这个实实在在的在内存中的地址,我们称之为物理地址。但是在用于内存芯片级的单元寻址,与处理器和CPU连接的地址总线相对应,相信并不是一个所谓的数组,但是做出这样的比拟,有利于更好的理解。

还依稀记得这张图:

 

逻辑地址:

与物理地址比较相对的是逻辑地址,实际上这个概念,我觉得可以这样理解,这个地址就是在程序中我们把它放到的位置;而这个位置通常是由编译器给出的。另外的一种理解是:逻辑地址指的是机器语言指令中,用来指定一个操作数或者是一条指令的地址。Intel段式管理中:,“一个逻辑地址,是由一个段标识符加上一个指定段内相对地址的偏移量,表示为 [段标识符:段内偏移量]。”比如我们在程序中定义一个变量 int g=3;相应的汇编代码应该是mov [g],3;那么这个g应该放在哪儿呢?实际上我们可以看到,这个g的地址总在在编译,链接之后就会一个确定的地址;而这个确定的地址我们叫做逻辑地址。

虚拟地址:

百度百科曰:Virtual Address,简称VA,由于Windows程序时运行在386保护模式下,这样程序访问存储器所使用的逻辑地址称为虚拟地址。实际上因为我们现代程序中地址都是虚拟的,所以这里的虚拟地址和线性地址是等价了的。

 

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

 

问题的产生:

当我们写一个程序,定义了一个变量(全局或者局部),如果这个地址就是之前说的物理地址,那么很显然会出很大问题:为了避免进程之间有地址的冲突必须为每个进程分配空间,做出一系列保护措施,可想而知,为了运行一个进程操作系统要做的工作是巨大的,而且很多的程度需要硬件的配合;

那么很显然PC不是这么处理的,既然不能给程序真的物理地址,那么就给它“假”的,或者说一种内存管理映像,但是毕竟程序是要放在内存中才能运行的,所以在假和真之间必须经过某种映射处理才行。而这个假的地址就是:虚拟内存,它对整个内存(不要与机器上插那条对上号)的抽像描述。它是相对于物理内存来讲的,可以直接理解成“不真实的”,“假的”内存,例如,一个0x08000000内存地址,它并不对就物理地址上那个大数组中0x08000000 - 1那个地址元素;进程使用虚拟内存中的地址,由操作系统协助相关硬件,把它“转换”成真正的物理地址。有了这样的抽像,一个程序,就可以使用比真实物理地址大得多的地址空间,而且只要我们控制好转换的过程就不会出现地址冲突,这样多个进程可以使用相同的地址,因为转换后的物理地址并非相同的。

 

 

 

一个小小的实验:

#include <stdio.h>

int a =0;

void func(void)

{

 int b=0x11223344;

}

 

int main()

{

 

 int g=3;

 g=a;

 func();

 return 0;

}

在windows下使用gcc编译,链接之后我们得到一个a.exe文件,这是一个可执行程序,我们这里使用工具ollydbg打开看下这个exe文件究竟是什么样的。

 

这里我们可以看到地址是从0x00400000开始的,里面还有这样的信息:

E8 70050000这显然是机器码,而在右边,ollydbg给出的翻译是call 00401500

后面给出的是一个确定的地址信息。这里只是一个例子,事实上,如果我们在vs中做好一定的配置,那么会得到:

 

但在使用全局变量a 的语句中(g=a),汇编代码为:mov eax,ds:[00417140h];

一定程序上符合我们之前说的:段:偏移量的做法;

 

一个比较有意思的问题能够说明我们所看到的exe中这些地址究竟是实的还是虚的:

#include <stdio.h>

 

#include <stdlib.h>

 

int main()

{

char * p = (char *)malloc(sizeof(char));

printf("address=%x\n",p);

 

//如果有兴趣可以添加这句,观察每次运行的值

//printf("%x\n",&p);

 

getchar();

return 0;

}

这个程序运行之后,你会发现每次运行的结果都是不同的,这能说明什么呢?其实这什么都说明不了。

首先可以断定不是物理内存,因为如果是物理内存是不会直接给应用程序的。

那么究竟是逻辑地址还是还是线性地址呢?随便一搜网上都说是线性地址,但是如果验证它是线性地址而不是逻辑地址呢,我想从两个方面入手:1.malloc分配的内存是在堆上,定义一个赋值的数组很容易看到这段区域,实际上跟栈区紧挨着,而且跟函数是有关系的。2.malloc之后总是要由我们自己来进行free操作,是不是和线性地址物理地址之间映射有关系的。 这是有趣的问题,其实使用oolydbg或者vc修改机器码运行可以很快的得出结论的。

(二)复杂的地址转换

现在回到问题的本质,现在一个程序要运行,必须加载到事实在在的内存中去,不管你做多少次变化,程序需要的是那个确确实实存在的物理地址,也就是说其他的地址都是浮云,这些地址只不过是为了完成某种工作而出现的中间产物罢了,事实上这也是我的个人理解我:这几个地址的存在也是因为为了解决RAM容量小,而又想运行多任务而出现的。

 

CPU需要将一个虚拟内存空间中的地址转换为物理地址,也就是:将一个逻辑地址转换为物理地址。需要进行两步:首先将给定一个逻辑地址,CPU要利用其段式内存管理单元,先将为个逻辑地址转换成一个线程地址,再利用其页式内存管理单元,转换为最终物理地址。

这样的两次转换真真切切在我们的CPU中,当然也在很多的教辅材料中,不想花太多的时间描述,因为里面涉及到的内容太复杂了,很多问题我都没有清晰的答案:从开机启动的内存,cache初始化到boot loader,GDT/LDT等等,而且这还没有考虑到操作系统之间的差异,因为linux,windows对待地址转换问题是有些不同的。

接下给出几幅我觉得很不错的图片,形象思维一哈:

 

这个图很清晰的描述了逻辑地址到物理地址的转换过程:

逻辑地址=====》线性地址====》物理地址

1CPU段式内存管理:逻辑地址转换为线性地址;

 一个逻辑地址由两部份组成,段标识符: 段内偏移量。段标识符是由一个16位长的字段组成,称为段选择符。其中前13位是一个索引号。后面3位包含一些硬件细节:

 

最后两位涉及权限检查。

索引号,或者直接理解成数组下标——那它总要对应一个数组,它应该是指向一个东西的?而这个东西就是“段描述符(segment descriptor)”,呵呵,段描述符具体地址描述了一个段。这样,很多个段描述符,就组了一个数组,叫“段描述符表”,这样,可以通过段标识符的前13位,直接在段描述符表中找到一个具体的段描述符,这个描述符就描述了一个段,每一个段描述符由8个字节组成,如下图:

 

 而在汇编里面我们用一个数据结构这样定义:

 

Base字段,它描述了一个段的开始位置的线性地址。

 

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,就是要转换的线性地址了。   对于软件来讲,原则上就需要把硬件转换所需的信息准备好,就可以让硬件来完成这个转换了。

详细的描述也用一副图:

 

但是实际的情况并不是这么简单:linux和windows的做法貌似是不同的。

2.CPU的页式内存管理

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

另一类“页”,我们称之为物理页,或者是页框、页桢的。是分页单元把所有的物理内存也划分为固定长度的管理单位,它的长度一般与内存页是一一对应的。这里注意到,这个total_page数组有2^20个成员,每个成员是一个地址(32位机,一个地址也就是4字节),那么要单单要表示这么一个数组,就要占去4MB的内存空间。为了节省空间,引入了一个二级管理模式的机器来组织分页单元。看图:

 

 

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

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

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

 转换步骤:

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

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

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

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

 

总结:以上只是简单的介绍下转换机制,但是这里的每个小的问题都可以进行不断的优化和改进的,详细的过程呢,我想是这样的:如果有兴趣肯定已经在学,或者学过了,如果没兴趣,那么肯定连这些都觉得厌恶了,所以就写到这里。

 

后记:上面的地址变换过程复杂而且我总感觉有些部分是冗余的,但是限于我对于这些内容都理解都是粗浅的,不能够很好地指出问题是出在哪里,但是我相信,如果我们能够合理的在CPU中根据我们自己需要搭建一些转换机构/寄存器,肯定是能够做到一定程序的优化。这是复杂的问题,标准已经被那些优先者制定,而我们这些悲惨的后来者,连自己“芯”片都没有的后来者,只有可悲地可耻地顺从地说:哦,原来是这样。。。从一个问题上升到另外一个问题:中国应该有自己的芯片,应该有自己的操作系统,无论从那个角度,我们都应该有。。。

 

参考链接: add in 2014.04.08

http://www.cnblogs.com/diyingyun/archive/2012/01/03/2311327.html

 

 

转载于:https://www.cnblogs.com/chen-blog/archive/2012/04/28/2475680.html

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值