C程序编译过程及常见选项
了解gcc的编译流程,我们可以根据自己的需要让gcc在编译的任何阶段结束,以便检查或使用编译器在该阶段的输出信息。或者对最后生成的二进制文件进行控制,以便通过加入不同数量和种类的调试代码来为今后的调试做好准备。
一、编译过程
//filename:hello.c
#include <stdio.h>
#define NUM 10 /* */
int main (int argc, char **argv)
{
int i;
for(i = 0; i < NUM; i++)
{
printf("hello wolrd\n");
}
return 0;
}
从.c源文件到.out可执行文件,可分为4个大步骤:
- 预处理(Preprocessing)
- 编译(Compilation)
- 汇编(Assembly)
- 链接(Linking)
1.预处理
预处理是读取c源文件,对伪指令“替换”,生成一个没有宏定义、没有条件编译指令、没有特殊符号的输出文件。但它仍是C文件,内容有所改变。
伪指令包括:
(1)宏定义指令,如#define Name TokenString,#undef以及编译器内建的一些宏,如__DATE__,
FILE, LINE, TIME, FUNCTION 等。
(2)条件编译指令,如#ifdef,#ifndef,#else,#elif,#endif等。
(3) 头文件包含指令,如#include "FileName"或者#include 等。
预处理的过程主要处理包括以下过程:
- 将所有的#define删除,并且展开所有的宏定义
- 处理所有的条件预编译指令,比如#if #ifdef #elif #else #endif等
- 处理#include 预编译指令,将被包含的文件插入到该预编译指令的位置。
- 删除所有注释 “//”和”/* */”. 添加行号和文件标识,以便编译时产生调试用的行号及编译错误警告行号。
- 保留所有的#pragma编译器指令,因为编译器需要使用它们
通常使用下面命令进行预处理,参数-E表示进行预处理:
gcc -E filename.c -o filename.i
也可以使用以下指令完成预处理过程,其中cpp是预处理器:
cpp filename.c > filename.i
用cat和vim可查看.i文件
可看到原来的宏定义NUM已经替换成10,而头文件也被替换了,生成了一个700多行的源文件。
2.编译
编译程序所要作得工作就是通过词法分析和语法分析,在确认所有的指令都符合语法规则之后,将其翻译成等价的中间代码表
示或汇编代码。
使用下面命令进行编译生成汇编文件:
gcc -S filename.i > filename.s
可以看到相比.i的预处理文件,代码精简了很多,只有50多行的代码。
我们使用PC的编译器gcc就会编译生成x86的汇编,而使用ARM的编译器则生成ARM的汇编文件。同一份C代码不作任何修改,使用不同的编译器编译就生成在不同机器上运行的程序,这就是C程序的可移植性。我们在PC上编写程序,在PC上用ARM的交叉编译器,生成在ARM平台上的可执行程序的过程叫做交叉编译。
3.汇编
汇编过程实际上指把汇编语言代码翻译成目标机器指令的过程。对于被翻译系统处理的每一个C语言源程序,都将最终经过这
一处理而得到相应的目标文件。目标文件中所存放的也就是与源程序等效的目标的机器语言代码。 目标文件由段组成。通常一
个目标文件中至少有两个段:
- 代码段(文本段):该段中所包含的主要是程序的指令。该段一般是可读和可执行的,但一般却不可写;
- 数据段:主要存放程序中要用到的各种常量、全局变量、静态的数据。一般数据段都是可读,可写,可执行的;
gcc -c filename.s -o filename.o
4.链接
汇编程序生成的目标文件并不能立即就被执行,其中可能还有许多没有解决的问题。例如,某个源文件中的函数可能引用了另一个源文件中定义的某个符号(如变量或者函数调用等);在程序中可能调用了某个库文件中的函数,等等。所有的这些问题,都需要经链接程序的处理方能得以解决。链接处理可分为两种:
1.静态链接
2.动态链接
对于可执行文件中的函数调用,可分别采用动态链接或静态链接的方法。使用动态链接能够使最终的可执行文件比较短小,并且当共享对象被多个进程使用时能节约一些内存,因为在内存中只需要保存一份此共享对象的代码。但并不是使用动态链接就一定比使用静态链接要优越。在某些情况下动态链接可能带来一些性能上损害。
(1)静态链接
静态链接是由链接器在链接时将库的内容加入到可执行程序中的做法。在实际开发中,我们都会将不同功能的代码放到不同的源文件,而且它们存在一定的依赖关系,但是每个源文件是独立编译的,每个.c文件会生成.o文件,为了满足依赖关系,就需要对这些目标文件进行链接,从而形成一个可执行文件。这个过程就是静态链接。
静态链接的优缺点:
造成空间浪费,静态链接库的时候是以目标文件为单位,如果多个程序对同一个函数有调用,那么就对同一个目标文件有所依赖,例如多个程序调用printf()函数,那么可执行文件中就存在多个printf.o的副本。若库文件修改了,就要对程序进行重新编译。静态链接的优点是,可执行文件中具备程序运行的所有需要的文件,执行速度毋庸置疑就快了。
静态链接的过程
(1)动态链接
可解决静态链接的两个问题:1是空间浪费,2是更新困难。
动态链接,把链接这个过程推迟到了运行时再进行,在可执行文件装载时或运行时,由操作系统的装载程序加载库。动态链接的基本思想是把程序按照模块拆分成各个相对独立部分,在程序运行时才将它们链接在一起形成一个完整的程序,而不是像静态链接一样把所有程序模块都链接成一个单独的可执行文件。
动态链接的优缺点:
优点是多个程序共用一个.o文件副本,便以更新,自动链接新版本的目标文件。缺点是把链接推迟到程序运行时,每次执行程序都需要链接,因此性能会损失。
du (disk usage): 显示每个文件和目录的磁盘使用空间,也就是文件的大小。
命令参数:
-k 、 -m #显示目录中文件的大小,-k 单位KB,-m 单位MB
-h #以K M G为单位显示,提高可读性(最常用的一个)
--max-depth=1 #显示层级
du -h --max-depth=1
同一个hello.o文件,分别用静态和动态的方法链接,通过du 命令查看文件的大小,可以看到动态链接的文件比静态链接的文件小很多。
二、ELF可执行文件格式
ELF文件由4部分组成,分别是ELF头(ELF header)、程序头表(Program header table)、节(Section)和节头表(Section header table)。
Linux/Unix的可执行文件以及动态库都是以ELF(Executable Linkage Format)存在的。在Linux下,可以使用readelf命令查看ELF文件,关于加载过程所需要的信息都在ELF文件头里面,可以用使readefl filename -e来查看EFL文件头部的信息。我们可以先来查看下hello.c编译出来的hello可执行文件的ELF头信息:
readelf -d filename(可执行文件)显示动态节区的内容
readelf -a filename(可执行文件),打印ELF详细信息,可查看函数入口地址;
数据段和文本段的地址是在链接的时候决定的,而栈是动态的。
ldd命令可用来查看动态链接可执行文件依赖于哪些动态库文件
linux-vdso.so.1是Linux的装载器,其解析ELF文件信息,并加载到相应的地址空间上去
三、静态库和动态库
库是一种可以执行代码的二进制形式,可被操作系统载入内存执行。调用别人的库时,我们需要遵守许可协议,使用库可以为我们节省大量的时间,提高开发效率。Linux下库文件分两种,静态库和动态库,均有.o文件生产。
1.静态库
- 静态库文件名的命名方式是“libxxx.a”,库名前加”lib”,windows和linux下都是后缀用”.a”,“xxx”为静态库名,windows下的静态库名也叫libxxx.a;
- 链接时间: 静态库的代码是在编译过程中被载入程序中。
- 链接方式:静态库的链接是将整个函数库的所有数据都整合进了目标代码。这样做优点是在编译后的执行程序不在需要外部的函数库支持,因为所使用的函数都已经被编进去了。缺点是,可执行文件占用内存空间较大,且如果所使用的静态库发生更新改变,你的程序必须重新编译。
2.动态库
- 动态库的命名方式与静态库类似,前缀相同为“lib”,linux下后缀名为“.so(sharedobject)”即libxxx.so;而windows 下后缀名为“.dll(dynamic linklibrary)”即libxxx.dll;
- 链接时间:动态库在编译的时候并没有被编译进目标代码,而是当你的程序执行到相关函数时才调用该函数库里的相应函数。这样做缺点是因为函数库并没有整合进程序,所以程序的运行环境必须提供相应的库。优点是动态库的改变并不影响你的程序,所以动态函数库升级比较方便。
它们两个还有很明显的不同点:当同一个程序分别使用静态库,动态库两种方式生成两个可执行文件时,静态链接所生成的文件所占用的内存要远远大于动态链接所生成的文件。这是因为静态链接是在编译时将所有的函数都编译进了程序,而动态链接是在程序运行时由操作系统帮忙把动态库调入到内存空间中使用。另外如果动态库和静态库同时存在时,链接器优先使用动态库。
3.制作静态库和动态库
例如我们希望把file1.c、file2.c、…fileN.c做成库文件。
静态库制作:需将各.c文件逐个编译成.o文件再打包生产库文件。
gcc -c file1.c
gcc -c file2.c
...
gcc -c fileN.c
ar -rcs libname.a file1.o file2.o ... fileN.o
动态库制作:
gcc -shared -fPIC -o libname.so file1.c file2.c ... fileN.c
使用静态库或动态库:
gcc main.c -o myapp -L lib_path -lname
-L 就是要告诉编译链接器,把库文件链接进来;-lname的name要去掉库文件前缀和后缀。
示例:将pf.c先转成目标文件pf.o,再转成库文件libpf.a,main.c文件调用了在pf.c定义的func函数,所以main.c编译成可执行文件的时候 需要用到头文件pf.h以及库文件libpf.a
pf.c文件
#include <stdio.h>
int func(void)
{
printf("hello wolrd!\n");
return 0;
}
pf.h文件
#ifndef _PF_H_
#define _PF_H_
/* this function used to print "hello world!" */
extern int func(void);
#endif /* ----- #ifndef _PF_H_ ----- */
制作静态库libpf.a过程如下
制作动态库libpf.so过程如下 :
其中头文件解决编译的问题,而静态库解决链接的问题,动态库解决链接和运行时的问题。具体使用步骤如下:
main.c文件:
#include <stdio.h>
#include "pf.h" //""表示在当前目录下寻找头文件,当前目录找不到时请系统库文件找
int main (int argc, char **argv)
{
func();//调用了在pf.c里面定义的函数
printf("func() execute successfully!\n");
return 0;
}
main.c程序编译
我们可以看到main.c编译时在当前路径下找不到pf.h文件,因为我们的pf.h文件定义在了ch10/library下。
gcc在编译的时候,找不到头文件。这时我们可以使用编译器的 -I(大写i)选项来指定头文件的路径:
gcc在链接的时候,找不到函数的定义(即库文件)。这时要告诉链接器在链接的时候要链接包含这些函数的库文件,通过-l (小写l) 选项来指
定:
这时候你一定很惊讶,为什么指定了库文件的名字,却找不到它,原因是链接器默认到系统动态库路径(/lib、/lib64、/usr/lib)下查找相应的库文件,如果找不到就出错。链接的时候抛出错误,去链接库找原因。如果使用的动态库不在这些路径下,我们就可以使用-L(大写l)选项来指定相应库的路径:
从上面的结果可以看出,如果动态库和静态库同时存在则优先使用动态库链接。而如果想使用静态库链接,一种方法是把在/ch10/library下的动态库移除,另外一种方法是在编译时加上链接选项 -static,当然这样程序中所有的库都使用静态链接库了。
程序运行:
静态编译的程序,因为所有代码段和数据段都被链接进可执行程序中,所以可以直接运行:
而动态编译的程序,动态库中的代码段和数据段并没有被链接进可执行程序中,只是记录了需要他们的一些信息。所以程序在运行时和生成可执行文件一样需要操作系统帮忙加载这些动态库程序。这样直接执行就会出错:
Linux下在运行程序时,会默认到 /lib、/lib64、/usr/lib以LD_LIBRARY_PATH环境变量指定的路径下查找所需的动态库下查找所需的动态库文件,如果没有则抛错。而这时libmycrypto.so并不在系统库路径下,所以会出错。这时有两种解决方法:
- 将所需要的libmycrypto.so文件拷贝到/usr/lib路径下,当然这需要root权限;
- 使用export命令在LD_LIBRARY_PATH环境变量中添加该动态库所在的路径,注意该命令只是临时生效,重启后失效。另
外指定的路径必须是绝对路径;
四、GCC常见编译选项
gcc的编译选项非常非常多,我们可以使用man手册查看,下面是我们经常使用的一些编译选项:
选项 说明
-E 只进行预处理,不编译
-S 只编译,不汇编
-c 只编译、汇编,不链接
-g 编译生成可执行文件包含gdb调试信息,可被gdb调试
-o 指定编译生成可执行文件名
-I 指定include包含文件的搜索目录
-L 指定链接所需库(动态库或静态库)所在路径
-l 指定所需链接库的库名
-ansi ANSI标准
-std=c99 C99标准
-Werror 不区分警告和错误,遇到任何警告都停止编译
-Wall 开启大部分警告提示
--static 静态编译
-static 静态链接
-O0 关闭所有优化选项
-O1 第一级别优化,使用此选项可使可执行文件更小、运行更快,并不会增加太多编译时间,可以简写
为-O
-O2 第二级别优化,采用了几乎所有的优化技术,使用此选项会延长编译时间
-O3 第三级别优化,在-O2的基础上增加了产生inline函数、使用寄存器等优化技术
-Os 此选项类似于-O2,作用是优化所占用的空间,但不会进行性能优化,常用于生成最终版本