是否可以通过名称来唯一标识动态导入的函数?
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)
[...]
是否可以安全地假设与这些符号关联的名称,例如tcsetattr
或 access
总是唯一的?或者是否有可能,或合理*),有一个动态符号table(过滤FUNC
和UND
) 其中包含两个具有相同关联字符串的条目?
我问的原因是我正在寻找动态导入函数的唯一标识符...
*) 动态链接器不会将所有同名的“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 解析它们的方式是定义的(似乎它总是解析它遇到的第一个符号)。请注意,这是一种病态情况,因为您通常不会这样做。在您朝这个方向发展的情况下,有更好的方法来处理符号命名,因此它们在导出时将是唯一的(符号版本控制等)
总而言之,是的,您可以根据名称唯一地标识该函数。如果碰巧有多个同名符号,您可以使用库解析的顺序(从 ldd
或 objdump
等)来识别正确的符号。是的,在这种情况下,您需要更多的信息,只是它的名称,但如果您有可执行文件来检查,这是可能的。
请注意,在您的情况下,第一个函数导入的名称不仅是 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 库会在多个库中调用相同的函数来得到答案。一个普通的配置在三个库中调用同一个函数!我已经看到从一个函数调用中在五个不同的库中调用了相同的函数,这不仅仅是有用的限制。需要对动态链接器进行特殊调用才能执行此操作,我还不熟悉执行此操作的机制,但是如此加载的库模块的链接没有什么特别之处。
我用过
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)
[...]
是否可以安全地假设与这些符号关联的名称,例如tcsetattr
或 access
总是唯一的?或者是否有可能,或合理*),有一个动态符号table(过滤FUNC
和UND
) 其中包含两个具有相同关联字符串的条目?
我问的原因是我正在寻找动态导入函数的唯一标识符...
*) 动态链接器不会将所有同名的“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 解析它们的方式是定义的(似乎它总是解析它遇到的第一个符号)。请注意,这是一种病态情况,因为您通常不会这样做。在您朝这个方向发展的情况下,有更好的方法来处理符号命名,因此它们在导出时将是唯一的(符号版本控制等)
总而言之,是的,您可以根据名称唯一地标识该函数。如果碰巧有多个同名符号,您可以使用库解析的顺序(从 ldd
或 objdump
等)来识别正确的符号。是的,在这种情况下,您需要更多的信息,只是它的名称,但如果您有可执行文件来检查,这是可能的。
请注意,在您的情况下,第一个函数导入的名称不仅是 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 库会在多个库中调用相同的函数来得到答案。一个普通的配置在三个库中调用同一个函数!我已经看到从一个函数调用中在五个不同的库中调用了相同的函数,这不仅仅是有用的限制。需要对动态链接器进行特殊调用才能执行此操作,我还不熟悉执行此操作的机制,但是如此加载的库模块的链接没有什么特别之处。