热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

ld无法找到项目符号链接失败_链接选项rpath的原理和应用

女主宣言在测试和部署C动态库时,经常遇到的问题就是程序链接到了系统路径下的动态库,有时候make编译时链接到本地路径的动态库,但实际mak

女主宣言

在测试和部署 C++ 动态库时,经常遇到的问题就是程序链接到了系统路径下的动态库,有时候 make 编译时链接到本地路径的动态库,但实际 make install 时则会丢失这个依赖。本文将要介绍的就是一种通用解决方法,使用 RPATH 来绑定链接路径。

PS:丰富的一线技术、多元化的表现形式,尽在“360云计算”,点关注哦!

1

动态库编译和使用简单示例

给出以下示例库:

// foo.h#pragma once#ifdef __cplusplusextern "C" {#endifvoid foo();#ifdef __cplusplus}#endif

// foo.cc#include "foo.h"#include void foo() { printf("foo\n"); }

生成动态库 libfoo.so:

g++ -o libfoo.so -fPIC -shared foo.cc

然后给出调用代码:

// main.cc#include "foo.h"int main(int argc, char* argv[]) { foo(); return 0;}

编译时链接到当前目录:

$ gcc main.c -L . -lfoo$ ./a.out foo$ ldd a.out | grep libfoo libfoo.so (0x00007fc2010f7000)

至此,一切正常。

2

依赖动态库的动态库

 

实际编写程序时,往往会依赖一些第三方库来避免重复造轮子。比如,这里我们要写一个库依赖于 libfoo.so。

目录层次:

.├── include│ └── bar.h├── src│ └── bar.cc└── thirdparty ├── include │ └── foo.h └── lib └── libfoo.so

然后编译生成 libbar.so:

g++ -o libbar.so -fPIC -shared src/bar.cc \  -I include/ -I thirdparty/include/ \  -L thirdparty/lib/ -lfoo

问题来了,编译出的 libbar.so 找不到 libfoo.so 的依赖:

$ ldd libbar.so | grep foo libfoo.so => not found

当然,这样的话,你编译依赖 libbar.so 的程序时会直接失败,从而提醒你去寻找依赖的 libfoo.so:

/usr/bin/ld: warning: libfoo.so, needed by ./libbar.so, not found (try using -rpath or -rpath-link) ./libbar.so: undefined reference to `foo'

注意这里我们第一次见到 rpath 这个概念。

但是问题更大的是,假如 libfoo.so 是一个旧版的库,而开发机上有个其他用户完全无视影响,直接将 libfoo.so 安装到了系统目录,比如 /usr/lib64 下面。这样,你的程序依赖的 libbar.so 将会找到系统目录下旧的 libfoo.so,而不是你自己维护的新版。如果新版 libfoo.so 的 ABI 发生了改变而 API 不变:

// foo.h#pragma oncevoid foo(int i = 0);

// foo.cc#include "foo.h"#include void foo(int i) { printf("foo: %d\n", i); }

API 兼容指的是,调用 foo() 仍然合法,但是由于 C++ 的 name mangling,带有默认参数的 foo 对应的符号发生了变化,因此 foo 可能还会出现这样的错误(main.cc 仅仅是调用 bar() 函数,这里就不贴代码了):

$ g++ main.cc -L . -lbar./libbar.so: undefined reference to `foo(int)'collect2: error: ld returned 1 exit status

查看 libbar.so 的依赖就什么都明白了:

$ ldd libbar.so | grep libfoo libfoo.so => /usr/lib64/libfoo.so (0x00007efd6daf1000)

原因是 foo 的函数签名变成了 void foo(int),而链接到的动态库却是全局的 libfoo.so。简单的解决方式是,将本地库的路径加入 LD_LIBRARY_PATH 中:

$ export LD_LIBRARY_PATH=$PWD/thirdparty/lib:$LD_LIBRARY_PATH$ g++ main.cc -L . -lbar$ ./a.out barfoo: 0

用 ldd 也能查看 libbar.so 依赖的 libfoo.so 不再是全局的,而是本地的。但问题是,如果发布单独的 libbar.so 给用户,而用户又因为某些原因无法升级全局的 libfoo.so,那么每次用户都要手动设置 LD_LIBRARY_PATH

此时,另一种解决方法刚好能避免这个问题,也就是使用 rpath。

3

rpath 的使用

 

rpath 即 runtime path,运行时路径。既可以指定相对路径也可以指定绝对路径。

