1

我最近在学习动态链接并尝试了一下:

动态的.c

int global_variable = 10;

int XOR(int a) {
        return global_variable;
}

测试.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 放在 test 的数据部分,并在 dynamic.so 中进行异或。所以间接访问 global_variable。

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

4

2 回答 2

0

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

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

于 2021-12-25T12:31:54.240 回答
0

结果是我clang默认制作的图片,所以它弄乱了结果。

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


在深入研究该主题后,我注意到编译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

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

是否也可以编译dynamic.so为非PIC?原来它显然曾经是10 年前(这篇文章将示例编译为 32 位,它们不必在 64 位上工作!)。链接文章描述了如何在加载时重定位非 PIC 共享库 - 基本上,每次加载后需要重定位的地址出现在机器代码中时,它都会被设置为零并设置某种类型的重定位在图书馆。在加载库期间,加载器用所需的数据/代码的实际运行时地址填充零。重要的是要注意它不能应用于您的,因为64 位共享库不能由非 PICSource)制成。

如果您编译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位于部分内部的偏移量 0x4010 处.data。此外,虽然.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[...]

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

这对你意味着什么?我链接的第一篇文章中名为“Extra credit #2”的部分包含对类似情况的解释。尽管它已有 10 年历史,使用 32 位代码并且共享库不是 PIC,但它与您的情况有一些相似之处,并且可能会解释您在问题中提出的问题。

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

Stack Overflow 上还有另外两个与我相关的答案:herehere。答案和评论。


原答案:

我尝试使用与您提供的代码和编译命令完全相同的代码和编译命令来重现您的问题,但似乎两者都main使用XORGOT 来访问global_variable. 我将通过提供用于检查数据流的命令的示例输出来回答。如果您的输出与我的不同,则意味着我们的环境之间存在其他一些差异(我的意思是很大的差异,如果只有地址/值不同,那么就可以了)。找到这种差异的最佳方法是提供您最初使用的命令及其输出。

第一步是检查每当发生写入或读取时访问的地址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   $0x3,(%rax)
    1161:       bf 10 00 00 00          mov    $0x10,%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    $0x0,%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;代码中的行。为了确认这一点,您可以使用-gfor 调试符号编译程序并使用-S. 这将在相应程序集上方显示源代码。

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

下一条指令又是 a mov,后缀l告诉我们这次数据大小是 4 个字节。它在 rax 的当前值所指向的位置存储一个 4 字节整数,其值等于 0x3(而不是在 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

查看 Address 和 Size 列,我们可以看到该部分在内存中的偏移量 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   $0x3,(%rax)
   0x0000555555555161 <+17>:    mov    $0x10,%edi
   0x0000555555555166 <+22>:    call   0x555555555040 <XOR@plt>
   0x000055555555516b <+27>:    mov    %eax,%esi
   0x000055555555516d <+29>:    lea    0xe90(%rip),%rdi        # 0x555555556004
   0x0000555555555174 <+36>:    mov    $0x0,%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 的绝对地址。.got我们可以通过在 GDB 中发出来再次检查该位置是否来自该段maintenance info sections,这将列出一长串段及其内存映射。对我来说.got是放在这个地址范围内:

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

其中包含 0x555555557fe8。

为了最终检查global_variable它自己的地址,我们可以通过发出来检查那个内存的内容x/xag 0x555555557fe8xag该命令的参数x处理正在检查的数据的大小、格式和类型 - 用于解释help xGDB 中的调用。在我的机器上,命令返回:

0x555555557fe8: 0x7ffff7fc4020 <global_variable>

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

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

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

如果上述命令的任何输出对您来说不同(值的变化是可以的 - 我的意思是根本区别,例如不属于某些地址范围的地址等),请提供有关您到底做了什么的更多信息global_variable存储在该部分中的结论.data- 您调用了哪些命令以及它们产生了哪些输出。

于 2021-12-25T16:23:43.797 回答