静态库(Static Link Library)
静态库的概念和作用
静态库的概念
静态库是一种包含了多个对象文件(object file)的文件,这些对象文件在链接阶段会被整合到最终的可执行文件中。静态库的文件扩展名通常为 .a
(在Unix-like系统中)或 .lib
(在Windows系统中)
在c文件生成可执行文件时会经过四个阶段——预处理(.c)
、编译(.s)
、汇编(.o)
、链接。
而静态库文件本质上是一个特殊的目标文件(.o)
。在链接阶段和所有的目标文件进行整合、链接,最后生成了一个可执行文件。
但是在实际开发中往往不会将静态库直接做成一个.o
文件,而是会通过其他命令,生成一个.a
文件。
主要原因有以下几点:
- 方便管理:在大型开发中,一个静态库往往由众多数量的.o文件构成,如果仅仅使用.o文件,那么将会非常混乱。
- 提高编译效率:多个.o文件在程序链接过程中是非常影响效率,所以将这些文件整合编译成一个.a文件,软件编译的链接阶段则只需要链接这一个库即可。
- 安全性:.a文件方便进行权限设置,限制对代码的修改或者盗用
静态库的优缺点:
静态库的优点有如下几点:
- 代码复用:静态库可以包含一些常用的函数和数据结构,这样就可以在多个程序中复用这些代码,而无需每次都重新编写。
- 模块化:通过将相关的函数和数据结构打包到一个静态库中,可以使得代码结构更清晰,更易于管理和维护。
- 减少编译时间:当静态库的代码没有改变时,链接静态库的程序无需重新编译库中的代码,这可以大大减少编译时间
静态库的缺点:
- 扩大可执行文件:因为静态库需要将所有的代码复制到程序中共同编译,所以会增大最终生成的可执行文件的大小
- 占用内存:如果有多个程序都调用了同一个静态库,那么在内存中依旧会复制同样数量级的代码,占用不同的空间,而不是共同同一空间。
- 更新问题:由于静态库是在编译源代码时参与编译过程,所以如果库发生了即便非常微小的变化,也会导致整个软件重新编译。
如何创建静态库
1、编译源代码
使用gcc
命令将.c
文件编译成.o
文件。
//单个文件编译有两种方式,第二种可以在-o后面自己指定名称
gcc -c file1.c 或 gcc file.c -o file.o -c
//多个文件编译,多文件编译没有-o形式
gcc -c file1.c file2.c
编译之后会得到一个或者多个.o
文件
2、创建静态库
将一个或者多个.o
文件生成.a
文件需要使用ar
命令(archiver 归档器)
ar
命令一般跟随三个参数rcs
r
:代表”replace”,意为替换。如果静态库中已经存在同名的成员,那么这个选项会用新的对象文件替换它。c
:代表”create”,意为创建。这个选项会创建一个新的静态库。如果静态库已经存在,那么这个选项不会产生任何效果。s
:代表”index”,意为索引。这个选项会创建一个对象文件的索引,以加快链接器的查找速度。
注意:在静态库的命名中有一个约定——lib文件名.a
//示例命令--假设目标文件为temp.o
ar rcs libtemp.a temp.o
//多文件编译
ar rcs libtemp.a temp1.o temp2.o temp3.o
3、补充
静态库编译并没有类似gcc编译c文件成可执行文件的缩减命令,即使用gcc -o将四个步骤一步到位。但是可以使用shell
命令中的&&
命令(若前一个命令执行成功则执行后一个命令)进行类似的操作。
//单文件编译
gcc temp.c -o temp.o -c && ar rcs libmytemp.a temp.o
//多文件编译
gcc -c temp1.c temp2.c && ar rcs libtemp.a temp1.o temp2.o
如何链接静态库
链接程序时指定静态库。可以通过在gcc命令行中添加-L
选项(用于指定库的搜索路径,既可以是相对路径也可以是绝对路径)和-l
选项(用于指定库的名称)来完成。但是这个库不需要写lib
和.a
后缀
不过即便是存在静态库,但是在写源代码的时候,依然需要对静态库函数进行声明。所以通常一个静态库会包含一个.h
头文件。在编写源代码的时候include
这个头文件。如果头文件在其他地方也可以使用-I
来定位
//示例命令,假设有一个静态库libtemp.a
gcc main.c -o main -L./ -ltemp
示例1:
假设有一个文件main.c其同文件家中有1.h 1.c 2.h 2.c 。
//1.h
#ifndef _1_H
#define _1_H
#include<stdio.h>
void func1();
#endif
//1.c
#include"1.h"
void func1(){
printf("这是1.c\n");
}
//2.h
#ifndef _2_H
#define _2_H
#include<stdio.h>
void func2();
#endif
//2.c
#include"2.h"
void func2(){
printf("这是2.c\n");
}
//main.c
#include"1.h"
#include"2.h"
int main(){
func1();
func2();
}
将1.c和2.c编译成静态库
gcc -c 1.c 2.c && ar rcs libtemp.a 1.o 2.o
如果我们假设这个静态库在同目录的tempdir目录中,那么应该这样链接libtemp.a
gcc main.c -o main -L./tempdir -ltemp
输入./main
运行文件,可以得到这样的结果:
这是1.c
这是2.c
动态库(Dynamic Link Library)
动态库基础
动态库的概念和作用
动态库的概念
动态库是一种包含了多个对象文件(object file)的文件,这些对象文件在程序运行时才被加载和链接到程序中。动态库的文件扩展名通常为 .so
(在Unix-like系统中)或 .dll
(在Windows系统中)。
和静态库不同的是,动态库只有在编译成.so文件后才能发挥出特有功能
- 运行时链接:程序链接动态库是在运行过程中,而不是在编译阶段,所以在封装为动态库文件.so时才能实现这个功能。
- 代码共享:多个程序如果相对同一个动态库操作,只有封装成.so文件才能实现这个功能
动态库的优点和缺点
动态库的优点:
- 节省内存空间:动态库可以实现同一个代码被多个程序访问,而非单独复制给每个程序,大大减少了内存占用。
- 降低可执行文件的大小:在编译阶段,是不需要链接动态库的,所以大大减少了代码数量
- 版本控制:如果想修改库的内容,只需要对库进行单独的编译,而不需要对所有程序进行重新编译
- 灵活度高:由于动态库是独立于程序之外的代码,所以其可操作性大大提升。
动态库的缺点:
- 运行时依赖:由于动态库是在程序运行过程中加载,所以如果动态库的位置发生移动、文件出现损坏,或者库的版本不兼容都可能导致依赖于此动态库的程序出现异常。
- 安全问题:由于动态库在程序运行阶段被加载,所以这个库是可以被恶意修改或者替换,由此引发数据泄露、程序崩溃等问题。
- 地狱依赖问题:如果一个程序依赖两个动态库,而这两个动态库依赖同一个库,但是这两个动态库依赖的这一个库的版本不同,那么便可能出现不兼容问题。
如何创建动态库
1、编译源代码
动态库和静态库类似,也是需要将源代码编译成.o
目标文件,但是和静态库有区别, 因为静态库是和程序一起编译成可执行文件的,所以在内存中程序是知道静态库函数的位置的,就好比一个人,静态库编译之后就是成为了这个人的一部分,比如手(静态库),人(程序)可以清楚地感知到手的位置。
但是动态库不同,动态库是在程序运行是加载的,程序调用它的时候,无法知道它的位置,那么为了解决这一个问题,就需要在编译的过程中加上-fPIC
选项(Position Independent Code,位置无关代码),从而消除位置的影响。
//示例命令
gcc temp.c -o temp.o -fPIC -c
2、创建静态库
使用gcc编译器和-shared
选项将目标文件链接为一个动态库。在这个过程中,你可以使用-o
选项来指定输出文件的名称。动态库同样有一个约定俗成的命名方式——lib文件名.so
//示例命令
gcc temp.o -o libtemp.so -shared
3、补充
不过这个命令是和静态库不同,是可以存在比较简约的编译命令的。
//示例命令
gcc temp.c -o libtemp.so -fPIC -shared
如何调用动态库
动态库的调用和静态库有着一些区别,首先是程序编译时链接动态库。
链接程序时指定动态库库。可以通过在gcc
命令行中添加-L
选项(用于指定库的搜索路径,既可以是相对路径也可以是绝对路径)和-l
选项(用于指定库的名称)来完成。但是这个库不需要写lib
和.
so后缀。
和静态库类似,也需要在源代码阶段时声明函数,动态库也会提供.h
文件,可以使用include
包含.h
文件。
//示例命令(假设当前目录存在libtemp.so动态库)
gcc main.c -o main -L./ -ltemp
不过这仅仅是让源代码编译的时候知道有这个动态库的存在,在实际运行程序的时候,还应该通过其他额外的操作来指定动态库的位置,以告诉操作系统在什么地方能找到该动态库
以下提供几种解决方案。
1、使用rpath或RUNPATH
rpath
或RUNPATH
是链接器的一个选项,它允许在编译程序时指定动态库的搜索路径。这个路径会被嵌入到生成的可执行文件中。
使用-Wl,-rpath,路径
选项来设置rpath
。-Wl
告诉gcc
将后面的选项传递给链接器,-rpath
是链接器的一个选项,后面跟着的是你要设置的路径。
优点:rpath
或RUNPATH
会被嵌入到可执行文件中,因此无论你在何处运行程序,它都能找到正确的动态库,这使得程序的移植性更强。
缺点:如果动态库的位置改变了,你可能需要重新编译你的程序,
示例:(假设在当前目录存在一个动态库libtemp.so)
gcc main.c -o main -L. -ltemp -Wl,-rpath,.
补充:
这种方式是将系统指定了一个新的寻找库的路径,实际上如果在路径中找不到需要的库,系统便会回到系统路径中寻找,所以就可能产生不同路径同名库的情况,如果同名库功能不同或者版本不同,可能会出现程序异常。
2、使用LD_LIBRARY_PATH
环境变量
LD_LIBRARY_PATH
是一个环境变量,用于在Linux和其他类Unix系统中指定动态链接器搜索动态库的路径。当你运行一个程序时,动态链接器需要找到程序所需的所有动态库。
不过这种方式是一个临时性的操作,只有在当前的shell中有用,所以往往被用于对库的测试。
优点:LD_LIBRARY_PATH
可以临时改变动态库的搜索路径,这在测试新库或者非标准安装的库时非常有用
缺点:LD_LIBRARY_PATH
只对当前的shell会话有效,如果开启了一个新的shell会话,需要重新设置LD_LIBRARY_PATH
。
示例:(假设在当前目录存在一个动态库libtemp.so)
export LD_LIBRARY_PATH=/home/user/mylibs
补充:
这中方式是临时性的将系统指定了一个新的寻找库的路径,实际上如果在路径中找不到需要的库,系统便会回到系统路径中寻找,所以就可能产生不同路径同名库的情况,如果同名库功能不同或者版本不同,可能会出现程序异常。而且对于不同的设备,甚至同一设备的不同shell都会出现差异,所以几乎没有移植性可言,而实际开发中这种方式比较适合测试
3、使用/etc/ld.so.conf
文件
/etc/ld.so.conf
是一个系统级别的配置文件,用于指定动态链接器搜索动态库的路径。这个文件中可以包含一系列的目录路径,每个路径占一行。当你运行一个程序时,动态链接器会在这些目录中查找需要的动态库。
优点:这是一个全局的设置,对所有用户和程序都有效。
缺点:需要管理员权限。此外,每次修改/etc/ld.so.conf
后,都需要运行ldconfig
来更新配置。
示例1:假设动态库位于/home/user/mylibs
目录下
echo "/home/user/mylibs" | sudo tee -a /etc/ld.so.conf
sudo ldconfig
这个命令会将/home/user/mylibs
添加到/etc/ld.so.conf
文件的末尾。tee -a
命令用于将输入追加到文件中,sudo
命令用于获取管理员权限,因为修改/etc/ld.so.conf
文件通常需要管理员权限。不过要注意的是动态库的路径必须使用绝对路径
补充:一些Linux发行版(如Debian,Ubuntu等)提供了/etc/ld.so.conf.d/
目录,你可以在这个目录下创建新的配置文件,而不是直接修改/etc/ld.so.conf
文件。这可以让你更方便地管理你的库路径。
示例2:假设动态库位于/home/user/mylibs
目录下
echo "/home/user/mylibs" | sudo tee /etc/ld.so.conf.d/mylibs.conf
sudo ldconfig
4、将动态库放入系统路径(例如/usr/lib)
优点:简单、不需要额外的操作。
缺点:可移植性差
动态库的高级主题
动态操作库
dl库的概念
在实际开发中,可能会遇到以下场景:在开发一个程序后,可能在后期有动态库对这个程序功能进行修改。由于这个动态库在目前处于未开发阶段,所以头文件甚至是库名称都处于未知状态。那么如何在不重新修改源代码的情况下让动态库加载到程序中。
为了实现这种动态操作的功能,那么就需要用到库——libdl.so
,头文件dlfcn.h
libdl.so库是一操作系统级别的库,遵循POSIX标准(Portable Operating System Interface),主要用于支持程序在运行时动态地加载和卸载共享对象文件(也就是动态链接库),并获取其中定义的符号(symbol)并进行调用。
这个库存在动态库版本和静态库版本,在默认情况下使用动态版本也就是.so
//示例命令
gcc main.c -o main -ldl
如果希望使用静态库可以特指静态库
//示例命令
gcc main.c -o main -ldl -static
这个库中提供了一组函数,主要为:
dlopen
:这个函数用于打开一个动态链接库文件并返回一个句柄,该句柄用于后续的操作,如查找符号、关闭库等dlsym
:这个函数用于在打开的动态链接库中查找指定的符号,并返回符号的地址dlclose
:这个函数用于关闭先前打开的动态链接库dlerror
:这个函数返回最近一次动态链接库操作的错误信息
加载动态库
dlopen函数
是在Unix-like
系统中常用的动态库加载函数,它的作用是在运行时动态地加载共享库文件,并返回一个该库的句柄,以供后续使用
函数原型:
void *dlopen(const char *filename, int flag);
函数参数:
filename
:这是你想要打开的动态链接库的文件名。如果filename
是NULL
,那么返回的句柄是主程序的句柄flag
:这是打开库的方式,主要有两种:RTLD_LAZY
:进行懒惰绑定,只有在引用到库中的函数时才会解析符号。RTLD_NOW
:立即解析库中所有未定义的符号。如果无法解析所有的符号,dlopen
会返回NULL
。
RTLD(run-time dynamic loading)
函数返回值:
返回动态库的句柄
使用示例:
#include<dlfcn.h>
//...
void* handle = dlopen("./home/user/mylib/libtemp.so",RTLD_LAZY);//懒加载
//...
查找函数
dlsym函数
用于在运行时查找动态链接库中的符号。这对于实现插件架构或需要在运行时加载代码的应用程序非常有用
函数原型:
void *dlsym(void *handle, const char *symbol);
函数参数:
handle
:这是一个由dlopen函数返回的句柄,它指向你想要查询的动态链接库。symbol
:这是你想要查找的符号的名称,可以是变量名或函数名。
函数返回值:
- 如果找到了符号,dlsym函数会返回一个指向该符号的指针。
- 如果没有找到符号,dlsym函数会返回NULL,并设置相应的错误消息,可以通过dlerror函数获取。
函数示例:
假设./home/user/mylib/libtemp.so
库中有一个函数int add(int a,int b)
#include<dlfcn.h>
//...
void* handle = dlopen("./home/user/mylib/libtemp.so",RTLD_LAZY);
int (*lib_add)(int,int) = (int (*)(int,int))dlsym(handle,"add");
//...
错误处理。
dlerror
函数用于获取最近一次动态链接操作的错误消息。
函数原型:
char *dlerror(void);
函数返回值:
dlerror
函数返回一个描述最近一次从dlopen
,dlsym
或dlclose
发生的错误的错误信息。- 如果自初始化以来或自上次调用以来没有发生错误,
dlerror
函数将返回NULL
。
函数示例:
假设./home/user/mylib/libtemp.so
库中有一个函数int add(int a,int b)
#include<dlfcn.h>
//...
void* handle = dlopen("./home/user/mylib/libtemp.so",RTLD_LAZY);
int (*lib_add)(int,int) = (int (*)(int,int))dlsym(handle,"add");
char *error = dlerror();
//...
卸载动态库
函数原型:
int dlclose(void *handle);
函数参数:
handle
:这是一个由dlopen函数返回的句柄,它指向你想要关闭的动态链接库
函数返回值:
- 如果成功关闭了引用的符号表句柄,
dlclose
函数将返回0。 - 如果
handle
不指向一个打开的符号表句柄,或者如果无法关闭符号表句柄,dlclose函数将返回一个非零值。 - 更详细的诊断信息可以通过
dlerror
函数获得。
函数作用:
dlclose
函数用于通知系统,应用程序不再需要由handle
指定的符号表句柄。- 当符号表句柄被关闭时,实现可能会卸载由
dlopen
在打开符号表句柄时加载的可执行对象文件,以及由dlsym
在使用由handle
标识的符号表句柄时加载的那些。 - 一旦符号表句柄已经被关闭,应用程序应该假设任何使用
handle
使之可见的符号(函数标识符和数据对象标识符)对进程来说都不再可用
函数示例:
假设./home/user/mylib/libtemp.so
库中有一个函数int add(int a,int)
#include<dlfcn.h>
//...
void* handle = dlopen("./home/user/mylib/libtemp.so",RTLD_LAZY);
//...
dlclose(handle);
//...
综合
假设存在main.c
文件,且同目录下存在mylib
目录,该目录中存在一个动态库libtemp.so
,该库中有一个函数int add(int a,int b)
//main.c
#include<stdio.h>
#include<dlfcn.h> //dl库
#include<stdlib.h>
int main(){
void *handle = dlopen("./mylib/libtemp.so",RTLD_LAZY);//以懒加载的方式加载libtemp.so动态库,获取该库的句柄
if(!handle){//判断是否获取成功
fprintf(stderr,"%s\n",dlerror());//将错误信息输入到标准错误流中
exit(EXIT_FAILURE);
}
dlerror();//获取并清理之前的错误信息。
int (*add)(int,int) = (int (*)(int ,int ))dlsym(handle,"add");//进行动态库的函数符号解析
if(!add){//判断是否解析失败
fprintf(stderr,"%s\n",dlerror());//将错误信息输入到标准错误流中
exit(EXIT_FAILURE);
}
int a = 1, b = 2;
printf("a + b = %d\n",add(a,b));//调用函数add
if(dlclose(handle)){//判断是否动态库关闭失败
fprintf(stderr,"%s\n",dlerror());//将错误信息输入到标准错误流中
exit(EXIT_FAILURE);
}
exit(EXIT_SUCCESS);
}
编译:
gcc main.c -o main -ldl
输出:
a + b = 3
补充:
通过访问dlfcn.h文件,发现其中还有其他的函数:
dlmopen
:这个函数类似于dlopen
,但是它会请求在一个新的命名空间中分配对象。dlvsym
:这个函数类似于dlsym
,但是它会查找具有指定版本的符号。dladdr
:这个函数接受一个地址,然后返回一个包含该地址的对象的信息。dladdr1
:这个函数类似于dladdr
,但是它会根据指定的标志设置额外的信息。dlinfo
:这个函数用于获取动态链接库的信息。
函数和参数均有其他拓展,详情请自行访问dlfcn.h
文件
动态库版本控制
在Linux系统中,动态库的版本控制是一个重要的问题。动态库的版本控制主要通过以下几个方面来实现
1、版本号:
Linux通过版本号来管理动态库的版本,版本号最多有3级,其格式为libname.so.x.y.z:
- x表示主版本号,非兼容修改,可能对接口做了大改动,比如重命名、增加或减少参数等
- y表示次版本号,不改变兼容性,但是增加了新接口
- z表示修订版本号,不改变兼容性,仅仅是修复bug、或者优化代码实现、优化性能等
2、软链接
软链接是文件系统的中一种元素,存储了包含另一个文件路径的字符串。当提供了新版本的动态库时,只需将新版本的动态库文件复制到之前版本的目录下,然后将软链接指向修改到新版本的文件即可
3、SONAME
SONAME是编译生成动态链接库时通过-soname选项指定的一个字段,会被写入so的文件当中,用于管理版本。通过形如gcc -Wl,-soname,libhello.so.1 的方式指定。
4、ldconfig
ldconfig是一个动态链接库管理命令,其目的为了让动态链接库为系统所共享。ldconfig默认搜寻/lib和/usr/lib,以及配置文件/etc/ld.so.conf内所列的目录下的库文件。
补充
库的拓展
系统库:
- glibc:GNU C库,是大多数Linux系统默认的C库,提供了系统调用和基本功能如输入/输出处理。
- libm:这是一个数学库,提供了许多数学函数的实现。
- libpthread:这是线程编程库,提供了POSIX线程编程的API。
第三方库:
- OpenSSL:这是一个强大的安全套接字层密码库,包括主要的密码算法、常用的密钥和证书封装管理功能以及SSL协议,并提供丰富的应用程序供测试或其它目的使用。
- libcurl:这是一个免费的、易用的URL语法库,支持DICT、FILE、FTP、FTPS、GOPHER、HTTP、HTTPS、IMAP、IMAPS、LDAP、LDAPS、POP3、POP3S、RTMP、RTSP、SCP、SFTP、SMTP、SMTPS、TELNET和TFTP等协议。
- libpng:这是一个用于处理PNG图像的库。
- libjpeg:这是一个用于处理JPEG图像的库。
- freetype:这是一个用于渲染字体到位图的库,支持多种字体格式。
- zlib:这是一个压缩和解压缩库。
可拓展方向
ABI——全称为Application Binary Interface,即应用程序二进制接口。它定义了在同一操作系统中的应用程序和系统之间或其他应用程序之间的低级接口
动态库的二进制植入攻击
攻击者控制软件中的某个动态库搜索路径,那么就可以针对程序的接口进行恶意代码的编写替换,然后对软件进行攻击。
预防的方法也有很多,如:
- 可以使用数字签名验证动态的安全性。
- 使用 chroot 环境:chroot 可以创建一个隔离的环境,使得程序只能访问这个环境中的文件和目录。这样就可以防止程序加载到 chroot 环境外部的恶意动态库。
动态库代码共同冲突问题
在多个程序同时调用同一个动态库中的同一个变量时,通常不会出现冲突。这是因为每个程序都有自己的地址空间,程序中的全局变量和静态变量都存储在程序自己的地址空间中,不会与其他程序的变量冲突。