你的位置:首页 > 操作系统

[操作系统]内存数据


3. 内存数据

 

前面我们知道了,内存是按字节编址,每个地址的存储单元可以存放8bit的数据。我们也知道CPU通过内存地址获取一条指令和数据,而他们存在存储单元中。现在就有一个问题。我们的数据和指令不可能刚好是8bit,如果小于8位,没什么问题,顶多是浪费几位(或许按字节编址是为了节省内存空间考虑)。但是当数据或指令的长度大于8bit呢?因为这种情况是很容易出现的,比如一个16bit的Int数据在内存是如何存储的呢?

 

 

3.1 内存数据存放

 

其实一个简单的办法就是使用多个存储单元来存放数据或指令。比如Int16使用2个内存单元,而Int32使用4个内存单元。当读取数据时,一次读取多个内存单元。于是这里又出现2个问题:

  1. 多个存储单元存储的顺序?
  2. 如何确定要读几个内存单元?

 

3.1.1 大端和小端存储

  1. Little-Endian 就是低位字节排放在内存的低地址端,高位字节排放在内存的高地址端。
  2. Big-Endian 就是高位字节排放在内存的低地址端,低位字节排放在内存的高地址端。

需要说明的是,计算机采用大端还是小端存储是CPU来决定的, 我们常用的X86体系的CPU采用小端,一下ARM体系的CPU也是用小端,但有一些CPU却采用大端比如PowerPC、Sun。判断CPU采用哪种方式很简单:

[cpp] view plain copy 
 print?在CODE上查看代码片派生到我的代码片

  1. bool IsBigEndian()    
  2. {    
  3.     int vlaue = 0x1234;    
  4.     char lowAdd =  *(char *)&value;     
  5.     if( lowAdd == 0x12)    
  6.     {    
  7.         return true;    
  8.     }    
  9.     return false;    
  10. }  


既然不同计算机存储的方式不同,那么在不同计算机之间交互就可能需要进行大小端的转换。这一点我们在Socket编程中可以看到。这里就不介绍了,对以我们单一CPU来说我们可以不需要管这个转换的问题,另外我们目前个人PC都是采用小端方式,所以我们后面默认都是这种方式。

 

 

3.1.2 CPU指令

 

前面我们多次提到了指令的概念,也知道指令是0和1组成的,而汇编代码提高了机器码的可读性。为什么突然在这里介绍CPU指令呢? 主要是解释上面的第二个问题,当我读取一个数据或指令时,我怎么知道需要读取多少个内存单元。

 

 

3.1.2.1 CPU指令格式

 

首先我们来看看CPU指令的格式,我们知道CPU质量主要就是告诉CPU做什么事情,所以一条CPU指令一般包含操作码(OP)和操作

 

  操作码字段   地址码字段

 

 

 

根据一条指令中有几个操作数地址,可将该指令称为几操作数指令或几地址指令。

 

 操作码 A1 A2 A3

 

 

三地址指令: (A1) OP (A2) --> A3

 

 操作码 A1 A2

 

 

二地址指令: (A1) OP (A2) --> A1

 

 操作码  A1

 

 

一地址指令: (AC) OP (A) --> AC   

 

 操作码 

 

 

    零地址指令

A1为被操作数地址,也称源操作数地址; A2为操作数地址,也称终点操作数地址; A3为存放结果的地址。 同样,A1,A2,A3以是内存中的单元地址,也可以是运算器中通用寄存器的地址。所以就有一个寻址的问题。关于指令寻址后面会介绍。

CPU指令设计是十分复杂的,因为在计算机中都是0和1保存,那计算机如何区分一条指令中的操作数和操作码呢?如何保证指令不会重复呢?这个不是我们讨论的重点,有兴趣的可以看看计算机体系结构的书,里面都会有介绍。从上图来看我们知道CPU的指令长度是变长的。所以CPU并不能确定一条指令需要占用几个内存单元,那么CPU又是如何确定一条指令是否读取完了呢?

 

 

3.1.2.2 指令的获取

现在的CPU多数采用可变长指令系统。关键是指令的第一字节。 当CPU读指令时,并不是一下把整个指令读近来,而是先读入指令的第一个字节。指令译码器分析这个字节,就知道这是几字节指令。接着顺序读入后面的字节。每读一个字节,程序计数器PC加一。整个指令读入后,PC就指向下一指令(等于为读下一指令做好了准备)。

Sample1:

[plain] view plain copy 
 print?在CODE上查看代码片派生到我的代码片

  1. MOV AL,00  机器码是1011 0000 0000 0000  


