Linux 链接库编译与多重依赖

时间:2022-08-09 02:25:29

现有如下问题:

我们在第三方动态库(比如 boost 库)的基础上,开发了自己的动态库供公司内部项目使用。在使用自己的这个动态库的时候,该如何进行编译呢?即,依赖链条是这样的情况下:

程序–(依赖)–>libA.so–(依赖)–>libB.so

该如何进行编译。

为了研究这个问题,我们建立一个目录结构,写几个简单程序来模拟一下。以下内容将从构建动态库开始,一步步展示如何达成我们的目标。

建立试验目录结构、程序

目录结构

learn
|
|--main.cpp
|
|--upperLayer
| |--uLayer.h
| |--uLayer.cpp
|
|--bottomLayer
     |--bLayer.h
     |--bLayer.cpp

我们将在这个目录下,生成 libblayer.so, libulayer.so, main(可执行文件),其依赖链条是:

main–>libulayer.so–>libblayer.so

特别的,对于 main 的编译来说,其编译参数无需指定 libblayer.so,它只需关心自己直接依赖的 libulayer.so 即可(当然,由于 libulayer.so 依赖 libblayer.so,系统中必须安装 libblayer.so)。

程序

bottomLayer/bLayer.h

#ifndef B_LAYER_H
#define B_LAYER_H

int addOne(int n);

#endif

bottomLayer/bLayer.cpp

#include "bLayer.h"

int addOne(int n)
{
    return n + 1;
}

upperLayer/uLayer.h

#ifndef U_LAYER_H
#define U_LAYER_H

int needAddOne(int n);

#endif

upperLayer/uLayer.cpp

#include "uLayer.h"
#include "../bottomLayer/bLayer.h"

int needAddOne(int n)
{
    return addOne(n);
}

main.cpp

#include "upperLayer/uLayer.h"
#include <iostream>

using namespace std;

int main()
{
    cout << needAddOne(4) << endl;
    return 0;
}

这几个程序都非常简单,无需多言。

开始逐层编译

现在,我们从最底层开始逐步往上层编译。

libblayer.so

bottomLayer$ g++ -fPIC -shared bLayer.cpp -o libblayer.so

libblayer.so 比较简单,直接生成就好了。-fPIC 是要生成位置无关的代码,-shared 是要生成动态库。

libulayer.so

upperLayer$ g++ -fPIC -shared uLayer.cpp -o libulayer.so -L ../bottomLayer/ -lblayer

编译 libulayer.so 的命令前半部分和 libblayer.so 是一样的,但是由于它需要依赖于 libblayer.so,所以加了 -L 和 -l 参数,让它去 ../bottomLayer/ 找 libblayer.so 。要注意的是, -L 和 -l 不加可能也可以编译通过,但是因为找不到所依赖的文件,在最后运行可执行文件的时候,一定会出错,所以这里必须加上这两个参数。

现在我们来看看生成的 libulayer.so 是怎么样的依赖关系:

upperLayer$ ldd libulayer.so
    linux-vdso.so.1 =>  (0x00007fffb2f6f000)
    libblayer.so => not found

什么鬼!libblayer.so 找不到?!不是指定了 -L 和 -l 两个参数了么?不要慌。-L 和 -l 指定的是编译时要依赖的库文件和路径,但是动态库是在运行时才加载的,系统会到默认的路径(比如 /usr/lib)下去查找动态库,但是我们的 libblayer.so 并不在那些目录中,因此 not found 。

由于我们只是在试验,并不打算真的安装这些库,所以我们可以编译的时候把路径硬编码,告诉程序去哪里找 libblayer.so 。因此生成 libulayer.so 的真正命令是:

upperLayer$ g++ -fPIC -shared uLayer.cpp -o libulayer.so -L ../bottomLayer/ -lblayer -Wl,-rpath /home/neko/coding/learn/bottomLayer/

-Wl,-rpath 是告诉程序,在运行的时候如果找不到动态库,可以去 /home/neko/coding/learn/bottomLayer/ 看看(这个是我本机的路径,应设为绝对路径)。现在我们再来 ldd 一下:

upperLayer$ ldd libulayer.so 
    linux-vdso.so.1 =>  (0x00007ffd85ecb000)
    libblayer.so => /home/neko/coding/learn/bottomLayer/libblayer.so (0x00007ffafef75000)

main 可执行文件

现在万事具备,只欠东风了,我们来编译 main 可执行文件:

g++ main.cpp -o main -L upperLayer/ -lulayer -Wl,-rpath upperLayer/

可以注意到,main 的编译参数只关心自己直接用到的 libulayer.so,而无需关心更底层的 libblayer.so 。运行一下看看:

learn$ ./main 
5

得到了正确结果。

使用 CMake

