Chinaunix首页 | 论坛 | 博客
  • 博客访问: 535586
  • 博文数量: 139
  • 博客积分: 6000
  • 博客等级: 准将
  • 技术积分: 1840
  • 用 户 组: 普通用户
  • 注册时间: 2008-05-11 22:40
文章分类

全部博文(139)

文章存档

2011年(1)

2009年(3)

2008年(135)

我的朋友

分类: C/C++

2008-07-07 15:56:21

从历史上讲,C程序一直由下面几部分组成:

(1) 栈
由编译器自动分配释放管理。局部变量及每次函数调用时返回地址、以及调用者的环境信息(例如某些机器寄存器)都存放在栈中。新被调用的函数在栈上为其自动和临时变量分配存储空间。通过以这种方式使用栈,C函数可以递归调用。递归函数每次调用自身时,就使用一个新的栈帧,因此一个函数调用实例中的变量集不会影响另一个函数调用实例中的变量。
    a.局部变量
    b.函数调用时返回地址
    c.调用者的环境信息(例如某些机器寄存器)

(2) 堆
需要由程序员分配释放管理,若程序员不释放,程序结束时可能由OS回收。通常在堆中进行动态存储分配。
 如程序中的malloc, calloc, realloc等函数都从这里面分配。堆是从下向上分配的。

(3) 非初始化数据段
通常将此段称为bss段,这一名称来源于早期汇编程序的一个操作符,意思是“block started by symbol(由符号开始的块)”,未初始化的全局变量和静态变量存放在这里。在程序开始执行之前,内核将此段初始化为0。函数外的说明:long sum[1000] ; 使此变量存放在非初始化数据段中。
    a.未初始化的全局变量
    b.未初始化的静态变量

(4) 初始化的数据
通常将此段称为数据段,它包含了程序中需赋初值的变量。初始化的全局变量和静态变量存放在这里。例如,C程序中任何函数之外的说明:int maxcount = 99; 使此变量以初值存放在初始化数据段中。
    a.初始化的全局变量
    b.初始化的静态变量

(5) 正文段
CPU执行的机器指令部分。通常,正文段是可共享的,所以即使是经常环境指针环境表环境字符串执行的程序(如文本编辑程序、C编译程序、s h e l l等)在存储器中也只需有一个副本,另外,正文段常常是只读的,以防止程序由于意外事故而修改其自身的指令。
 
下面的内存结构显示了这些段的典型安排:

下面给出一般的c程序存储布局的典型安排:

用户空间的程序使用低2G的虚拟内存,内核空间使用高2G

高地址    ——0x7FFFFFFF———

                命令行参数和环境变量

                 ——————————

                 栈空间,向下增长

               ___________________

                堆空间,向上增长

               ———————————

                 未初始化的数据

              ———————————

                已初始化的数据

              ———————————

               正文段

低地址—0x00000000————

 可以注意到未初始化的数据段的内容并不放在磁盘上的程序文件中,因为,在程序开始运行前他们都被设置为0。需要存放在程序文件中的只有正文段和初始化数据段。
参考:unix环境高级编程第二版p152
 
C程序存储空间布局(二)—— 内存对齐
本文测试环境是Linux系统,FC7,gcc 版本 4.1.2 20070502

表示方式:
    内存地址                (H)字节内容(L)        
0x0000 0000            B3B2 B1B0
0x0000 0004            B3B2 B1B0
0x0000 0008            B3B2 B1B0
...
...
...

首先看下面的程序:
#include

char g_c1;
short g_s;
char g_c2;
int g_i;

