有没有办法“静态地”将共享的.so(或.o)库插入到可执行文件中?

时间:2013-08-19 08:41:32

标签: c++ c gcc shared-libraries

首先,请考虑以下情况。

以下是一个程序:

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

正如您所看到的,在第一种情况下,该程序使用printf中的libc.so,而在第二种情况下,它使用来自printf的{​​{1}}。

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

**请注意,通过LD_PRELOAD的插入不是我想要的。我想获得一个与我需要的库直接相关的exectuable。我再次强调,我只能访问第一个二进制文件和我想要的外部对象&#34;静态&#34;介入**

8 个答案:

答案 0 :(得分:7)

有可能。了解shared library interposition

  

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

答案 1 :(得分:2)

您将不得不修改二进制文件。看看patchelf http://nixos.org/patchelf.html

它可以让你设置或修改RPATH甚至是“解释器”,即ld-linux-x86-64.so到其他地方。

从实用程序的描述:

  

动态链接的ELF可执行文件始终指定动态链接器或   解释器,它是一个实际加载可执行文件的程序   以及它的所有动态链接库。 (内核只是   加载解释器,而不是可执行文件。)例如,在a上   Linux / x86系统ELF解释器通常是文件   /lib/ld-linux.so.2。

所以你可以做的是用你自己的解释器运行问题二进制文件(即测试),然后加载你的库......这可能很难,但是ld-linux-so的源代码是可用的。 ..

选项2是自己修改库列表。至少patchelf为您提供了一个起点,即代码遍历库列表(请参阅代码中的DT_NEEDED)。

elf specification文档确实表明订单确实很重要:

  

DT_NEEDED:此元素保存以null结尾的字符串表偏移量   string,给出所需库的名称。偏移量是一个指数   进入DT_STRTAB条目中记录的表。请参阅''共享对象   关于这些名称的更多信息的依赖关系''。动态   数组可能包含此类型的多个条目。这些条目'   相对顺序是重要的,虽然它们与条目的关系   其他类型不是。

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

或许你的意图是完成patchelf的创作......无论如何,希望这会有所帮助!

答案 2 :(得分:1)

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

答案 3 :(得分:1)

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

你问题的关键是 -

  

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

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

但是,您还有其他替代方法可以解决此技术限制。


那你有什么选择?

1。在目标系统

上使用LD_PRELOAD

Maxim's answer中详细描述了共享库的插入。

2。准备预链接的独立可执行文件

elf-statifier是用于创建可移植的,自包含的Linux可执行文件的工具。

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

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

但是,并不保证这种方法适用于所有情况。这是因为最近的Linux内核引入了VDSOASLR

对此的商业替代方案是ermine。它可以work around VDSO and ASLR limitations

答案 4 :(得分:0)

通过使用dlopen()动态加载库,使用dlsym()访问函数的符号作为函数指针,然后通过函数指针。在this网站上有一个很好的例子。

我为上面的例子量身定制了这个例子:

// 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) - 如果它替换现有符号,您可能需要“撤消”你以后的行动。这实际上取决于你的程序的背景,以及你想要做的整体。

答案 5 :(得分:0)

不是静态的,但您可以使用Anthony Shoumikhin创建的elf-hook实用程序将共享库中重定向动态加载的符号重定向到您自己的函数。

典型用法是重定向您无法编辑的第三方共享库中的某些功能调用。

假设您的第三方图书馆位于/tmp/libtest.so,并且您想重新定位从图书馆内拨打的printf次来电,但是从其他位置保留对printf的来电不受影响。< / p>

示例app:

<强> lib.h

#pragma once

void test();

<强> lib.cpp

#include "lib.h"
#include <cstdio>

void test()
{
    printf("hello from libtest");
}

在此示例中,上述2个文件被编译到共享库libtest.so并存储在/tmp

<强>的main.cpp

#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! ]]

因此,您可以看到可以在没有LB_PRELOAD的情况下插入自己的函数,并且可以更好地控制截获哪些函数。

但是,这些功能不是静态插入,而是动态重定向

elf-hook库的GitHub源是here,Anthony Shoumikhin撰写的完整代码项目文章是here

答案 6 :(得分:0)

这是可能的。您只需编辑ELF标题并在动态部分添加库。 您可以使用readelf -d <executable>检查“动态部分”的内容。此外,readelf -S <executable>会告诉您.dynsym.dynstr的偏移量。在.dynsym中,您可以找到Elf32_DynElf64_Dyn个结构的数组,其中d_tag应为DT_NEEDED,而d_un.d_ptr应指向位于字符串"libext.so"的{​​{1}}在.dynstr部分。

/usr/include/elf.h中描述了ELF标题。

答案 7 :(得分:0)

可以更改二进制文件。

例如,使用像ghex这样的工具,您可以更改二进制文件的十六进制代码,在libc.so的每个实例的代码中搜索,然后用libext.so替换它。