上文通过实例简单介绍了 Linux 动态库编译与多重依赖的解决方法,编译命令直接使用的是 gcc。下面,我们来看看怎么用 CMake 来完成这项工作。

和上面的步骤类似,我们先生成 libblayer.so,然后再生成 libulayer.so,最后生成 main 可执行文件。我们的目标是提供给用户这样一个目录结构的程序包:

|
|--main
|
|--libs
     |--libulayer.so
     |--libblayer.so

可执行程序 main 调用 libulayer.so,libulayer.so 根据需要调用 libblayer.so。这里我们无需把共享库安装到系统目录里,要让用户直接可以把 main 给运行起来。

libblayer.so

在 bottomLayer 目录下,新建文件 CMakeLists.txt 。输入如下内容:

CMAKE_MINIMUM_REQUIRED(VERSION 2.8)
SET(PROJECT_NAME "blayer")
PROJECT(${PROJECT_NAME} CXX)

SET(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_SOURCE_DIR})
AUX_SOURCE_DIRECTORY(./ SRC_LIST)
ADD_LIBRARY(${PROJECT_NAME} SHARED ${SRC_LIST})

然后新建 build 文件夹,进入 build,进行编译

mkdir build
cd build
cmake ..
make

然后我们就得到了 libblayer.so,这个过程很简单很普通,不多讲。

libulayer.so

libulayer.so 需要依赖于 libblayer.so,CMakeLists.txt 如下:

CMAKE_MINIMUM_REQUIRED(VERSION 2.8)
SET(PROJECT_NAME "ulayer")
PROJECT(${PROJECT_NAME} CXX)

SET(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_SOURCE_DIR})

# 设置 libblayer.so 的路径
LINK_DIRECTORIES("${CMAKE_SOURCE_DIR}/../bottomLayer") 
# 不允许 CMake 自行加入任何 RPATH
SET(CMAKE_SKIP_BUILD_RPATH TRUE) 
# 生成的 libulayer.so 的 RPATH 是 "./" 和 "./libs"
SET(CMAKE_SHARED_LINKER_FLAGS "-Wl,-rpath,./:./libs")

AUX_SOURCE_DIRECTORY(./ SRC_LIST)
ADD_LIBRARY(${PROJECT_NAME} SHARED ${SRC_LIST})

编译步骤和上面一样,我们将得到一个 libulayer.so。通过 readelf 命令,我们可以来检查 rpath 是否已经设置正确:

$ readelf -d libulayer.so 

Dynamic section at offset 0xdb8 contains 30 entries:
  Tag        Type                         Name/Value
 0x0000000000000001 (NEEDED)             Shared library: [libblayer.so]
 0x0000000000000001 (NEEDED)             Shared library: [libstdc++.so.6]
 0x0000000000000001 (NEEDED)             Shared library: [libm.so.6]
 0x0000000000000001 (NEEDED)             Shared library: [libgcc_s.so.1]
 0x0000000000000001 (NEEDED)             Shared library: [libc.so.6]
 0x000000000000000e (SONAME)             Library soname: [libulayer.so]
 0x000000000000000f (RPATH)              Library rpath: [./:./libs]

...

从上面可以看到,libulayer.so 的 rpath 已经正确设置,而且它依赖于 libblayer.so。下面我们将开始最终的工作,生成 main 可执行文件。

main 可执行文件

在有了 libblayer.so 和 libulayer.so 以后,我们新建一个 libs 文件夹,把它们放进来,目录结构上文已述。

CMakeLists.txt 如下:

CMAKE_MINIMUM_REQUIRED(VERSION 2.8)
SET(PROJECT_NAME "main")
PROJECT(${PROJECT_NAME} CXX)

# 注意这里变成 RUNTIME 了
SET(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_SOURCE_DIR})

LINK_DIRECTORIES("${CMAKE_SOURCE_DIR}/libs")

# 下面这句不能写,写了就编译不过了。但是不写的话,rpath 就会被 CMake 设置,加上了一个路径
# 我水平有限,不知道怎么解决或者解释,暂且这样吧
#SET(CMAKE_SKIP_BUILD_RPATH TRUE)
# 注意这里变成 EXE 了
SET(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -Wl,-rpath,./:./libs")

AUX_SOURCE_DIRECTORY(./ SRC_LIST)
ADD_EXECUTABLE(${PROJECT_NAME} ${SRC_LIST})

TARGET_LINK_LIBRARIES(${PROJECT_NAME} ulayer)

至此,生成的多重依赖的可执行文件 main,可以正常的运行了。把 main,libs/libblayer.so, libs/libulayer.so 保持目录结构地移动到其它地方去,也都可以正常运行。目标达成。

本文链接:http://blog.csdn.net/shutdown_r_now/article/details/51991076
参考文档:http://blog.csdn.net/nodeathphoenix/article/details/9982209