静态库与动态库

0x81 静态库与动态库

库的概念在我们日常开发和使用软件中是非常常见的,它们通常是一组经过编译的代码的组合,比如Linux发行版当中就存在大量的共享库提供给操作系统其他软件使用,它们可能存在多个版本,我们平时安装的依赖包有80%是程序二进制运行所需要的库文件。按一般说法,库通常分为静态库和动态库,前者是编译进了最终的二进制文件,后者以一种动态加载的方式提供功能。以C++为例,静态库通常是.a/.lib结尾,动态库通常是.so/.dll结尾。

0x82 如何编译一个静态库

静态库一般是一组代码的集合,它们可以独立于main函数之外,专于提供相应的功能,链接器在链接静态库时会将所有的内容链接到可执行二进制文件或其他库中。下面我们举个简单的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// StaticMath.h
#pragma once
class StaticMath {
public:
StaticMath(void);
~StaticMath(void);

static double add(double a, double b); // 加法
static double sub(double a, double b); // 减法
static double mul(double a, double b); // 乘法
static double div(double a, double b); // 除法

void print();
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// StaticMath.cpp
#include "StaticMath.h"

#include <iostream>

StaticMath::StaticMath() {
}

StaticMath::~StaticMath() {
}

double StaticMath::add(double a, double b) {
return a + b;
}

double StaticMath::sub(double a, double b) {
return a - b;
}

double StaticMath::mul(double a, double b) {
return a * b;
}

double StaticMath::div(double a, double b) {
return a / b;
}

void StaticMath::print() {
std::cout << "Print" << std::endl;
}

如上定义了StaticMath的一组头文件和实现文件,我们将在main.cpp中使用这个类及其方法,进行如下操作生成静态库并链接到可执行文件main:

1
2
3
g++ -c StaticMath.cpp
ar -crv libstaticmath.a StaticMath.o
g++ main.cpp -L. -lstaticmath -O2 -o main

这三句命令首先生成了StaticMath.o中间文件,然后使用ar打包并令生成libstaticmath.a静态库文件,最后使用g++调用链接器链接staticmath库并生成可执行文件main,我们看下执行结果:

main执行结果和依赖

通过ldd的分析结果我们可以发现,依赖库中并没有staticmath,这说明静态库是编译进了二进制文件当中。

0x83 如何编译一个动态库

我们创建DynamicMath.h和DynamicMath.cpp文件,其内容除了类名和StaticMath没什么区别,然后我们执行以下命令生成可执行文件dynamic:

1
2
3
g++ -fPIC -c DynamicMath.cpp
g++ -shared -o libdynamicmath.so DynamicMath.o
g++ dynamic.cpp -L. -ldynamicmath -O2 -o dynamic

其中-fPIC选项表示生成的代码是支持重定向的,-shared参数用于将中间文件生成动态库(这里不再是使用打包命令ar),最后像链接静态库一样链接动态库,我们看下执行结果:

dynamic执行结果和依赖

可以发现无法执行,提示找不到动态库,和ldd的分析结果一致。

0x84 如何确保二进制文件搜索到需要的动态库

上面执行dynamic失败的结果很明显,结果也很明确,程序在执行时没有找到自己的依赖库因此没办法进行下去。其实发生这种状况的原因就是elf在执行时到固定的目录去搜索自己需要的动态库,如果所有的路径都找不到,那就无法执行,搜索路径的规则不在这里展开说,网上一查都有,这里只说一下解决方案(其实解决方案也是根据搜索规则来的)。

  1. ldconfig
    这种方式是比较标准的方式,通过前面的ldd分析,我们发现在/lib等目录下的库是可以找到的,也就是说系统本身存在一些默认的搜索路径以供系统程序运行,那像mariadb这些第三方软件要怎么办呢?解决办法就是下载依赖包,将需要的动态库文件放入到默认的搜索路径下,比如/lib、/lib64、/usr/lib、/usr/lib64等,所以我们的程序要执行只需要将libdynamicmath.so放到对应arch下的搜索路径下就可以了。

    但是如果第三方软件的依赖库其实是自己家的,目前只有自己的软件能用到,也一股脑的扔到默认路径下么,很明显这不是一个明智的解决方式,因此操作系统允许对搜索路径进行配置,第三方软件在安装时将自己的动态库放到单独的目录并通过配置搜索路径的方式提供服务。这种方法很简单,我们就是通过修改/etc/ld.so.conf文件(大多数发行版已经对该文件提供动态支持,会搜索/etc/ld.so.conf.d目录下的所有配置,这种方式更灵活也不会破环依赖关系检查),然后调用ldconfig命令重新生成ld.so,cache缓存文件,这样你的可执行程序就能找到动态库了。

  2. LD_LIBRARY_PATH变量
    它是一个环境变量,在login_shell中执行的时候库加载器也会优先查找这个路径,因此我们只需要export LD_LIBRARY_PATH=./然后执行我们的二进制文件,就可以正常执行了。但这种方法只能算是个临时的解决方案,当然你可以把你的启动过程包装成一个脚本,这样每次启动也能正确的加载了。

  3. 编译时指定查找路径
    这是编译器的一个可配置项,它通过告诉链接器rpath来知会目标文件在执行时到配置目录下去寻找动态库:g++ dynamic.cpp -L. -ldynamicmath -Wl,-rpath=./ -O2 -o dynamic其中-Wl便是将要告知链接器的参数,在这里还可以指定soname。我们看下新的运行结果:

    dynamic执行结果和依赖

就说这么多,这篇笔记的目的就是为了加深自己对基础概念的理解和GCC编译器的实践,还是那句话,学无止境,码不停题。