1、编译过程
1.预处理:解释并展开源程序当中的所有的预处理指令,此时生成 *.i 文件。
2.编译:词法和语法的分析,生成对应硬件平台的汇编语言文件,此时生成 *.s 文件。
3.汇编:将汇编语言文件翻译为对应处理器的二进制机器码,此时生成 *.o 文件。
4.链接:将多个 *.o 文件合并成一个不带后缀的可执行文件。
bash
gec@ubuntu:~$ gcc hello.c -o hello.i -E
gec@ubuntu:~$ gcc hello.i -o hello.s -S
gec@ubuntu:~$ gcc hello.s -o hello.o -c
gec@ubuntu:~$ gcc hello.o -o hello -lc
gcc hello.c -o hello
2.ELF格式
2.1概述
对于上述编译过程,重点关注最后一步库文件的链接(gcc hello.o -o hello -lc):链接实际上是将多个.o文件合并在一起的过程。这些 *.o 文件合并前是 ELF 格式,合并后也是 ELF 格式。
ELF全称是 Executable and Linkable Format,即可执行可链接格式。ELF文件由多个不同的段(section)组成,如下图所示:
ELF格式的合并,实际上就是将多个文件中各自对应的段合并在一起,形成一个统一的ELF文件。在此过程中,必然需要对各个 *.o 文件中的静态数据(包括常量)、函数入口的地址做统一分配和管理,这个过程就叫做 重定位,因此未经链接的单独的 *.o 文件又被称为可重定位文件,经过链接处理合并了相同的段的文件称为可执行文件。
库的本意是library图书馆,库文件就是一个由很多 *.o 文件堆积起来的集合。
2.2 相关命令
(1) readelf 可以用来查看 ELF 格式文件的具体细节:
bash
# 查看文件格式头部信息
gec@ubuntu:~$ readelf -h a.out
# 查看各个section信息
gec@ubuntu:~$ readelf -S a.out
# 查看符号表
gec@ubuntu:~$ readelf -s a.out
3.库文件
3.1概述
库的本意是library图书馆,库文件就是一个由很多 *.o 文件堆积起来的集合。本质上来说库是一种可执行代码的二进制形式,这个文件可以在编译时由编译器直接链接到可执行程序中,也可以在运行时由操作系统的runtime enviroment根据需要动态加载到内存中。
3.2分类
库文件分为两类:静态库和动态库。如:
bash
win32平台下,静态库通常后缀为.lib,动态库为.dll ;
linux平台下,静态库通常后缀为.a,动态库为.so 。
静态库:libx.a
动态库:liby.so
库文件的名称遵循这样的规范:
lib库名.后缀
其中,lib是任何库文件都必须有的前缀,库名就是库文件真正的名称,比如上述例子中两个库文件分别叫x和y,在链接它们的时候写成 -lx 和 -ly ,后缀根据静态库和动态库,可以是 .a 或者 .so:
- 静态库的后缀:.a (archive,意即档案)
- 动态库的后缀:.so (share object,意即共享对象)
注意:不管是静态库,还是动态库,都是可重定位文件 *.o 的集合。
3.3目的
- 模块化:库文件将功能模块化,使得程序结构更加清晰,易于管理和维护。
- 简化部署:使用库文件可以简化软件的部署过程,因为它们可以在不同的程序之间共享,而不需要重复包含相同的代码。
- 动态链接:动态库文件允许在程序运行时才链接,这样可以在不重新编译程序的情况下更新库,提供了更大的灵活性。
- 减少内存占用:使用动态库时,由于多个程序可以共享同一份库文件,因此可以减少每个程序的内存占用。
- 易于更新和维护:库文件的更新只需要替换原有文件,而不需要重新编译使用该库的所有程序,简化了维护工作。
- 跨平台兼容性:库文件可以被设计为跨平台使用,增加了软件的可移植性。
总的来说,库文件的使用是为了提高软件开发的效率、灵活性和可维护性,同时减少资源的重复占用。
4、静态库
1所谓静态库,就是在静态编译时由编译器到指定目录寻找并且进行链接,一旦链接完成,最终的可执行程序中就包含了该库文件中的所有有用信息,包括代码段、数据段等。
静态链接库在程序编译时会被链接到目标代码中,目标程序运行时将不再需要改动态库,移植方便,体积较大,浪费空间和资源,因为所有相关的对象文件与牵涉到库都被链接合成一个可执行文件,这样导致可执行文件的体积较大。
2.静态库的制作
假设功能文件 a.c、b.c 包含了一些通用的程序模块,可以被其他程序复用,那么可以将它们制作成静态库,具体的步骤是:
第一步,制作 *.o 原材料
gec@ubuntu:~$ gcc a.c -o a.o -c
gec@ubuntu:~$ gcc b.c -o b.o -c
第二步,将 *.o 合并成一个静态库
gec@ubuntu:~$ ar crs libx.a a.o b.o
可见制作静态库非常简单,制作完成之后,可以用命令 ar 查看库中所包含的 *.o 文件:
gec@ubuntu:~$ ar -t libx.a
- 静态库的常见操作
3.1 查看静态库中的 .o 列表
gec@ubuntu:~$ ar t libx.a #(t意即table,以列表方式列出 .o文件)
a.o
b.o
3.2 删除静态库中的 .o 文件
gec@ubuntu:~$ ar d libx.a b.o #(d意即delte,删除掉指定的 .o文件)
gec@ubuntu:~$ ar t libx.a
a.o
3.3 向静态库增加 .o 文件
gec@ubuntu:~$ ar r libx.a b.o #(r意即replace,添加或替换(重名时)指定的 .o文件)
gec@ubuntu:~$ ar t libx.a
a.o
b.o
3.4 提取静态库中的 .o 文件
gec@ubuntu:~$ ar x libx.a #(x意即extract,将库中所有的 .o文件释放出来)
gec@ubuntu:~$ ar x libx.a a.o #(指定释放库中的a.o文件)
4.静态库的使用
库文件最大的价值,在于代码复用。假设在上述库文件所包含的 *.o 文件中,已经包含了若干函数接口,那么只要能链接这个库,就无需再重复编写这些接口,直接链接即可。
使用静态库 要是用静态库libadd.a,只需要包含add.h,就可以使用函数add()、sub()。
#include <stdio.h>
#include "add.h"
void main(){
printf("add(5,4) is %d\n",add(5,4));
printf("sub(5,4) is %d\n",sub(5,4));
}
静态库的文件可以放在任意的位置,编译时只需要找到该库文件即可。
gcc -c -I /home/xxxx/include -L /home/xxxxx/lib libadd.a test.c
1). 通过-I(是大i)指定对应的头文件
2). 通过-L制定库文件的路径,libadd.a就是要用的静态库。
3). 在test.c中要包含静态库的头文件。
总结:
编译时:gcc a.c liba.a -o project
相当于liba.a 代替了b.c c.c参与编译
5、动态库
1.概述
不管是动态库还是静态库,它们都是 *.o 文件的集合。动态库指的是以.so后缀的库文件。动态库在程序编译时并不会被链接到目标代码中,而是在程序运行时才被载入,因为可执行文件体积较小。有了动态库,程序的升级会相对比较简单,比如某个动态库升级了,只需要更换这个动态库的文件,而不需要去更换可执行文件。但要注意的是,可执行程序在运行时需要能找到动态库文件。可执行文件时动态库的调用者。
在实际应用中,动态库应用场合要远多于静态库,因为虽然动态库的运行时装载特性会使得程序性能有略微的下降,但换来的是不仅仅节省了大量的存储空间,更重要的是使得主程序和库松耦合,不互相捆绑,当库升级的时候,应用程序无需任何改动即可获得新版库文件的功能,这极大地提高了程序的灵活性。
2.库文件命名
静态库的名字一般为libxxxx.a,其中xxxx是该lib的名称;动态库的名字一般为libxxxx.so.x.y.z,含义如下图所示:
此处,符号链接的作用不是"快捷方式",而是为了可以让动态库在升级版本的时候更加方便地向前兼容。一般而言,完整的动态库文件名称是:
lib库名.so.主版本号.次版本号.修订版本号
比如: libx.so.1.3.1
当动态库迭代升级时,其版本号会发生相应的改变。比如下面的版本更迭:
2021年3月08日发布:libx.so.1.0.0
2021年4月02日发布:libx.so.1.0.1
2021年4月23日发布:libx.so.1.0.2
2021年5月18日发布:libx.so.1.0.3
2021年8月09日发布:libx.so.1.1.0
2021年9月12日发布:libx.so.1.1.1
可以看到,修订版本号的更迭会比较频繁,次版本号次之,主版本号再次之。为了避免每次版本号的修改而重新编译,动态库一般会用一个只带主版本号的符号链接来链接程序,如:
bash
gec@ubuntu:~$ ls -l
lrwxrwxrwx 1 root root 15 Jan 16 2020 libbsd.so.0 -> libbsd.so.0.8.7
-rw-r--r-- 1 root root 80104 Jan 16 2020 libbsd.so.0.8.7
gec@ubuntu:~$
这样一来,未来不管版本号如何变迁,只要主版本号不变,那么用户链接的库名永远都是 libbsd.so.0,而无需关心具体某个版本。而如果连主版本号都发生了改变,这一般是因为库不再向前兼容,比如删除了某些原有的接口,这种情况下,用户就需要重新编译程序。
3.制作库文件常用的参数
首先需要了解gcc编译库要用到一些参数,很重要。
4.制作动态库
不管是静态库还是动态库,都是用来被其他程序链接的一个个功能模块。与静态库一致,制作动态库的步骤如下:
将 *.c 编译生成 *.o
将 *.o 编译成动态库
#第一步:将源码编译为 *.o
gcc -fPIC -o libadd.o -c add.c
#第二步:将 *.o 编译为动态库
gcc -shared -o libadd.so libadd.o
也可以直接使用一条命令
gcc -fPIC -shared -o libadd.so add.c
5.动态库的使用
动态库的编译跟静态库并无二致,如:
gec@ubuntu:~$ pwd
/home/gec
gec@ubuntu:~$ ls lib/
gec@ubuntu:~$ gcc main.c -o main -L./lib -lx
说明:
-L 选项后面跟着动态库所在的路径。
-l 选项后面跟着动态库的名称。
运行时链接
动态库的最大特征,就是编译链接后程序并不包含动态库的代码,这些程序会在每次运行时,动态地去寻找并定位其所依赖的库文件中的模块,这是他们为什么被称为动态库的原因。
也就是说,如果程序运行时找不到动态库,运行就会失败,例如:
gec@ubuntu:~$ ./main
报错
出现上述错误的原因,就是因为运行程序 main 时,无法找到其所依赖的动态库 libx.so,解决这个问题,有三种办法:
1.编译时预告:
gec@ubuntu:~$ gcc main.c -o main -L. -lx -Wl,-rpath=/home/gec/lib
2.设置环境变量:
gec@ubuntu:~$ export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/home/gec/lib
3.将库文件拷贝到根目录下的/lib里面
总结
可以通过动态库(也称为共享库或共享对象文件)再构建一个动态库。在链接过程中,一个动态库可以依赖于其他动态库或静态库。
当你使用编译器(如gcc或clang)来构建动态库时,你可以指定其他动态库作为链接时的依赖。这些依赖的库在运行时会被动态加载。
以下是一个简单的例子,展示了如何使用gcc来从一个动态库(libA.so)构建一个依赖于它的新动态库(libB.so):
编译和链接第一个动态库(libA.so)
假设你有一个源文件a.c,你可以这样编译和链接它:
bash
gcc -shared -o libA.so a.c
编译和链接第二个动态库(libB.so),它依赖于libA.so
假设你有一个源文件b.c,它调用了在libA.so中定义的函数。为了构建libB.so,你需要链接到libA.so:
bash
gcc -shared -o libB.so b.c -L. -lA
注意-L.选项告诉链接器在当前目录(.表示当前目录)中查找库,而-lA选项告诉链接器链接到名为libA.so的库(注意,在-l选项后,库名通常不包含前缀lib和后缀.so)。