C程序的内存结构

【版权声明:本文为博主原创文章,未经博主允许不得转载】

==================================================================

运行环境:Ubuntu 18.04 LTS 64bit

     Linux 4.15.0-34-generic x86_64 GNU/Linux

     gcc-7.3.0

==================================================================

一、概述

一般情况下,一个可执行C程序在内存中主要包含5个区域,分别是代码段(text),数据段(data),BSS段,堆段(heap)和栈段(stack)。其中前三个段(text,data,bss)是程序编译完成就存在的,此时程序并未载入内存进行执行。后两个段(heap,stack)是程序被加载到内存中时,才存在的。下面分别介绍:

本文的参考示例代码(test.c)如下:

int gDataA = 10; 
int gDataB;
const int gDataC = 20; 

int func(int x,int y,int z)
{
    return (x + y + z + gDataC);
}

int main(void)
{
    int x = 2;
    static int DataA = 4;
    gDataB = func(x,DataA,gDataA);
    DataA = gDataB - DataA;
    return 0;
}

编译:gcc test.c -o test -O2 -fno-inline

二、代码段

代码段(text):就是C程序编译后的机器指令,也就是我们常见的汇编代码,汇编代码可以通过objdump查看,如下图所示:

# objdump - test > test.hex

# vi test.hex

其中:

红框表示该段是text段;

绿框表示的是程序的虚拟地址;

蓝框表示的是text段的实际内容,也就是一些连续的二进制机器码;

黄框的汇编内容实际上并不存在于text段中,它只是objdump程序根据蓝框中机器码解析出来的汇编信息,提高可读性。

通过objdump -h test,读取text段的信息,可见text段是READONLY的,这也防止其他程序恶意修改程序。

 

二、数据段

数据段(data):用来存放显式初始化的全局变量或者静态(全局)变量。例如以下反汇编代码片段

 

其中DataA.1804可以理解为DataA的一个别名,与编译器有关,在此不再深入分析。
可见gDataA和DataA两个显式初始化的全局变量都存储在data段中。其值分别是0x000a和0x0004。(x86_64小端处理器)

上图红框中的内容是在二进制文件中实际存储的内容,而黄框中的内容是对应二进制解析出来的,对于数据段中内容解析出来后是没有意义的,因为它们是数据,不是指令。可以理解为objdump工具的“过度解析”(下同,不再累述)。

 

三、BSS段

BSS段(Block Started by Symbol):存储未初始化的全局变量或者静态(全局)变量。例如以下反汇编代码片段:

gData是未被初始化的全局变量,因此存放在bss段,其值默认是0。

 

四、栈段(stack)

我们很多时候习惯统一来说“堆栈”,但是堆和栈是两个不同的区域,对于我们程序员来说还是要搞清楚的。

栈段(stack):存放函数调用相关的参数、局部变量的值,以及在任务切换的上下文信息。为了更便于说明问题,使用下面的示例代码(本代码仅仅是为了说明问题,风格不值得借鉴):

int func(int a1,int a2,int a3,int a4,int a5,int a6,int a7,int a8,int a9)
{
        return (a1 + a2 + a3 + a4 + a5 + a6 + a7 + a8 + a9);
}

int main(void)
{
        int a1,a2,a3,a4,a5,a6,a7,a8,a9;
        int sum;
        a1 = 0x11;
        a2 = 0x22;
        a3 = 0x33;
        a4 = 0x44;
        a5 = 0x55;
        a6 = 0x66;
        a7 = 0x77;
        a8 = 0x88;
        a9 = 0x99;
        sum = func(a1,a2,a3,a4,a5,a6,a7,a8,a9);
        return sum;
}

编译并objdump:

# gcc stack.c -o stack -O2 -fno-inline  // 使用-fno-inline的目的是为了禁止内联,便于展现函数调用及参数传递过程。感兴趣的同学可以不加该参数编译下。

# objdump -M intel -D stack > stack.hex

 查看main函数和func函数的反汇编代码:

对于main函数中,为何有的使用mov,有的使用push可以参考函数调用过程中的参数传递规则(简言之就是x86_64可以使用6个寄存器(分别是edi,esi,edx,ecx,r8d,r9d)直接传递参数,而不用压栈和出栈,提高性能)。

后面的参数都使用压栈的方式传递给被调用函数func。

从这里也可以知道,在我们自己写函数的情况的时候,参数个数最好控制在6个以内,否则就需要用堆栈,而堆栈的访问速度是没有寄存器快的,性能就会下降。这是x86_64架构的情况,其它的如x86、ARM、ARM64、MIPS传参的规则是不一样的,就需要根据实际情况考虑了。

对于func函数,直接使用edi、esi、edx、ecx、r8d和r9d获得前6个参数值,后面的参数就需要从stack上获取。

栈区是由操作系统分配和管理的区域。

 

五、堆段(heap)

动态内存分配的区域,也就是malloc申请的内存区。注意malloc申请的内存不用的时候必须free,并指向NULL。

堆区是由程序员显示分配与管理的。

 

六、小结

下面来自经典书籍《UNIX环境高级编程》中对此有如下描述:

对于C代码编译出来的二进制,代码段(text)、数据段(data)和BSS段是代码编译后就确定了,但是编译后的程序存储在硬盘中,当要执行的的时候,操作系统负责将该程序load到内存中。
在程序运行的之后,就会涉及到堆区和栈区的操作。一般情况下,堆区是向上生长的,栈区是向下生长的,当然不同的处理器架构是不同的。

posted @ 2018-09-20 18:33  编编码旅旅行  阅读(1823)  评论(0编辑  收藏  举报