可执行文件会通过 GOT 访问共享库的全局变量吗?

Will an executable access shared-libraries' global variable via GOT?

最近在学习动态链接,试了一下:

dynamic.c

int global_variable = 10;

int XOR(int a) {
        return global_variable;
}

test.c

#include <stdio.h>
extern int global_variable;
extern int XOR(int);

int main() {
        global_variable = 3;
        printf("%d\n", XOR(0x10));
}

编译命令为:

clang -shared -fPIC -o dynamic.so dynamic.c
clang -o test test.c dynamic.so

我原以为在可执行测试中,主要功能将通过 GOT 访问 global_variable。然而,相反,global_variable 放在测试的数据部分,并且 dynamic.so 中的 XOR 间接访问 global_variable。

谁能告诉我为什么编译器不要求测试通过 GOT 访问 global_variable,而是要求共享对象文件这样做?

共享库的部分要点是将一个副本加载到内存中,并且多个进程可以访问该副本。但是每个程序都有自己的每个库变量的副本。如果它们是相对于库的 GOT 访问的,那么它们将在使用库的进程之间共享,就像函数一样。

还有其他可能性,但每个可执行文件为自己提供所需的所有变量是干净且一致的。这要求库函数相对于程序间接访问其所有具有静态存储持续时间的变量(不仅仅是外部变量)。这就是普通的动态链接,只是和你通常认为的方向相反

结果是我的 clang 默认生成了 PIC,所以它弄乱了结果。

我会在这里留下更新的答案,原文可以在下面阅读。


深入研究该主题后,我注意到 test.c 的编译本身不会生成 .got 部分。您可以通过将可执行文件编译成目标文件并暂时省略链接步骤来检查它(-c 选项):

clang -c -o test.o test.c

如果你用 readelf -S 检查生成的目标文件的部分,你会注意到那里没有 .got:

Section Headers:
  [Nr] Name              Type             Address           Offset
       Size              EntSize          Flags  Link  Info  Align
  [ 0]                   NULL             0000000000000000  00000000
       0000000000000000  0000000000000000           0     0     0
  [ 1] .text             PROGBITS         0000000000000000  00000040
       0000000000000035  0000000000000000  AX       0     0     1
  [ 2] .rela.text        RELA             0000000000000000  00000210
       0000000000000060  0000000000000018   I      11     1     8
  [ 3] .data             PROGBITS         0000000000000000  00000075
       0000000000000000  0000000000000000  WA       0     0     1
  [ 4] .bss              NOBITS           0000000000000000  00000075
       0000000000000000  0000000000000000  WA       0     0     1
  [ 5] .rodata           PROGBITS         0000000000000000  00000075
       0000000000000004  0000000000000000   A       0     0     1
  [ 6] .comment          PROGBITS         0000000000000000  00000079
       0000000000000013  0000000000000001  MS       0     0     1
  [ 7] .note.GNU-stack   PROGBITS         0000000000000000  0000008c
       0000000000000000  0000000000000000           0     0     1
  [ 8] .note.gnu.pr[...] NOTE             0000000000000000  00000090
       0000000000000030  0000000000000000   A       0     0     8
  [ 9] .eh_frame         PROGBITS         0000000000000000  000000c0
       0000000000000038  0000000000000000   A       0     0     8
  [10] .rela.eh_frame    RELA             0000000000000000  00000270
       0000000000000018  0000000000000018   I      11     9     8
  [11] .symtab           SYMTAB           0000000000000000  000000f8
       00000000000000d8  0000000000000018          12     4     8
  [12] .strtab           STRTAB           0000000000000000  000001d0
       000000000000003e  0000000000000000           0     0     1
  [13] .shstrtab         STRTAB           0000000000000000  00000288
       0000000000000074  0000000000000000           0     0     1

这意味着 test 可执行文件中存在的整个 .got 部分实际上来自 dynamic.so,因为它是 PIC 并使用 GOT。

