c++ - 为什么对共享库本身中定义的符号使用全局偏移表?

标签 c++ assembly symbols dynamic-linking got

考虑以下简单的共享库源代码:

库.cpp:

static int global = 10;

int foo()
{
    return global;
}

编译 -fPIC clang 中的选项,它导致这个对象程序集(x86-64):
foo(): # @foo()
  push rbp
  mov rbp, rsp
  mov eax, dword ptr [rip + global]
  pop rbp
  ret
global:
  .long 10 # 0xa

由于符号是在库中定义的,编译器按预期使用 PC 相对寻址:mov eax, dword ptr [rip + global]
但是如果我们改变 static int global = 10;int global = 10;使其成为具有外部链接的符号,生成的程序集为:
foo(): # @foo()
  push rbp
  mov rbp, rsp
  mov rax, qword ptr [rip + global@GOTPCREL]
  mov eax, dword ptr [rax]
  pop rbp
  ret
global:
  .long 10 # 0xa

正如您所看到的,编译器使用全局偏移表添加了一个间接层,在这种情况下这似乎完全没有必要,因为符号仍然定义在同一个库(和源文件)中。

如果符号是在 中定义的另一个 共享库,GOT 是必要的,但在这种情况下,它感觉多余。为什么编译器还在 GOT 中添加这个符号?

注:相信this question与此类似,但答案不相关,可能是由于缺乏细节。

最佳答案

全局偏移表有两个目的。一种是允许动态链接器“插入”与可执行文件或其他共享对象不同的变量定义。第二个是允许生成位置无关代码以引用某些处理器架构上的变量。

ELF 动态链接将整个进程、可执行文件和所有共享对象(动态库)视为共享一个全局命名空间。如果多个组件(可执行文件或共享对象)定义了相同的全局符号,那么动态链接器通常会选择该符号的一个定义,并且所有组件中对该符号的所有引用都引用该定义。 (但是,ELF 动态符号解析很复杂,由于各种原因,不同的组件最终可能会使用同一全局符号的不同定义。)

为了实现这一点,在构建共享库时,编译器将通过 GOT 间接访问全局变量。对于每个变量,将在 GOT 中创建一个包含指向该变量的指针的条目。如您的示例代码所示,编译器将使用此条目来获取变量的地址,而不是尝试直接访问它。当共享对象加载到进程中时,动态链接器将确定是否有任何全局变量已被另一个组件中的变量定义取代。如果是这样,那些全局变量将更新其 GOT 条目以指向替代变量。

通过使用“隐藏的”或“ protected ”ELF 可见性属性,可以防止全局定义的符号被另一个组件中的定义取代,从而消除在某些体系结构上使用 GOT 的需要。例如:

extern int global_visible;
extern int global_hidden __attribute__((visibility("hidden")));
static volatile int local;  // volatile, so it's not optimized away

int
foo() {
    return global_visible + global_hidden + local;
}

当用 -O3 -fPIC 编译时使用 GCC 的 x86_64 端口生成:
foo():
        mov     rcx, QWORD PTR global_visible@GOTPCREL[rip]
        mov     edx, DWORD PTR local[rip]
        mov     eax, DWORD PTR global_hidden[rip]
        add     eax, DWORD PTR [rcx]
        add     eax, edx
        ret 

如您所见,只有 global_visible使用 GOT,global_hiddenlocal不要使用它。 “ protected ”可见性的工作原理类似,它防止定义被取代,但使其对动态链接器仍然可见,以便其他组件可以访问它。 “隐藏”可见性将符号完全隐藏在动态链接器中。

使代码可重定位以允许共享对象在不同进程中加载​​到不同地址的必要性意味着静态分配的变量,无论它们具有全局作用域还是局部作用域,在大多数体系结构上都不能通过单个指令直接访问。我所知道的唯一异常(exception)是 64 位 x86 架构,如上所示。它支持既与 PC 相关的内存操作数,又具有大的 32 位位移,可以到达同一组件中定义的任何变量。

在所有其他架构上,我熟悉以位置相关方式访问变量需要多条指令。具体如何因架构而异,但通常涉及使用 GOT。例如,如果您使用 -m32 -O3 -fPIC 使用 GCC 的 x86_64 端口编译上面的示例 C 代码你得到的选项:
foo():
        call    __x86.get_pc_thunk.dx
        add     edx, OFFSET FLAT:_GLOBAL_OFFSET_TABLE_
        push    ebx
        mov     ebx, DWORD PTR global_visible@GOT[edx]
        mov     ecx, DWORD PTR local@GOTOFF[edx]
        mov     eax, DWORD PTR global_hidden@GOTOFF[edx]
        add     eax, DWORD PTR [ebx]
        pop     ebx
        add     eax, ecx
        ret
__x86.get_pc_thunk.dx:
        mov     edx, DWORD PTR [esp]
        ret

GOT 用于所有三个变量访问,但如果您仔细观察 global_hiddenlocal处理方式与 global_visible 不同.对于后者,指向变量的指针通过 GOT 访问,前两个变量通过 GOT 直接访问。在 GOT 用于所有位置独立变量引用的体系结构中,这是一个相当常见的技巧。

32 位 x86 架构在这方面的一个方面是特殊的,因为它具有大的 32 位位移和 32 位地址空间。这意味着可以通过 GOT 库访问内存中的任何地方,而不仅仅是 GOT 本身。大多数其他架构只支持更小的位移,这使得某些东西与 GOT 基础的最大距离要小得多。使用此技巧的其他体系结构只会将小(本地/隐藏/ protected )变量放在 GOT 本身中,大变量存储在 GOT 之外,并且 GOT 将包含一个指向该变量的指针,就像普通可见性全局变量一样。

关于c++ - 为什么对共享库本身中定义的符号使用全局偏移表?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/55587313/

相关文章:

linux - 我们需要 EXPORT_SYMBOL 和头文件声明吗

c++ - 如何释放 Kiss FFT 分配的内存?

assembly - 为什么 x86 mul 和 div 指令仅采用源操作数?

iphone - 无法读取 Xcode 4.2 中的符号错误

无法使用 TMR4、T4CON 等访问定时器 4 pic32

x86-64 程序集的性能优化 - 对齐和分支预测

php - 正则表达式取代 reg 商标

c++ - c4458 警告 - 如何查找使用了哪个变量

c++ - 如何为我自己的类型声明自定义 char_traits<>?

c++ - 尝试构建 C++ .DLL 时出现类型/转换错误