很久就想写一篇关于动态库和静态库互相引用的一篇文章,但是总感觉准备不充分,而一直没有勇气下笔,最近在和同事的讨论中,似乎有了一些新的认识,想把这些记录下来,和大家进行一次分享,同时也是做一次记录。
这篇文章将从以下几个方面进行分析讲解
1.程序的编译过程
2.什么是静态编译,动态编译
3.如何生成静态库,如何生成动态库
4.动态库和静态库相互引用后,应用程序是否可以只使用一个库
(例如:应用用到了静态库a,而静态库a里使用了库b,那么应用程序是不是只要链接a就可以了呢)
5.动态库的两种加载方式
1.程序的编译过程我们通常说的编译程序,是指生成可执行的二进制文件,主要分为四个步骤
1.预处理 -E
处理所有以#开头的代码,包括 头文件 宏定义 条件编译
预处理后的文件我们通常.i结尾
gcc -E hello.c -o hello.i
2.编译 -S
语法检查以及将C语言变成汇编语言
编译后的文件我们通常.s 结尾
gcc -E hello.i -o hello.s
3.汇编 -c
将汇编语言变成二进制文件
编译后的文件我们通常.o结尾
gcc -c hello.s -o hello.o
4.链接 (啥也不写默认就是链接)
链接代码需要用到的其他文件(其他库等)
gcc hello.o -o hello
这个过程也就是说明里,未经过链接的.o文件是不能被执行的。
一道面试题:为什么汇编生成的二进制文件需要链接后才能执行?
编译生成.o文件时,它是一个可重定位文件,编译器还不清楚一些外部函数(变量)的地址,当链接器将.o文件链接成为可执行文件时,必须确定那些函数(变量)的性质,如果是静态目标模块提供的按照静态链接的规则,如果是动态共享对象提供的按照动态链接规则。动态链接时,并不是真正的链接,而是对符号的引用标记为动态链接符号,不重定位,装载时再进行。
所以目标文件还是需要先链接成为可执行文件。动态链接也不是完全在运行时执行链接这个过程,而是先链接对符号进行处理,在运行时重定位。
2.什么是静态编译,动态编译所谓的动态编译(动态链接)和静态编译(静态链接) 指的是生成可执行文件的情况下。
在生成库的时候,不会存在这两个概念,这个要注意区分。
比方说 main函数里用到里printf函数,如果动态编译到话,程序运行到时候
还是需要在运行环境中有libc的库,而静态编译的话,把这个可以执行的程序拿到没有libc库的环境一样可以运行。可执行文件,要求代码里必须有main函数
2.1 静态编译必须要链接静态库吗是的,如果你强制指定了静态编译,那么就会链接静态库
例子:我在main函数里指定了,链接两个库,显示对进行 静态链接
我对指令如下
gcc main.c -o main -static -I ./add -I ./sub -L ./add -L ./sub -l add -l sub
会报下面对错误,
/usr/bin/ld: cannot find -lsub
collect2: error: ld returned 1 exit status
因为在我指定的目录 sub下没有静态库,也就是如果显示的指定静态链接的话
默认的话,用到对所有库都是静态库,如果没有静态库就会报错。
2.2 不能指定 ,只使用动态库的,也就是不能直接写成这个shared当不写的时候,会优先找到动态库,如果没有就会使用静态库,(注意一个问题,就是不能指定为-shared 否则会报段错误)
而我在github的代码上,又会营造这样的场景,就是我的库里面,一个是静态的,一个是动态的
那么我采用 不写的方式,会编译链接成功吗,答案是可以。
gcc main.c -o main -I ./add -I ./sub -L ./add -L ./sub -l add -l sub
具体的代码见 github 链接
https://github.com/zhc2019github/static_compileand_dynamic_compile.git
3.如何生成静态库,如何生成动态库 3.1 静态库的制作方法1. gcc -c xxx.c xxx.c(预处理,编译,汇编,链接 -c是汇编阶段)
2.ar -crv libx.a xxx.o xxx.o
lib 是固定写法,我们库的名字是x
ar -t libx.a
可以查看静态链接库包含的文件
用nm指令可以查看静态库里包含哪些函数的定义,哪些函数的引用,定义没在这里。
程序使用静态库
gcc hello.c -o hello -static -L . -l x
-L 代表 库的路径 -l 代表 库的名字
3.2 动态库的制作方法1.gcc -fPIC -shared -o libxx.so xxx.c xxx.c
程序使用动态库(有两种使用方式,这个是第一种,第一种和第二种的区别参考 动态库的两种加载方式)
gcc hello.c -o hello -L . -l xx
-L 代表 库的路径 -l 代表 库的名字
ldd hello 可以查看动态库的 依赖的库的路径在哪里
用nm指令可以查看静态库里包含哪些函数的定义,哪些函数的引用,定义没在这里。
4.动态库和静态库相互引用后,应用程序是否可以只使用一个库这个问题也是本篇文章的重点所在,我先要强调一点是,这个的使用场景是这样的,作为一个提供库的一方,在形成自己的库的时候,可能会引入其它第三方库,那么为了简单话,我作为提供者,我不太想让应用程序感知到,我使用哪些库了,而我提供了一个总的库,就可以了,这样也是对应用层屏蔽他们不关心的东西,但是这里这个库之间的引用,就比较麻烦了,设计到下面的四种情况
静态库引用静态库
静态库引用动态库
动态库引用静态库
动态库引用动态库
下面给出分析,然后,再进行代码上的验证。先说下我的验证思路,就是我现在在主函数里进行如下调用。先调用b,然后b再调用a. 具体的目录结构如下:
然后主要的逻辑是在这个shell脚本,build.sh里,build.sh里的具体代码如下:
#!/bin/bash
buildStaticA(){
cd funcA
echo "Build Static A"
gcc -c funcA.c -shared -fPIC -o funcA.o
ar -crv libstaticA.a funcA.o
echo "=======nm libstaticA.a======"
nm libstaticA.a
cd ..
}
buildStaticB(){
cd funcB
echo "Build Static B"
path=""
lib=""
case "$1" in
1)
path=funcA
lib=staticA
;;
2)
path=funcA
lib=sharedA
;;
*)
;;
esac
gcc -c -I${PWD}/../${path}/ funcB.c -L${PWD}/../${path}/ -l${lib}
ar -crv libstaticB.a funcB.o
echo "=======nm libstaticB.a======"
nm libstaticB.a
cd ..
}
buildSharedA(){
echo "Build Shared A"
cd funcA
gcc -c funcA.c
gcc -shared -fPIC -o libsharedA.so funcA.o
echo "=======nm libsharedA.so======"
nm libsharedA.so | grep "func*"
cd ..
}
buildSharedB(){
echo "Build Shared B"
cd funcB
path=""
lib=""
case "$1" in
1)
path=funcA
lib=staticA
;;
2)
path=funcA
lib=sharedA
;;
*)
;;
esac
echo ${lib}
#gcc -c -I${PWD}/../${path}/ funcB.c -L${PWD}/../${path}/ -l${lib}
gcc -o funcB.o -c funcB.c -I${PWD}/../${path}/
gcc -o libsharedB.so funcB.o -shared -fPIC -L${PWD}/../${path}/ -l${lib}
#gcc -shared -fPIC -o libsharedB.so funcB.c -I${PWD}/../${path}/ -L${PWD}/../${path}/ -l${lib}
echo "=======nm libsharedB.so======"
nm libsharedB.so | grep "func*"
cd ..
}
buildMain(){
echo "Build Main"
rm a.out *.a -rf
case "$1" in
1)
# ./build.sh 1 1 1 a b 都是静态库,一起使用A和B
gcc -I${PWD}/funcB/ -I${PWD}/funcA/ main.c -L${PWD}/funcB/ -lstaticB -L${PWD}/funcA/ -lstaticA
echo "=======nm a.out======"
nm a.out | grep "func*"
echo "=======run a.out======"
./a.out
;;
2)
# ./build.sh 1 1 2 a b 都是静态库,独立使用B,结论是不可以
gcc -I${PWD}/funcB/ main.c -L${PWD}/funcB/ -lstaticB
;;
3)
# ./build.sh 1 1 3 a b 都是静态库,打包使用A和B
ar -crT libstaticAB.a ${PWD}/funcA/libstaticA.a ${PWD}/funcB/libstaticB.a
echo "=======nm libstaticAB.a======"
nm libstaticAB.a
gcc -I${PWD}/funcB/ main.c -L${PWD} -lstaticAB
echo "=======nm a.out======"
nm a.out | grep "func*"
echo "=======run a.out======"
./a.out
;;
4)
# ./build.sh 2 1 4 a是动态,b是静态,一起使用a和b
gcc -I${PWD}/funcB/ -I${PWD}/funcA/ main.c -L${PWD}/funcB/ -lstaticB -L${PWD}/funcA/ -lsharedA
echo "=======nm a.out======"
nm a.out | grep "func*"
echo "=======run a.out======"
export LD_LIBRARY_PATH=${PWD}/funcA;./a.out
;;
5)
# ./build.sh 2 1 5 a是动态,b是静态,独立使用b不可以
gcc -I${PWD}/funcB/ main.c -L${PWD}/funcB/ -lstaticB
;;
6)
# ./build.sh 1 2 6 a是静态,b是动态,独立使用b就可以了
gcc -I${PWD}/funcB/ main.c -L${PWD}/funcB/ -lsharedB
echo "=======nm a.out======"
nm a.out | grep "func*"
echo "=======run a.out======"
export LD_LIBRARY_PATH=${PWD}/funcB;./a.out
;;
7)
# ./build.sh 2 2 7 a 是动态,b是动态,一起使用a和b
gcc -I${PWD}/funcB/ -I${PWD}/funcA/ main.c -L${PWD}/funcB/ -lsharedB -L${PWD}/funcA/ -lsharedA
echo "=======nm a.out======"
nm a.out | grep "func*"
echo "=======run a.out======"
export LD_LIBRARY_PATH=${PWD}/funcB:${PWD}/funcA;./a.out
;;
8)
# ./build.sh 2 2 8 a是动态,b是动态,独立使用b不可以
gcc -I${PWD}/funcB/ main.c -L${PWD}/funcB/ -lsharedB
echo "=======nm a.out======"
nm a.out | grep "func*"
echo "=======run a.out======"
export LD_LIBRARY_PATH=${PWD}/funcB:${PWD}/funcA;./a.out
;;
*)
echo "do nothing"
;;
esac
}
clear() {
find . -name "*.o" | xargs rm
find . -name "*.a" | xargs rm
find . -name "*.so" | xargs rm
rm a.out
}
clear
case "$1" in
1)
buildStaticA
;;
2)
buildSharedA
;;
*)
echo "do nothing"
;;
esac
case "$2" in
1)
buildStaticB $1
;;
2)
buildSharedB $1
;;
*)
echo "do nothing"
;;
esac
buildMain $3
脚本执行的时候,传入三个参数,具体的含义解释如下:
第一个参数决定 a库编译成静态库还是动态库,1 静态库,2,动态库
第二个参数决定b 库编译成静态库还是动态库,1静态库,2.动态库
第三个参数主要用来区分所有的情况,具体如下:
1)
# ./build.sh 1 1 1 a b 都是静态库,一起使用A和B
2)
# ./build.sh 1 1 2 a b 都是静态库,独立使用B,结论是不可以
3)
# ./build.sh 1 1 3 a b 都是静态库,打包使用A和B
4)
# ./build.sh 2 1 4 a是动态,b是静态,一起使用a和b
5)
# ./build.sh 2 1 5 a是动态,b是静态,独立使用b不可以
6)
# ./build.sh 1 2 6 a是静态,b是动态,独立使用b就可以了
7)
# ./build.sh 2 2 7 a 是动态,b是动态,一起使用a和b
8)
# ./build.sh 2 2 8 a是动态,b是动态,独立使用b不可以
如上图,也是build脚本的运行时使用的参数
还有我们假设,我们是库b,然后使用了第三方库a,然后我们手里有第三方库的 .a和.so(也就是静态库和动态库),但是没有第三库的.o文件。
4.1 静态库引用静态库4.1.1 静态库a和静态库b应用程序都引用,是没有问题的
对应的执行脚本就是 ./build.sh 1 1 1 a b 都是静态库,一起使用A和B
执行后的输出如下图:
主要看下我们nm指令的输出,其中的T 是代表库中含有这个函数的实现,U 代表只是引用了这个函数,并没有这个函数的实现。
我们在 libstatic.a里看到了它有函数funcA 的实现。
而在这个libstatic.b里看到了,它没有函数funcA 的实现,只是进行了引用。
这也就解释了,应用程序中。单独链接库b,是不可以的。因为它根本没有funcA的实现。
静态库不是可以执行的二进制文件,他只是目标文件的集合(库是预编译的目标文件(object files)的集合,它们可以被链接进程序)
编译静态库时只有编译过程,没有链接过程,静态库引用其它库并不会在编译的时候把引用的库函数编译到生成的 lib 中,只是简单的将编译后的中间文件打包,在编译最终的可执行项目(.exe 和 .dll)的时候,需要引用所有的库,进行符号消解。
4.1.2 独立使用静态库b
./build.sh 1 1 2 a b 都是静态库,独立使用B,结论是不可以
4.1.3 打包使用静态库a和b,就是把a和b打包后进行使用。
./build.sh 1 1 3 a b 都是静态库,打包使用A和B
具体看下shell脚本中的代码如下:
3)
# ./build.sh 1 1 3 a b 都是静态库,打包使用A和B
ar -crT libstaticAB.a ${PWD}/funcA/libstaticA.a ${PWD}/funcB/libstaticB.a
echo "=======nm libstaticAB.a======"
nm libstaticAB.a
gcc -I${PWD}/funcB/ main.c -L${PWD} -lstaticAB
echo "=======nm a.out======"
nm a.out | grep "func*"
echo "=======run a.out======"
./a.out
;;
我虽然没看过,ar -crT的底层实现,但是我感觉大致实现就是。反得到两个库的.o,然后把两个点o连接到一起,进行生成 libstaticAB.a
4.2 静态库引用动态库4.2.1 静态库引用动态库后,不能链接到静态库里,需要一起使用a和b
# ./build.sh 2 1 4 a是动态,b是静态,一起使用a和b
4.2.2 验证只是使用静态库会报错的过程
# ./build.sh 2 1 5 a是动态,b是静态,独立使用b不可以
4.3 动态库引用静态库这个引用后,是可以的,静态库会被链接进动态库。
# ./build.sh 1 2 6 a是静态,b是动态,独立使用b就可以了
执行脚本后的输出如下:
Build Static A
a - funcA.o
=======nm libstaticA.a======
funcA.o:
0000000000000000 T funcA
U _GLOBAL_OFFSET_TABLE_
U puts
Build Shared B
staticA
=======nm libsharedB.so======
0000000000000677 T funcA
000000000000065a T funcB
Build Main
=======nm a.out======
U funcB
=======run a.out======
main
func B enter
func A enter
我们可以清楚的看到这个,libsharedB.so中已经有了这个 func的实现了,对应的类型是T。
但是这个要注意下:
这个编译静态库A的时候,要使用 -shared -fPIC,这个具体的原因可以参考我在文章末尾给出的链接。
4.4 动态库引用动态库4.4.1 动态库b引用动态库a后,也不能把a中的函数链接到b中,所以还是要一起使用b和a
# ./build.sh 2 2 7 a 是动态,b是动态,一起使用a和b
4.4.2 验证单独使用b会出现问题
# ./build.sh 2 2 8 a是动态,b是动态,独立使用b不可以
对应刚才程序的github链接如下:
github代码链接
5.动态库的两种加载方式这个后续再进行补充
参考链接:
动态库(.so)链接静态库(.a)的总结 - 很实用讲解很清楚_sevencheng798的博客-CSDN博客_动态库静态链接