int main(void)
{
    char c1;
    short s;
    char c2;
    int i;
    
    printf("sizeof: char=%d,short=%d,int=%d\n", sizeof(char), sizeof(short), sizeof(int));
    printf(    "Global variable init value is 0: g_c1=%d, g_s=%d, g_c2=%d, g_i=%d\n",     g_c1, g_s, g_c2, g_i);
    printf("Local variable init value is random: c1=%d, s=%d, c2=%d, g_i=%d\n", c1, s, c2, i);
    printf("Global variable: g_c1=%p, g_s=%p, g_c2=%p, g_i=%p\n", &g_c1, &g_s,    &g_c2, &g_i);
    printf("Local variable: c1=%p, s=%p, c2=%p, i=%p\n", &c1, &s, &c2, &i);
    return 0;
}
运行输出:
sizeof: char=1,short=2,int=4
Global variable init value is 0: g_c1=0, g_s=0, g_c2=0, g_i=0
Local variable init value is random: c1=80, s=12276, c2=79, g_i=1096046812
Global variable: g_c1=0x8049944, g_s=0x8049946, g_c2=0x8049948, g_i=0x804994c
Local variable: c1=0xbfe9562f, s=0xbfe9562c, c2=0xbfe9562b, i=0xbfe95624
全局变量由编译器自动初始化为0,局部变量初始化的值是随机的。

下面看变量的内存分配:

全局变量(放在静态数据存储区,全局可见):

    内存地址                (H)字节内容(L)       
 
g_s 和  g_c1            BBxB 由低位字节开始    ,  g_c1 放置在B0字节, g_s 放置在B3B2两个字节处(g_c1=0x8049944)
0x0804 9944            B3B2 B1B0

g_c2                    xxxB 由低位字节开始    ,  g_c2 放置在B0字节(g_c2=0x8049948)
0x0804 9948            B3B2 B1B0

i                        BBBB    g_i 放置在 B3B2B1B0 四个字节,一个机器字长
0x0804 994c            B3B2 B1B0

内存存放时候有字节序对齐,以32位为准,且在内存中存放的顺序与定义时的顺序一致,即先定义的变量在内存中的低地址,后定义的变量在高地址。
同时,不足四字节的变量,优先放在一个机器字长(4字节)低位。


局部变量(放在栈空间,局部可见):
    内存地址                (H)字节内容(L)       
 
i                     BBBB    i 放置在 B3B2B1B0 四个字节,一个机器字长
0xbfe9 5624            B3B2 B1B0

c2                       Bxxx 由高位字节开始    ,   c2 放置在B3字节(c2=0xbfe9562b)
0xbfe9 5628            B3B2 B1B0

c1 和  s                BxBB 由高位字节开始    ,  c1 放置在 B3 高位字节,s放在 B1B0 低位字节
0xbfe9 562C            B3B2 B1B0
内存存放时候有字节序对齐(内存对齐),以32位为准,在内存中存放的顺序与定义时的顺序正好相反,即先定义的变量在内存中的高地址,后定义的变量在低地址。
同时,不足四字节的变量,优先放在一个机器字长(4字节)高位。

