前言:关于Linux中环境的配置,编译等工作,有很多的坑,前面的一片文章中已经比较详细的介绍了GCC编译套件的一些东西,具体请参考:
VSCode开发C、C++环境搭建系列(二)——GCC/G++编译器对头文件、静态库、动态库的搜索路径详解
一、从动态库的编译说起
下面通过一个例子来介绍如何生成一个动态库。
这里有一个头文件:so_test.h,
三个.c文件:test_a.c、test_b.c、test_c.c,
我们将这几个文件编译成一个动态库:libtest.so。
//so_test.h:
#include "stdio.h"
void test_a();
void test_b();
void test_c();
//test_a.c:
#include "so_test.h"
void test_a()
{
printf("this is in test_a...\n");
}
//test_b.c:
#include "so_test.h"
void test_b()
{
printf("this is in test_b...\n");
}
//test_c.c:
#include "so_test.h"
void test_c()
{
printf("this is in test_c...\n");
}
将这几个文件编译成一个动态库:libtest.so
$ gcc test_a.c test_b.c test_c.c -fPIC -shared -o libtest.so
关于gcc编译的这几个参数后面会再说明的。
二、动态链接库的使用
在上面的一中,我们已经成功生成了一个自己的动态链接库libtest.so,下面我们通过一个程序来调用这个库里的函数。程序的源文件为:test.c。
//test.c:
#include "so_test.h"
int main()
{
test_a();
test_b();
test_c();
return 0;
}
将test.c与动态库libtest.so链接生成执行文件test:
$ gcc test.c -L. -ltest -o test
现在生成了一个可执行文件test,那么这个可执行文件到底有没有成功链接到动态链接库呢?我么可以使用下面的命令来查看:
ldd test
测试是否动态连接,如果列出libtest.so,那么应该是连接正常了
执行test,可以看到它是如何调用动态库中的函数的。
关于ldd命令后面也会有所总结,这是一个专门查看应用程序中使用了哪一些动态链接库的程序。
三、GCC编译参数解析
3.1 动态编译
最主要的是GCC命令行的一个选项:
- (1)-shared 。该选项指定gcc编译器生成动态连接库,而不是可执行文件
- (2)-fPIC:表示编译为位置独立的代码,不用此选项的话编译后的代码是位置相关的所以动态载入时是通过代码拷贝的方式来满足不同进程的需要,而不能达到真正代码段共享的目的。正是使用这个,使得动态链接库不用再编译时拷贝库函数的完整代码,实现真正的动态链接。
- (3)-L:指定编译的时候动态链接库的位置,这里使用 -L. 后面跟了一个点表示要连接的库在当前目录中
- (4)-ltest:编译器查找动态连接库时有隐含的命名规则,即在给出的名字前面加上lib,后面加上.so来确定库的名称
另外:
(1)LD_LIBRARY_PATH:这个环境变量指示动态连接器可以装载动态库的路径。
(2)当然如果有root权限的话,可以修改/etc/ld.so.conf文件,然后调用 /sbin/ldconfig来达到同样的目的,不过如果没有root权限,那么只能采用输出LD_LIBRARY_PATH的方法了。
动态链接库详细的顺序请参考前面一篇文章。
3.2 静态编译
注意,gcc会在静态库名前加上前缀lib,然后追加扩展名.a得到的静态库文件名来查找静态库文件,因此,我们在写需要连接的库时,只写名字就可以,如libmyhello.a的库,只写:-lmyhello
gcc -o hello main.c -static -L. -lmyhello
-static代表使用静态链接库,-L.代表静态链接库搜索路径 .代表当前路径
3.3 动态编译可能存在的问题
使用如下命令进行编译,使用libmyhello.so动态链接库编译成一个hello的可执行文件
gcc -o hello main.c -L. -lmyhello
生成hello可执行文件,注意执行的时候可能会报错,说找不到这个
libmyhello.so文件,如果放在/lib或者/usr/lib下,那么默认就能找到,如果放在其他目录下,需要编辑/etc/ld.so.conf文件,加入库文件所在目录的路径,然后 运行ldconfig 目录名字,该命令会重建/etc/ld.so.cache文件即可。
注意:关于什么是 /etc/ld.so.conf、/etc/ld.so.conf.d、/etc/ld.so.cache、什么是ldconfig?后面会介绍
四、Linux下静态库,动态库,以及arm平台下库的基本概念
4.1、什么是库
在 windows 平台和 Linux 平台下都大量存在着库。
本质上来说库是 一种可执行代码的二进制形式,可以被操作系统载入内存执行。
由于 windows 和 linux 的平台不同(主要是编译器、汇编器和连接器 的不同),因此二者库的二进制是不兼容的。
本文仅限于介绍 linux 下的库。
4.2、 库的种类
linux 下的库有两种:静态库和共享库(动态库)。
二者的不同点在于代码被载入的时刻不同。
(1)静态库的代码在编译过程中已经被载入可执行程序,因此体积较大。
静态用.a为后缀, 例如: libhello.a
(2)共享库(动态库)的代码是在可执行程序运行时才载入内存的,在编译过程中仅简单的引用,因此代码体积较小。
动态通常用.so为后缀, 例如:libhello.so
共享库(动态库)的好处是,不同的应用程序如果调用相同的库,那么在内存里只需要有一份该共享库的实例。
为了在同一系统中使用不同版本的库,可以在库文件名后加上版本号为后缀,例如: libhello.so.1.0,由于程序连接默认以.so为文件后缀名。所以为了使用这些库,通常使用建立符号连接的方式。
ln -s libhello.so.1 libhello.so
4.3、静态库,动态库文件在linux下是如何生成的
以下面的代码为例,生成上面用到的hello库:
/* hello.c */
#include "hello.h"
void sayhello()
{
printf("hello,world ");
}
首先用gcc编绎该文件,在编绎时可以使用任何合法的编绎参数,例如-g加入调试代码等:
$gcc -c hello.c -o hello.o
(1)生成静态库 生成静态库使用 ar工具,其实ar是archive的意思
$ar cqs libhello.a hello.o
(2)生成动态库 用gcc来完成,由于可能存在多个版本,因此通常指定版本号:
$gcc -shared -o libhello.so.1.0 hello.o
4.4、库文件是如何命名的,有没有什么规范:
在 linux 下,库文件一般放在/usr/lib和/lib下,
- 静态库的名字一般为libxxxx.a,其中 xxxx 是该lib的名称;
- 动态库的名字一般为libxxxx.so.major.minor,xxxx 是该lib的名称,major是主版本号,minor是副版本号
注意:
在静态库和动态库中,Linux有自己的命名约定,我们在使用的时候可以使用全名,也可以使用简单缩写名,即
- libxxxx.a 等价于 -lxxxx
- libxxxx.so 等价于 -lxxxx
所以如果是一个程序中需要同时链接到同名称的静态库和动态库怎么办呢?
当一个库同时存在静态库和动态库时,比如libmysqlclient.a和libmysqlclient.so同时存在时:
在Linux下,动态库和静态库同事存在时,gcc/g++的链接程序,默认链接的动态库。
可以使用下面的方法,给连接器传递参数,看是否链接动态库还是静态库。
-WI,-Bstatic -llibname //指定让gcc/g++链接静态库
使用:
gcc/g++ test.c -o test -WI,-Bstatic -llibname
-WI,-Bdynamic -llibname //指定让gcc/g++链接动态库
使用:
gcc/g++ test.c -o test -WI,-Bdynamic -llibname
如果要完全静态加在,使用-static参数,即将所有的库以静态的方式链入可执行程序,这样生成的可执行程序,不再依赖任何库,同事出现的问题是,这样编译出来的程序非常大,占用空间。
1.5、可执行程序在执行的时候如何定位静态库与共享库(动态库)文件 :
当系统加载可执行代码(即库文件)的时候,能够知道其所依赖的库的名字,但是还需要知道绝对路径,此时就需要系统动态载入器 (dynamic linker/loader),一般有如下的搜索顺序
(1)静态库链接时搜索路径的顺序:
- ld会去找gcc/g++命令中的参数-L;ld 指的就是GNU套件中的 ld工具
- 再找gcc的环境变量LIBRARY_PATH,它指定程序静态链接库文件搜索路径;
- 再找默认库目录 /lib /usr/lib /usr/local/lib,这是当初compile gcc时写在程序内的。
(2)动态链接时、执行时搜索路径顺序:
- 编译目标代码时指定的动态库搜索路径;
- 环境变量LD_LIBRARY_PATH指定动态库搜索路径,它指定程序动态链接库文件搜索路径;
- 配置文件/etc/ld.so.conf中指定的动态库搜索路径;
- 默认的动态库搜索路径/lib;/usr/lib。
五、一些常用的可执行程序工具
(1)ldd工具
使用ldd工具,查看可执行程序依赖那些动态库或着动态库依赖于那些动态库:
ldd 命令可以查看一个可执行程序依赖的共享库,
例如
ldd /bin/lnlibc.so.6
=> /lib/libc.so.6 (0×40021000)/lib/ld-linux.so.2
=> /lib/ld- linux.so.2 (0×40000000)
(2)nm工具
使用nm工具,查看静态库和动态库中有那些函数名(T类表示函数是当前库中定义的,U类表示函数是被调用的,在其它库中定义的,W类是当前库中定义,被其它库中的函数覆盖)。:
有时候可能需要查看一个库中到底有哪些函数,nm工具可以打印出库中的涉及到的所有符号,这里的库既可以是静态的也可以是动态的。
nm列出的符号有很多, 常见的有三种::
- 一种是在库中被调用,但并没有在库中定义(表明需要其他库支持),用U表示;
- 一种是在库中定义的函数,用T表示,这是最常见的;
- 另外一种是所 谓的"弱态"符号,它们虽然在库中被定义,但是可能被其他库中的同名符号覆盖,用W表示。
例如,假设开发者希望知道上文提到的hello库中是否引用了 printf():
$nm libhello.so | grep printf
发现printf是U类符号,说明printf被引用,但是并没有在库中定义。
由此可以推断,要正常使用hello库,必须有其它库支持,使用ldd工具查看hello依赖于哪些库:
$ldd hello libc.so.6=>/lib/libc.so.6(0x400la000) /lib/ld-linux.so.2=>/lib/ld-linux.so.2 (0x40000000)
从上面的结果可以继续查看printf最终在哪里被定义。
(3)ar工具
使用ar工具,可以生成静态库,同时可以查看静态库中包含那些.o文件,即有那些源文件构成。
可以使用 ar -t libname.a 来查看一个静态库由那些.o文件构成。
可以使用 ar q libname.a xxx1.o xxx2.o xxx3.o ... xxxn.o 生成静态库
静态库的后缀是.a,它的产生分两步
- 由源文件编译生成一堆.o,每个.o里都包含这个编译单元的符号表
- ar命令将很多.o转换成.a,成为静态库
(4)ld工具
ld 命令是二进制工具集 GNU Binutils 的一员,是 GNU 链接器,用于将目标文件(.o)与库链接为可执行文件或库文件