GCC编译C源码有四个步骤:
预处理-----> 编译 ----> 汇编 ----> 链接
一、 编译和链接的流程
C语言的编译链接过程要把我们编写的一个c程序(源代码)转换成可以在硬件上运行的程序(可执行代码),需要进行编译和链接。编译就是把文本形式源代码翻译为机器语言形式的目标文件的过程。链接是把目标文件、操作系统的启动代码和用到的库文件进行组织形成最终生成可执行代码的过程。过程图解如下:
从图上可以看到,整个代码的编译过程分为编译和链接两个过程,编译对应图中的大括号括起的部分,其余则为链接过程。
下面是对应的GNU工具链生成文件的过程:
说明:这些后缀并不是必须的,这只是常见的后缀方式,对于C++,对应一般为.cpp,.ii,.s,.o,exec/.so等。参考http://zhidao.baidu.com/question/89958523.html。当然,本质上,.c/.i/.s文件都是文本文件,可以直接查看内容的。.o/exec/.so等文件需要工具查看一些信息。
二、. 编译的三个阶段:
1. 预处理阶段
在正式的编译阶段之前进行,预处理阶段将根据已放置在文件中的预处理指令来修改源文件的内容。预处理处理的主要内容包括:
A、宏定义,如#define PI 3.14
简单来说就是进行宏替换。
B、条件编译,如#ifdef,#ifndef,#else,#endif等等
预编译程序根据这些指令,将不必要参与编译的代码过滤掉。
C、头文件包含,如#include <filename.h>, #include "filename.h"
D、特殊符号,预编译程序可以识别一些特殊的符号
典型的就是__LINE__、__FILE__等编译器内置的预定义宏了。
总之,预处理的核心工作就是“替换”。当然,预处理也会去掉代码中的注释内容,总之,预处理的目的就是简化编译阶段扫描的内容。
对应的GCC选项:-E(预处理,但不编译),输出为对.c文件预处理后的结果,默认输出到控制台,使用-o指定输出到文件。对于GNU工具链,其提供了独立的预处理器,为cpp。
举例如下:
// File: test.h #define MACRO_A 1 int foo();
// File: test.cpp //#include <stdio.h> #include "test.h" #define MACRO_B "macro_B" int main() { printf("MACRO_A is :%d; MACRO_B is: %s\n", foo(), MACRO_B); return 1; } int foo() { return MACRO_A; }
编译:
gcc test.cpp -E -o out.ii
说明:这里的test.cpp中,注释了#include <stdio.h>进行预处理是不会报错的,只有在编译的时候才会提示printf错误。下面是输出的out.ii的内容:
# 1 "test.cpp" # 1 "<built-in>" # 1 "<命令行>" # 1 "test.cpp" # 1 "test.h" 1 int foo(); # 4 "test.cpp" 2 int main() { printf("MACRO_A is :%d; MACRO_B is: %s\n", foo(), "macro_B"); return 1; } int foo() { return 1; }
说明:这里可以看到,里面有一些#开头的信息,这些信息在编译阶段是会忽略(不会被扫描)的,这些行的内容是一些记录一些行信息和文件的信息等等。之所以上面把#include <stdio.h>注释掉,是因为加入这一句之后,stdio.h中的内容也会被替换到out.ii中,这样内容太多,不适合贴在这里。
如果使用cpp进行预处理,那么就是(GCC内部还是调用cpp处理的):
cpp test.cpp -o out.ii
2. 编译和优化
经过预编译得到的输出文件中,只有常量;如数字、字符串、变量的定义,以及C语言的关键字,如main,if,else,for,while,{,}, +,-,*,\等等。
编译就是指传统的编译原理中提到的内容了,词法分析、语法分析、中间代码生成、汇编代码生成等。(说明:这里的中间代码生成是编译原理中的中间表达式的代码,不是.o目标文件,好像有时候也把目标文件称之为中间文件,这里区分一下)
优化也是编译原理中涉及的内容,优化涉及的内容很多,优化可以是对中间代码本身的优化或者目标代码的生成进行(即由中间代码生成汇编代码的过程或者生成目标文件的过程),关于优化,这里不深入探讨。
总之,这里说的编译的过程,就是由预处理的文件,编译优化得到汇编文件的过程。
对应的GCC选项:-S(编译,但不汇编)。输出为.s文件。说明:gcc编译可以以源文件作为输入,其实也是可以直接用预处理的.i文件作为输入的。当然,唯一要注意的是如果原来的源文件为cpp而输出是.i不是.ii,那么.i作为输入的时候,最好使用g++,否则可能编译有问题(gcc/g++会根据文件后缀判断是c还是c++的文件,所以对应就好了)。
对于GNU工具链,其提供了独立的预编译器,为ccl(好像没有这个命令呢?总之,ccp和ccl几乎不会用到,直接用gcc就可以了)。
还是上面的例子(把#include<stdio.h>取消注释):
$ gcc test.cpp -E -o test.ii $ gcc test.cpp -S $ gcc test.ii -S $
说明:其中-S默认输出到文件中,所以不使用-o也是可以输出到文件中的。
3. 汇编
汇编实际上指把汇编语言代码翻译成目标机器指令的过程。其输出就是目标文件了(.o)。目标文件中所存放的也就是与源程序等效的目标的机器语言代码。目标文件由段组成。通常一个目标文件中至少有两个段:
代码段:该段中所包含的主要是程序的指令。该段一般是可读和可执行的,但一般却不可写。
数据段:主要存放程序中要用到的各种全局变量或静态的数据。一般数据段都是可读,可写,可执行的。
说明:汇编之后,编译的过程就完成了,得到了目标文件(.o)。
对应的GCC选项:-c(汇编,但不链接)。
还是上面的例子(用.ii/.s/.cpp作为输入都是可以的,gcc会根据后缀知道输入是什么文件,从而在此基础上继续处理):
$ gcc test.ii -c $ gcc test.s -c $ gcc test.cpp -c $
其中gcc test.s -c,就是直接编译汇编文件,而gcc test.cpp -c就会从预处理开始进行处理了。对于GNU工具链,其提供了独立的汇编器,为as。也可以使用as编译汇编文件(当然,也只能编译汇编文件了),上面的gcc test.s -c相当于:
$ as test.s -o test.o
(不使用-o test.o,那么输出默认为a.o)
总结:编译主要包括预处理、编译、汇编三个阶段,通过GCC的选项能控制GCC处理到某一步之后就停止,当然,实际的GCC处理,可能不一定是完全的一步一步的处理的,可能会有一些优化的处理方式。
三、链接
由汇编程序生成的目标文件并不能立即就被执行,其中可能还有许多没有解决的问题。
例如,某个源文件中的函数可能引用了另一个源文件中定义的某个符号(如变量或者函数调用等);在程序中可能调用了某个库文件中的函数,等等。所有的这些问题,都需要经链接程序的处理方能得以解决。
链接程序的主要工作就是将有关的目标文件彼此相连接,也即将在一个文件中引用的符号同该符号在另外一个文件中的定义连接起来,使得所有的这些目标文件成为一个能够诶操作系统装入执行的统一整体。
(1)静态链接
在这种链接方式下,函数的代码将从其所在地静态链接库中被拷贝到最终的可执行程序中。这样该程序在被执行时这些代码将被装入到该进程的虚拟地址空间中。静态链接库实际上是一个目标文件的集合,其中的每个文件含有库中的一个或者一组相关函数的代码。
(2) 动态链接
在此种方式下,函数的代码被放到称作是动态链接库或共享对象的某个目标文件中。链接程序此时所作的只是在最终的可执行程序中记录下共享对象的名字以及其它少量的登记信息。在此可执行文件被执行时,动态链接库的全部内容将被映射到运行时相应进程的虚地址空间。动态链接程序将根据可执行程序中记录的信息找到相应的函数代码。
对于可执行文件中的函数调用,可分别采用动态链接或静态链接的方法。使用动态链接能够使最终的可执行文件比较短小,并且当共享对象被多个进程使用时能节约一些内存,因为在内存中只需要保存一份此共享对象的代码。但并不是使用动态链接就一定比使用静态链接要优越。在某些情况下动态链接可能带来一些性能上损害。
四、整体过程
下面是整体的过程:
解释型语言: