浅析Linux下gcc编译过程

1. gcc简介及基本用法

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 主要包含以下四部分功能

  • 预处理(Preprocessing)
  • 编译(Compilation)
  • 汇编(Assembly)
  • 链接(Linking)

gcc xxx.c常见的编译选项

  • -E : 进行预处理,将处理的结果默认输出到控制台,预处理生成 .i 文件。
      eg: gcc -E xxx.c -o xxx.i
  • -S : 进行编译处理,生成汇编文件 xxx.s
      eg:gcc -S xxx.i -o xxx.s
  • -c : 进行汇编处理,生成目标文件 xxx.o
      eg: gcc -c xxx.s -o xxx.o
  • -o:进行链接处理,生成可执行文件 。
      eg:gcc xxx.o -o xxx
  • -v:主要查看gcc版本。
  • -g:主要用于生成调试信息(gdb)。
  • -O:主要用于代码优化处理:
      -O0:关闭所有优化选项。
      -O1:第一级别优化,采用该优化可是执行文件更小,运行快,且不会增加编译时间,简写为-O,默认为该优化。
      -O2:第二级别优化,采用了几乎所有优化技术,但会延长编译时间。
      -O3:第三级别优化,在-O2的基础上增加了产生inline函数、使用寄存器等优化技术。
      -Os:该选项类似于-O2,作用是优化所占用的空间但不会进行性能优化,常用于产生最终版本。
  • -I:指定所需链接库名。
  • -L:指定连接所需库(动态库或静态库)的路径。
  • -std:主要用于编译时遵循c99标准。
  • -Wall:主要用于尽可能产生警告信息。
  • -Werror:主要用于将警告当作错误处理。

2. gcc编译过程

  C语言的编译过程非常复杂,其中包含了编译器、硬件、工具链的知识。编译程序的过程是对源程序进行词法和语法的分析,将高级语言指令转换位等效功能的汇编代码,再由汇编转为机器语言并且按照操作系统对可执行文件格式的要求链接生成可执行文件。即:c源程序—》预编译处理(.c)—》编译优化(.s、.asm)—》汇编程序(.obj、.o、.a、.ko)—》链接程序(.exe、.elf、.axf)。如下图所示:
在这里插入图片描述
  我们通过一个编译hello.c的例子来了解一下编译的过程:

#include <stdio.h>
int main()
{
	printf(“Hello, world.\n”);
	return 0;
}

  原来我们可能会直接:

panghu@Ubuntu-14:~$gcc hello.c -o hello
panghu@Ubuntu-14:~$./hello

  来编译运行生成可执行文件hello,我们来从gcc的四个过程来看。

  1. 预处理(Preproceessing)
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<stdio.h>等。
  编译完成后可通过cat hello.i来查看预处理后的代码(前面好多头文件预处理命令,但最后几行还是c的语句):
在这里插入图片描述

  1. 编译(Compilation)
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将于源文件有关的目标文件建立彼此的链接,使得所有目标文件成为一个可被操作的系统执行的统一整体,即可执行文件。
在这里插入图片描述
  编译过程可分为六个步骤:词法分析、语法分析、语义分析、源代码优化、代码生成、目标代码优化。

3. 静态链接和动态链接

  在编译的最后连接过程中主要是把各个模块间的引用联系在一起,使得模块间能够按有顺序的衔接。其主要过程包括:地址和空间的分配(Address and Storage Allocation),符号决议(Symbol Resolution),重定位(Relocation)等。连接处理的方式又分为两种:

  • 静态链接:在编译阶段直接把静态库加入到可执行文件中去,这样会使可执行文件变大。
  • 动态链接:在连接阶段仅加入一些描述信息,而执行程序时再从系统中把相应的动态库加载到内存中去。

  对于可执行文件中的函数调用,通常会采用上述的两种方式进行链接,动态链接虽然能够使可执行文件变小,而内存中只需要保存一份代码可供多个共享对象的进程调用,从而节省一些内存,但不是所有情况都适用。
  在Linux/Unix下的可执行文件及动态库都是以ELF(Executable Linkage Format)格式存在的,在Linux可调用readelg filename -e命令来查看ELF文件的头:
在这里插入图片描述

4. 静态库和动态库

  库用于将相似的函数打包到一个单元中,然后这些单元可与其他开发人员共享,Linux支持两种类型的库,每种库各有其优缺点,静态库包含在编译时静态绑定到一个程序的函数,动态库(共享库)则是在加载应用程序时被加载,且在程序运行时绑定。在Linux下几个重要的函数库存放在/lib/usr/lib;头文件则放在/usr/include
静态库:这类库的名字一般为libxxx.a,利用静态库编译成的文件比较大,整个函数库都会被加载到目标代码中,而优点就是编译完成后就不需要外部的函数库支持,但如果静态函数库改变则程序需要重新编译。
动态库:这类库的名字一般为libxxx.so,动态库又称共享库,动态库在编译时没有被编译到目标代码中,在程序执行到相关函数才调用函数库中的相应函数。因此可执行文件较小,但需要注意的是程序运行环境中要提供相应的库,与静态库相比动态库的改变不需要进行重新编译,如果多个程序要用到同一函数库,则可考虑选择动态库。
  静态函数库和动态函数库都由*.o目标文件生成。

4.1 静态库的制作和使用

  静态库制作步骤:

  1. 生成目标文件
  2. 使用 ar 命令创建静态函数库

  静态函数库的命名必须是 lib[库的名字].a

创建过程
  首先创建下面文件:
add.h

#ifndef ADD_H
#define ADD_H
int add(int x,int y); 
#endif

add.c

#include <stdio.h>
#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 <stdio.h>
#include "sub.h"
int sub(int x, int y)
{
    return (x - y);
}

main.c

#include <stdio.h>
#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依然可以运行,因为静态库的内容已经加载进去了。结果如下:
在这里插入图片描述
  静态链接过程图:
在这里插入图片描述

4.2 动态库的制作和使用

  接着上面得步骤操作建立动态库的链接和使用,用下面的命令可产生 libaddsub.so 的动态库 :

panghu@Ubuntu-14:~/add$ gcc -shared -fpic -o libaddsub.so add.c sub.c
  • 其中 -fpic 表示与源码位置无关;
  • -shared 表示生成动态库。
      编译命令和静态库中的一样:
panghu@Ubuntu-14:~/add$ gcc -o out main.c -L. -laddsub

  此时会生成 out 可执行文件但是还不能运行,需要将生成的动态函数库放到自动查找的目录**/usr/lib** 或**/lib**中去,因此我们需要:

  1. 最直接最简单的方法就是把libaddsub.so拉到/usr/lib 或/lib中去。
  2. 还可以在/etc/ld.so.conf文件里加入我们生成的库的目录,然后/sbin/ldconfig。
      /etc/ld.so.conf是非常重要的一个目录,里面存放的是链接器和加载器搜索共享库时要检查的目录,默认是从/usr/lib /lib中读取的,所以想要顺利运行,我们也可以把我们库的目录加入到这个文件中并执行/sbin/ldconfig。

  完成后我们可通过ll命令来查看各文件所占有的内存大小:
在这里插入图片描述
  我们可发现有静态库链接生成的 main 文件所占用空间大于由动态库链接生成的 out 文件所占用的空间,这个差距还会随着代码规模的增大而增大。

参考资料:https://www.cnblogs.com/WindSun/p/11287927.html
http://smilejay.com/2012/01/c_compilation_stages/

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值