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

时间:2015-05-15 16:43:26

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

我用过

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

3 个答案:

答案 0 :(得分:13)

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

一个说明性的例子

考虑以下两个文件:

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等)识别正确的符号。是的,在这种情况下,您需要更多信息,只需要它的名称,但如果您有可执行文件可以检查。

答案 1 :(得分:3)

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

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

将来可能会有一个libc.so导出一个tcsetattr@GLIBC_2.0和一个不同的tcsetattr@GLIBC_2.42,然后使用version标签来查找一个部分ELF对象引用的那个。< / p>

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

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

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

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

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

答案 2 :(得分:2)

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