编译方式:

$ g++ -o libbar.so -fPIC -shared src/bar.cc \ -I include/ -I thirdparty/include/ \ -L thirdparty/lib/ -lfoo -Wl,-rpath=thirdparty/lib/$ ldd libbar.so | grep foo libfoo.so => thirdparty/lib/libfoo.so (0x00007f8319965000)

注意最后的 -Wr,-rpath 指定的是动态库的路径。看似和 -L 重复,实际不然。-L 指定的是编译时链接的 libfoo.so 路径,而 -Wl,-rpath 指定的是(libbar.so)运行时链接的 libfoo.so 路径。这里指定的是相对路径。

因此如果我们安装 libbar.so 到全局又不影响全局的 libfoo.so,比如安装到 /usr/lib64 下面:

$ sudo cp libbar.so /usr/lib64

我们继续编译 libbar.so 的使用程序:

$ g++ main.cc -lbar$ ldd a.out | grep foo libfoo.so => thirdparty/lib/libfoo.so (0x00007f5d3d79f000)$ ldd a.out | grep bar libbar.so => /usr/lib64/libbar.so (0x00007fed07e78000)$ ls /usr/lib64/libfoo.so /usr/lib64/libfoo.so

这样想发布依赖高版本 libfoo.so 的 libbar.so 时,用户只需要在编译和运行时,相对路径 thirdparty/lib 下面有高版本 libfoo.so 就行了,无需覆盖全局路径下的低版本 libfoo.so。

注意如果换个路径运行 a.out,由于 rpath 指定的是相对路径,此时会找不到 libfoo.so。

所以 rpath 指定绝对路径的做法也是比较常见的,比如编译 libbar.so 时将 libfoo.so 置于不会冲突的系统目录:

$ sudo mkdir -p /usr/lib64/foo-1.1$ sudo cp thirdparty/lib/libfoo.so /usr/lib64/foo-1.1$ g++ -o libbar.so -fPIC -shared src/bar.cc \ -I include/ -I thirdparty/include/ \ -L /usr/lib64/foo-1.1 -lfoo -Wl,-rpath=/usr/lib64/foo-1.1 $ ldd libbar.so | grep foo libfoo.so => /usr/lib64/foo-1.1/libfoo.so (0x00007f83df270000)

那么用户部署时,只需要将 libfoo.so 放置在 /usr/lib64/foo-1.1 下面就行,这里的 1.1 用于标识版本号。由于该目录并不会被自动连接,从而防止了其他程序自动链接到这个版本的 libfoo.so。

4

回到现代,使用 CMake

 

但凡稍有规模的程序,直接使用 GCC 编译来构建项目是难以维护的。即使有了 Makefile,管理和维护起来还是相对麻烦。C++ 缺乏类似 Maven 那样的构建系统,但退而求其次,CMake 已经成为了事实上的 C++ 构建通用解决方案。

以一个极简的 CMakeLists.txt 为例,将 rpath 指定为相对路径:

cmake_minimum_required(VERSION 2.8.12)project(Bar CXX)# 设置 find_path/find_library 查找的根目录,默认会从 include 以及 lib 子目录查找set(CMAKE_PREFIX_PATH "${PROJECT_SOURCE_DIR}/thirdparty")find_path(FOO_INCLUDE_DIR NAMES foo.h)find_library(FOO_LIB NAMES libfoo.so)add_library(bar SHARED src/bar.cc)include_directories(./include ${FOO_INCLUDE_DIR})target_link_libraries(bar ${FOO_LIB})# 设置 rpath,这里是绝对路径set(CMAKE_INSTALL_RPATH "${PROJECT_SOURCE_DIR}/thirdparty/lib")# 安装到 lib 子目录,该相对路径是相对 CMAKE_INSTALL_PREFIX 而言的install(TARGETS bar LIBRARY DESTINATION lib)

说是回到现代,我这个 CMake 还是老式的风格,现代 CMake 又是另一个话题了,不熟悉 CMake 的话,可以直接从现代 CMake 学起(版本至少 3.1)。

当前目录层次:

.├── CMakeLists.txt├── include│ └── bar.h├── main.cc├── src│ └── bar.cc└── thirdparty ├── include │ └── foo.h └── lib        └── libfoo.so

使用 CMake 构建项目:

$ mkdir _builds && cd _builds/$ cmake .. -DCMAKE_INSTALL_PREFIX=$PWD/..$ make$ make install

