这是理论问题。我知道也许最佳做法是使用共享库。但是我遇到了这个问题,似乎在任何地方都找不到答案。
如何构造代码并以 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_program
或file 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:为了避免符号冲突,导入的符号和指针被他们分配以更好地具有不同的名称。请参阅此处的评论。