库是写好的现有的,成熟的,可以复用的代码。现实中每个程序都要依赖很多基础的底层库,不可能每个人的代码都从零开始,因此好用的轮子大家一起用。本质上来说库是一种可执行代码的二进制形式,可以被操作系统载入内存执行。库有两种:
静态库.a(win 系统下是lib)和动态库.so(win 系统下是.dll)。
1.静态库和动态库的特点
静态库:编译过程中已经被载入可执行程序,因此程序体积较大。
共享库:可执行程序运行时才载入内存的,在编译过程中仅简单的引用,因此程序体积较小
针对2者的特点可以发现一些问题
静态库:方便移植,运行程序不需要库的存在,程序已经包含库了,库发生任何改变都要重新编译程序
动态库:不利于移植,运行程序需要库的存在,多个程序共享同一个动态库(不同虚拟地址映射同一物理地址),利于升级程序
命名特点:Linux:lib$(libname).a, lib$(libname).so, 链接 -l$(libname), Windows: $(libname).lib, l$(libname).dll
2.静态库和动态库制作
静态库
gcc -c math.c --------------编译生成.o,只会检查语法错误,对于只有声明,没有定义也可以执行成功
ar crs libmath.a math.o --------利用math.o生成libmath.a库,ar命令+命令参数
动态库
gcc -c -fPIC math.c ----------fPIC生成地址无关程序,为了能够在多个应用程序共享,最好不要-fPIC移到下面
gcc -shared -o libmath.so math.o ------ -shared生成动态库,
上面是Linux环境下的制作方法,Windows下,可以利用VS IDE(vs2010举例)。
新建项目--->创建win32程序-->选择生成dll/静态库,并打勾空项目
实际在项目-->属性-->常规-->配置类型(lib、dll、exe这些可选),并且注意这些库默认是32位的,还可以配置生成64位
3.库的路径设置
Linux的默认库路径
/lib、/usr/lib、/usr/local/lib(这个路径一般放第3方库)
如果安装在其他目录,需要将其添加到/etc/ld.so.cache文件中,步骤如下:
编辑/etc/ld.so.conf文件,加入库文件所在目录的路径或者在ld.so.conf.d下新建一个.conf文件
运行ldconfig ,该命令会重建/etc/ld.so.cache文件
静态库连接时路径:对于自己写和不常用一般不会放在链接库的默认路径下,只会放在自己创建的目录下,链接的时候用-L来指定需要链接的库的路径;对于系统标准库和常用的第3方库,一般会放在默认路径下,因为在装第3方库的时候,会把库和库的头文件放在相应的库的默认路径下
动态库链接以及运行时路径:除了上面的设置以外,还有一种临时简单的设置,通过LD_LIBRARY_PATH制定。例如LD_LIBRARY_PATH = /share/lib ./test ,指定运行test程序的库路径
Windows下不是很熟悉,对于静态库来说,只需要在IDE中设置好链接时的输入和目录就行,对于动态库没有什么研究,主要是Windows的动态库和我认知不太一样,生成动态库的时候,也有链接输入选项,而且还必须输入这个动态库依赖的静态库,这点和Linux不同,动态库的路径设置没有研究
4.库的使用
库的使用好像很简单,那么不知道你们思考过这样的问题吗, 1个进程中静态库和动态库的混合使用;1个进程中动态库和静态库都要依赖,且动态库依赖于另一个静态库;2个进程中依赖的动态库都依赖某一个静态库;
这些问题让我很迷惑,说到底动态库的原理没有理解透,我的原则是,最好不要出现1个进程中动态库和静态库都要依赖,且动态库依赖于另一个静态库,这有悖于程序设计理念(模块化,抽象,去耦合),2个动态库都依赖同一个静态库,这个问题还不太清楚,我认为是没有问题的,现在想想动态库的实现真的很复杂
库的使用举例:
gcc main.c -o main -L . -lmath -lutil ----->默认情况下优先链接动态库,-L .指定库路径为当前目录, -I指定库名
gcc main.c -o main_all_static -L . -static -lmath -lutil ------->所有链接的库都为静态库包括标准c库,所以生成的文件很大
gcc main.c -o main_libm_sou -L . Wl,-Bstatic -lmath Wl,-Bdynamic -lutil ----->链接的math为静态库,其余为动态库
gcc main.c -o main_libu_som -L . Wl,-Bstatic -lutil Wl,-Bdynamic -lmath ----->链接的util为静态库,其余为动态库
-Wl告诉链接器接下来,链接那种库
上面的这些编译,运行都没问题,但是并不代表在多个进程都要使用上面2个库(util为动态库,math为静态库)的时候没问题,因为util要依赖math
静态链接和动态链接混用可以避免一些兼容问题
链接静态库更好的办法是-l:libxxx.a
有些链接器,链接静态库的时候,链接顺序也会导致链接错误,加入a依赖b,那么a应该放前面,b放后面
5.进阶知识
-fPIC的思考:不会使用绝对地址,只会使用相对地址,
-fPIC使.so文件的代码段变为真正意义上的共享
如果不加-fPIC,则加载.so文件的代码段时,代码段引用的数据对象需要重定位, 重定位会修改代码段的内容,这就造成每个使用这个.so文件代码段的进程在内核里都会生成这个.so文件代码段的copy.每个copy都不一样,取决于 这个.so文件代码段和数据段内存映射的位置.
non-PIC 与 PIC 代码的区别主要在于 access global data, jump label 的不同。
比如一条 access global data 的指令,
non-PIC 的形势是:ld r3, var1
PIC 的形式则是:ld r3, var1-offset@GOT,意思是从 GOT 表的 index 为 var1-offset 的地方处
指示的地址处装载一个值,即 var1-offset@GOT 处的4个 byte 其实就是 var1 的地址。这个地址只有在运行的时候才知道,
是由 dynamic-loader(ld-linux.so) 填进去的。
再比如 jump label 指令
non-PIC 的形势是:jump printf ,意思是调用 printf。
PIC 的形式则是:jump printf-offset@GOT,意思是跳到 GOT 表的 index 为 printf-offset 的地方处
指示的地址去执行,这个地址处的代码摆放在 .plt section,每个外部函数对应一段这样的代码,其功能是呼叫
dynamic-loader(ld-linux.so) 来查找函数的地址(本例中是 printf),然后将其地址写到 GOT 表的 index 为 printf-offset 的地方,
同时执行这个函数。这样,第2次呼叫 printf 的时候,就会直接跳到 printf 的地址,而不必再查找了。
GOT 是 data section, 是一个 table, 除专用的几个 entry,每个 entry 的内容可以再执行的时候修改;
PLT 是 text section, 是一段一段的 code,执行中不需要修改。
查看执行文件和动态库的动态库依赖的工具 :ldd file_name