前言:
1、gcc是一套编译工具集合,它包含了编译器,连接器等等工具链。可以编译c/c++/objc/java的主流常用语言,所以gcc是C语言编译器的理解是非常片面的。
2、gcc 和 g++ 的区别:
- gcc和g++都是gcc工具套件里面的命令
- 只要是 GCC 支持的编程语言,都可以使用 gcc 命令完成编译,它根据文件后缀来自动选择要编译的语言,比如.c(C语言);.cpp(c++语言);.m(Objective-C);.go(go语言)
- 而g++ 指令无论目标文件的后缀名是什么都按照编译 C++ 代码的方式编译
gcc生成可执行程序流程
这里以C语言为例,它的编译流程如下:
- 1、将C语言源程序预处理,生成
.i
文件 - 2、预处理后的.i文件编译成为汇编语言,生成
.s
文件。 - 3、 将汇编语言编译成生成目标文件
.o
文件。 - 4、将各个模块的
.o
文件链接起来生成一个可执行程序文件。
gcc -E main.c -o main.i
gcc -c main.i -o main.o
gcc main.o -o main
./main
gcc静态库和动态库
静态库和动态库可以理解为目标文件(.o)的集合,它们的区别就是在被连接到可执行程序中时:
静态库是直接拷贝到可执行程序中,而动态库则只是在可执行程序中保留一份动态库的说明信息,加载则分为程序加载时加载动态库和程序运行时加载动态库两种方式。
- 1、静态库的生成
创建math.h/math.c文件内容如下:
// math.h
int add(int a,int b);
// math.c
#include "math.h"
int add(int a,int b)
{
return a+b;
}
生成libmath.a静态库
gcc -c math.c -o math.o
ar -rcs math.o libmath.a
ar命令参数 r:表示向静态库中添加模块;c:表示静态库存在就替换 s:如果静态库中存在模块,则替换
- 2、引用静态库
创建main.c内容如下:
// main.c
#include<stdio.h>
#include "math.h"
int main()
{
printf("a+b=%d",add(5,6));
return 0;
}
编译main.c并连接math静态库
gcc -c main.c -o main.o
gcc main.o -o main -lmath -L./
然后执行./main,可以看到成功打印 a+b=11
- 3、生成动态库
依然用上面math.c文件来生成libmath.so动态库
gcc -fPIC -c math.c -o math.o
gcc -shared math.o -o libmath.so
-fPIC 表示生成位置无关代码,如果要让动态库再被多个程序引用时内存中也只有一份拷贝则编译动态库时必须要有此选项,否则和静态库一样了。
- 4、引用动态库
依然用上面main.c文件,编译时引用libmath.so动态库
gcc -c main.c -o main.o
gcc main.o -o main -lmath -L./
可以看到这里引用动态库和静态库的编译命令是一样的,默认情况下,如果当前目录中同时存在libmath.a和libmath.so库,gcc将优先引用动态库。
tips:如果强行要使用静态库则使用 gcc main.o libmath.a -o main的方式即可
- 5、查看可执行程序依赖的动态库
ldd main
输出如下:
linux-vdso.so.1 (0x00007ffcf2956000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f2c7bfdd000)
/lib64/ld-linux-x86-64.so.2 (0x00007f2c7c5d0000)
可以看到每一个程序,它都会默认链接libc.so这样一个C标准库
- 6、动态库中引用静态库或者动态库
编译动态库的流程原理和编译可执行程序中引用静态库与动态库一样,即如果引用静态库,那么最终生成的动态库中含有该静态库的一份拷贝,则该动态库被连接到可执行程序中时就不需要指明它引用的静态库了。如果引用动态库,那么最终生成的动态库中只是包含要引用动态库的一份信息说明,则该动态库被连接到可执行程序中时不仅要指明动态库还要指明动态库引用的动态库
tips:如果要编译一个静态库,而该源码中引用了另外的静态库或者动态库,则用ar打包为静态库时要手动将这些库都合并进来,否则这个静态库被连接到可执行程序时也需要同时指明这个静态库所引用的静态库或者动态库
创建math.h/math.c;log.h/log.c;main.c文件,分别编译两个库math和log,其中log会引用math库,最终main.c文件引用log库来模拟上述情况
各个文件内容如下:
// math.h
int add(int a,int b);
// math.c
#include "math.h"
int add(int a,int b)
{
return a+b;
}
// log.h
int add(int a,int b);
// log.c
#include "log.h"
int print()
{
printf("a+b=%d",add(5,6));
}
首先将math.c编译为libmath.a静态库
gcc -c math.c -o math.o
ar -rcs libmath.a math.o
然后将log.c编译为动态库
gcc -fPIC -c log.c -o log.o
gcc -shared -o liblog.so log.o -llog -L./
最后编译main.c可执行程序
gcc -c main.c -o main.o
gcc main.o -o main -llog -L./
./main
可以正确连接和执行;如果换成如下的流程也是可以的
gcc -c math.c -o math.o
ar -rcs libmath.a math.o
gcc -fPIC -c log.c -o log.o
gcc -shared -o liblog.so log.o
gcc -c main.c -o main.o
gcc main.o -o main -llog -lmath -L./
./main
也就是说当编译动态库的时候,如果动态库中引用了静态库,可以不用显示指明对静态库的引用(即上面去掉-lmath -L./),只需再最终执行可执行程序连接阶段时指明即可;如果引用了动态库也是一样。所以充分说明了gcc -shared -o指令只是类似于ar的一个打包器,并不会调用连接器,只有gcc -o指令最终生成可执行程序时才会调用连接器
现在将log.c编译成静态库然后在main.c中引用
gcc -fPIC -c math.c -o math.o
gcc -shared math.o -o libmath.so
gcc -c log.c -o log.o
ar -rcs liblog.a log.o
gcc -c main.c -o mian.o
gcc main.o liblog.a -o main -lmath -L./
./main
如果同时存在动态库和静态库连接器将优先选择动态库,所以这里通过gcc main.o liblog.a -o main 显示指定引用静态库log,同时还要显示引用math库,因为编译log库的时候并没有将math库合并进去
也可以换成如下方式
gcc -fPIC -c math.c -o math.o
gcc -shared math.o -o libmath.so
gcc -c log.c -o log.o
ar -rcs liblog.a log.o libmath.so
gcc -c main.c -o mian.o
gcc main.o liblog.a -o main
./main
gcc静态链接和动态链接
不管是静态链接还是动态链接都是在生成可执行程序时连接器的连接行为,他们的区别为:
- 静态链接,只能用于静态库,连接器对要引用的库都会拷贝到可执行程序中
- 动态链接,只能用于动态库,连接器对要引用的库都只会在可执行程序中保留一份库的引用信息,库要等到程序加载时或者运行时动态的载入内存
- gcc默认情况下可同时引用动态库和静态库,如果要引用的动态库和静态库同时存在默认选择动态库,除非显示指明。如果用-static参数显示指定,那么只能引用静态库
仍然以上面创建的math.h/math.c/log.h/log.c/main.c为例,演示-static参数的使用
gcc -c math.c -o math.o
ar -rcs libmath.a math.o
gcc -c log.c -o log.o
ar -rcs liblog.a log.o
gcc -c main.c -o mian.o
gcc -static main.o -o main -llog -lmath -L./
./main
然后执行 ldd main 打印 not a dynamic executable;如果上面不存在liblog.a或者libmath.a库那么连接将报错 "/usr/bin/ld: cannot find -lxxx"
这里有一点要注意,gcc编译器引用的标准C语言的库libc提供了libc.a和libc.so两份库
gcc符号决议
在理解符号决议的概念之前首先看一段常用的C语言代码(test.c)
// 定义未初始化的全局变量,该变量可以再其它文件中引用
int global_uinit;
// 定义初始化的全局变量,该变量可以再其它文件中引用
int global_init = 1;
// 定义未初始化的全局私有变量变量,static声明后该变量只能再其本文件中引用
static int private_uinit;
// 定义初始化的全局变量,static声明后该变量只能再其本文件中引用
static int private_init = 1;
// 声明全局变量,该变量定义在其它文件
extern int global_external_uinit;
// 声明函数,该函数定义在其它文件
extern int func_external();
// 声明并且定义函数,static声明后该函数只能在本文件使用
static int func_private_a()
{
// 局部变量
int a = 4;
return a;
}
// 声明并且定义函数,该函数可以在本文件和其它文件使用
int func_global_a()
{
// 局部变量
int a = 4;
return a;
}
当该文件被gcc编译后成目标文件后(test.o),目标文件一般包括如下部分:
- 数据段:用来保存源文件中的所有定义的函数如上面中的func_global_a()和func_private_a()
- 代码段:用来保存源文件中的所有定义的变量如上面中的global_uinit,global_init,private_uinit,private_uinit
- 符号表:描述源文件定义的变量和函数以及引用的变量和函数
......
所以编译阶段,对于源文件中引用的外部变量以及函数,只要找到了变量和函数声明即可通过编译,所以这也是前面提到的为什么在编译动态库的时候(就算动态库引用了其它库)不需要显示指明也可以通过编译的原因。
符号决议:存在于连接阶段,连接器将每个目标文件中引用的外部变量及函数的找出来,然后去其它目标文件中查找他们的定义,如果未找到那么符号决议失败,连接器将提示"undefined reference xxx错误"。
gcc重定位
在将源代码编译为目标文件后会在目标文件中保留引用的函数以及变量的地址,该地址为相对目标文件的地址,连接阶段连接器会将各个目标文件的变量以及函数的地址重新调整规划以保证每个变量及函数的地址是唯一的,这个过程就称为重定位,重定位只是相对于静态链接来说的,动态链接的规则又不一样了。所以程序运行是的变量及函数地址是在连接阶段确定的(备注:这些地址都是虚拟内存地址,真实的地址会经过转换得到)
参考文章
连接器工作原理系列:[https://segmentfault.com/a/1190000016433947]