为什么linkage会影响同一section的相对跳转是否需要重定位?

Why does linkage affect whether relocations are needed for relative jumps in the same section?

在这个简单的程序中,我在 main 获得了 compute 的重定位,但 compute2 没有:

static int compute2()
{
    return 2;
}

int compute()
{
    return 1;
}


int main()
{
    return compute() + compute2();
}

我在 Ubuntu 21.10.

上使用 gcc 11.2.0 用 gcc -c main.cpp 编译了这个

这是 objdumpmain 的评价:

000000000000001e <main>:
  1e:   f3 0f 1e fa             endbr64 
  22:   55                      push   rbp
  23:   48 89 e5                mov    rbp,rsp
  26:   53                      push   rbx
  27:   e8 00 00 00 00          call   2c <main+0xe>    28: R_X86_64_PLT32  compute()-0x4
  2c:   89 c3                   mov    ebx,eax
  2e:   e8 cd ff ff ff          call   0 <compute2()>
  33:   01 d8                   add    eax,ebx
  35:   48 8b 5d f8             mov    rbx,QWORD PTR [rbp-0x8]
  39:   c9                      leave  
  3a:   c3                      ret    

如您所见,对于 compute2 的调用(内部链接)有一个没有重定位的相对跳转。但是对于 compute 的调用(外部链接)有一个重定位,即使所有三个函数都在同一目标文件的同一部分。

为什么需要搬迁?我以为链接器永远不会拆分一个部分,所以无论这个部分在哪里加载,相对地址应该还是一样的?为什么链接似乎会影响这一点?

我相信实现此行为是为了启用 符号插入 – 通过将 compute 调用公开为可重定位操作码,您可以 运行 您的代码

> LD_PRELOAD=custom_compute.so ./main

并且您的 compute 调用将被重新定位到 .so 中定义的自定义 compute 函数。


此功能对于像 compute2 这样的静态函数是禁用的 - 它们是内部链接的,不应该用于符号插入。


如评论中所述,此行为不仅适用于 LD_PRELOAD,而且更普遍地与共享库相关 - 例如,在此示例中,如果要加载两个共享库,都定义 compute - 第二个库对 compute 的调用将被重新定位到第一个库的函数。

并不是说 重定位 本身是必需的,而是编译器选择通过 PLT 进行间接寻址(因为可能符号插入,或者在主可执行文件或更早的共享库定义符号的情况下)。注意重定位类型 R_X86_64_PLT32.

如果您查看编译器的 asm 输出(不是 .o 的反汇编),您会看到 call compute@plt.

一个static函数肯定总是使用同一个翻译单元中的定义,但全局符号的其他定义可以优先。


这应该只发生在 -fPIC,而不是构建主要可执行文件本身(-fPIE 在大多数现代发行版中默认打开),对于在同一个 .c(翻译单元)中定义的符号。

https://godbolt.org/z/qYYWsYf6a 显示 GCC -fPIE 仍在使用 call compute。显然 Ubuntu 启用了一些其他选项,使它与众不同? (Godbolt 的 gcc 没有默认启用大多数发行版所做的几件事,因此您需要一些选项来匹配 GCC 在 Ubuntu 上的配置方式。-fstack-protector-strong 不相关,IDK 还有什么会。)

请注意,当 link 执行可执行文件(而非共享库)时,调用应该“放松”为不通过 PLT 的直接调用。所以 GCC 可以将所有调用发出为 call foo@plt.

如果您也使用 -fno-plt,调用将作为 call *foo@gotplt(%rip) 发出,这需要 6 个字节,因此将其放宽到直接 5 字节调用 rel32 需要一个字节的填充符; ld 使用无意义的地址大小前缀。 (例如,请参阅我在 上的回答。)


如果您首先不想使用此 PLT 间接寻址,则可以为该符号设置 ELF visibility = hidden。在创建共享库时,这是一个非常好的主意,因为在那种情况下,linker 将无法通过 PLT 放宽所有间接调用,以便对您不打算允许的函数进行内部调用符号-插入.

您可以使用 -fvisibility=hidden 使其成为所有原型的默认值,因此调用将使用 call rel32,而不是通过 PLT 间接调用(或使用 -fno-plt 的 GOT)。然后对于共享库想要导出的任何函数或变量,使用 __attribute__((visibility("default")))

对于你的情况,-fvisibility=hidden 可能会解决你遇到的问题,即使你没有构建可以进入共享库(-fPIC)。

另见