机器码是16位在内存中占用2个字节:

【00000000】 <- 0x0002

【10110000】 <- 0x0001

 

比如上面这条MOV汇编指令,把立即数00存入AL寄存器。而CPU获取指令过程如下:

  1. 从程序计数器获取当前指令的地址0x0001。
  2. 存储控制器从0x0001中读出整个字节,发送给CPU。PC+1 = 0X0002.
  3. CPU识别出【10110000】表示:操作是MOV AL,并且A2是一个立即数长度为一个字节,所以整个指令的字长为2字节。
  4. CPU从地址0x0002取出指令的最后一个字节
  5. CPU将立即数00存入AL寄存器。

 

这里的疑问应该是在第3步,CPU是怎么知道是MOV AL 立即数的操作呢?我们在看下面一个列子。

 

Sample2:

[plain] view plain copy 
 print?在CODE上查看代码片派生到我的代码片

  1. MOV AL,[0000] 机器码是1010 0000 0000 0000 0000 0000  

 

这里同样是一条MOV的汇编指令,整个指令需要占用3个字节。

【00000000】 <-0x0003

【00000000】 <- 0x0002

【10100000】 <- 0x0001

 

我们可以比较一下2条指令第一个字节的区别,发现这里的MOV  AL是1010 0000,而不是Sample1中的1011 000。CPU读取了第一个字节后识别出,操作是MOV AL [D16],表示是一个寄存器间接寻址,A3操作是存放的是一个16位就是地址偏移量(为什么是16位,后面文章会介绍),CPU就判定这条指令长度3个字节。于是从内存0x0002~0x0003读出指令的后2个字节,进行寻址找到真正的数据内存地址,再次通过CPU读入,并完成操作。

 

从上面我们可以看出一个指令会根据不同的寻址格式,有不同的机器码与之对应。而每个机器码对应的指令的长度都是在CPU设计时就规定好了。8086采用变长指令,指令长度是1-6个字节,后面可以添加8位或16位的偏移量或立即数。 下面的指令格式相比上面2个就更加复杂。

 

  • 第一个字节的高6位是操作码,W表示传说的数据是字(W=1)还是字节(W=0),D表示数据传输方向D=0数据从寄存器传出,D=1数据传入寄存器。
  • 第二个字节中REG表示寄存器号,3位可以表示8种寄存器,根据第一字节的W,可以表示是8位还是16位寄存器。表3-1中列出了8086寄存器编码表
  • 第二个字节中的MOD和R/M指定了操作数的寻址方式,表3-2列出了8086的编码

这里没必要也无法更详细介绍CPU指令的,只需要知道,CPU指令中已经定义了指令的长度,不会出现混乱读取内存单元的现象。有兴趣的可以查看引用中的连接。

 

 

3.1.3  内存数据

 

3.1.3.1 内存数据的操作

 

从上面我们可以知道,操作数可以是立即数,可以存放在寄存器,也可以存放在内存。对于第一个例子,指令已经说明,操作时是一个字节,于是CPU可以从下一个内存地址读取操作时,而对于第二个列子,操作数只是地址偏移,所以当CPU获得这个数据后,需要转换成实际的内存地址,在进行一次内存访问,把数据读入到寄存器中。这里就出现我们前面提到的问题,这个数据我们要读几个存储单元呢?

[cpp] view plain copy 
 print?在CODE上查看代码片派生到我的代码片

  1.     MyClass cla;  
  2. 008C3EC9  lea         ecx,[cla]    
  3. 008C3ECC  call        MyClass::MyClass (08C1050h)    
  4. 008C3ED1  mov         dword ptr [ebp-4],0    
  5.     cla.num5 = 500;  
  6. 008C3ED8  mov         dword ptr [ebp-6Ch],1F4h    
  7.     int b1 = MyClass::num1;  
  8. 008C3EDF  mov         dword ptr [b1],64h    
  9.     int b2 = MyClass::num2;  
  10. 008C3EE6  mov         dword ptr [b2],0C8h    
  11.     int b3 = MyClass::num3;  
  12. 008C3EF0  mov         eax,dword ptr ds:[008C9008h]    
  13. 008C3EF5  mov         dword ptr [b3],eax    
  14.     int b4 = cla.num4;  
  15. 008C3EFB  mov         eax,dword ptr [cla]    
  16. 008C3EFE  mov         dword ptr [b4],eax    
  17.     int b5 = cla.num5;  
  18. 008C3F04  mov         eax,dword ptr [ebp-6Ch]    
  19. 008C3F07  mov         dword ptr [b5],eax    


