记录一下如何在Linux环境下运行时获取动态库路径。

只讨论Linux amd64和arm64环境,因为使用的办法都是平台相关的不具备可移植性。

准备

一般来说动态库并不需要关心自己所在的文件系统上的路径,但业务有那么多总有一两个会有特殊需求。

现在给定一个动态库里的函数A,需求是要知道这个函数A是哪个动态库里的以及这个库的存放路径。

测试对象有两个,第一个是标准库的函数printf,另一个是我们自己写的动态链接库里的PrintRandomText函数。

自定义动态库的名字叫libmycustom1.so,代码和编译生成的库都存放在libmycustom1目录下。代码如下:

// lib.h
#pragma once #include <unistd.h>
#include <sys/random.h> void PrintRandomText(ssize_t length); // lib.c
#include <stdio.h> #include "lib.h" void PrintRandomText(ssize_t length)
{
unsigned char buff[64] = {0};
length = (length + 1) / 2;
if (length == 0) {
return;
}
while (1) {
ssize_t count = getrandom(buff, 64, 0);
count = length > count ? count : length;
for (ssize_t i = 0; i < count; ++i) {
printf("%02X", buff[i]&0xff);
}
if (length <= count) {
break;
}
length -= count;
}
printf("\n");
}

函数很简单,从Linux的/dev/urandom随机设备中读取指定大小的数据然后打印输出,编译使用如下命令:

gcc -Wall -O2 -fPIC -shared lib.c -o libmycustom1.so

这样我们就得到了libmycustom1/libmycustom1.so。下面可以介绍如何在运行时获取动态库的路径了。

使用dladdr获取动态库路径

第一种方法是使用dladdr这个函数。dladdrlibdl.so中的一个函数,用来获取某个地址对应的动态库信息,而libdl是Linux上专门用来处理动态链接库的函数库。

dladdr获取的信息中恰巧有动态库的实际存放路径这一信息,我们可以加以利用:

#define _GNU_SOURCE // 这行不能少
#include <dlfcn.h> // for dladdr
#include <stdio.h> #include "libmycustom1/lib.h" int main()
{
Dl_info info1, info2;
if (dladdr((void*)&printf, &info1) == 0) {
fprintf(stderr, "cannot get printf's info\n");
return 1;
}
if (dladdr((void*)&PrintRandomText, &info2) == 0) {
fprintf(stderr, "cannot get PrintRandomText's info\n");
return 1;
}
// 还需要检查dli_fname字段是否是NULL,这里就省略了
printf("lib contains printf: %s\n", info1.dli_fname);
printf("lib contains PrintRandomText: %s\n", info2.dli_fname);
}

dladdr在出错的时候会返回0,这时可以用dlerror来获取具体的报错,不过这里我为了简单起见就省略了。

编译运行需要下面的命令:

$ gcc a.c -L./libmycustom1 -lmycustom1 -ldl
$ export LD_LIBRARY_PATH=./libmycustom1
$ ./a.out
lib contains printf: /lib/x86_64-linux-gnu/libc.so.6
lib contains PrintRandomText: ./libmycustom1/libmycustom1.so

编译时还需要链接libdl

因为库没有放在默认的系统搜索路径里,也没有单独设置ld.cache,因此我们需要设置环境变量LD_LIBRARY_PATH来告诉加载器我们的动态库在哪里。

可以看到对于存放在标准路径里的libc,dladdr给出了绝对路径,对于我们自定义的库,因为LD_LIBRARY_PATH设置成了相对路径,所以给我们的结果也是相对路径的。因此dladdr拿到的结果最好得先做一次相对路径到绝对路径的转换再使用。

dladdr受到广泛的支持,基本主要的Linux发行版上都能使用,因此实际中大家也都在用它,但它还是有几个缺点:

  1. 函数指针转void*在c/c++标准中都是不允许的,而且实际也有函数指针是胖指针的平台存在,但至少这一行为在x86_64和arm的gcc/clang上都没啥问题
  2. dladdr只能正常获取使用-fPIC编译成位置不相关代码的动态库信息,这个信息也不一定准确。

综上dladdr虽然能用,但不通用,而且可靠性也一般。

正如我在文章开头就说了,这次讨论的方案没有可移植性,需要限定在具体的系统和硬件平台上使用。

使用proc maps文件获取动态库路径

如果我不想再额外链接一个库,尤其是还得在文件开头定义#define _GNU_SOURCE,那么就需要使用方案二了。

