GCC(GNU Compiler Collection,GNU编译器套件)是由GNU开发的编程语言译器。GNU编译器套件包括C、C++、 Objective-C、 Fortran、Java、Ada和Go语言前端,也包括了这些语言的库(如libstdc++,libgcj等)。GCC的初衷是为了GNU操作系统专门编写一款编译器,现已被大多数类Unix操作系统(如Linux、BSD、Mac OS X等)采纳为标准的编译器。甚至在微软的Windows上也可使用GCC。
gcc 主要包含以下四部分功能:
gcc xxx.c常见的编译选项:
gcc -E xxx.c -o xxx.i
gcc -S xxx.i -o xxx.s
gcc -c xxx.s -o xxx.o
gcc xxx.o -o xxx
C语言的编译过程非常复杂,其中包含了编译器、硬件、工具链的知识。编译程序的过程是对源程序进行词法和语法的分析,将高级语言指令转换位等效功能的汇编代码,再由汇编转为机器语言并且按照操作系统对可执行文件格式的要求链接生成可执行文件。即:c源程序—》预编译处理(.c)—》编译优化(.s、.asm)—》汇编程序(.obj、.o、.a、.ko)—》链接程序(.exe、.elf、.axf)。如下图所示:
我们通过一个编译hello.c的例子来了解一下编译的过程:
#include
int main()
{
printf(“Hello, world.\n”);
return 0;
}
原来我们可能会直接:
panghu@Ubuntu-14:~$gcc hello.c -o hello
panghu@Ubuntu-14:~$./hello
来编译运行生成可执行文件hello,我们来从gcc的四个过程来看。
panghu@Ubuntu-14:~$ gcc -E hello.c -o hello.i
预处理是读取c的源程序,对程序中的伪指令(宏定义)和特殊符号进行代替处理,生成一个没有宏定义、没有条件编译指令和特殊符号的输出文件。这个文件含义与没处理前的源文件相同,仍为c文件,显示内容有所不同。主要包含以下三个方面:
(1)宏定义指令:如#define、#undef及编译器内建的一些宏命令:_ DATE_、_ FILE_、_ LINE_、_ _TIME__等。
(2)条件编译指令:如#ifdef、#ifndef、#else、#elif、#endif等。
(3)头文件包含指令:#include
编译完成后可通过cat hello.i
来查看预处理后的代码(前面好多头文件预处理命令,但最后几行还是c的语句):
panghu@Ubuntu-14:~$ gcc -S hello.i -o hello.s
在编译过程中通过词法分析和语法分析检查指令是否符合规则,然后翻译成等效功能的中间代码或汇编代码,在编译过程中往往会伴随着优化操作,优化一部分是对中间代码优化,该部分不依赖于计算机,另种优化则是针对目标代码的生成。
现在的gcc编译器将预处理和编译合为一个步骤,用cc1工具完成,其实gcc就是根据不同的参数调用相应的处理工具,例如预处理编译程序cc1、汇编器as、连接器ld。
编译后的hello程序汇编代码如下:
3. 汇编(Assembly)
panghu@Ubuntu-14:~$ gcc -c hello.s -o hello.o
汇编过程就是将汇编代码翻译成目标机器指令的过程,被翻译的系统处理每个C语言源程序都将经过汇编转换成目标文件,此时目标文件里是与源程序等效功能的机器代码。
目标文件由段组成,通常一个目标文件中至少有两个段:
目标文件中为机器码,打开后会发现是乱码:
4. 链接(Linking)
panghu@Ubuntu-14:~$ gcc hello.o -o hello
汇编后所生成的目标文件不会被立即执行,还有许多问题待解决,例如源文件中的函数会引用另一个源文件的函数或变量,在程序中调用了库函数等问题,因此需要链接程序解决这类问题,链接主要就是通过连接器ld将于源文件有关的目标文件建立彼此的链接,使得所有目标文件成为一个可被操作的系统执行的统一整体,即可执行文件。
编译过程可分为六个步骤:词法分析、语法分析、语义分析、源代码优化、代码生成、目标代码优化。
在编译的最后连接过程中主要是把各个模块间的引用联系在一起,使得模块间能够按有顺序的衔接。其主要过程包括:地址和空间的分配(Address and Storage Allocation),符号决议(Symbol Resolution),重定位(Relocation)等。连接处理的方式又分为两种:
对于可执行文件中的函数调用,通常会采用上述的两种方式进行链接,动态链接虽然能够使可执行文件变小,而内存中只需要保存一份代码可供多个共享对象的进程调用,从而节省一些内存,但不是所有情况都适用。
在Linux/Unix下的可执行文件及动态库都是以ELF(Executable Linkage Format)格式存在的,在Linux可调用readelg filename -e
命令来查看ELF文件的头:
库用于将相似的函数打包到一个单元中,然后这些单元可与其他开发人员共享,Linux支持两种类型的库,每种库各有其优缺点,静态库包含在编译时静态绑定到一个程序的函数,动态库(共享库)则是在加载应用程序时被加载,且在程序运行时绑定。在Linux下几个重要的函数库存放在/lib
和/usr/lib
;头文件则放在/usr/include
。
静态库:这类库的名字一般为libxxx.a,利用静态库编译成的文件比较大,整个函数库都会被加载到目标代码中,而优点就是编译完成后就不需要外部的函数库支持,但如果静态函数库改变则程序需要重新编译。
动态库:这类库的名字一般为libxxx.so,动态库又称共享库,动态库在编译时没有被编译到目标代码中,在程序执行到相关函数才调用函数库中的相应函数。因此可执行文件较小,但需要注意的是程序运行环境中要提供相应的库,与静态库相比动态库的改变不需要进行重新编译,如果多个程序要用到同一函数库,则可考虑选择动态库。
静态函数库和动态函数库都由*.o目标文件生成。
静态库制作步骤:
静态函数库的命名必须是 lib[库的名字].a
创建过程:
首先创建下面文件:
add.h
#ifndef ADD_H
#define ADD_H
int add(int x,int y);
#endif
add.c
#include
#include "add.h"
int add(int x, int y)
{
return (x + y);
}
sub.h
#ifndef _SUB_H_
#define _SUB_H_
int sub(int x, int y);
#endif
sub.c
#include
#include "sub.h"
int sub(int x, int y)
{
return (x - y);
}
main.c
#include
#include "sub.h"
#include "add.h"
int main()
{
int a, b;
a = add(1, 2);
b = sub(10, 5);
printf("a = % d, b = % d\n", a, b);
return 0;
}
上面中头文件中的#ifndef
为“if not defined”的简写是宏定义的一种,它可以根据是否定义了一个变量来进行分支选择,一般用于调试,他是预处理功能中三种(宏定义,文件包含和条件编译)中的第三种——条件编译。其定义如下:
#define x //定义一个宏
...
#endif
//C语言在对程序进行编译时,会先根据预处理命令进行“预处理”。C语言编译系统包括预处理,编译和链接等部分。
#ifndef x //先测试x是否被宏定义过
#define x
程序段1 //如果x没有被宏定义过,定义x,并编译程序段 1
#else
程序段2 //如果x已经定义过了则编译程序段2的语句,“忽视”程序段 1
#endif//终止if
最主要的目的是防止头文件重复包含和定义,虽然也可用条件语句,但条件语句会对整个源程序编译,生成目标代码较长,而条件编译会根据条件只编译其中的程序段1或程序段2,生成较短的目标程序。
然后执行gcc -c file1.c -o file1.o
生成目标文件:
panghu@Ubuntu-14:~/add$ gcc -c add.c
panghu@Ubuntu-14:~/add$ gcc -c sub.c
生成add.o 和 sub.o ,再通过ar -cr libname.a file1.o file2.o ··· dileN.o
制作动态库,其中-c表示create(创建),-r(replace)替换:
panghu@Ubuntu-14:~/add$ ar -cr libaddsub.a add.o sub.o
使用方法:
通过:gcc main.c -o main -L lib_path -lname
命令后会编译main.c会把静态库加载到main中:
panghu@Ubuntu-14:~/add$ gcc -o main main.c -L. -laddsub
-L指定静态函数库的位置供查找,其中L后面还有’.’,表示静态函数库在本目录下查找。
-l 制定了函数库名,由于静态库命名为libxxx.a因此忽略其中的lib和.a,编译后删除libaddsub.a后main依然可以运行,因为静态库的内容已经加载进去了。结果如下:
静态链接过程图:
接着上面得步骤操作建立动态库的链接和使用,用下面的命令可产生 libaddsub.so 的动态库 :
panghu@Ubuntu-14:~/add$ gcc -shared -fpic -o libaddsub.so add.c sub.c
panghu@Ubuntu-14:~/add$ gcc -o out main.c -L. -laddsub
此时会生成 out 可执行文件但是还不能运行,需要将生成的动态函数库放到自动查找的目录**/usr/lib** 或**/lib**中去,因此我们需要:
完成后我们可通过ll
命令来查看各文件所占有的内存大小:
我们可发现有静态库链接生成的 main 文件所占用空间大于由动态库链接生成的 out 文件所占用的空间,这个差距还会随着代码规模的增大而增大。
参考资料:https://www.cnblogs.com/WindSun/p/11287927.html
http://smilejay.com/2012/01/c_compilation_stages/