结果是我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 位共享库不能由非 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
位于部分内部的偏移量 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 上还有另外两个与我相关的答案:here和here。答案和评论。
原答案:
我尝试使用与您提供的代码和编译命令完全相同的代码和编译命令来重现您的问题,但似乎两者都main
使用XOR
GOT 来访问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;
代码中的行。为了确认这一点,您可以使用-g
for 调试符号编译程序并使用-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
所以我们看到了两者main
并XOR
使用 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 0x555555557fe8
。xag
该命令的参数x
处理正在检查的数据的大小、格式和类型 - 用于解释help x
GDB 中的调用。在我的机器上,命令返回:
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
- 您调用了哪些命令以及它们产生了哪些输出。