为什么链接器会修改一个--defsym“绝对地址”?

12

目标:使用一个来自不导出符号的可执行文件中的函数的共享库。

方法:gcc -Wl,--defsym,function=0x432238

该手册说明:

"--defsym symbol=expression" Create a global symbol in the output
file, containing the absolute address given by expression.

令我沮丧的是,dlopen()将共享库的基地址(这是64位代码)0x7ffff676f000添加到导出的“绝对符号地址”中:

        executable        shared library
        ---------- linker --------------
symbol: 0x432238   =====> 0x7ffff6ba1238

objdump显示库中的符号地址是正确的(0x432238),但一旦使用dlopen()加载后,该符号的地址就变成了0x7ffff6ba1238

如果加载后手动将库符号修补到正确的地址,则所有工作正常(否则,库会SEGFAULT)。

  • 为什么"绝对地址"会被修改?
  • 如何避免这种情况?

更新:

我质疑以下回答的技术相关性,更不用说它的'更新'了:

在PIC库/可执行文件中使用--defsym定义重新定位的符号是没有意义的(除了污染二进制文件而没有任何可用的功能之外)。

因此,在PIC共享库或PIC可执行文件中,--defsym的唯一相关用途应该是定义一个(非重新定位的)“绝对地址”。

顺便说一下,如果你费心阅读手册页面,这正是--defsym的官方目的:

"创建一个全局符号在输出文件中,包含给定表达式的绝对地址。"

最好的情况是,这是一个Linux链接器的缺陷,这很容易修复。对于那些等不及人们意识到(并修复)他们的错误的人,解决方案是在有缺陷的链接器加载二进制镜像后修补重定位表。

然后,--defsym在PIC库/可执行文件中变得有用,这对我来说是一个受欢迎的进步。


“--defsym” 的实现非常有用,而您声称当前的行为是无用的或者是一个 bug 是难以理解的。例如,在 Intel SGX 中,使用 --defsym __ImageBase=0 可以使运行在 enclave 中的代码找到 enclave 加载的地址。这允许获取堆栈跟踪、计算从 __ImageBase 偏移量,并在 enclave 外部进行符号化。这只是一个例子。按照您建议的方式更改行为“在加载二进制映像后修补重定位表”将会破坏无数项目。 - Chris Beck
https://download.01.org/intel-sgx/linux-2.2/docs/Intel_SGX_Developer_Reference_Linux_2.2_Open_Source.pdf - Chris Beck
2
@Chris Beck,我很惊讶回复的顺从、无菌“遵从”,以及它们缺乏解决技术问题带来解决方案的能力。此外,我很震惊没有人似乎意识到目前(功能失调的)行为可能是有意选择的,以产生无尽的安全问题。 - Gil
3个回答

9

您似乎基本上误解了--defsym的作用。

--defsym=symbol=expression
   Create a global symbol in the *output* file, ...

也就是说,你正在创建一个新的符号在你正在构建的库中。因此,该符号(自然地)与库一起重新定位。

我猜你想要的是这样的:

// code in library
int fn()
{
    // exe_fn not exported from the executable, but we know where it is.
    int (*exe_fn)(void) = (int (*)(void)) 0x432238;
    return (*exe_fn)();
}

如果您不想将 0x432238 硬编码到库中,而是在构建时通过命令行传递该值,请使用 -DEXE_FN=0x432238 来实现。
更新:
目标: 共享库使用可执行文件的函数
您选择的方法无法实现该目标。您需要使用其他方法。
为什么“绝对地址”被修改?
它没有被修改。当您要求链接器在绝对地址 0x432238 定义 function 时,它确切地做到了这一点。您可以在 objdumpnmreadelf -s 输出中看到它。
但是,由于符号是在共享库中定义的,对该符号的所有引用都会被重定位,即通过共享库加载地址进行调整(这是动态加载器完成的)。动态加载器做其他事情是毫无意义的。
如何避免它?
您不能。使用其他方法来实现您的目标。

