是否可以通过名称来唯一标识动态导入的函数?

Is it possible to uniquely identify dynamically imported functions by their name?

我用过

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

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

 [...]
 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 总是唯一的?或者是否有可能,或合理*),有一个动态符号table(过滤FUNCUND) 其中包含两个具有相同关联字符串的条目?

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

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

是的,给定符号名称和可执行文件link针对的一组库,您可以唯一地标识函数。此行为是 linking 和动态 linking 工作所必需的。


示例

考虑以下两个文件:

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

现在让 link 我们的共享库有一个可执行文件。考虑 linktest.c:

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

我们可以针对任一共享库编译并link它:

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

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

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

现在让link我们的可执行文件到两个库。每个都导出相同的符号 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)

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


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

请注意,在您的情况下,第一个函数导入的名称不仅是 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 ,例如它使用另一个链接到 [= 的动态库50=]足够新来提供它。版本标签确保旧的二进制文件和新的库都能获得它们期望从 C 库获得的功能。

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

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

在大多数情况下,通过谨慎的打包实践,这是很容易管理的,维护库源代码以生成有用的版本标签并保持向后兼容是不值得的。

但在 C 库和其他一些重要系统库的特殊情况下,更改库的名称将非常痛苦,因此维护人员跳过一些环节以确保它是有意义的再也不需要发生了。

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