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

时间:2019-04-09 07:31:02

标签: c++ assembly symbols dynamic-linking got

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

library.cpp:

static int global = 10;

int foo()
{
    return global;
}

使用clang中的-fPIC选项进行编译,它将导致该对象组合件(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与此相似,但是答案可能不适当,可能是由于缺乏细节。

2 个答案:

答案 0 :(得分:1)

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

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

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

通过使用“隐藏的”或“受保护的” 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没有使用GOT。 “受保护的”可见性的工作原理类似,它防止了定义的取代,但仍对动态链接器可见,因此可以被其他组件访问。 “隐藏”可见性完全隐藏了动态链接器中的符号。

使代码可重定位以便在不同的进程中将共享的对象加载到不同的地址的必要性意味着,静态分配的变量(无论是全局范围还是局部范围)在大多数情况下都不能直接用一条指令直接访问建筑。如上所示,我所知道的唯一例外是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基础的最大距离要小得多。其他使用此技巧的体系结构只会将小的(本地/隐藏/受保护的)变量放在GOT本身中,大的变量将存储在GOT之外,并且GOT将包含指向该变量的指针,就像普通可见性的全局变量一样。

答案 1 :(得分:0)

除了罗斯里奇(Ross Ridge)回答中的详细信息。

这是内部与内部的联系。如果没有if {} else {},则该变量具有外部链接,因此可以从任何其他翻译单元访问。任何其他翻译单元都可以将其声明为static并对其进行访问。

Linkage

  

外部链接。可以从其他翻译单位的范围中引用该名称。具有外部链接的变量和函数也具有语言链接,这使得可以链接以不同编程语言编写的翻译单元。

     

在命名空间范围内声明的以下任何名称均具有外部链接,除非命名空间未命名或包含在未命名的命名空间中(自C ++ 11起):

     
      
  • 上面未列出的变量和函数(即,未声明为静态的函数,未声明为静态的名称空间作用域非常量变量以及任何声明为extern的变量);
  •