如何编译ELF二进制文件,使其可以作为动态库加载?

How to compile ELF binary so that it can be loaded as dynamic library?

这是理论题。我知道也许最佳实践是使用共享库。但是我 运行 进入这个问题并且似乎无法在任何地方找到答案。

如何在C/C++中构造代码并编译成ELF格式的程序以便dlopen()加载?

例如,如果一个可执行文件包含某个函数的实现 int test() 并且我想从我的程序中调用该函数(最好是获取函数的结果),如果可能的话,我将如何去做那个吗?

在伪代码中我可以这样描述它:

ELF 可执行源:

void main() {
    int i = test();
    printf("Returned: %d", i);//Prints "Returned: 5"
}

int test() {
    return 5;
}

外部程序:

// ... Somehow load executable from above
void main() {
    int i = test();
    printf("Returned: %d", i);//Must print "Returned: 5"
}

ELF executable 不是 relocatable 并且它们通常被编译为从相同的起始地址开始(x86_64 为 0x400000),这意味着它在技术上是不可能的将其中两个加载到同一地址 space.

你可以做的是:

  • 编译你想要dlopen()的executable作为executable共享库(-pie)。从技术上讲,此文件是一个 ELF 共享对象,但可以执行。您可以检查该程序是 ELF executable 还是带有 readelf -h my_programfile my_program 的 ELF 共享对象。 (作为奖励,通过将您的程序编译为共享对象,您将能够从 ASLR 中受益)。

  • 通过将你的主程序编译为一个共享对象(这样它就被加载到虚拟地址space的另一个地方)你应该能够动态地link其他执行官table。 GNU 动态 linker 不想 dlopen 一个 executable 文件所以你必须自己制作动态 linking(你可能不想这样做这个).

  • 或者您可以 link 您的一个执行者 table 通过使用 linker 脚本来使用另一个基地址。与以前一样,您必须自己完成动态 linker 的工作。

解决方案 1:将动态加载的 executable 编译为 PIE

调用的executable:

// hello.c
#include <string.h>
#include <stdio.h>

void hello()
{
  printf("Hello world\n");
}

int main()
{
  hello();
  return 0;
}

调用者executable:

// caller.c
#include <dlfcn.h>
#include <stdio.h>

int main(int argc, char** argv)
{
  void* handle = dlopen(argv[1], RTLD_LAZY);
  if (!handle) {
    fprintf(stderr, "%s\n", dlerror());
    return 1;
  }
  void (*hello)() = dlsym(handle, "hello");
  if (!hello) {
    fprintf(stderr, "%s\n", dlerror());
    return 1;
  }
  hello();
  return 0;
}

正在努力让它发挥作用:

$ gcc -fpie -pie hello.c -o hello
$ gcc caller.c -o caller
$ ./caller ./hello
./hello: undefined symbol: hello

原因是你把hello编译成PIE的时候,动态linker并没有在动态符号table的基础上加上地狱符号(.dynsym):

$ readelf -s 
Symbol table '.dynsym' contains 12 entries:
   Num:    Value          Size Type    Bind   Vis      Ndx Name
     0: 0000000000000000     0 NOTYPE  LOCAL  DEFAULT  UND 
     1: 0000000000000200     0 SECTION LOCAL  DEFAULT    1 
     2: 0000000000000000     0 NOTYPE  WEAK   DEFAULT  UND _ITM_deregisterTMCloneTab
     3: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND puts@GLIBC_2.2.5 (2)
     4: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND __libc_start_main@GLIBC_2.2.5 (2)
     5: 0000000000000000     0 NOTYPE  WEAK   DEFAULT  UND __gmon_start__
     6: 0000000000000000     0 NOTYPE  WEAK   DEFAULT  UND _Jv_RegisterClasses
     7: 0000000000000000     0 NOTYPE  WEAK   DEFAULT  UND _ITM_registerTMCloneTable
     8: 0000000000000000     0 FUNC    WEAK   DEFAULT  UND __cxa_finalize@GLIBC_2.2.5 (2)
     9: 0000000000200bd0     0 NOTYPE  GLOBAL DEFAULT   24 _edata
    10: 0000000000200bd8     0 NOTYPE  GLOBAL DEFAULT   25 _end
    11: 0000000000200bd0     0 NOTYPE  GLOBAL DEFAULT   25 __bss_start

Symbol table '.symtab' contains 67 entries:
   Num:    Value          Size Type    Bind   Vis      Ndx Name
[...]
    52: 0000000000000760    18 FUNC    GLOBAL DEFAULT   13 hello
[...]

为了解决这个问题,您需要将 -E 标志传递给 ld(请参阅@AlexKey 的回答):

$ gcc -fpie -pie hello.c -Wl,-E hello.c -o hello
$ gcc caller.c -o caller
$ ./caller ./hello
Hello world
$ ./hello
Hello world
$ readelf -s ./hello
Symbol table '.dynsym' contains 22 entries:
   Num:    Value          Size Type    Bind   Vis      Ndx Name
[...]
    21: 00000000000008d0    18 FUNC    GLOBAL DEFAULT   13 hello
[...]

一些参考文献

有关更多信息,程序库 HOWTO 中的 4. Dynamically Loaded (DL) Libraries 是开始阅读的好地方。

根据评论和其他答案中提供的链接,这里是如何在编译时不链接这些程序的情况下完成的:

test1.c:

#include <stdio.h>

int a(int b)
{
  return b+1;
}

int c(int d)
{
  return a(d)+1;
}

int main()
{
  int b = a(3);
  printf("Calling a(3) gave %d \n", b);
  int d = c(3);
  printf("Calling c(3) gave %d \n", d);
}

test2.c:

#include <dlfcn.h>
#include <stdio.h>


int (*a_ptr)(int b);
int (*c_ptr)(int d);

int main()
{
  void* lib=dlopen("./test1",RTLD_LAZY);
  a_ptr=dlsym(lib,"a");
  c_ptr=dlsym(lib,"c");
  int d = c_ptr(6);
  int b = a_ptr(5);
  printf("b is %d d is %d\n",b,d);
  return 0;
}

编译:

$ gcc -fPIC  -pie -o test1 test1.c -Wl,-E
$ gcc -o test2 test2.c -ldl

执行结果:

$ ./test1
Calling a(3) gave 4 
Calling c(3) gave 5
$ ./test2 
b is 6 d is 8

参考文献:

  • building a .so that is also an executable
  • Compile C program using dlopen and dlsym with -fPIC

PS:为了避免符号冲突,导入的符号和分配给它们的指针最好有不同的名称。查看评论 here.