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

记录一下如何在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这个函数。dladdrlibdl.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发行版上都能使用,因此实际中大家也都在用它,但它还是有几个缺点:

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

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上动态库里的"函数"其实就是一段编译好的代码,加载进内存后它也会占用一段内存空间,调用动态库函数的时候实际上是下面这样的流程:

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

加载进内存的代码权限是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处理过就不能这么用了。本文介绍的两种方案是泛用性最高的。

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

相关推荐
Kaede626 分钟前
如何应对Linux云服务器磁盘空间不足的情况
linux·运维·服务器
iCxhust2 小时前
Prj10--8088单板机C语言8259测试(1)
c语言·开发语言
努力学习的小廉7 小时前
深入了解linux系统—— 进程池
linux·运维·服务器
int型码农7 小时前
数据结构第八章(一) 插入排序
c语言·数据结构·算法·排序算法·希尔排序
秃头菜狗7 小时前
各个主要目录的功能 / Linux 常见指令
linux·运维·服务器
2301_793102497 小时前
Linux——MySql数据库
linux·数据库
jiunian_cn9 小时前
【Linux】centos软件安装
linux·运维·centos
程序员JerrySUN9 小时前
[特殊字符] 深入理解 Linux 内核进程管理:架构、核心函数与调度机制
java·linux·架构
孤寂大仙v9 小时前
【计算机网络】非阻塞IO——select实现多路转接
linux·计算机网络