【问题标题】:Is it possible to uniquely identify dynamically imported functions by their name?是否可以通过名称唯一标识动态导入的函数?
【发布时间】:2015-07-28 03:50:48
【问题描述】:

我用过

readelf --dyn-sym my_elf_binary | grep FUNC | grep UND

要显示my_elf_binary 的动态导入函数,准确地说是来自.dynsym 部分的动态符号表。示例输出为:

 [...]
 3: 00000000     0 FUNC    GLOBAL DEFAULT  UND tcsetattr@GLIBC_2.0 (3)
 4: 00000000     0 FUNC    GLOBAL DEFAULT  UND fileno@GLIBC_2.0 (3)
 5: 00000000     0 FUNC    GLOBAL DEFAULT  UND isatty@GLIBC_2.0 (3)
 6: 00000000     0 FUNC    GLOBAL DEFAULT  UND access@GLIBC_2.0 (3)
 7: 00000000     0 FUNC    GLOBAL DEFAULT  UND open64@GLIBC_2.2 (4)
 [...]

假设与这些符号关联的名称是否安全,例如tcsetattraccess 总是独一无二的?或者是否有可能或合理的*),有一个动态符号表(过滤FUNCUND),其中包含两个具有相同关联字符串的条目?

我问的原因是我正在寻找动态导入函数的唯一标识符...

*) 动态链接器不会将所有同名的“UND FUNC 符号”解析为同一个函数吗?

【问题讨论】:

  • 我不确定,所以我不会“回答”你的问题。我支持“是的,假设是安全的”和“是的,动态链接器会将所有这些符号解析为相同的功能”。但不是答案!

标签: dynamic-linking elf systems-programming dynamic-loading


【解决方案1】:

是的,给定一个符号名称和可执行文件所链接的库集,您可以唯一标识该函数。此行为是链接和动态链接工作所必需的。


一个说明性的例子

考虑以下两个文件:

图书馆测试1.c:

#include <stdio.h>
int testfunction(void)
{
   printf("version 1");
   return 0;
}

和 librarytest2.c:

#include <stdio.h>
int testfunction(void)
{
   printf("version 2");
   return 0;
}

都编译成共享库:

% gcc -fPIC -shared -Wl,-soname,liblibrarytest.so.1 -o liblibrarytest.so.1.0.0 librarytest1.c -lc 
% gcc -fPIC -shared -Wl,-soname,liblibrarytest.so.2 -o liblibrarytest.so.2.0.0 librarytest2.c -lc

请注意,我们不能将同名的两个函数放入一个共享库中:

% gcc -fPIC -shared -Wl,-soname,liblibrarytest.so.0 -o liblibrarytest.so.0.0.0 librarytest1.c librarytest2.c -lc                                                                                                     
/tmp/cctbsBxm.o: In function `testfunction':
librarytest2.c:(.text+0x0): multiple definition of `testfunction'
/tmp/ccQoaDxD.o:librarytest1.c:(.text+0x0): first defined here
collect2: error: ld returned 1 exit status

这表明符号名称在共享库中是唯一的,但不必在一组共享库中。

% readelf --dyn-syms liblibrarytest.so.1.0.0 | grep testfunction 
12: 00000000000006d0    28 FUNC    GLOBAL DEFAULT   10 testfunction
% readelf --dyn-syms liblibrarytest.so.2.0.0 | grep testfunction 
12: 00000000000006d0    28 FUNC    GLOBAL DEFAULT   10 testfunction

现在让我们将共享库与可执行文件链接起来。考虑linktest.c:

int testfunction(void);
int main()
{
  testfunction();
  return 0;
}

我们可以编译并将其链接到任一共享库:

% gcc -o linktest1 liblibrarytest.so.1.0.0 linktest.c 
% gcc -o linktest2 liblibrarytest.so.2.0.0 linktest.c 

并运行它们中的每一个(注意我正在设置动态库路径,以便动态链接器可以找到不在标准库路径中的库):

% LD_LIBRARY_PATH=. ./linktest1                    
version 1%                                                                                                              
% LD_LIBRARY_PATH=. ./linktest2
version 2%

现在让我们将我们的可执行文件链接到 both 库。每个都导出相同的符号testfunction,并且每个库都有该函数的不同实现。

% gcc -o linktest0-1 liblibrarytest.so.1.0.0 liblibrarytest.so.2.0.0 linktest.c
% gcc -o linktest0-2 liblibrarytest.so.2.0.0 liblibrarytest.so.1.0.0 linktest.c

唯一的区别是库被编译器引用的顺序。

% LD_LIBRARY_PATH=. ./linktest0-1                                              
version 1%                                                                                                             
% LD_LIBRARY_PATH=. ./linktest0-2
version 2%    

这里是对应的ldd输出:

% LD_LIBRARY_PATH=. ldd ./linktest0-1 
    linux-vdso.so.1 (0x00007ffe193de000)
    liblibrarytest.so.1 => ./liblibrarytest.so.1 (0x00002b8bc4b0c000)
    liblibrarytest.so.2 => ./liblibrarytest.so.2 (0x00002b8bc4d0e000)
    libc.so.6 => /lib64/libc.so.6 (0x00002b8bc4f10000)
    /lib64/ld-linux-x86-64.so.2 (0x00002b8bc48e8000)
% LD_LIBRARY_PATH=. ldd ./linktest0-2
    linux-vdso.so.1 (0x00007ffc65df0000)
    liblibrarytest.so.2 => ./liblibrarytest.so.2 (0x00002b46055c8000)
    liblibrarytest.so.1 => ./liblibrarytest.so.1 (0x00002b46057ca000)
    libc.so.6 => /lib64/libc.so.6 (0x00002b46059cc000)
    /lib64/ld-linux-x86-64.so.2 (0x00002b46053a4000)