为什么会有内存对齐

   以下内容节选自《Intel Architecture 32 Manual》。
   字,双字,和四字在自然边界上不需要在内存中对齐。(对字,双字,和四字来说,自然边界分别是偶数地址,可以被4整除的地址,和可以被8整除的地址。)
   无论如何,为了提高程序的性能,数据结构(尤其是栈)应该尽可能地在自然边界上对齐。原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;然而,对齐的内存访问仅需要一次访问。
   一个字或双字操作数跨越了4字节边界,或者一个四字操作数跨越了8字节边界,被认为是未对齐的,从而需要两次总线周期来访问内存。一个字起始地址是奇数但却没有跨越字边界被认为是对齐的,能够在一个总线周期中被访问。
   某些操作双四字的指令需要内存操作数在自然边界上对齐。如果操作数没有对齐,这些指令将会产生一个通用保护异常(#GP)。双四字的自然边界是能够被 16整除的地址。其他的操作双四字的指令允许未对齐的访问(不会产生通用保护异常),然而,需要额外的内存总线周期来访问内存中未对齐的数据。


下面再看看结构体的内存空间:
#include

struct foo
{
   char c1;
   short s;
   char c2;
   int i;
};

struct bar
{
   char c1;
   char c2;
   short s;
   int i;
};

#pragma pack(1)
struct foo_pack
{
   char c1;
   short s;
   char c2;
   int i;
};
#pragma pack()

int main(void)
{
   struct foo a;
   struct bar b;
   struct foo_pack p;


   printf("struct foo c1=%p, s=%p, c2=%p, i=%p\n", &a.c1,&a.s,&a.c2,&a.i);
   printf("struct bar c1=%p, s=%p, c2=%p, i=%p\n", &b.c1,&b.s,&b.c2,&b.i);
   printf("struct foo_pack c1=%p, s=%p, c2=%p, i=%p\n", &p.c1,&p.s,&p.c2,&p.i);
  
   printf("sizeof foo is %d\n", sizeof(struct foo));
   printf("sizeof bar is %d\n", sizeof(struct bar));
   printf("sizeof foo_pack is %d\n", sizeof(struct foo_pack));
  
   return 0;
}
程序输出:
struct foo c1=0xbfe40dd8, s=0xbfe40dda, c2=0xbfe40ddc, i=0xbfe40de0
struct bar c1=0xbfe40dd0, s=0xbfe40dd2, c2=0xbfe40dd1, i=0xbfe40dd4
struct foo_pack c1=0xbfe40dc8, s=0xbfe40dc9, c2=0xbfe40dcb, i=0xbfe40dcc
sizeof foo is 12
sizeof bar is 8
sizeof foo_pack is 8


   缺省情况下,c/c++编译器默认将结构、栈中的成员数据进行内存对齐。因此,
struct foo
{
    char c1;
    short s;
    char c2;
    int i;
};
printf("struct foo c1=%p, s=%p, c2=%p, i=%p\n", &a.c1,&a.s,&a.c2,&a.i);
的输出为:
struct foo c1=0xbfe675f8, s=0xbfe675fa, c2=0xbfe675fc, i=0xbfe67600
c1、s各占两字节,c2和i各占4字节

编译器将未对齐的成员向后移,将每一个都成员对齐到自然边界上,从而也导致了整个结构的尺寸变大。尽管会牺牲一点空间(成员之间有空洞),但提高了性能。
也正是这个原因,我们不可以断言sizeof(foo) == 8。在这个例子中,sizeof(foo) == 12。

如何避免内存对齐的影响

   那么,能不能既达到提高性能的目的,又能节约一点空间呢?有一点小技巧可以使用。比如我们可以将上面的结构改成:

struct bar
{
   char c1;
   char c2;
   short s;
   int i;
};
   这样一来,每个成员都对齐在其自然边界上,从而避免了编译器自动对齐。在这个例子中,sizeof(bar) == 8。
  
如何使用c/c++中的对齐选项
有3种办法:

1. gcc的编译选项“-fpack-struct”

可以去除struct结构中额外的hole,缺点是,影响应用中所有的struct,包括从其它库中引入的struct结构

2. __attribute__ ((packed)) 声明

struct {char a; double b;} xyz __attribute__ ((packed));

不过这种简单格式的声明只对C有效,在C++中,你必须对struct中的每个成员(size>1)进行这样的声明:

struct {

char a;

double b __attribute__ ((packed));

int c __attribute__ ((packed));

} xyz;

3. 预编译选项 #pragma pack(n)

这里的n是通知编译器对此“pragma”行后出现的所有数据结构(包括stuct/union)采用n字节方式对齐(align)。如果n=1,那么表示全紧凑,struct中不会出现任何占位的hole。如果n是空,表示回复到编译器缺省的设置(一般=8)

#pragma pack(1)

//此段内所有数据结构全紧凑

#pragma pack()

  比如:

#pragma pack(1)
struct foo_pack
{
   char c1;
   short s;
   char c2;
   int i;
};
#pragma pack()  
这样sizeof(struct foo_pack)=8

参考:

unix环境高级编程第二版p152


阅读(2047) | 评论(1) | 转发(1) |
给主人留下些什么吧!~~

chinaunix网友2009-06-06 19:06:12

“且在内存中存放的顺序与定义时的顺序一致,即先定义的变量在内存中的低地址,后定义的变量在高地址" 这句话不对吧!