是否也可以将 dynamic.so 编译为非 PIC?结果显然 used to be 10 years ago (the article compiles examples to 32-bits, they dont have to work on 64 bits!). Linked article describes how a non-PIC shared library was relocated at load time - basically, every time an address that needed to be relocated after loading was present in machine code, it was instead set to zeroes and a relocation of a certain type was set in the library. During loading of the library the loader filled the zeros with actual runtime address of data/code that was needed. It is important to note that it cannot be applied in your though as 64-bit shared libraries cannot be made out of non-PIC (Source).

如果您将 dynamic.so 编译为共享的 32 位库而不使用 -fPIC 选项(您通常需要启用特殊存储库来编译 32 位代码并拥有 32 位已安装 libc):

gcc -m32 dynamic.c -shared -o dynamic.so

你会注意到:

// readelf -s dynamic.so
(... lots of output)
27: 00004010     4 OBJECT  GLOBAL DEFAULT   19 global_variable

// readelf -S dynamic.so
(... lots of output)
[17] .got              PROGBITS        00003ff0 002ff0 000010 04  WA  0   0  4
[18] .got.plt          PROGBITS        00004000 003000 00000c 04  WA  0   0  4
[19] .data             PROGBITS        0000400c 00300c 000008 00  WA  0   0  4
[20] .bss              NOBITS          00004014 003014 000004 00  WA  0   0  1

global_variable 位于 .data 部分内的偏移量 0x4010 处。此外,虽然 .got 存在(在偏移量 0x3ff0 处),但它仅包含来自除您的代码之外的其他来源的重定位:

// readelf -r
 Offset     Info    Type            Sym.Value  Sym. Name
00003f28  00000008 R_386_RELATIVE   
00003f2c  00000008 R_386_RELATIVE   
0000400c  00000008 R_386_RELATIVE   
00003ff0  00000106 R_386_GLOB_DAT    00000000   _ITM_deregisterTM[...]
00003ff4  00000206 R_386_GLOB_DAT    00000000   __cxa_finalize@GLIBC_2.1.3
00003ff8  00000306 R_386_GLOB_DAT    00000000   __gmon_start__
00003ffc  00000406 R_386_GLOB_DAT    00000000   _ITM_registerTMCl[...]

This article 介绍 GOT 作为 PIC 介绍的一部分,我发现很多地方都是这种情况,这表明 GOT 实际上只被 PIC 代码使用,尽管 我不是 100% 确定,我建议多研究一下这个话题。

这对您来说意味着什么?名为“Extra credit #2”的 section in the first article i linked 包含对类似场景的解释。虽然它已有 10 年历史,但使用 32 位代码并且共享库是非 PIC 的,它与您的情况有一些相似之处,可能会解释您在问题中提出的问题。

还要记住(虽然相似)-fPIE-fPIC 是两个单独的选项,效果略有不同,如果检查期间您的可执行文件未加载到 0x400000,则它可能已编译在您不知情的情况下作为 PIE,这也可能对结果产生影响。最后,这一切都归结为进程之间要共享哪些数据,可以在任意地址加载什么 data/code,必须在固定地址加载什么等。希望这会有所帮助。

另外两个关于 Stack Overflow 的答案似乎与我相关:here and here。答案和评论。


原回答:

我尝试使用与您提供的代码和编译命令完全相同的代码和编译命令重现您的问题,但似乎 mainXOR 都使用 GOT 访问 global_variable.我将通过提供我用来检查数据流的命令的示例输出来回答。如果您的输出与我的不同,则意味着我们的环境之间存在一些其他差异(我的意思是很大的差异,如果只有 addresses/values 不同那么没关系)。找出差异的最佳方法是提供您最初使用的命令及其输出。

第一步是检查在写入或读取 global_variable 时访问的地址。为此,我们可以使用 objdump -D -j .text test 命令反汇编代码并查看 main 函数:

0000000000001150 <main>:
    1150:       55                      push   %rbp
    1151:       48 89 e5                mov    %rsp,%rbp
    1154:       48 8b 05 8d 2e 00 00    mov    0x2e8d(%rip),%rax        # 3fe8 <global_variable>
    115b:       c7 00 03 00 00 00       movl   [=15=]x3,(%rax)
    1161:       bf 10 00 00 00          mov    [=15=]x10,%edi
    1166:       e8 d5 fe ff ff          call   1040 <XOR@plt>
    116b:       89 c6                   mov    %eax,%esi
    116d:       48 8d 3d 90 0e 00 00    lea    0xe90(%rip),%rdi        # 2004 <_IO_stdin_used+0x4>
    1174:       b0 00                   mov    [=15=]x0,%al
    1176:       e8 b5 fe ff ff          call   1030 <printf@plt>
    117b:       31 c0                   xor    %eax,%eax
    117d:       5d                      pop    %rbp
    117e:       c3                      ret    
    117f:       90                      nop

第一列中的数字不是绝对地址 - 相反,它们是相对于将加载可执行文件的基地址的偏移量。为了便于解释,我将它们称为“偏移量”。

偏移量 0x115b 和 0x1161 处的程序集直接来自代码中的 global_variable = 3; 行。为确认这一点,您可以使用 -g 编译程序以获得调试符号,并使用 -S 调用 objdump。这将在相应程序集上方显示源代码。

我们将重点关注这两条指令的作用。第一条指令是从内存中的一个位置到 rax 寄存器的 8 个字节的 mov。内存中的位置是相对于当前 rip 值给出的,由常数 0x2e8d 偏移。 Objdump 已经帮我们计算好了这个值,它等于 0x3fe8。所以这将在 0x3fe8 偏移处获取内存中存在的 8 个字节,并将它们存储在 rax 寄存器中。

下一条指令又是mov,后缀l告诉我们这次数据大小是4字节。它在 rax 的当前值指向的位置存储一个值等于 0x3 的 4 字节整数(不在 rax 本身!寄存器周围的括号如 (%rax) 表示指令中的位置是不是寄存器本身,而是它的内容指向的位置!)。

总而言之,我们从偏移量 0x3fe8 的某个位置读取指向 4 字节变量的指针,然后在该指针指定的位置存储立即数 0x3。现在的问题是:0x3fe8 的偏移量从何而来?

它实际上来自GOT。要显示 .got 部分的内容,我们可以使用 objdump -s -j .got test 命令。 -s 意味着我们要专注于该部分的实际原始内容,而不进行任何分解。我的输出是:

test:     file format elf64-x86-64

Contents of section .got:
 3fd0 00000000 00000000 00000000 00000000  ................
 3fe0 00000000 00000000 00000000 00000000  ................
 3ff0 00000000 00000000 00000000 00000000  ................

整个部分显然设置为零,因为GOT在将程序加载到内存后填充数据,但重要的是地址范围。我们可以看到 .got 从偏移量 0x3fd0 开始,到 0x3ff0 结束。这意味着它还包括 0x3fe8 偏移量 - 这意味着 global_variable 的位置确实存储在 GOT 中。

另一种查找此信息的方法是使用 readelf -S test 显示可执行文件的部分并向下滚动到 .got 部分:

[Nr] Name              Type             Address           Offset
       Size              EntSize          Flags  Link  Info  Align
(...lots of sections...)
[22] .got              PROGBITS         0000000000003fd0  00002fd0
       0000000000000030  0000000000000008  WA       0     0     8

查看地址和大小列,我们可以看到该部分加载到内存中的偏移量 0x3fd0 处,其大小为 0x30 - 这与 objdump 显示的内容相对应。请注意,在 readelf 输出中,“Offset”实际上是程序加载到文件格式中的偏移量——而不是我们感兴趣的内存中的偏移量。

通过在 dynamic.so 库上发出相同的命令,我们得到类似的结果:

