C - 内存布局

来源:互联网 发布:在线照片拼图软件 编辑:程序博客网 时间:2024/06/04 18:17

进程内存分布介绍

一个由C/C++编译的程序除了存放函数二进制代码的程序代码段(code段)外,数据占用的内存大致分为以下几个部分:
1、栈区(stack)
 
 
 存放局部变量、函数参数、返回数据、返回地址等。系统自动分配释放 ,其操作方式类似于数据结构中的栈。
需要注意的有三点:
 
第一, 退栈后销毁进栈时定义数据,请看下面这段。
char* fun(char *p)
{
char test[] = "hello";
p = test;
return p;
}
//下面是某调用函数中的代码
char *a = "world";
printf("%s\n",fun(a));
输出的结果是什么?hello还是world?
test[]数组是局部变量,当所属函数被调用时,系统为之在栈区分配一段内存空间,存六个字符以及一些记录信息,test对应该段内存空间首地址,当一个参数即指针p传入时,将test的值赋给p,而当函数调用结束的时候,发生退栈操作,刚才分配的内存被“销毁”,即写入“垃圾信息”。所以调用的结果是:给参数赋给了一个栈区地址,该地址指向的内存空间里是一堆垃圾信息,所以输出结果既不是hello又不是world。
 
 
第二,正是由于栈“后进先出”的特点,所以函数调用的机制是借助栈区来完成的,而当我们进行大量频繁的调用操作时,系统将随之进行大量的进栈退栈操作,从而消耗时间资源,使得程序的执行效率下降。C语言解决的方法是使用宏来代替那些短小而被频繁调用的函数,而C++则是引入了内联函数机制。除此之外,还有就是提高编程技巧,注意细节,比如这段代码,for (i=0; i如果循环过程不改变fun()的取值的话,那么强烈建议将此代码调整为
temp = fun();
for (i=0; i
{
//***************
}
由于减少了每次循环中调用函数所造成的进栈退栈开销,所以执行效率将大大提高。需要类比的一个例子是循环嵌套,同样为了降低开销,建议在允许的情况下,将循环次数多的循环放在里面。
 
 
第三,由于系统为栈分配的空间很有限,一般只有1M(可以调整设置),如果申请的栈空间太大,将会出现栈溢出的错误。一次试验时,我用的机器上能分配的最大栈空间为1036084byte,也就是0.988M。所以当在栈区定义“大数据”时,一定要敏感。
 
 
 
2、堆区(heap)
由程序员通过malloc()等函数分配释放,若程序员不释放,程序结束时可能由OS回收。分配方式类似于链表。
 
 
需要注意的有五点:
第一, 分配后马上进行分配成功与否的验证。
第二, 有分配必须有释放,否则将有可能造成内存泄露。遵循“谁分配谁释放”的原则。
第三, 操作上,malloc和free对应,new和delete对应。另外,前者是库函数,使用时需要加载相应头文件,后者是C++中的关键字,是运算符,不需要加特别的头文件。
第四, 运行效率没有栈高,而且大量频繁使用将造成更多的内存碎片。
第五, 虽然free()函数的参数是指针,但它释放的是内存而不是指针,所以当执行完free()操作后,还应该将指针置空,以避免野指针问题。
 
 
3、全局区(静态区)(static)
 
 
存放全局变量、静态变量。程序结束后由系统释放。
C程序根据全局变量和静态变量有没有进行显式初始化,还将它们分为两个不同的区域,即BSS和DATA。据说这种区别在C++中已经没有了,而且编码规范要求我们在定义一个变量时,一定要同时对它进行初始化(尤其是一个指针,最好将它置为空),所以我们尽量还是在允许的情况下,遵守这一规范。另外需要特别说明的是,当没有进行显式初始化时,它们的值将被初始化为0。
当函数或外部变量的前面冠以static时,它们的可见范围将限定在所在文件内,程序中其他文件无法见到它们,我们可以用这个办法来有效避免命名冲突。而当一个局部变量被static修饰时,它的可见范围并没有修改,还是限定在函数内,但它的生存期将延长为程序生存期,因为存储区域不是栈区,所以不会因为退栈而被销毁。
 
 
4、文字常量区
 
 
常量字符串就是放在这里的。 程序结束后由系统释放。
常量,顾名思义,不可进行写操作,关于这一点的注意事项,请看下面这段错误代码;
char *p = "atbcdef";
*(p+1) = 'b';
程序运行时将报非法写入错误。
关于局部的字符串常量是存放在全局的常量区还是栈区,不同的编译器有不同的实现。可以通过汇编语言查看一下。VC环境下,局部常量就像局部变量一样存储于栈中,全局常量、字符常量存储于文字常量区。TC在常量区。
有人把常量区也归在了全局静态区,也有人把常量归在代码段,真是费解啊,我也不知道归哪,哪天有空看一下汇编代码,深究一下吧,不过从编程的角度来考虑,似乎追究这个问题意义不大。
 
 
 
 
再强调两个问题:
//栈和堆生长方向问题
int i;
int a[5];
    for (i=0; i<=5; i++)
{
a[i] = 1;
}
 
 
//内存对齐问题,
struct stru1
{
char a;
int b;
char c;
};
struct stru2
{
int d;
char e;
char f;
};
printf("%d\t%d\n",sizeof(stru1),sizeof(stru2));

字节序

前几天编程遇到字节序的问题,虽然凭着以前学过的知识印象,解决了字节顺序的问题,但是具体的知识以及概念不是很清楚,所以在网络上搜集了一下相关的资料。在此做一些总结。

      

    在各种计算机体系结构中,对于字节、字等的存储机制有所不同,因而引发了计算机通信领域中一个很重要的问题,即通信双方交流的信息单元(比特、字节、字、双字等等)应该以什么样的顺序进行传送。如果不达成一致的规则,通信双方将无法进行正确的编 / 译码从而导致通信失败。目前在各种体系的计算机中通常采用的字节存储机制主要有两种:big-edian (大端)和 little-endian (小端)。


词源: 据 Jargon File 记载, endian 这个词来源于 Jonathan Swift 在 1726 年写的讽刺小说"Gulliver's Travels" (《格利佛游记》)。该小说在描述 Gulliver 畅游小人国时碰到了如下的一个场景。在小人国里的小人因为非常小(身高 6 英寸 )所以总是碰到一些意想不到的问题。有一次因为对水煮蛋该从大的一端( Big-End )剥开还是小的一端( Little-End )剥开的争论而引发了一场战争,并形成了两支截然对立的队伍:支持从 Big-End 剥开的人 Swift 就称作 Big-Endians 而支持从Little-End 剥开的人就称作 Little-Endians ……(后缀 ian 表明的就是支持某种观点的人 :- )。Endian 这个词由此而来。

   1980 年, Danny Cohen 在其著名的论文 "On Holy Wars and a Plea for Peace" 中为了平息一场关于在消息中字节该以什么样的顺序进行传送的争论而引用了该词。该文中, Cohen 非常形象贴切地把支持从一个消息序列的 MSB 开始传送的那伙人叫做 Big-Endians ,支持从 LSB 开始传送的相对应地叫做 Little-Endians 。此后 Endian 这个词便随着这篇论文而被广为采用。

   

最高有效位 MSB: Most Significant Bit

     最高有效位 (MSB) ,有时候叫做最左边的位,是在一个 n 位二进制数字中的 n-1 位,这个位有最高的权重 (2^(n-1)) 。第一个或最左边的位,当这个数字被用一般的方式书写时。

 

 

最低有效位 LSB: Least Significant Bit

     最低有效位 (LSB) 是给这些单元值的一个二进制整数位位置,就是,决定是否这个数字是偶数或奇数。 LSB 有时候是指最右边的位,因为写较不重要的数字到右边位置符号的协定。它类似于一个十进制整数的最不重要的数字,它是在一个 ( 最右边 ) 位置的数字。

大端字节序( big-endian 

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

  • 数据以8bit为单位 :
地址增长方向   →...0x0A0x0B0x0C0x0D...

示例中,最高有效位 (MSB, Most Significant Byte)是0x0A 存储在最低的内存地址处。下一个字节0x0B存在后面的地址处。正类似于十六进制字节从左到右的阅读顺序。

  • 数据以16bit为单位 :
地址增长方向   →...0x0A0B0x0C0D...

最高的16bit单元0x0A0B存储在低位。


 


小端字节序( little-endian 

  • 数据以8bit为单位 :
地址增长方向   →...0x0D0x0C0x0B0x0A...

最低有效位 (LSB,Least Significant Byte)是0x0D 存储在最低的内存地址处。后面字节依次存在后面的地址处。

 

  • 数据以16bit为单位 :
地址增长方向   →...0x0C0D0x0A0B...

最低的16bit单元0x0C0D存储在低位。

  • 更改地址的增长方向 :

当更改地址的增长方向,使之由右至左时,表格更具有可阅读性。

←  地址增长方向...0x0A0x0B0x0C0x0D...

最低有效位(LSB)是0x0D 存储在最低的内存地址处。后面字节依次存在后面的地址处。

←  地址增长方向...0x0A0B0x0C0D...

最低的16bit单元0x0C0D存储在低位。

 

网络字节序

     网络传输一般采用大端序,也被称之为网络字节序 ,或网络序 。IP 协议中定义大端序为网络字节序。

伯克利 socket API定义了一组转换函数,用于16和32bit整数在网络序和本机字节序 之间的转换。htonl,htons用于本机序转换到网络序;ntohl,ntohs用于网络序转换到本机序。

   4个字节的32 bit值以下面的次序传输:首先是0~7bit,其次8~15bit,然后16~23bit,最后是24~31bit。这种传输次序称作大端字节序。由于 TCP/IP首部中所有的二进制整数在网络中传输时都要求以这种次序,因此它又称作网络字节序。比如,以太网头部中2字节的“以太网帧类型”,表示后面数据的类型。对于ARP请求或应答的以太网帧类型 来说,在网络传输时,发送的顺序是 0x08,0x06。在内存中的映象如下图所示:
栈底 (高地址)
---------------
0x06 -- 低位 
0x08 -- 高位
---------------
栈顶 (低地址)
该字段的值为0x0806。按照大端方式存放在内存中。

 

高/低地址与高低字节

     首先我们要知道我们C程序映像中内存的空间布局情况:在《C专家编程》中或者《Unix环境高级编程》中有关于内存空间布局情况的说明,大致如下图:
----------------------- 最高内存地址 0xffffffff
 | 栈底
 .
 .              栈
 .
  栈顶
-----------------------
 |
 |
/|/

NULL (空洞)

/|/
 |
 |
-----------------------
                堆
-----------------------
未初始化的数据
----------------(统称数据段)
初始化的数据
-----------------------
正文段(代码段)
----------------------- 最低内存地址 0x00000000

以上图为例如果我们在栈上分配一个unsigned char buf[4],那么这个数组变量在栈上是如何布局的呢[注1]?看下图:
栈底 (高地址)
----------
buf[3]
buf[2]
buf[1]
buf[0]
----------
栈顶 (低地址)

现在我们弄清了高低地址,接着来弄清高/低字节,如果我们有一个32位无符号整型0x12345678(呵呵,恰好是把上面的那4个字节buf看成一个整型),那么高位是什么,低位又是什么呢?其实很简单。在十进制中我们都说靠左边的是高位,靠右边的是低位,在其他进制也是如此。就拿 0x12345678来说,从高位到低位的字节依次是0x12、0x34、0x56和0x78。

高低地址和高低字节都弄清了。我们再来回顾一下Big-Endian和Little-Endian的定义,并用图示说明两种字节序:
以unsigned int value = 0x12345678为例,分别看看在两种字节序下其存储情况,我们可以用unsigned char buf[4]来表示value:
Big-Endian: 低地址存放高位,如下图:
栈底 (高地址)
---------------
buf[3] (0x78) -- 低位
buf[2] (0x56)
buf[1] (0x34)
buf[0] (0x12) -- 高位
---------------
栈顶 (低地址)

Little-Endian: 低地址存放低位,如下图:
栈底 (高地址)
---------------
buf[3] (0x12) -- 高位
buf[2] (0x34)
buf[1] (0x56)
buf[0] (0x78) -- 低位
---------------
栈顶 (低地址)

在现有的平台上Intel的X86采用的是Little-Endian,而像Sun的SPARC采用的就是Big-Endian。

 

 

     在使用系统提供的字节序转换函数时,一定要清楚地知道要转换的数据的长度,比如说在UNIX/Linux上的(通用x86机)
uint32_t htonl(uint32_t hostlong);

uint16_t htons(uint16_t hostshort);

uint32_t ntohl(uint32_t netlong);

uint16_t ntohs(uint16_t netshort);
函数,但是我们可以看到,系统提供的函数是32bit和16bit 的,但是如果我要转换的是一个字符,或是小于一个字符的几个bit的数据呢?(完全可能,像IP包的首部长度就只有4bit)
答案是不必转换,因为在机器存储和网络发送的一个字符内部的bit位存储顺序是一致的,正如我们在上面讲的,不必去转换。

 

 

补充(2010.09.18):


这段时间又有遇到了这个问题,觉得前面的一些东西并不能完全很好的说明字节序的问题,或者说,这些只是理论知识,有一些东西并没有完全说明白,尤其是对于我这种刚刚接触嵌入式开发的人。所以,今天又查阅了很多资料,终于明白大端处理器、小端处理器以及字节序的问题。在此做一下补充:

(1)大端处理器其实就是先处理高字节的数据,后处理低字节的数据。小端处理器就是先处理低字节的数据,后处理高字节的数据。

    跟上面相结合,其实是无论大端处理器和小端处理器都是先处理低地址的数据,所以就有了大端(big-endian)是将高字节放入低地址,低字节放入高地址。小端(little-endian)同样。

(2)前面所提的高地址、低地址其实是针对大于一个字节的结构(类型)的数据的内存存放位置而言的,可是说这个高低之分是相对于一个结构(或者类型或者变量)内部而言的。跨过这个结构(类型、变量)的范围就没有字节序处理的意义和必要了。


验证

验证平台是x86架构(小端)下

int a = 0x12345678;for(int i=0; i<4; i++){printf("a[%d] %p = 0x%02x\n", i, ((char*)&a) + i, *(((char*)&a) + i));}
结果:
a[0] 0xbfda0078 = 0x78
a[1] 0xbfda0079 = 0x56
a[2] 0xbfda007a = 0x34
a[3] 0xbfda007b = 0x12
高地址到低地址一次为 0x12 0x34 0x56 0x78,是高地址存放高位字节,低地址存放地位字节。
虽然小端可能更符合人类的思维,高位数在高地址,低位数在低地址,但是在平时工作时,正好不符合正常思维,因为如果画内存图,一般左为高位,右为低位,则画出的图是0x78 0x56 0x34 0x12,正好和读的顺序相反;数据传输时,先传输低地址,后传输高地址,所以收到数的顺序也是0x78 0x56 0x34 0x12,和我们平时读数的顺序也是相反。


int i;int a[5];printf("a[%d] %p\n", 0, a);printf("a[%d] %p\n", 1, a+1);printf("a[%d] %p\n", 2, a+2);printf("a[%d] %p\n", 3, a+3);printf("a[%d] %p\n", 4, a+4);printf("a[%d] %p\n", 5, a+5);printf("i    %p\n", &i);for (i=0; i<=5; i++){a[i] = 1;}

打印结果:
a[0] 0xbfa34848
a[1] 0xbfa3484c
a[2] 0xbfa34850
a[3] 0xbfa34854
a[4] 0xbfa34858
a[5] 0xbfa3485c
i    0xbfa3485c
后进入死循环
因为我们知道,栈的分配是从高地址往低地址方向分配,而堆是从低地址往高地址方向分配,所以i的地址最高,后依次是a4 a3 a2 a1 a0,如果将a4加1得a5,即向高处移动四个字节,正好获得的是i的地址,这个时候将a5设置为1,其实就是将i设置为1,所以i从5又变为了1,进入了死循环。

int a = 10;static int b;int c;const char* s = "abcdefg";const char* s1 = "abcdefg";const char* s2 = s;void f1(){}int main(int argv, char** argc){int i;static int j;char* p = (char *)malloc(10);printf("a %p\n", &a);printf("b %p\n", &b);printf("c %p\n", &c);printf("s %p : %p\n", &s, s);printf("s1 %p : %p\n", &s1, s1);printf("s2 %p : %p\n", &s2, s2);printf("i %p\n", &i);printf("j %p\n", &j);printf("f1 %p\n", f1);printf("p %p\n", p);}



结果
a 0x804a024
b 0x804a044
c 0x804a038
s 0x804a028 : 0x80487a0
s1 0x804a02c : 0x80487a0
s2 0x804a03c : 0x80487a0
i 0xbfac0448
j 0x804a048
f1 0x8048564
p 0x8112008

内存中从低地址到高地址依次是 f1 "abcdefg" a s s1 c s2 b j p i
符合我们了解的内存模型,从低地址到高地址依次是代码段 常量区 初始化的数据段 未初始化的数据段 堆 栈
其中,f1属于代码段,a s s1初始化数据段, c属于未初始化数据段, 静态b属于未初始化数据段,局部静态j属于全局数据段,p所指向内存属于堆段,i属于局部变量属于栈。


原创粉丝点击