C语言 如何编译 ELF 二进制文件,以便它可以作为动态库加载



这是理论问题。我知道也许最佳做法是使用共享库。但是我遇到了这个问题,似乎在任何地方都找不到答案。

如何构造代码并以 ELF 格式编译 C/C++ 中的程序,以便可以加载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 可执行文件不可重定位,它们通常编译为在同一起始地址(0x400000 表示 x86_64),这意味着从技术上讲不可能在同一地址空间中加载其中两个。

您可以做的是:

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

  • 通过将主程序编译为共享对象(以便将其加载到虚拟地址空间中的其他位置),您应该能够动态链接其他可执行文件。GNU 动态链接器不想dlopen可执行文件,因此您必须自己进行动态链接(您可能不想这样做)。

  • 或者,可以使用链接器脚本链接其中一个可执行文件以使用另一个基址。与以前一样,您必须自己完成动态链接器的工作。

解决方案 1:将动态加载的可执行文件编译为 PIE

被调用的可执行文件:

// hello.c
#include <string.h>
#include <stdio.h>
void hello()
{
  printf("Hello worldn");
}
int main()
{
  hello();
  return 0;
}

调用方可执行文件:

// 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, "%sn", dlerror());
    return 1;
  }
  void (*hello)() = dlsym(handle, "hello");
  if (!hello) {
    fprintf(stderr, "%sn", dlerror());
    return 1;
  }
  hello();
  return 0;
}

试图让它工作:

$ gcc -fpie -pie hello.c -o hello$ gcc caller.c -o caller$ ./caller ./hello./hello:未定义的符号:hello

原因是当您将 hello 编译为 PIE 时,动态链接器不会将 hell 符号添加到动态符号表 ( .dynsym ):

$ readelf -s符号表 '.dynsym' 包含 12 个条目:   num:值大小类型绑定 vis ndx 名称     0: 00000000000000000 0 NOTYPE 本地默认值 UND     1:0000000000000200 0节本地默认1     2: 00000000000000000 0 无类型弱默认 和 _ITM_deregisterTMCloneTab     3: 00000000000000000 0 FUNC 全局默认值 和 puts@GLIBC_2.2.5 (2)     4: 00000000000000000 0 FUNC 全局默认值 __libc_start_main@GLIBC_2.2.5 (2)     5: 000000000000000000 0 NOTYPE 弱默认和__gmon_start__     6: 000000000000000000 0 NOTYPE 弱默认 和 _Jv_RegisterClasses     7: 000000000000000000 0 NOTYPE 弱默认 和 _ITM_registerTMCloneTable     8: 00000000000000000 0 FUNC 弱默认 UND __cxa_finalize@GLIBC_2.2.5 (2)     9: 00000000000200bd0 0 NOTYPE 全局默认值 24 _edata    10: 0000000000200bd8 0 NOTYPE 全局默认值 25 _end    11: 0000000000200bd0 0 NOTYPE 全局默认值 25 __bss_start符号表".symtab"包含 67 个条目:   num:值大小类型绑定 vis ndx 名称[...]    52: 0000000000000760 18 FUNC 全局默认值 13 你好[...]

为了解决这个问题,你需要将-E标志传递给ld(参见@AlexKey的 anwser):

$ gcc -fpie -pie hello.c -wl,-e hello.c -o hello$ gcc caller.c -o caller$ ./caller ./hello世界您好$ ./hello世界您好$ readelf -s ./hello符号表 '.dynsym' 包含 22 个条目:   num:值大小类型绑定 vis ndx 名称[...]    21: 00000000000008d0 18 FUNC 全局默认值 13 你好[...]

一些参考资料

有关更多信息,请参阅 4.程序库中的动态加载 (DL) 库 HOWTO 是开始阅读的好地方。

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

测试1.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);
}

测试2.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 %dn",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

参考资料

  • 构建一个 .so 这也是一个可执行文件
  • 使用 dlopen 编译 C 程序,使用 -fPIC 编译 dlsym 程序

PS:为了避免符号冲突,导入的符号和指针被他们分配以更好地具有不同的名称。请参阅此处的评论。

最新更新