原创 windows 和linux 下C/C++程序的内存分布 (编)

2011-9-18 23:01 1793 10 10 分类: MCU/ 嵌入式

C程序的内存分布

C程序一般分为

1.程序段(text:  程序段为程序代码在内存中的映射.一个程序可以在内存中多有个副本.

2.文字常量区: 常量字符串就是放在这里的。

3.初始化过的数据(data):  在程序运行之初已经对变量进行初始化的.全局变量和静态变量的存储是放在一块的,初始化的全局变量和静态变量在一块区域, 未初始化的全局变量和未初始化的静态变量在相邻的另一块区域。

4.未初始化过的数据(bss: 在程序运行初未对变量进行初始化的数据。

6.堆栈(stack): 存储局部,临时变量,在程序块开始时自动分配内存,结束时自动释放内存.存储函数的返回指针. 当函数被调用时,它们被压入栈;当函数返回时,它们就要被弹出堆栈。

7.(heap): 存储动态内存分配,需要程序员手工分配(cmalloc函数,c++new函数),手工释放(free delete 函数).

34称为静态存储区,67称为动态存储区。

 

windows进程中的内存结构

#include <stdio.h>

#include <string.h>

 

int a = 0; //全局初始化区

char *p1; //全局未初始化区

main()

{

int b; //

char s[] = "abc"; //

char *p2; //

char *p3 = "123456";// 123456\0在常量区,p3在栈上。

static int c =0;// 全局(静态)初始化区

static int uc,uc1,uc2;// 全局(静态)未初始化区

p1 = (char *)malloc(10);

p2 = (char *)malloc(20);

//分配得来得1020字节的区域就在堆区。

strcpy(p1, "123456");// 123456\0放在常量区,编译器可能会将它与p3所指向的"123456"优化成一个地方。

 

printf("p1 \t\t\t0x%08x\n",p1);

printf("p2 \t\t\t0x%08x\n",p2);

printf("&p3 \t\t\t0x%08x\n",&p3);

printf("&p2 \t\t\t0x%08x\n",&p2);

printf("s \t\t\t0x%08x\n",s);

printf("&s[1] \t\t0x%08x\n",&s[1]);

printf("&b \t\t\t0x%08x\n",&b);

printf("main地址\t\t0x%08x\n",main);

printf("文本常量区\t\t0x%08x\n",p3);

printf("全局初始化区\t\t0x%08x\n",&a);

printf("静态初始化区\t0x%08x\n",&c);

printf("全局未初始化区\t\t0x%08x\n",&p1);

printf("静态未初始化区\t0x%08x\n",&uc);

printf("静态未初始化区\t0x%08x\n",&uc1);

printf("静态未初始化区\t0x%08x\n",&uc2);

}

你可以看到这些变量在内存是连续分布的,但是本地变量和全局变量分配的内存地址差了十万八千里,而全局变量和静态变量分配的内存是连续的。这是因为本地变量和全局/静态变量是分配在不同类型的内存区域中的结果。对于一个进程的内存空间而言,可以在逻辑上分成3个部份:代码区,静态数据区和动态数据区。动态数据区一般就是“堆栈”。“栈(stack)”和“堆(heap)”是两种不同的动态数据区,栈是一种线性结构,堆是一种链式结构。进程的每个线程都有私有的“栈”,所以每个线程虽然代码一样,但本地变量的数据都是互不干扰。一个堆栈可以通过“基地址”和“栈顶”地址来描述。全局变量和静态变量分配在静态数据区,本地变量分配在动态数据区,即堆栈中。程序通过堆栈的基地址和偏移量来访问本地变量。

 

 

├———————┤低端内存区域

……

├———————┤

动态数据区

├———————┤

……

├———————┤

代码区

├———————┤

静态数据区

├———————┤

……

├———————┤高端内存区域

堆栈是一个先进后出的数据结构,栈顶地址总是小于等于栈的基地址。

 

linux c程序存储空间布局

进程在内存中的影像.

      我们假设现在有一个程序, 它的函数调用顺序如下.

      main(...) ->; func_1(...) ->; func_2(...) ->; func_3(...)

      : 主函数main调用函数func_1; 函数func_1调用函数func_2; 函数func_2调用函数func_3

      当程序被操作系统调入内存运行, 其相对应的进程在内存中的影像如下图所示.

        (内存高址)

        +--------------------------------------+

        |             ......                   |  ... 省略了一些我们不需要关心的区

        +--------------------------------------+

        |  env strings (环境变量字串)          | \

        +--------------------------------------+  \

        |  argv strings (命令行字串)           |   \

        +--------------------------------------+    \

        |  env pointers (环境变量指针)         |    SHELL的环境变量和命令行参数保存区

        +--------------------------------------+    /

        |  argv pointers (命令行参数指针)      |   /

        +--------------------------------------+  /

        |  argc (命令行参数个数)               | /

        +--------------------------------------+

        |            main 函数的栈帧           | \

        +--------------------------------------+  \

        |            func_1 函数的栈帧         |   \

        +--------------------------------------+    \

        |            func_2 函数的栈帧         |     \

        +--------------------------------------+      \

        |            func_3 函数的栈帧         |      Stack ()

        +......................................+      /

        |                                      |     /

                      ......                        /

        |                                      |   /

        +......................................+  /

        |            Heap ()                 | /

        +--------------------------------------+

        |        Uninitialised (BSS) data      |  非初始化数据(BSS)

        +--------------------------------------+

        |        Initialised data              |  初始化数据区

        +--------------------------------------+

        |        Text                          |  文本区

        +--------------------------------------+

        (内存低址)

        这里需要说明的是:

        i)   随着函数调用层数的增加, 函数栈帧是一块块地向内存低地址方向延伸的.              随着进程中函数调用层数的减少, 即各函数调用的返回, 栈帧会一块块地              被遗弃而向内存的高址方向回缩. 各函数的栈帧大小随着函数的性质的不同而不等, 由函数的局部变量的数目决定.

        ii)  进程对内存的动态申请是发生在Heap()里的. 也就是说, 随着系统动态分配给进程的内存数量的增加, Heap()有可能向高址或低址延伸, 依赖于不             CPU的实现. 但一般来说是向内存的高地址方向增长的. 堆和栈相向而生,堆和栈之间有个临界点,称为stkbrk.

        iii) BSS数据或者Stack()的增长耗尽了系统分配给进程的自由内存的情况下, 进程将会被阻塞, 重新被操作系统用更大的内存模块来调度运行.              (虽然和exploit没有关系, 但是知道一下还是有好处的)

        iv)  函数的栈帧里包含了函数的参数(至于被调用函数的参数是放在调用函数的栈帧还是被调用函数栈帧, 则依赖于不同系统的实现),它的局部变量以及恢复调用该函数的函数的栈帧(也就是前一个栈帧)所需要的数据, 其中包含了调用函数的下一条执行指令的地址.

        v)   非初始化数据(BSS)区用于存放程序的静态变量, 这部分内存都是被初始化为零的. 初始化数据区用于存放可执行文件里的初始化数据. 这两个区统称为数据区.

        vi)  Text(文本区)是个只读区, 任何尝试对该区的写操作会导致段违法出错. 文本区是被多个运行该可执行文件的进程所共享的. 文本区存放了程序的代码.

    2) 函数的栈帧.

       函数调用时所建立的栈帧包含了下面的信息:

       i)   函数的返回地址. 返回地址是存放在调用函数的栈帧还是被调用函数的栈帧里, 取决于不同系统的实现.

       ii)  调用函数的栈帧信息, 即栈顶和栈底.

       iii) 为函数的局部变量分配的空间

       iv)  为被调用函数的参数分配的空间--取决于不同系统的实现.

 

