16

首先,考虑以下情况。

下面是一个程序:

// 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!

如您所见,在第一种情况下,程序使用printffrom libc.so,而在第二种情况下,它使用printffrom libext.so

我的问题是:从第一种情况下获得的可执行文件和libext的目标代码(.so或.o),是否有可能获得第二种情况下的可执行文件?换句话说,是否可以将链接替换为链接到后者中定义的所有符号libc.so的链接?libext.so

**请注意,通过 LD_PRELOAD 插入不是我想要的。我想获得一个直接链接到我需要的库的可执行文件。我再次强调这一事实,我只能访问第一个二进制文件和我想“静态”插入的外部对象**

4

8 回答 8

9

有可能的。了解共享库插入

编译使用动态库的程序时,二进制文件中包含未定义符号列表,以及与程序链接的库列表。符号和库之间没有对应关系;这两个列表只是告诉加载器要加载哪些库以及需要解析哪些符号。在运行时,使用第一个提供它的库来解析每个符号。这意味着,如果我们可以在其他库之前加载包含我们的包装函数的库,则程序中未定义的符号将被解析为我们的包装函数,而不是真正的函数。

于 2013-08-19T08:46:17.280 回答
3

你所要求的传统上是不可能的。这已经在这里这里讨论过。

你问题的症结在于——

如何静态链接动态共享对象?

这是无法做到的。原因是静态链接库实际上与获取该库的编译结果、在当前项目中解压缩它们并像使用它们是您自己的对象一样使用它们。*.a文件只是一堆文件的*.o档案,其中所有信息都完好无损。另一方面,动态库已经链接;符号重定位信息已被丢弃,因此无法静态链接到可执行文件中。

但是,您确实有其他替代方案来解决此技术限制。


那么你有什么选择呢?

1.LD_PRELOAD在目标系统上使用

共享库插入在Maxim 的回答中得到了很好的描述。

2.准备一个预链接的独立可执行文件

elf-statifier是用于创建可移植的、独立的 Linux 可执行文件的工具。

它试图将动态链接的可执行文件和所有动态链接的库打包到一个独立的可执行文件中。该文件可以独立复制并在另一台机器上运行。

所以现在在您的开发机器上,您可以设置LD_PRELOAD并运行原始可执行文件并验证它是否正常工作。此时elf-statifier创建进程内存映像的快照。此快照保存为 ELF 可执行文件,其中包含所有必需的共享库(包括您的自定义libext.so)。因此,无需对LD_PRELOAD运行新生成的独立可执行文件的目标系统进行任何修改(例如 to )。

但是,这种方法不能保证在所有情况下都有效。这是因为最近的 Linux 内核引入了VDSOASLR

对此的商业替代品是ermine。它可以绕过 VDSO 和 ASLR 限制

于 2013-08-28T10:14:48.960 回答
2

您将不得不修改二进制文件。看看 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)。

精灵规范文档确实表明顺序确实很重要:

DT_NEEDED:此元素保存以空字符结尾的字符串的字符串表偏移量,给出所需库的名称。偏移量是记录在 DT_STRTAB 条目中的表的索引。有关这些名称的更多信息,请参阅“共享对象依赖项”。动态数组可能包含多个具有这种类型的条目。这些条目的相对顺序很重要,尽管它们与其他类型的条目的关系不重要。

您的问题的性质表明您熟悉编程 :-) 可能是为 patchelf 做出补充的好时机......在二进制文件中修改库依赖项。

或者,也许您的意图是完全按照 patchelf 创建的目的......无论如何,希望这会有所帮助!

于 2013-08-21T22:32:03.380 回答
1

这是可能的。您只需要编辑 ELF 标头并将您的库添加到动态部分。您可以使用 . 查看“动态部分”的内容readelf -d <executable>。还会readelf -S <executable>告诉你 和 的偏移.dynsym.dynstr。在.dynsym您可以找到您的 d_tag 应该位于的数组Elf32_Dyn或结构,并且应该指向位于section 中的字符串。Elf64_DynDT_NEEDEDd_un.d_ptr"libext.so".dynstr

ELF 标头在/usr/include/elf.h.

于 2013-08-28T12:15:14.093 回答
1

Statifier可能会做你想做的事。它需要一个可执行文件和所有共享库,并输出一个静态可执行文件。

于 2013-08-23T02:52:20.233 回答
0

可以更改二进制文件。

例如,使用 ghex 之类的工具,您可以更改二进制文件的十六进制代码,在代码中搜索 libc.so 的每个实例,然后将其替换为 libext.so

于 2013-08-28T13:33:20.953 回答
0

不是静态的,但是您可以使用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,另外还有一个好处是您可以更细粒度地控制拦截哪些函数。

但是,函数不是静态插入的,而是动态重定向的

elf-hook 库的 GitHub 源代码在这里,由 Anthony Shoumikhin 编写的完整代码项目文章在这里

于 2013-08-26T21:35:22.207 回答
0

可以通过使用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);

}

手册页应该提供更多的见解dlopendlsym您需要尝试一下,因为尚不清楚如何dlsym处理冲突符号(在您的示例中,printf) - 如果它替换现有符号,您可能需要稍后“撤消”您的操作。这实际上取决于您的程序的上下文,以及您总体上要做什么。

于 2013-08-22T22:29:00.260 回答