让我们看一段C++代码和对应的汇编代码,操作很简单,创建一个Myclass对象后,对成员变量赋值。而赋值都是试用Mov操作符。对于这些变量我们有赋值操作和取值操作,那么是如何确定要读取或写入数据的大小呢?

[cpp] view plain copy 
 print?在CODE上查看代码片派生到我的代码片

  1. cla.num5 = 500;  
  2. 08C3ED8  mov         dword ptr [ebp-6Ch],1F4h    


我看先看看赋值操作,往dword ptr [ebp-6Ch]内存存入一个立即数, [ebp-6Ch]是num5的内存地址,而前面的dword ptr 表示这是进行一个双子操作。还记得上面指令格式中第一个字节的W字段吗? 在8086中只能进行字节或字操作,而现在CPU都可以进行双字操作。

[cpp] view plain copy 
 print?在CODE上查看代码片派生到我的代码片

  1. int b5 = cla.num5;  
  2. 08C3F04  mov         eax,dword ptr [ebp-6Ch]    


同样,当我们要从一个内存读取数据的时候,也要指定读取数据的操作类型,这里也是双字操作。这样以来,就能从内存中正确的读出需要的长度了。就这么一个简单的赋值操作,获取你从来没想过在内存中怎么存放,又是怎么读取的。这一切都是编译器和CPU在背后为我们完成了。

 

 

3.1.3.2 内存对齐

 前面我们清楚了CPU是如何正确读取数大小不同的数据的,最后一部分来看看有关内存对齐的问题。对于大部分程序员来说,内存对齐应该是透明的。内存对齐是编译器的管辖范围。编译器为程序中的每个数据单元安排在适当的位置上。

 

 

3.1.3.2.1 对齐原因

从前面我们知道,目前计算机内存按照字节编址,每个地址的内存大小为1个字节。而读取数据的大小和数据线有关。比如数据线为8位那么一次读取一个字节,而如果数据线为32位,那么一次需要读取32个字节,这样是为了一次更多的获取数据提高效率。否则读取一个int变量就需要进行4次内存操作。对于内存访问一般有以下两个条件:

  1. CPU进行一次内存访问读取的数据和字长相同。
  2. 有些CPU只能对字长倍数的内存地址进行访问。

对于第一个条件一般来说,目前存储器一个cell是8bit,进行位扩展使他和字长还有数据线位数是相同,那么一次就能传送CPU可以处理最多的数据。而前面我们说过目前是按字节编址可能是因为一个cell是8bit,所以一次内存操作读取的数据就是和字长相同。

也正是因为和存储器扩展有关(参考1.2.1的图),每个DRAM位扩展芯片使用相同RAS。如果需要跨行访问,那么需要传递2次RAS。所以以32位CPU为例,CPU只能对0,4,8,16这样的地址进行寻址。而很多32位CPU禁掉了地址线中的低2位A0,A1,这样他们的地址必须是4的倍数,否则会发送错误。

如上图,当计算机数据线为32位时,一次读入4个地址范围的数据。当一个int变量存放在0-3的地址中时,CPU一次就内存操作就可以取得int变量的值。但是如果int变量存放在1-4的地址中呢? 根据上面条件2的解释,这个时候CPU需要进行2次内存访问,第一次读取0-4的数据,并且只保存1-3的内容,第二次访问读取4-7的数据并且只保存4的数据,然后将1-4组合起来。如下图:

所以内存对齐不但可以解决不同CPU的兼容性问题,还能减少内存访问次数,提高效率。当然目前关于这个原因争论很多,可以看看CSDN上的讨论:http://bbs.csdn.net/topics/30388330

 

3.1.3.2.2 如何对齐内存

内存对齐有一个对齐系数,一般是2,4,8,16字节这样。而不同平台上的对齐方式不同,这个主要是编译器来决定的。

具体的规则可以参考之前转的一篇文章,这里就不详细写了: http://blog.csdn.net/cc_net/article/details/2908600

 

 

 

总结

 

 

通过这一篇对内存工作的介绍,我们从内存的硬件结构,存储方式过渡到了内存的编址方式,然后又探讨了按字节编址带来的问题和解决的办法。这里就涉及到了CPU的指令格式,编译器的支持。最后我们也是从硬件和软件方面讨论了内存对齐的问题。

 

我自己感觉,内存的访问管理是计算机中最重要的部分,也是计算机硬件和软件之间交互的过渡的一个地方。所以理解了内存的工作原理,对于后面理解不同的内存模型很有帮助。

 参考 http://blog.csdn.net/cc_net/article/details/11097267