另外:

返回值即使放在栈中也未必不行。因为每个进程拥有自己的栈空间,只要在其它函数运行之前,把返回值取出来就行。栈中的数据一般不会被自动销毁,栈指针动了一下而已,数据还在那里。

返回值如何实现取于编译器和采用的编译规则, 其中并没有通用的标准. 比如要从寄存器返回, 但不同体系的机器,其寄存器是不同的, 无法统一. Stroustrup 说有些系统中c++是解析的, 那么这些c++的解析实现采用的返回方式与编译实现采用的返回方式可能也不同. 所以我们只能讨论范围限制在某种特定机器上的某个编译器上.在这里,我们约定是x86/gcc3 (linux) 当返回值能容纳在一个寄存器中时, 通常都用一个寄存器返回.这是没有问题的. 当返回值足够小能容纳在两个寄存器中, 比如 edx:eax, 通过这两个寄存器返回. 当要求返回的对象比较大时, 比如 x = foo (), sizeof (x) 比较大, 则调用者将 x 的地址通过栈传递给被调用函数 foo, foo 把返回值写到 x . 有点象这样:

x = foo ();

转化为 (void) foobar (&x);

再强调一次, 如何返回并不是c的一个组成部分.  c标准是抽象的, 并不关心"实际如何返回". 实际如何返回, 是编译器的事.

视情况的不同, 调用者在调用前, 也许要为被调用者在堆栈中提供一些空间,供被调用者使用.

下图左边的是UNIX/LINUX系统的执行文件,右边是对应进程逻辑地址空间的划分情况。

 

PARTNER CONTENT

文章评论0条评论)

登录后参与讨论
我要评论
0
10
关闭 站长推荐上一条 /3 下一条