Linux下编译、链接以及库的制作
本篇博客主要介绍C语言在linux环境下的编译、链接以及静态库、共享库的一些命令的使用。
以下命令的权限普通用户(
$
)和超级用户(
#
)均可。
一、编译
GCC的编译流程分为四个步骤:
1、预处理(Pre-processing):处理以#
开头的句子(包含、宏定义、条件编译)
2、编译(Compiling):生成汇编代码
3、汇编(Assembling):生成目标文件
4、链接(linking):生成执行文件
1.1 单源文件编译方法
选项-E
用法:#gcc -E test.c -o test.i
作用:生成 预处理文件。
选项-S
用法:#gcc -S test.i -o test.s
作用:生成汇编文件。
选项-c
(小写的C)
用法:#gcc -c test.s -o test.o
作用:生成目标文件。
选项-o
用法:#gcc test.o -o test
作用:生成可执行文件,-o选项用来指定输出文件的文件名。如果未指定,则生成的文件名为a.out
上述四个步骤可以一次完成:
#gcc test.c -o test
选项-O
用法:#gcc -O1 test.c -o test
作用:使用编译优化级别1编译程序。级别为1~3
,级别越大优化效果越好,但编译时间越长。
-O0
:关闭所有优化选项;
-Os
:这个级别是用来优化代码尺寸的,他只是给一些CPU缓存或是磁盘空间小的机器使用。(参考文献:linux GCC -O参数设置整理)
选项-L
和-l
(小写的L)
假设库libvector.a(libvector.so)在/home/linux目录下
用法:#gcc test.c -L/home/linux -lvector -o test
等同于:#gcc test -o test /home/linux/libvector.a
作用:-L指明库所在的路径; -l 指明库名,如果不指定路径和库名,那么编译就会从默认的路径下查找,不会在当前路径下搜索。
选项-I
用法:#gcc test.c main.c -o main -I+头文件路径
作用:指明头文件所在的路径。
选项-D
用法:#gcc test.c main.c -o main -D+宏名
作用:指明宏定义名称,相当于定义了这个宏。
注意: E、S、c、o、O
后面必须加空格,然后加相关的信息;L、l
(小写的L)、I、D
后面可以直接加相关的信息,当然加空格也可以。
1.2 多源文件编译方法
如果有多个源文件,基本上有两种编译方法:
(假设有两个源文件为test1.c和test2.c)
1、多个文件一起编译
用法:#gcc test1.c test2.c -o test
作用:将test1.c和test2.c分别编译后链接成test可执行文件。
2、分别编译各个源文件,之后对编译后输出的目标文件链接。
用法:
#gcc -c test1.c
//将test1.c编译成test1.o
#gcc -c test2.c
//将test2.c编译成test2.o
#gcc test1.o test2.o -o test
//将test1.o和test2.o链接成test
以上两种方法相比较,第一种方法编译时需要所有文件重新编译,而第二种方法可以只重新编译修改的文件,未修改的文件不用重新编译。
1.3、警告提示功能选项
选项-pedantic
作用:当gcc在编译不符合ANSI/ISO C 语言标准的源代码时,将产生相应的警告信息。
选项-Wall
作用:使 gcc 产生尽可能多的警告信息。
选项-Werror
作用:将所有的警告当成错误进行处理。
选项-Wcast-align
作用:当源程序中地址不需要对齐的指针指向一个地址需要对齐的变量地址时,则产生一个警告。例如,char * 指向一个 int * 地址,而通常在机器中 int 变量类型是需要地址能被2或4整除的对齐地址。
参考自:gcc的使用简介与命令行参数说明 🚀
二、链接与库
2.1 链接
链接是将各种代码和数据片段收集并组合成为一个单一文件的过程,这个文件可被加载(复制)到内存并执行。
链接可以执行于编译时(compile time),也就是在源代码被翻译成机器代码时;也可以执行于加载时(load time),也就是在程序被加载器(loader)加载到内存并执行;甚至执行于运行时(run time),也就是由应用程序来执行(dlopen/dlsym/dlclose
函数加载库)。
链接的对象:重定位目标文件、静态库、共享库。
库是一个二进制文件,包含的代码可被程序调用。
在linux中库常放在/lib
,/usr/lib
目录下。
链接器的两个主要任务是符号解析和重定位,符号解析将目标文件中每个全局符号绑定到一个唯一的定义,而重定位确定每个符号的最终内存地址,并修改对那些目标的引用。
符号解析完成后,将各种目标文件合并成一个文件,在这一步中,链接器将所有相同类型的节合并为同一类型的新的聚合节,然后进行重定位,重定位由两步组成:1、重定位节和符号定义;2、重定位节中的符号引用。
符号解析主要用到重定位目标文件(ELF格式)中的.symtab
(符号表)
重定位主要用到重定位目标文件中的.rel.text
节和.rel.data
节
.symtab
(符号表):它存放程序中定义和引用的函数和全局变量信息。
.rel.text节和.rel.data节:存放的是重定位条目(一个结构体,每个条目表示一个必须被重定位的引用,并指明如何计算被修改的引用)。汇编器为每个引用后面一行上都有一个重定位条目(指向.rel.text和.rel.data节)。
下图所示为:静态库链接
在linux系统中,静态库以一种称为存档(archive
)的特殊文件格式存放在磁盘中,存档文件是一组连接起来的可重定位目标文件的集合,有一个头部用来描述每个成员目标文件的大小和位置。存档文件名由后缀.a
标识。
在静态链接时,链接器将只复制被程序引用的目标模块。
静态库的特点:
1、程序中包含代码,运行时不再需要静态库;
2、程序运行时无需加载库,运行速度更快;
3、占用更多磁盘和内存空间;
4、静态库升级后,程序需要重新编译链接。
下图所示为:共享库链接
加载器将可执行文件的内容映射到内存,并运行这个程序。
共享库解决静态库的两个问题:1、库更新;2、内存浪费
共享库有时也称为动态库。
共享库的基本思路是当创建可执行文件时,静态执行一些链接,然后在程序加载时,动态完成链接过程。此时没有任何libvector.so的代码和数据节被复制到可执行文件prog21中,反之,链接器复制了一些重定位和符号表信息,它们使得运行时可以解析对libvector.so中代码和数据的引用。
共享库的特点:
1、程序不包含库中代码——尺寸小;
2、多个运行程序可共享同一个库(正在运行的多个程序都需要用到某个共享库的话,那么这个库只需要在内存中有一个就可以了,不同的程序都可以来调用共享库中的代码);
3、程序运行时需要加载库;
4、库升级方便,无需重新编译程序;
5、使用更加广泛。
2.2 静态库、共享库的制作与使用
2.2.1 静态库的制作与使用
静态库的制作:
1、编译生成目标文件hello.o:#gcc -c hello.c
2、创建静态库libhello.a:#ar crs libhello.a hello.o
(可以加多个*.o 文件)
3、查看库中符号信息:#nm libhello.a
静态库的使用:
1、编译应用程序*.c(假设为main.c,main.c中包含 hello.a 中的函数原型)
2、编译main.c并链接静态库libhello.a :#gcc -o main main.c -L. -lhello
静态库链接时搜索路径顺序:
1、ld会去找GCC命令中的参数-L
2、再找gcc的环境变量LIBRARY_PATH
3、再找内定目录 /lib、/usr/lib、/usr/local/lib 这是当初compile gcc时写在程序内的
2.2.2 共享库的制作与使用
共享库的制作:
1、编译生成目标文件hello.o和bye.o:#gcc -c hello.c bye.c
2、创建共享库common:#gcc -fPIC -shared -o libcommon.so.1 hello.o bye.o
(共享库文件命名规则:以lib开始,后面紧跟库名,.so表示为共享库,.1表示库的版本。)
(-fPIC选项指示编译器生成与位置无关的代码)
3、为共享库文件创建链接文件(目的:为了让编译器在编译时能够找到共享库):#ln -s libcommon.so.1 libcommon.so
(-s表示软链接)
(符号链接文件命名规则:lib<库名>.so)
步骤2和3可以用另一种方法:
#gcc -fPIC -shared -o libcommon.so hello.o bye.o
共享库的使用:
1、编译应用程序*.c(假设为main.c,main.c中包含 libcommon.so 中的函数原型)
2、编译main.c并链接共享库libcommon.so :#gcc -o main main.c -L. -lcommon
或 #gcc -o main main.c ./libcommon.so
(默认首先找共享库libcommon.so,如果没有则找静态库libsommon.a,如果还没找到则报错)
(加上-static
告诉链接器直接找静态库,而不链接共享库)
3、运行可执行文件时,发现找不到共享库,可以:
(1)将库拷贝到/usr/lib或/lib目录下。(不推荐这么做,因为往往该目录下拷贝文件需要管理员权限,其次一般系统的库会放到该目录下,程序员开发的库可能放在其它路径下更加合适)
(2)在LD_LIBRARY_PATH环境变量中添加库所在路径。(只对当前shell有效,如果重新运行shell那么环境变量中还是没有路径)
查看环境变量的值:#echo $LD_LIBRARY_PATH
设置环境变量的值:#export LD_LIBRARY_PATH=.
(.表示当前目录下)
删除环境变量的值:#unset LD_LIBRARY_PATH
(3)添加/etc/ld.so.conf.d/*.conf
文件,执行ldconfig
刷新
①新建配置文件:sudo vi /etc/ld.so.conf.d/my.conf
②将路径添加到配置文件①中:/home/linux/lib/share
③刷新:sudo ldconfig
共享库链接时搜索路径顺序:
1、编译目标代码时指定的共享库搜索路径
2、环境变量LD_LIBRARY_PATH指定的共享库搜索路径
3、配置文件/etc/ld.so.conf中指定的共享库搜索路径
4、默认的共享库搜索路径/lib
5、默认的共享库搜索路径/usr/lib
若有理解出错的地方,望不吝指正。
三、参考文献
[1]: GCC常用 编译命令
[2]: Linux下动态库和静态库的生成命令
[3]: Randal E. Bryant, David R. O’Hallaron. 深入理解计算机系统[M]. 机械工业出版社, 2017.