00000000000010f0 <XOR>:
    10f0:       55                      push   %rbp
    10f1:       48 89 e5                mov    %rsp,%rbp
    10f4:       89 7d fc                mov    %edi,-0x4(%rbp)
    10f7:       48 8b 05 ea 2e 00 00    mov    0x2eea(%rip),%rax        # 3fe8 <global_variable@@Base-0x38>
    10fe:       8b 00                   mov    (%rax),%eax
    1100:       5d                      pop    %rbp
    1101:       c3                      ret

所以我们看到mainXOR都使用GOT找到了global_variable的位置。

至于global_variable的位置我们需要运行程序来填充GOT。为此,我们可以使用 GDB。我们可以 运行 通过这样调用 GDB 中的程序:

LD_LIBRARY_PATH="$LD_LIBRARY_PATH:." gdb ./test

LD_LIBRARY_PATH 环境变量告诉链接器在哪里寻找共享对象,所以我们扩展它以包括当前目录“。”这样它就可以找到 dynamic.so.

GDB载入我们的代码后,我们可以调用break main在main处设置断点,run到运行程序。程序执行应该在 main 函数的开头暂停,让我们可以在可执行文件完全加载到内存后查看我们的可执行文件,并填充 GOT。

运行 disassemble main 在此状态下将向我们显示内存中的实际绝对偏移量:

Dump of assembler code for function main:
   0x0000555555555150 <+0>:     push   %rbp
   0x0000555555555151 <+1>:     mov    %rsp,%rbp
=> 0x0000555555555154 <+4>:     mov    0x2e8d(%rip),%rax        # 0x555555557fe8
   0x000055555555515b <+11>:    movl   [=20=]x3,(%rax)
   0x0000555555555161 <+17>:    mov    [=20=]x10,%edi
   0x0000555555555166 <+22>:    call   0x555555555040 <XOR@plt>
   0x000055555555516b <+27>:    mov    %eax,%esi
   0x000055555555516d <+29>:    lea    0xe90(%rip),%rdi        # 0x555555556004
   0x0000555555555174 <+36>:    mov    [=20=]x0,%al
   0x0000555555555176 <+38>:    call   0x555555555030 <printf@plt>
   0x000055555555517b <+43>:    xor    %eax,%eax
   0x000055555555517d <+45>:    pop    %rbp
   0x000055555555517e <+46>:    ret    
End of assembler dump.
(gdb) 

我们的0x3fe8偏移量变成了一个等于0x555555557fe8的绝对地址。我们可以通过在 GDB 中发出 maintenance info sections 再次检查此位置是否来自 .got 部分,这将列出一长串部分及其内存映射。对我来说 .got 放在这个地址范围内:

[21]     0x555555557fd0->0x555555558000 at 0x00002fd0: .got ALLOC LOAD DATA HAS_CONTENTS

其中包含 0x555555557fe8.

为了最终检查 global_variable 本身的地址,我们可以 ex 通过发出 x/xag 0x555555557fe8 来检查该内存的内容。 x 命令的参数 xag 处理正在检查的数据的大小、格式和类型 - 为了解释在 GDB 中调用 help x。在我的机器上,命令 returns:

0x555555557fe8: 0x7ffff7fc4020 <global_variable>

在您的机器上它可能只显示地址和数据,没有“”助手,这可能来自我安装的名为 pwndbg 的扩展。没关系,因为该地址的值就是我们所需要的。我们现在知道 global_variable 位于内存中地址 0x7ffff7fc4020 下。现在我们可以在 GDB 中执行 info proc mappings 来找出这个地址属于哪个地址范围。我的输出很长,但在列出的所有范围中,有一个是我们感兴趣的:

0x7ffff7fc4000     0x7ffff7fc5000     0x1000     0x3000 /home/user/test_got/dynamic.so

该地址在该内存区域内部,GDB 告诉我们它来自 dynamic.so 库。

如果上述命令的任何输出对您来说不同(更改值是可以的 - 我的意思是根本区别,例如不属于特定地址范围的地址等),请提供更多有关确切原因的信息你得出的结论是 global_variable 存储在 .data 部分 - 你调用了什么命令以及它们产生了什么输出。