浅析如何在Linux环境下运行时获取动态库路径

 更新时间:2025年06月09日 08:30:15   作者:apocelipes  
这篇文章主要来和大家记录一下如何在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发行版上都能使用,因此实际中大家也都在用它,但它还是有几个缺点:

  • 函数指针转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和代码段等的内存地址,总体上来说可以算作进程的内存空间分布概览。一个例子是:

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文件然后对比内存地址范围,就能知道函数对应的库和路径了。

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

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环境下运行时获取动态库路径的文章就介绍到这了,更多相关Linux获取动态库路径内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • CentOS7安装GlusterFS集群的全过程

    CentOS7安装GlusterFS集群的全过程

    GlusterFS是一款分布式文件系统,能支持的clinet数很多,并能很好的管理各个节点,下面这篇文章主要给大家介绍了关于CentOS7安装GlusterFS集群的相关资料,需要的朋友可以参考下
    2022-04-04
  • linux安装图形化界面的操作方法

    linux安装图形化界面的操作方法

    这篇文章主要介绍了linux安装图形化界面的操作方法,非常不错,具有一定的参考借鉴价值,需要的朋友可以参考下
    2019-11-11
  • Linux之性能监测命令解读

    Linux之性能监测命令解读

    这篇文章主要介绍了Linux之性能监测命令,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2023-06-06
  • 基于Linux系统中查看硬件等信息的方法详解

    基于Linux系统中查看硬件等信息的方法详解

    本篇文章是对Linux系统中查看硬件等信息的方法进行了详细的分析介绍,需要的朋友参考下
    2013-06-06
  • ubuntu16.04下vim安装失败的原因分析及解决方案

    ubuntu16.04下vim安装失败的原因分析及解决方案

    重装了ubuntu系统,安装vim出现了很多奇葩问题,今天百度查阅资料才顺利解决。今天小编特此把解决思路分享到脚本之家平台,需要的朋友参考下吧
    2016-11-11
  • centos中文件与权限的基本操作教程

    centos中文件与权限的基本操作教程

    这篇文章主要给大家介绍了关于centos文件与权限的相关资料,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2018-09-09
  • Apache配置域名跳转的详细步骤

    Apache配置域名跳转的详细步骤

    域名跳转就是实现URL的跳转和隐藏真实地址,基于Perl语言的正则表达式规范,平时帮助我们实现拟静态,拟目录,域名跳转,防止盗链等,本文小编给大家介绍了Apache配置域名跳转的详细步骤,需要的朋友可以参考下
    2025-04-04
  • Linux cut 命令详解

    Linux cut 命令详解

    我们可以使用 cut 命令从一行字符串中于以字节,字符,字段(分隔符)等单位截取一部分内容出来。这篇文章主要介绍了Linux cut 命令的相关知识,需要的朋友可以参考下
    2020-07-07
  • 浅谈Linux vfork与fork简单对比分析

    浅谈Linux vfork与fork简单对比分析

    本篇文章主要介绍了浅谈Linux vfork与fork简单对比分析,小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧
    2017-12-12
  • CentOS7下从零开始安装ethereum/以太坊

    CentOS7下从零开始安装ethereum/以太坊

    大家好,本篇文章主要讲的是CentOS7下从零开始安装ethereum/以太坊,感兴趣的同学赶快来看一看吧,对你有帮助的话记得收藏一下,方便下次浏览
    2021-12-12

最新评论