完成后的目录层次(忽略中间目录):

.├── CMakeLists.txt├── include│ └── bar.h├── lib│ └── libbar.so├── main.cc├── src│ └── bar.cc└── thirdparty ├── include │ └── foo.h └── lib └── libfoo.so

类似地,为了部署的话,可以将 libfoo.so 部署到系统目录 /usr/lib64 的子目录。

也可以修改成相对路径:

set(CMAKE_SHARED_LINKER_FLAGS ${CMAKE_SHARED_LINKER_FLAGS} -Wl,-rpath,'$ORIGIN/thirdparty')

其中 $ORIGIN 会被替换成动态库所处的绝对路径,也就是说只要 libfoo.so 处于 libbar.so 同级目录 thirdparty 下面,如下图所示:

.
├── libbar.so
└── thirdparty
└── libfoo.so

之后 libbar.so 就会链接到 thirdparty/libfoo.so,并且都是绝对路径。(这里指的是查找 libfoo.so 的路径不随当前操作目录而改变)

5

Linux 动态库查找路径

最后一节,以理论来结尾。前文侧重实践,有了实践作为基础,回过头来看原理就更有体会了。

一个典型的 C/C++ 程序的构建流程是:预处理,汇编,编译,链接。而执行链接的程序其实是 ld,通常编译器比如 GCC 都会自动调用 ld 去进行链接,用户不必关注其中的细节。而 ld 查找动态库的顺序是:

  1. rpath 指定的目录;

  2. 环境变量 LD_LIBRARY_PATH 指定的目录;

  3. runpath 指定的目录;

  4. /etc/ld.so.cache 缓存文件,通常包含 /etc/ld.so.conf 文件编译出的二进制列表(比如 CentOS 上,该文件会使用 include 从而使用 ld.so.conf.d 目录下面所有的 *.conf 文件,这些都会缓存在 ld.so.cache 中)

  5. 系统默认路径,比如 /lib,/usr/lib

在编译时若使用 -z nodefaultlib 选项,则会跳过 4 和 5。至于 runpath,和 rpath 类似,都是二进制(ELF 格式)文件的动态 section 属性(分别为 DT_RUNPATH 和 DT_RPATH),唯一区别就是是否优先于 LD_LIBRARY_PATH 来查找。这里就不详述了。

6

总结

至此,读者对如何编译/部署动态库,以及动态库之间的依赖关系应该有了一定的认识。

相比而言,静态链接部署简单,像 Golang 这种语言直接全部静态链接,因此受到了不少用户的青睐,而且静态链接占用体积大的问题在现代几乎不再是需要特别考虑的问题。

但动态库有动态库的好处,比如在大型项目有多个组件依赖时,如果全部静态链接,则每次修改依赖的模块,都要将主模块重新编译一遍,对于 C++ 这种编译速度可能会非常耗时的语言是灾难性的。

另外,提供插件给解释型语言(比如 Python 和 PHP)来调用时,动态库是必须的,解释器可以动态加载动态库。如果使用静态链接,恐怕没人愿意每换一个插件就要将解释器重新编译一遍。

360云计算

由360云平台团队打造的技术分享公众号,内容涉及数据库、大数据、微服务、容器、AIOps、IoT等众多技术领域,通过夯实的技术积累和丰富的一线实战经验,为你带来最有料的技术分享

1d666b616b9b5365b210f43baa80fbeb.png