在这里我们可以看到,虽然符号不是唯一的,但链接器解析它们的方式已定义(似乎它总是解析它遇到的第一个符号)。请注意,这有点病态,因为您通常不会这样做。在您朝这个方向发展的情况下,有更好的方法来处理符号命名,以便在导出时它们是唯一的(符号版本控制等)


总而言之,是的,您可以根据函数的名称唯一地识别函数。如果碰巧有多个同名符号,您可以使用解析库的顺序(从lddobjdump 等)识别正确的符号。是的,在这种情况下,除了名称之外,您还需要更多信息,但如果您有要检查的可执行文件,则有可能。

【讨论】:

  • 只是检查:所以如果我们将我们的可执行文件链接到两个库,相同的符号将在可执行文件的动态符号表中出现两次 BUT动态链接器会将这两个事件解析为相同的值(由动态链接器首先查看的库提供)。对吗?
  • @stackoverflowwww 不,可执行文件的动态符号对其未定义函数只有一个条目(它不知道也不关心有多个实现)。事实上,readelf --dyn-syms linktest0-2 | grep FUNC | grep UND 命令与linktest0-1 命令相同。每个库都有一个名为testfunction 的导出函数,它是选择将未定义符号映射到一个全局符号的动态链接器,它似乎总是选择第一个提供全局符号的库来解析未定义的符号。
  • 此信息以及您的第一篇(甚至更多)详细帖子提供了一个很好的答案!我很佩服。非常感谢。
  • 即使我与一个共享对象链接时,我也会看到相同的符号出现两次,例如gzopen64 下面(一次使用单个'@',再次使用'@@':180: 0000000000000000 0 FUNC GLOBAL DEFAULT UND gzopen64@ZLIB_1.2.3.3 (6) 6054: 0000000000000000 0 FUNC GLOBAL DEFAULT UND gzopen64@@ZLIB_1.2.3.3 不应该有单个 gzopen64 链接到 zlib 库吗?
【解决方案2】:

请注意,在您的情况下,第一个函数导入的名称不仅仅是tcsetattr,而是tcsetattr@GLIBC_2.0@ 是 readelf 程序显示版本化符号导入的方式。

GLIBC_2.0 是一个版本标记,glibc 使用它来保持与旧二进制文件的二进制兼容,以防其某个函数的二进制接口需要更改(不寻常但可能)。编译器生成的原始.o文件将只导入tcsetattr,没有版本信息,但是在静态链接期间,链接器注意到lic.so导出的实际符号带有GLIBC_2.0标签,因此它创建了一个坚持导入具有版本GLIBC_2.0 的特定tcsetattr 符号的二进制文件。

将来可能会有一个 libc.so 导出一个tcsetattr@GLIBC_2.0 和一个不同的tcsetattr@GLIBC_2.42,然后版本标签将用于查找特定 ELF 对象引用的那个。

同样的进程也有可能同时使用tcsetattr@GLIBC_2.42,例如如果它使用另一个动态库,该库链接到一个足够新的libc.so来提供它。版本标签确保旧的二进制文件和新的库都能从 C 库中获得它们期望的功能。

大多数库使用这种机制,如果需要对其二进制接口进行重大更改,则只需重命名整个库即可。例如,如果您转储 /usr/bin/pngtopnm,您会发现它从 libnetpbm 和 libpng 导入的符号是 not 版本的。 (或者至少这是我在我的机器上看到的)。

这样做的代价是你不能有一个链接到一个版本的 libpng 的二进制文件,也不能链接到另一个本身链接到 不同 libpng 版本的库;从两个 libpng 导出的名称会发生​​冲突。

在大多数情况下,这可以通过仔细的打包实践来管理,因此维护库源以产生有用的版本标签并保持向后兼容是不值得的。

但是在 C 库和其他一些重要的系统库的特殊情况下,更改库的名称将非常痛苦,以至于维护人员跳过一些障碍以确保它永远不会需要再次发生。

【讨论】:

  • 所以是符号导出动态库来确定导入的可执行文件/库的动态符号表中的符号是否有版本标签?换句话说,如果导出的符号有版本标签,是否所有导入此符号的可执行文件/库会在编译期间自动添加(必需的)版本标签?
  • @stackoverflowwww:是的,这是我的理解。 (除了它是“在链接期间”而不是“在编译期间”)。
  • 可能有一种方法可以说服链接器生成一个二进制文件,该二进制文件可以导入具有特定版本标记的符号,而不是让库自己确定它,甚至生成一个导入相同符号的二进制文件有两个不同的版本标签用于不同的重定位[当然,如果其他一切都失败了,之后在二进制文件上使用十六进制编辑器],但这绝对不是日常用例。
  • @Henning:哦,是的,它是链接而不是编译。谢谢指正。
【解决方案3】:

虽然在大多数情况下每个符号都是唯一的,但也有少数例外。我最喜欢的是 PAM(可插入身份验证模块)和 NSS(名称服务开关)使用的多个相同符号导入。在这两种情况下,为任一接口编写的所有模块都使用具有标准名称的标准接口。一个常见且经常使用的示例是当您按名称调用 get host 时发生的情况。 nss 库会调用多个库中的同一个函数来得到答案。一个通用的配置在三个库中调用相同的函数!我已经看到一个函数调用在五个不同的库中调用了相同的函数,但这并不是有用的限制。需要对动态链接器进行特殊调用来执行此操作,而我并不熟悉执行此操作的机制,但是对于如此加载的库模块的链接并没有什么特别之处。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2021-08-17
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2012-02-03
    • 1970-01-01
    • 1970-01-01
    • 2023-03-24
    相关资源
    最近更新 更多