记录一下如何在Linux环境下运行时获取动态库路径。
只讨论Linux amd64和arm64环境,因为使用的办法都是平台相关的不具备可移植性。
准备
一般来说动态库并不需要关心自己所在的文件系统上的路径,但业务有那么多总有一两个会有特殊需求。
现在给定一个动态库里的函数A,需求是要知道这个函数A是哪个动态库里的以及这个库的存放路径。
测试对象有两个,第一个是标准库的函数printf,另一个是我们自己写的动态链接库里的PrintRandomText
函数。
自定义动态库的名字叫libmycustom1.so
,代码和编译生成的库都存放在libmycustom1
目录下。代码如下:
c
// 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随机设备中读取指定大小的数据然后打印输出,编译使用如下命令:
bash
gcc -Wall -O2 -fPIC -shared lib.c -o libmycustom1.so
这样我们就得到了libmycustom1/libmycustom1.so
。下面可以介绍如何在运行时获取动态库的路径了。
使用dladdr获取动态库路径
第一种方法是使用dladdr
这个函数。dladdr
是libdl.so
中的一个函数,用来获取某个地址对应的动态库信息,而libdl
是Linux上专门用来处理动态链接库的函数库。
dladdr
获取的信息中恰巧有动态库的实际存放路径这一信息,我们可以加以利用:
c
#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
来获取具体的报错,不过这里我为了简单起见就省略了。
编译运行需要下面的命令:
console
$ 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发行版上都能使用,因此实际中大家也都在用它,但它还是有几个缺点:
- 函数指针转
void*
在c/c++标准中都是不允许的,而且实际也有函数指针是胖指针的平台存在,但至少这一行为在x86_64和arm的gcc/clang上都没啥问题 dladdr
只能正常获取使用-fPIC
编译成位置不相关代码的动态库信息,这个信息也不一定准确。
综上dladdr
虽然能用,但不通用,而且可靠性也一般。
正如我在文章开头就说了,这次讨论的方案没有可移植性,需要限定在具体的系统和硬件平台上使用。
使用proc maps文件获取动态库路径
如果我不想再额外链接一个库,尤其是还得在文件开头定义#define _GNU_SOURCE
,那么就需要使用方案二了。
方案二很简单也很直接,读取进程的/proc/<pid>/maps
,对比地址范围就能找到函数所在的动态库以及库的路径。
Linux加载动态链接库是用的类似mmap的形式,库实际只会被加载一次,然后被映射到每个需要这个库的进程的地址空间里。
而/proc/<pid>/maps
记载了进程的内存地址空间里所有的mmap映射的文件,包括普通文件、共享库和匿名映射。当然这个文件里还包含了vdso和代码段等的内存地址,总体上来说可以算作进程的内存空间分布概览。一个例子是:
text
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上动态库里的"函数"其实就是一段编译好的代码,加载进内存后它也会占用一段内存空间,调用动态库函数的时候实际上是下面这样的流程:
- 根据函数名称跳转到对应的符号表项目上
- 检查函数是否被加载,有加载就跳过下面步骤直接到4
- 未加载时loader会去动态库文件里读取对应函数的代码,存入内存,然后把项目内容用代码在内存里的起始地址覆盖
- 程序跳转到函数代码所在的内存地址上,开始一条条加载执行这些代码
加载进内存的代码权限是r-xp
,代表内存里的内容可以被执行。
现在出于安全考虑有些程序会使用编译选项把这些工作提前到程序加载运行时就完成,但大致上是一样的。被加载的函数的内存会被记载进maps文件,所以我们只要读取maps文件然后对比内存地址范围,就能知道函数对应的库和路径了。
因为我们只看函数地址,因此不用查的太细,只要地址在范围内就可以,无需查看权限。知道原理后就可以写个脚本去解析了:
lua
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文件也就没了,所以测试代码也得改一下不要让进程那么快退出:
c
#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处理过就不能这么用了。本文介绍的两种方案是泛用性最高的。
另外也别太依赖这些结果,因为隐藏或者篡改这些信息太过简单。如果你的想要动态库的路径,应该使用构建系统注入信息或者干脆做出输入选项,而不是依靠这些可靠性和可移植性都欠佳的方案。