推荐阅读
  • C++字符字符串处理及字符集编码方案
    本文介绍了C++中字符字符串处理的问题,并详细解释了字符集编码方案,包括UNICODE、Windows apps采用的UTF-16编码、ASCII、SBCS和DBCS编码方案。同时说明了ANSI C标准和Windows中的字符/字符串数据类型实现。文章还提到了在编译时需要定义UNICODE宏以支持unicode编码,否则将使用windows code page编译。最后,给出了相关的头文件和数据类型定义。 ... [详细]
  • 本文介绍了C函数ispunct()的用法及示例代码。ispunct()函数用于检查传递的字符是否是标点符号,如果是标点符号则返回非零值,否则返回零。示例代码演示了如何使用ispunct()函数来判断字符是否为标点符号。 ... [详细]
  • CF:3D City Model(小思维)问题解析和代码实现
    本文通过解析CF:3D City Model问题,介绍了问题的背景和要求,并给出了相应的代码实现。该问题涉及到在一个矩形的网格上建造城市的情景,每个网格单元可以作为建筑的基础,建筑由多个立方体叠加而成。文章详细讲解了问题的解决思路,并给出了相应的代码实现供读者参考。 ... [详细]
  • 本文介绍了在mac环境下使用nginx配置nodejs代理服务器的步骤,包括安装nginx、创建目录和文件、配置代理的域名和日志记录等。 ... [详细]
  • Android源码深入理解JNI技术的概述和应用
    本文介绍了Android源码中的JNI技术,包括概述和应用。JNI是Java Native Interface的缩写,是一种技术,可以实现Java程序调用Native语言写的函数,以及Native程序调用Java层的函数。在Android平台上,JNI充当了连接Java世界和Native世界的桥梁。本文通过分析Android源码中的相关文件和位置,深入探讨了JNI技术在Android开发中的重要性和应用场景。 ... [详细]
  • 本文介绍了深入浅出Linux设备驱动编程的重要性,以及两种加载和删除Linux内核模块的方法。通过一个内核模块的例子,展示了模块的编译和加载过程,并讨论了模块对内核大小的控制。深入理解Linux设备驱动编程对于开发者来说非常重要。 ... [详细]
  • 负载均衡_Nginx反向代理动静分离负载均衡及rewrite隐藏路径详解(Nginx Apache MySQL Redis)–第二部分
    nginx反向代理、动静分离、负载均衡及rewrite隐藏路径详解 ... [详细]
  • C语言注释工具及快捷键,删除C语言注释工具的实现思路
    本文介绍了C语言中注释的两种方式以及注释的作用,提供了删除C语言注释的工具实现思路,并分享了C语言中注释的快捷键操作方法。 ... [详细]
  • baresip android编译、运行教程1语音通话
    本文介绍了如何在安卓平台上编译和运行baresip android,包括下载相关的sdk和ndk,修改ndk路径和输出目录,以及创建一个c++的安卓工程并将目录考到cpp下。详细步骤可参考给出的链接和文档。 ... [详细]
  • 《数据结构》学习笔记3——串匹配算法性能评估
    本文主要讨论串匹配算法的性能评估,包括模式匹配、字符种类数量、算法复杂度等内容。通过借助C++中的头文件和库,可以实现对串的匹配操作。其中蛮力算法的复杂度为O(m*n),通过随机取出长度为m的子串作为模式P,在文本T中进行匹配,统计平均复杂度。对于成功和失败的匹配分别进行测试,分析其平均复杂度。详情请参考相关学习资源。 ... [详细]
  • PHPMailer邮件类邮件发送功能的使用教学及注意事项
    本文介绍了使用国外开源码PHPMailer邮件类实现邮件发送功能的简单教学,同时提供了一些注意事项。文章涵盖了字符集设置、发送HTML格式邮件、群发邮件以及避免类的重定义等方面的内容。此外,还提供了一些与PHP相关的资源和服务,如传奇手游游戏源码下载、vscode字体调整、数据恢复、Ubuntu实验环境搭建、北京爬虫市场、进阶PHP和SEO人员需注意的内容。 ... [详细]
  • 开发笔记:实验7的文件读写操作
    本文介绍了使用C++的ofstream和ifstream类进行文件读写操作的方法,包括创建文件、写入文件和读取文件的过程。同时还介绍了如何判断文件是否成功打开和关闭文件的方法。通过本文的学习,读者可以了解如何在C++中进行文件读写操作。 ... [详细]
  • ShiftLeft:将静态防护与运行时防护结合的持续性安全防护解决方案
    ShiftLeft公司是一家致力于将应用的静态防护和运行时防护与应用开发自动化工作流相结合以提升软件开发生命周期中的安全性的公司。传统的安全防护方式存在误报率高、人工成本高、耗时长等问题,而ShiftLeft提供的持续性安全防护解决方案能够解决这些问题。通过将下一代静态代码分析与应用开发自动化工作流中涉及的安全工具相结合,ShiftLeft帮助企业实现DevSecOps的安全部分,提供高效、准确的安全能力。 ... [详细]
  • 第四讲ApacheLAMP服务器基本配置Apache的编译安装从Apache的官方网站下载源码包:http:httpd.apache.orgdownload.cgi今 ... [详细]
  • R语言拼接字符串_paste的用法说明
    这篇文章主要介绍了R语言拼接字符串_paste的用法说明,具有很好的参考价值,希望对大家有所帮助。一 ... [详细]
author-avatar
添莺_764
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有