谢谢回复,但这并不能解决我的问题,因为在共享库中定义将不会被定义为符号。我该如何实际创建一个GDB能够列出的符号? - Gil
我知道如何制作能够运行的丑陋黑科技,但有没有更“干净”的方法呢?这就是我的问题。 - Gil
关于'man'页面,"包含由表达式给出的绝对地址"是什么意思? - Gil
“define”不会被定义为共享库中的符号。您不希望在共享库中定义一个符号,而是希望在可执行文件中定义一个符号。由于您无法(或不想)重新构建可执行文件以便导出该符号,因此只能使用一些hack方法。 - Employed Russian
我确实希望在共享库中有一个符号,并且有各种方法可以使其按预期工作,从简单的包装器到在重定位之前修补链接器手册所称的“绝对地址”。无论如何,感谢您的回复,这可能会帮助其他人。 - Gil
显示剩余2条评论

2

提供一个不同的观点:确实存在使用这种方法的场景,但我认为它是有缺陷的,不仅适用于动态库还适用于位置无关可执行文件。

ld 在将二进制文件嵌入可执行文件时会使用符号:

ld -r -b binary hello_world.txt -o hello_world.o

这将生成一个目标文件,并包含以下符号之一:

最初的回答已经存在了。

000000000000000c g       .data  0000000000000000 _binary_hello_world_txt_end
000000000000000c g       *ABS*  0000000000000000 _binary_hello_world_txt_size
0000000000000000 g       .data  0000000000000000 _binary_hello_world_txt_start

这样,一个包含它们的可执行文件只需使用extern变量即可访问它们(例如:来自hello_world.txt的“hello world”文本是部分中唯一的内容,长度为0xc)。

将此目标文件链接到可执行文件中(不剥离符号)会导致下面的结果:

Original Answer翻译成"最初的回答"

0000000000411040 g     .data  0000000000000000              _binary_hello_world_txt_start
000000000041104c g     .data  0000000000000000              _binary_hello_world_txt_end
000000000000000c g     *ABS*  0000000000000000              _binary_hello_world_txt_size

最初的回答是,我们可以做一些诸如

这样的事情


extern char _binary_hello_world_txt_start;
extern char _binary_hello_world_txt_size; // "char" is just made up in this one

// (...)
printf("text: %s\n", &_binary_hello_world_txt_start);
printf("number of bytes in it: %d\n", (int) (&_binary_hello_world_txt_size));

(是的,看起来我们正在寻找某个东西的地址(通常用于符号),然后将其视为整数...但实际上它确实有效。)

请注意,链接器确实知道应该重新定位什么以及不应该重新定位什么;数据指针相对于.data,而大小为*ABS*,正如Gil所描述的那样,不应该重新定位(因为它没有相对于任何东西计算)。

但是,这仅适用于非位置独立可执行文件。一旦您从-fPIE(这是gcc在现代Linux发行版中的默认设置,就像它看起来一样)切换到-no-pie,动态链接器会重新定位所有内容,包括*ABS*符号。这发生在运行时链接时间:符号表看起来相同,无论可执行文件是如何编译的。

相同的事情也发生在共享库中似乎是同样的结果:动态放置的二进制文件(无论是位置独立的可执行文件还是共享库)的重定位会导致类似的重定位,这对于包含在二进制文件本身中的函数是有意义的,但不适用于*ABS*数据。

可悲的是,我对这两个问题都没有答案:我也认为它做得不正确,并且我不知道如何修复它(请参见从C获取*ABS*符号的值,以了解另一个遇到相同问题的问题)。

但是,考虑到即使GNU ld本身也选择以这种方式嵌入大小作为符号...我认为这个应用程序/问题完全有效,因此作为答案:

  • ...这样做是因为实现实际上并不正确
  • 作为解决方法,“生成带有绝对地址的头文件”在Employed Russian的回答中提到

...但我实际上很想知道如何像Gil在问题中提到的那样修补重定位表!


2
--defsym的行为在gcc 5.4.0(使用Ubuntu 16.04.4)和7.3.0(Ubuntu 18.04)之间发生了变化。在5.40中,--defsym创建了一个代表绝对、不可重定位地址的符号。在7.3.0中,readelf -s显示该符号为"ABS",但实际上该符号在程序执行时被重新定位。(这也给我的应用程序带来了问题。)
绝对地址可能表示像内存映射设备寄存器或中断向量这样的东西,无论应用程序加载到哪里都会保持在一个位置上。更早的行为是正确的-绝对地址不得被重新定位。如果在将可执行映像加载到内存时发生了重定位,那么这可能不是一个gcc问题,但它是一个问题。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接