方案二很简单也很直接,读取进程的/proc/<pid>/maps,对比地址范围就能找到函数所在的动态库以及库的路径。

Linux加载动态链接库是用的类似mmap的形式,库实际只会被加载一次,然后被映射到每个需要这个库的进程的地址空间里。

/proc/<pid>/maps记载了进程的内存地址空间里所有的mmap映射的文件,包括普通文件、共享库和匿名映射。当然这个文件里还包含了vdso和代码段等的内存地址,总体上来说可以算作进程的内存空间分布概览。一个例子是:

55bce8e1c000-55bce8e1d000 r--p 00000000 08:20 3337                       /home/apocelipes/dladdrtest/a.out
55bce8e1d000-55bce8e1e000 r-xp 00001000 08:20 3337 /home/apocelipes/dladdrtest/a.out
55bce8e1e000-55bce8e1f000 r--p 00002000 08:20 3337 /home/apocelipes/dladdrtest/a.out
55bce8e1f000-55bce8e20000 r--p 00002000 08:20 3337 /home/apocelipes/dladdrtest/a.out
55bce8e20000-55bce8e21000 rw-p 00003000 08:20 3337 /home/apocelipes/dladdrtest/a.out
55bd039bf000-55bd039e0000 rw-p 00000000 00:00 0 [heap]
7f7bffb36000-7f7bffb39000 rw-p 00000000 00:00 0
7f7bffb39000-7f7bffb61000 r--p 00000000 08:20 49817 /usr/lib/x86_64-linux-gnu/libc.so.6
7f7bffb61000-7f7bffce9000 r-xp 00028000 08:20 49817 /usr/lib/x86_64-linux-gnu/libc.so.6
7f7bffce9000-7f7bffd38000 r--p 001b0000 08:20 49817 /usr/lib/x86_64-linux-gnu/libc.so.6
7f7bffd38000-7f7bffd3c000 r--p 001fe000 08:20 49817 /usr/lib/x86_64-linux-gnu/libc.so.6
7f7bffd3c000-7f7bffd3e000 rw-p 00202000 08:20 49817 /usr/lib/x86_64-linux-gnu/libc.so.6
7f7bffd3e000-7f7bffd4b000 rw-p 00000000 00:00 0
7f7bffd53000-7f7bffd54000 r--p 00000000 08:20 3397 /home/apocelipes/dladdrtest/libmycustom1/libmycustom1.so
7f7bffd54000-7f7bffd55000 r-xp 00001000 08:20 3397 /home/apocelipes/dladdrtest/libmycustom1/libmycustom1.so
7f7bffd55000-7f7bffd56000 r--p 00002000 08:20 3397 /home/apocelipes/dladdrtest/libmycustom1/libmycustom1.so
7f7bffd56000-7f7bffd57000 r--p 00002000 08:20 3397 /home/apocelipes/dladdrtest/libmycustom1/libmycustom1.so
7f7bffd57000-7f7bffd58000 rw-p 00003000 08:20 3397 /home/apocelipes/dladdrtest/libmycustom1/libmycustom1.so
7f7bffd58000-7f7bffd5a000 rw-p 00000000 00:00 0
7f7bffd5a000-7f7bffd5b000 r--p 00000000 08:20 49814 /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
7f7bffd5b000-7f7bffd86000 r-xp 00001000 08:20 49814 /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
7f7bffd86000-7f7bffd90000 r--p 0002c000 08:20 49814 /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
7f7bffd90000-7f7bffd92000 r--p 00036000 08:20 49814 /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
7f7bffd92000-7f7bffd94000 rw-p 00038000 08:20 49814 /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
7fff6ded9000-7fff6defb000 rw-p 00000000 00:00 0 [stack]
7fff6dfaa000-7fff6dfae000 r--p 00000000 00:00 0 [vvar]
7fff6dfae000-7fff6dfb0000 r-xp 00000000 00:00 0 [vdso]

可以看到libc和我们自己的库都被记载进文件里了。每行内容是空格分开的,对于匿名映射不会有最后的路径。第一列的就是内存地址,以“-”连字符分隔,第一部分是内存映射区域开始地址,第二部分是结束地址。

这和获取函数对应的动态库有什么关系呢?关系肯定是有的,在Linux上动态库里的“函数”其实就是一段编译好的代码,加载进内存后它也会占用一段内存空间,调用动态库函数的时候实际上是下面这样的流程:

  1. 根据函数名称跳转到对应的符号表项目上
  2. 检查函数是否被加载,有加载就跳过下面步骤直接到4
  3. 未加载时loader会去动态库文件里读取对应函数的代码,存入内存,然后把项目内容用代码在内存里的起始地址覆盖
  4. 程序跳转到函数代码所在的内存地址上,开始一条条加载执行这些代码

