动态导入的函数能否通过它们的名称唯一识别?

11

我使用了

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符号”解析为同一个函数,不是吗?


1
我不确定,所以我不会“回答”你的问题。我倾向于“是的,可以安全地假设”和“是的,动态链接器将把所有这些符号解析为同一个函数”。但这并不是一个答案! - user208139
3个回答

15

是的,通过给定符号名称和可执行文件链接的库集合,您可以唯一地识别函数。这种行为对于链接和动态链接是必需的。


一个说明性示例

考虑以下两个文件:

librarytest1.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%

现在让我们把可执行文件与两个库链接起来。每个库都导出相同的符号 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 等工具中查看)来确定正确的符号。是的,在这种情况下,你需要比仅有函数名更多的信息,但如果你有可执行文件进行检查,是可以做到的。


只是确认一下:如果我们将可执行文件链接到这两个库,那么同一个符号将出现在可执行文件的动态符号表中_两次_,但是动态链接器将解析这两个出现为相同的值(由动态链接器首先查看的库提供)。正确吗? - langlauf.io
2
@stackoverflowwww 不会,可执行文件的动态符号只有一个未定义函数入口(它不知道也不关心是否有多个实现)。事实上,命令 readelf --dyn-syms linktest0-2 | grep FUNC | grep UNDlinktest0-1 完全相同。每个库都将导出一个名为 testfunction 的函数,而动态链接器选择将未定义符号映射到其中一个全局符号,似乎它总是选择提供全局符号以解析未定义符号的第一个库。 - casey
2
这些信息连同您的第一篇(更加详细)的帖子,提供了一个非常好的答案!我印象深刻。非常感谢。 - langlauf.io
即使我只链接一个共享对象,我仍然会看到相同的符号出现两次,例如下面的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 难道它不应该只有一个链接到zlib库的gzopen64吗? - Amit

3
请注意,在您的情况下,第一个函数导入的名称不仅是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导入的符号没有带有版本信息。(或者至少这是我在我的机器上看到的)。
这样做的成本是,您无法拥有一个链接到libpng的一个版本并且还链接到另一个库的二进制文件,该库本身链接到不同的 libpng版本; 来自两个libpng的导出名称将冲突。
在大多数情况下,通过谨慎的打包实践来管理这一点是可以的,因此维护库源以生成有用的版本标记并保持向后兼容性并不值得麻烦。
但是,在C库和其他一些关键系统库的特定情况下,更改库的名称将非常痛苦,因此维护者需要跳过一些障碍,以确保永远不需要再次更改名称。

那么,是符号导出的动态库决定了导入可执行文件/库的动态符号表中的符号是否具有版本标签吗?换句话说,如果导出的符号具有版本标签,那么所有导入此符号的可执行文件/库在编译期间自动添加(必需的)版本标签吗? - langlauf.io
1
@stackoverflowwww:是的,那是我的理解。(除了是“在链接期间”而不是“在编译期间”)。 - hmakholm left over Monica
可能有一种方法可以说服链接器生成一个二进制文件,该文件导入具有特定版本标签的符号,而不是让库本身确定它,甚至可以生成一个二进制文件,该文件为不同的重定位导入相同的符号,但这绝对不是每天都会使用的情况。 - hmakholm left over Monica
@ Henning:是的,是链接而不是编译。感谢您的纠正。 - langlauf.io

2
尽管在大多数情况下每个符号都是唯一的,但也有少数例外。我最喜欢的是PAM(可插入认证模块)和NSS(名称服务开关)使用的多个相同符号导入。在这两种情况下,为任一接口编写的所有模块都使用标准接口和标准名称。一个常见且经常使用的示例是调用get host by name时会发生什么。nss库将在多个库中调用相同的函数来获取答案。一个常见的配置调用三个库中的同一个函数!我曾经看到同一个函数从一个函数调用中调用了五个不同的库,并且那不是极限,只是有用的部分。需要特殊调用动态链接器来实现这一点,我还没有熟悉如何进行此操作的机制,但加载的库模块的链接没有什么特别之处。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接