您好,欢迎来到个人技术集锦。
搜索
当前位置:首页Linux c 运行时获取动态库所在路径

Linux c 运行时获取动态库所在路径

个人技术集锦 2025-06-09
导读记录一下如何在Linux环境下运行时获取动态库路径。 只讨论Linux amd64和arm64环境,因为使用的办法都是平台相关的不具备可移植性。 准备 一般来说动态库并不需要关心自己所在的文件系统上的路径,但业务有那么多总有一两个会有特殊需求。 现在给定一个动态库里的函数A,需求是要知道这个函数A是哪个动态库里的以及这个库的存放路径。 测试对象有两个,第一个是标准库的函数printf,另一个是我们自己写的动态链接库里的PrintRandomText函数。 自定义动态库的名字叫libmycusto

记录一下如何在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获取的信息中恰巧有动态库的实际存放路径这一信息,我们可以加以利用:

#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,那么就需要使用方案二了。

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]

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

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

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

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(); // 阻塞进程直到收到信号
}

运行结果:

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

总结

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

Copyright © 2019- zgxue.com 版权所有 京ICP备2021021884号-5

违法及侵权请联系:TEL:199 1889 7713 E-MAIL:2724546146@qq.com

本站由北京市万商天勤律师事务所王兴未律师提供法律服务