[英]An executable and a shared library dependent on a same statically linked library
[英]Is there a way to “statically” interpose a shared .so (or .o) library into an executable?
首先,考虑以下情况。
下面是一个程序:
// test.cpp
extern "C" void printf(const char*, ...);
int main() {
printf("Hello");
}
下面是一个库:
// ext.cpp (the external library)
#include <iostream>
extern "C" void printf(const char* p, ...);
void printf(const char* p, ...) {
std::cout << p << " World!\n";
}
现在我可以用两种不同的方式编译上面的程序和库。
第一种方式是在不链接外部库的情况下编译程序:
$ g++ test.cpp -o test
$ ldd test
linux-gate.so.1 => (0xb76e8000)
libc.so.6 => /lib/i386-linux-gnu/libc.so.6 (0xb7518000)
/lib/ld-linux.so.2 (0xb76e9000)
如果我运行上面的程序,它会打印:
$ ./test
Hello
第二种方式是编译带有外部库链接的程序:
$ g++ -shared -fPIC ext.cpp -o libext.so
$ g++ test.cpp -L./ -lext -o test
$ export LD_LIBRARY_PATH=./
$ ldd test
linux-gate.so.1 => (0xb773e000)
libext.so => ./libext.so (0xb7738000)
libc.so.6 => /lib/i386-linux-gnu/libc.so.6 (0xb756b000)
libstdc++.so.6 => /usr/lib/i386-linux-gnu/libstdc++.so.6 (0xb7481000)
/lib/ld-linux.so.2 (0xb773f000)
libm.so.6 => /lib/i386-linux-gnu/libm.so.6 (0xb743e000)
libgcc_s.so.1 => /lib/i386-linux-gnu/libgcc_s.so.1 (0xb7421000)
$ ./test
Hello World!
如您所见,在第一种情况下,程序使用libc.so
printf
,而在第二种情况下,它使用libext.so
printf
。
我的问题是:从第一种情况下获得的可执行文件和 libext 的目标代码(如 .so 或 .o),是否有可能获得第二种情况下的可执行文件? 换句话说,是有可能取代在连杆libc.so
以链接到libext.so
用于在后者中定义的所有的符号?
**请注意,通过 LD_PRELOAD 进行插入不是我想要的。 我想获得一个直接链接到我需要的库的可执行文件。 我再次强调,我只能访问第一个二进制文件和我想“静态”插入的外部对象**
有可能的。 了解共享库插入:
编译使用动态库的程序时,二进制文件中包含未定义符号列表,以及程序链接的库列表。 符号和库之间没有对应关系; 这两个列表只是告诉加载器要加载哪些库以及需要解析哪些符号。 在运行时,每个符号都使用提供它的第一个库来解析。 这意味着,如果我们可以得到一个包含我们的包装函数的库在其他库之前加载,程序中未定义的符号将被解析为我们的包装器而不是真正的函数。
你问题的关键是——
如何静态链接动态共享对象?
这是无法做到的。 原因是静态链接库实际上与获取该库的编译结果、在当前项目中解压缩它们并像使用它们一样使用它们是您自己的对象一样。 *.a
文件只是一堆*.o
文件的存档,其中包含完整的所有信息。 另一方面,动态库已经链接; 符号重定位信息已被丢弃,因此无法静态链接到可执行文件中。
但是,您确实有其他替代方法可以解决此技术限制。
LD_PRELOAD
共享库插入在Maxim的回答中有很好的描述。
elf-statifier
是用于创建可移植的、自包含的 Linux 可执行文件的工具。
它试图将动态链接的可执行文件和所有动态链接的库打包到一个独立的可执行文件中。 该文件可以独立复制并在另一台机器上运行。
所以现在在您的开发机器上,您可以设置LD_PRELOAD
并运行原始可执行文件并验证它是否正常工作。 此时elf-statifier
创建进程内存映像的快照。 此快照保存为 ELF 可执行文件,其中包含所有必需的共享库(包括您的自定义libext.so
)。 因此,无需在运行新生成的独立可执行文件的目标系统上进行任何修改(例如对LD_PRELOAD
)。
但是,这种方法不能保证在所有场景中都有效。 这是因为最近的 Linux 内核引入了VDSO和ASLR 。
对此的商业替代品是ermine 。 它可以解决 VDSO 和 ASLR 的限制。
您将不得不修改二进制文件。 看看 patchelf http://nixos.org/patchelf.html
它可以让你设置或修改 RPATH 甚至“解释器”,即 ld-linux-x86-64.so 到别的东西。
从实用程序的描述:
动态链接的 ELF 可执行文件总是指定一个动态链接器或解释器,这是一个实际加载可执行文件及其所有动态链接库的程序。 (内核只加载解释器,而不是可执行文件。)例如,在 Linux/x86 系统上,ELF 解释器通常是文件 /lib/ld-linux.so.2。
所以你可以做的是用你自己的解释器在有问题的二进制文件上运行 patchelf(即测试),然后加载你的库......这可能很困难,但 ld-linux-so 的源代码是可用的......
选项 2 是自己修改库列表。 至少 patchelf 为您提供了一个起点,因为代码会遍历库列表(请参阅代码中的 DT_NEEDED)。
elf 规范文档确实表明顺序确实很重要:
DT_NEEDED:该元素保存以空字符结尾的字符串的字符串表偏移量,给出所需库的名称。 偏移量是记录在 DT_STRTAB 条目中的表的索引。 有关这些名称的更多信息,请参阅“共享对象依赖项”。 动态数组可能包含多个此类型的条目。 这些条目的相对顺序很重要,但它们与其他类型条目的关系并不重要。
您的问题的性质表明您熟悉编程:-) 可能是为 patchelf 添加一个补充的好时机... 修改二进制文件中的库依赖项。
或者,您的意图可能正是创建 patchelf 要做的事情……无论如何,希望这会有所帮助!
Statifier可能会做你想要的。 它需要一个可执行文件和所有共享库,并输出一个静态可执行文件。
这是可能的。 您只需要编辑 ELF 标头并在动态部分添加您的库。 您可以使用readelf -d <executable>
检查“动态部分”的内容。 另外readelf -S <executable>
会告诉你.dynsym
和.dynstr
偏移量。 在.dynsym
您可以找到Elf32_Dyn
或Elf64_Dyn
结构的数组,其中 d_tag 应该是DT_NEEDED
并且d_un.d_ptr
应该指向位于.dynstr
部分的字符串"libext.so"
。
ELF 头文件在/usr/include/elf.h
中描述。
通过使用dlopen()动态加载库,使用dlsym () 作为函数指针访问函数的符号,然后通过函数指针调用它,可能可以完成您的要求。 在这个网站上有一个很好的例子说明该怎么做。
我根据你上面的例子定制了这个例子:
// test.cpp
#include <stdio.h>
typedef void (*printf_t)(const char *p, ...);
int main() {
// Call the standard library printf
printf_t my_printf = &printf;
my_printf("Hello"); // should print "Hello"
// Now dynamically load the "overloaded" printf and call it instead
void* handle = dlopen("./libext.so", RTLD_LAZY);
if (!handle) {
std::cerr << "Cannot open library: " << dlerror() << std::endl;
return 1;
}
// reset errors
dlerror();
my_printf = (printf_t) dlsym(handle, "printf");
const char *dlsym_error = dlerror();
if (dlsym_error) {
std::cerr << "Cannot load symbol 'printf': " << dlsym_error << std::endl;
dlclose(handle);
return 1;
}
my_printf("Hello"); // should print "Hello, world"
// close the library
dlclose(handle);
}
dlopen
和dlsym
的手册页应该提供更多信息。 您需要尝试一下,因为尚不清楚dlsym
将如何处理冲突符号(在您的示例中, printf
) - 如果它替换现有符号,您可能需要稍后“撤消”您的操作。 这真的取决于你的程序的上下文,以及你想要做的整体。
不是静态的,但您可以使用 Anthony Shoumikhin 创建的elf-hook
实用程序将共享库中动态加载的符号重定向到您自己的函数。
典型用法是从您无法编辑的第 3 方共享库中重定向某些函数调用。
假设您的第 3 方库位于/tmp/libtest.so
,并且您希望重定向从库内进行的printf
调用,但不影响从其他位置对printf
调用。
示例应用程序:
库文件
#pragma once
void test();
库文件
#include "lib.h"
#include <cstdio>
void test()
{
printf("hello from libtest");
}
本例中将上述2个文件编译成共享库libtest.so
存放在/tmp
主程序
#include <iostream>
#include <dlfcn.h>
#include <elf_hook.h>
#include "lib.h"
int hooked_printf(const char* p, ...)
{
std::cout << p << " [[ captured! ]]\n";
return 0;
}
int main()
{
// load the 3rd party shared library
const char* fn = "/tmp/libtest.so";
void* h = dlopen(fn, RTLD_LAZY);
// redirect printf calls made from within libtest.so
elf_hook(fn, LIBRARY_ADDRESS_BY_HANDLE(h), "printf", (void*)hooked_printf);
printf("hello from my app\n"); // printf in my app is unaffected
test(); // test is the entry point to the 3rd party library
dlclose(h);
return 0;
}
输出
hello from my app
hello from libtest [[ captured! ]]
因此,如您所见,可以在不设置LD_PRELOAD
情况下插入您自己的函数,另外的好处是您可以更细粒度地控制拦截哪些函数。
但是,这些功能不是静态插入的,而是动态重定向的
可以更改二进制文件。
例如,使用 ghex 之类的工具,您可以更改二进制文件的十六进制代码,在代码中搜索 libc.so 的每个实例,然后将其替换为 libext.so
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.