加载进内存的代码权限是r-xp,代表内存里的内容可以被执行。

现在出于安全考虑有些程序会使用编译选项把这些工作提前到程序加载运行时就完成,但大致上是一样的。被加载的函数的内存会被记载进maps文件,所以我们只要读取maps文件然后对比内存地址范围,就能知道函数对应的库和路径了。

因为我们只看函数地址,因此不用查的太细,只要地址在范围内就可以,无需查看权限。知道原理后就可以写个脚本去解析了:

local function searchAddr(pid, addr)
local file = io.open("/proc/" .. pid .. "/maps", "r")
if not file then
print("进程不存在: " .. pid)
return
end for line in file:lines() do
local parts = {}
for word in line:gmatch("%S+") do
table.insert(parts, word)
end if #parts > 5 then
local addrParts = {}
for addr in parts[1]:gmatch("[^%-]+") do
table.insert(addrParts, addr)
end if #addrParts == 2 then
local startAddr = tonumber(addrParts[1], 16) or 0
local endAddr = tonumber(addrParts[2], 16) or 0
if startAddr <= addr and addr < endAddr then
print(parts[#parts])
break;
end
end
end
end file:close()
end if #arg ~= 2 then
print("no enough args")
os.exit(1)
end
local addr = tonumber(arg[2]) or 0
if addr == 0 then
print("addr can not be 0")
os.exit(1)
end
searchAddr(arg[1], addr)

c语言处理字符串太折磨了,所以我用lua偷个懒,代码就不解释了因为很简单,你可以让ai代劳解读一下。

进程退出后proc文件也就没了,所以测试代码也得改一下不要让进程那么快退出:

#include <stdio.h>

#include "libmycustom1/lib.h"

int main()
{
printf("pid %d\n", getpid());
printf("printf address: %p\n", (void*)&printf);
printf("PrintRandomText address: %p\n", (void*)&PrintRandomText);
pause(); // 阻塞进程直到收到信号
}

运行结果:

可以看到我们顺利找到了函数对应的库以及库的存放路径。

使用proc maps的优点是不需要额外的依赖,而且得到的路径都是绝对路径。缺点则是需要函数指针转换成地址值,以及proc是Linux等少数系统独有的,不通用,而且读取maps文件需要有专门的权限,这个权限默认打开但是可以选择关闭。

总结

运行时获取动态库地址除了dladdr和解析/proc/<pid>/maps还可以有一些别的做法。比如可以用nm获取库文件的符号表进行对比,但如果库文件被strip处理过就不能这么用了。本文介绍的两种方案是泛用性最高的。

另外也别太依赖这些结果,因为隐藏或者篡改这些信息太过简单。如果你的想要动态库的路径,应该使用构建系统注入信息或者干脆做出输入选项,而不是依靠这些可靠性和可移植性都欠佳的方案。

Linux c 运行时获取动态库所在路径的更多相关文章

  1. linux 给运行程序指定动态库路径

    1. 连接和运行时库文件搜索路径到设置 库文件在连接(静态库和共享 库)和运行(仅限于使用共享库的程序)时被使用,其搜索路径是在系统中进行设置的.一般 Linux 系统把 /lib 和 /usr/li ...

  2. Linux下动态库查找路径的问题

    说到和动态库查找路径相关的问题,总体上可以分为两类:    第一类: 通过源代码编译程序时出现的找不到某个依赖包的问题,而如果此时你恰好已经按照它的要求确确实实.千真万确.天地良心地把依赖库给装好了, ...

  3. 谈谈Linux下动态库查找路径的问题 ldconfig LD_LIBRARY_PATH PKG_CONFIG_PATH

    谈谈Linux下动态库查找路径的问题 ldconfig LD_LIBRARY_PATH  PKG_CONFIG_PATH 转载自:http://blog.chinaunix.net/xmlrpc.ph ...

  4. 谈谈Linux下动态库查找路径的问题

    学习到了一个阶段之后,就需要不断的总结.沉淀.清零,然后才能继续"上路".回想起自己当年刚接触Linux时,不管是用源码包编译程序,还是程序运行时出现的和动态库的各种恩恩怨怨,心里 ...

  5. [转]谈谈Linux下动态库查找路径的问题

    http://blog.chinaunix.net/uid-23069658-id-4028681.html 学习到了一个阶段之后,就需要不断的总结.沉淀.清零,然后才能继续“上路”.回想起自己当年刚 ...

  6. 转:谈谈Linux下动态库查找路径的问题

    http://blog.chinaunix.net/uid-23069658-id-4028681.html 学习到了一个阶段之后,就需要不断的总结.沉淀.清零,然后才能继续“上路”.回想起自己当年刚 ...

  7. Linux 指定运行时动态库路径【转】

    转自:http://www.cnblogs.com/cute/archive/2011/02/24/1963957.html 众所周知, Linux 动态库的默认搜索路径是 /lib 和 /usr/l ...

  8. 【转载】Linux动态库搜索路径的技巧

    转自:http://soft.chinabyte.com/os/232/11488732_2.shtml 众所周知,Linux动 态库的默认搜索路径是/lib和/usr/lib.动态库被创建后,一般都 ...

  9. Linux动态库搜索路径的技巧

    众所周知,Linux动态库的默认搜索路径是/lib和/usr/lib.动态库被创建后,一般都复制到这两个目录中.当程序执行时需要某动态库,并且该动态库还未加载到内存中,则系统会自动到这两个默认搜索路径 ...

  10. iOS开发——运行时OC篇&使用运行时获取系统的属性:使用自己的手势修改系统自带的手势

    使用运行时获取系统的属性:使用自己的手势修改系统自带的手势 有的时候我需要实现一个功能,但是没有想到很好的方法或者想到了方法只是那个方法实现起来太麻烦,一或者确实为了装逼,我们就会想到iOS开发中最牛 ...

随机推荐

  1. FastAPI安全防护指南:构建坚不可摧的参数处理体系

    扫描二维码关注或者微信搜一搜:编程智域 前端至全栈交流与成长 探索数千个预构建的 AI 应用,开启你的下一个伟大创意 第一章:输入验证体系 1.1 类型安全革命 from pydantic impor ...

  2. 解决 Docker 日志文件太大的问题

    Docker 在不重建容器的情况下,日志文件默认会一直追加,时间一长会逐渐占满服务器的硬盘的空间,内存消耗也会一直增加,本篇来了解一些控制日志文件的方法. 清理单个文件 运行时控制 全局配置 Dock ...

  3. 网络设备开局配置生成器(第三次更新) QQ交流群:(4817315)

    下载:链接: https://pan.baidu.com/s/1BIvh3u7VfbaQtBsUOjl1IA?pwd=kgtw 提取码: kgtw 网络设备开局配置生成器(SecureCRT vbs脚 ...

  4. 从问题排查到源码分析:ActiveMQ消费端频繁日志刷屏的秘密

    引言 最近遇到了一个 ActiveMQ 消费端的问题:在没有消息时,日志频繁打印,每秒打印2000多条空消息,导致日志文件迅速膨胀,甚至影响系统性能.经过一番排查,最终定位到问题根源并成功解决.本文将 ...

  5. BUUCTF---这是什么

    题目 题目给出apk 解题

  6. 异常--java进阶day08

    1.异常 java中,所有的异常都是类 2.异常的体系结构 3.编译时异常与运行时异常 1.编译时异常 语法完全正确,但是代码就是会报错,如下图 上图中,写的是时间格式化类的使用,parse方法将给的 ...

  7. 【软件】Ubuntu下QT的安装和使用

    [软件]Ubuntu下QT的安装和使用 零.前言 QT是应用得比较广泛的程序框架,是因为其跨平台特性比较好,且用C/C++作为开发语言,性能也比较好,故本文介绍如何安装和使用QT,用的版本是QT 6. ...

  8. 【教程】Windows10系统激活

    Windows10系统激活 一.找一个激活码 到百度搜索,筛选发表日期在最近一个月或者一周之内的 二.以管理员身份打开cmd 按Win+R键,输入cmd打开命令行窗口 按Ctrl+Shift+Esc键 ...

  9. VsCode写Markdown使用snippet

      文件->首选项->用户片段   输入markdown   输入代码片段 Ctrl+P,输入settings.json   加入下面个这个选项 "[markdown]" ...

  10. 请详细描述 MySQL 的 B+ 树中查询数据的全过程

    MySQL 的 B+ 树中查询数据的全过程 在 MySQL 中,B+ 树被广泛用于实现索引,特别是 InnoDB 存储引擎中的聚簇索引.B+ 树是一种平衡树,具有良好的查询性能.本文将详细描述在 B+ ...