老青菜

程序结构

2019-01-21

Linux中,可执行文件(.o)是主要是由datatextbss三个段组成。我们可以使用命令size xxx查看结构。

admin$ size Test
Text    data     bss     dec     hex     filename
4096    4096      0      982     3D6     test

下图为Linux中可执行文件的结构图,其他系统的可执行文件(mach-o、pe)可能会稍有区别。

对照上面的图,我们看下面这段代码:

int a = 0; //全局初始化区,data段
char *p1; //全局未初始化区,bss段
int main()
{
    int b; //栈区
    char s[] = "abc"; //栈区
    char *p2; //栈区
    char *p3 = "123456"; //123456在常量区,即rodata段,也有可能在text段;p3在栈上
    static int c =0;//全局(静态)初始化区,data段
    p1 = (char *)malloc(10);//10字节区域在堆区
    p2 = (char *)malloc(20);//20字节区域在堆区
    strcpy(p1, "123456"); //123456放在常量区,即rodata段,也有可能在text段;编译器可能会将p1、p3指向一个地方
    return 0;
}

接下来,我们来看下每个段的概念。

stack栈

栈区,当程序运行后,会开启一块空间,用来存储局部变量、函数的参数、函数返回值等,系统自动分配、释放。
栈有两个特性:

  • 栈只能在一端操作,先入栈的后出,即先进后出
  • 栈区的地址是从高地址向下增长的,这些是由编译器决定的。

如何理解栈的地址从高地址向低地址增长?我们来看一段c代码:

#include "stdio.h"

int main(int argc, const char * argv[]) {
    int a;
    char b;
    int c;
    char d;
    int e[4] = {0,1,2,3};
    int f[4] = {4,5,6,7};
    printf("&a : %p\n", &a);
    printf("&b : %p\n", &b);
    printf("&c : %p\n", &c);
    printf("&d : %p\n", &d);
    printf("&e : %p,%p,%p,%p\n", &(e[0]), &(e[1]), &(e[2]), &(e[3]));
    printf("&f : %p,%p,%p,%p\n", &(f[0]), &(f[1]), &(f[2]), &(f[3]));
}

这里声明了 a、b、c、d、e、f 6个变量,接着打印他们在栈上的地址。结果如下:

&a : 0x7ffeefbff5cc
&b : 0x7ffeefbff5cb
&c : 0x7ffeefbff5c4
&d : 0x7ffeefbff5c3
&e : 0x7ffeefbff5f0,0x7ffeefbff5f4,0x7ffeefbff5f8,0x7ffeefbff5fc
&f : 0x7ffeefbff5e0,0x7ffeefbff5e4,0x7ffeefbff5e8,0x7ffeefbff5ec

可以看出它们的地址顺序:&e>&f > &a>&b>&c>&d,结论如下:

  • 先入栈的变量放在高地址,后面入栈的变量,依次向下,往低地址存放。
  • 数组的地址高于int char,尽管是后声明的(由编译器决定)。

数组

上面还有一个细节问题,我们发现数组内的元素e[0]、e[1]、e[2]、e[3]地址依次是向上增长的。这又是为什么呢?
其实针对数组,C与C++语言规范都规定了数组元素是分布在连续递增的地址上的,其实int e[4]这个声明告诉编译器:我需要在栈帧里分配一块连续的空间,大小为sizeof(int)*4,并且e[0]引用该空间的起始位置(最低地址)。
而不是栈的增长方向,先分配e[0],然后分配e[1],再分配e[2],最后分配e[3]
所以e[4]的每个元素的地址是这样的:

0x7ffeefbff5f0,0x7ffeefbff5f4,0x7ffeefbff5f8,0x7ffeefbff5fc


heap堆

堆区,当程序运行后,系统会开辟一块内存,用来动态申请内存,如mallocnew操作,由使用者自己分配、释放。
和栈不同,堆的内存是随机分配的,没有增长方向,内存空间是不连续的。

bss段

bss segment,(Block Started by Symbol segment),用来存放程序中未初始化或者初始化为0的全局变量、静态变量,一般在程序初始化时会将BSS段清零。

  • bss段上的全局变量由系统初始化,只占运行时的内存空间,而不占文件空间。

我们来看一段代码:

#include "stdio.h"

int a[1024*1024];//声明一个数组,长度1024*1024
int main(int argc, const char * argv[]) {
    printf("%d",a[0]);
    return 0;
}

这里我们声明一个长度1024*1024的数组,大小是sizeof(int)*1024*1024(大部分情况是4M)。
编译之后,我们看下可执行文件大小是多少?进入编译后的目录,执行命令:

$ ls -l Test
-rwxr-xr-x  1 admin  staff  23440  1 24 11:10 Test

最后得到文件大小是23kb。由此可见,bss段上的全局变量不占文件空间。

mach-o

Mac中,可执行文件(mach-o)结构基本相同,包含了__TEXT__DATA__OBJCothers等等段。没有bss段,应该是合并到了__DATA段里。
进入编译后的目录,执行命令:

admin$ size Test
__TEXT    __DATA    __OBJC    others    dec    hex
4096    4194304    0    4294987776    4299186176    100406000

可以看到,我们声明的未初始化的全局变量放在_data段里,大约4M,但是可执行文件依然是23KB

data段

data segment,可读可写,用于存放在编译阶段就能确定的数据(非运行时),即初始化的全局变量、常量、静态变量(全局或局部)。

  • 和bss段不同,data段存储的变量保存在目标文件中,占用可执行文件空间。

看一段简单的代码:

#include "stdio.h"

int a[1024*1024] = {0,1,2,3,4,5,6};
int main(int argc, const char * argv[]) {
    printf("%d",a[0]);
    return 0;
}

这里我们声明一个长度1024*1024的数组,并初始化7个元素,大小是sizeof(int)*1024*1024(大部分情况是4M)。

编译之后,我们看下可执行文件大小是多少?进入编译后的目录,执行命令:

$ ls -l Test
-rwxr-xr-x  1 admin  staff  4254736  1 24 11:51 Test

最后得到文件大小是>4M。包含了我们声明的全局变量数组,接着我们来看下内存布局,执行命令:

admin$ size Test
__TEXT    __DATA    __OBJC    others    dec    hex
4096    4198400    0    4295020544    4299223040    10040f000

__DATAdata段,所包含的变量,占用可执行文件的空间。

text段

text segment | code segment,通常是指用来存放程序执行代码的一块内存区域。编译时已经确定,一般是只读的区域。

  • text段里可能包含一些只读的常数变量,例如字符串常量等。

rodata段

readonly data,只读的数据段,存放C中的字符串和#define定义的常量。

参考链接

wikipedia - .bss

wikipedia - data segment

使用支付宝打赏
使用微信打赏

若你觉得我的文章对你有帮助,欢迎点击上方按钮对我打赏

扫描二维码,分享此文章