一、程序的编译流程(以 C 语言为例)
编译一个 C 程序从可以分为四个阶段:预处理 --> 编译(生成汇编代码)--> 汇编 --> 链接。
下面以大家最熟悉的 hello world 程序为例,编译器为 linux 下的 gcc。下面是 hello.c 文件的内容:
#include <stdio.h>
#define HELLO "hello world marco\n"
int main()
{
#ifdef HELLO
printf(HELLO);
#else
printf("hello world\n");
#endif
return 0;
}
使用 gcc 直接编译可以生成一个 a.out 的可执行文件:
gcc hello.c
执行该文件可以打印 hello world:
下面我们把几个步骤分开执行来理解每个步骤所完成的工作。
1.1 预处理(Preprocessing)
预处理完成的工作:
- 头文件包含:将 #include 的内容复制到 .c 文件里面,如果 .h 文件内还有 .h 文件,则进行递归替换。
- 宏定义替换:将 #define 的内容进行替换。
- 条件宏:如 #ifdef,#ifndef,#else,#elif,#endif等,预处理只保留条件满足的代码。
- 去掉注释。
使用 gcc 的 -E 选项可以让 gcc 只执行预处理步骤:
gcc -E hello.c -o hello.i
查看预处理生成的 hello.i 文件可以发现文件的 #include <stdio.h> 被 stdio.h 头文件的内容所替换,#define 的 HELLO 宏也被替换到代码中,并且代码只保留了 #ifdef 条件为真的部分的代码:
1.2 编译(Compile)
编译完成的工作:
- 检查词法和语法规则,如果有问题则报错。
- 将预处理生成的代码转换成汇编代码。
使用如下命令执行编译步骤:
gcc -S hello.i -o hello.s
生成的 hello.s 文件内容如下:
1.3 汇编(Assemble)
汇编过程将编译生成的汇编代码(hello.s)转换成机器码,生成目标文件(hello.o),是二进制格式。
使用如下命令执行汇编步骤:
gcc -c hello.s -o hello.o
1.4 链接(Link)
C/C++代码经过汇编之后生成的目标文件(*.o
)并不是最终的可执行二进制文件,而仍是一种中间文件(或称临时文件),目标文件仍然需要经过链接 (Link) 才能变成可执行文件。
既然目标文件和可执行文件的格式是一样的(都是二进制格式),为什么还要再链接一次呢?
因为编译只是将我们自己写的代码变成了二进制形式,它还需要和系统组件(比如标准库、动态链接库等)结合起来,这些组件都是程序运行所必须的。
链接(Link)其实就是一个“打包”的过程,它将所有二进制形式的目标文件(.o)和系统组件组合成一个可执行文件。完成链接的过程也需要一个特殊的软件,叫做链接器(Linker)。
使用如下命令完成最后的链接操作:
gcc -o hello hello.o
执行链接生成的可执行文件:
二、C语言存储空间的布局
C 程序编译后生成的目标文件由下面几个部分组成:
- 正文段(code segment/text segment,.text 段):也被称为代码段,通常是用来存放程序执行代码的一块内存区域。这部分区域的大小在程序运行前就已经确定,并且该内存区域通常属于只读,某些架构也允许代码段为可写,即允许修改程序。在代码段中,也有可能包含一些只读的常量,例如字符串常量等。CPU执行的机器指令部分。( 存放函数体的二进制代码 )
- 只读数据段(read only data segment,.rodata 段):只读数据段是程序使用的一些不会被改变的数据,使用这些数据的方式类似查表式的操作,由于这些变量不需要修改,因此只需放在只读存储器中。
- 已初始化读写数据段(data segment,.data 段):通常用来存放程序中已初始化全局变量(或被 static 修饰的已初始化局部变量)的一块内存区域,数据段属于静态内存分配。已初始化读写数据段(RW data,.data)指的是在程序中声明,并且具有初值的全局变量,或者是被 static 修饰的已初始化局部变量,这些变量需要占用存储器空间,在程序执行时它们需要位于可读写的内存区域,并具有初值,以供程序读写。
- BSS 段(bss segment,.bss段):通常用来存放程序中未初始化的全局变量(或被 static 修饰的未初始化局部变量)的一块内存区域。BSS是英文 Block Started by Symbol 的简称,BSS 段属于静态内存分配。未初始化数据是在程序中声明,但是不具有初值的变量,这些变量在程序运行之前不需要占用存储空间。读取BSS段大小,然后在内存中紧跟data段之后分配空间,并且清零(这也是为什么全局表量和static局部变量不初始化会有0值得原因)。在 C++中,已经不再严格区分 bss 和 data了,它们共享一块内存区域。静态存储区包括bbs段和data段。
- 堆(heap):堆是用于存放进程运行中被动态分配的内存段,它的大小并不固定,可动态扩张或缩减。当进程调用 malloc 等函数分配内存时,新分配的内存就被动态添加到堆上(堆被扩张);当利用 free 等函数释放内存时,被释放的内存从堆上被剔除(堆被缩减)。一般由程序员分配释放(new/malloc/calloc delete/free),若程序员不释放,程序结束时可能由 OS 回收。注意:它与数据结构中的堆是两回事,但分配方式倒类似于链表。
- 栈(stack):栈又称堆栈,是用户存放程序临时创建的局部变量,也就是我们函数大括号 "{}" 中定义的变量(不包括 static 声明的变量)。除此以外,在函数被调用时,其参数也会被压入发起调用的进程栈中,并且等调用结束后,函数的返回值也会被存放在回栈中。由于栈的先进先出特性,所有栈特别方便用来保存/恢复调用现场。从这个意义上讲,把堆栈看成一个寄存、交换临时数据的内存区。由编译器自动分配释放,存放函数的参数值,局部变量的值等。其操作方式类似于数据结构中的栈。