可执行文件会通过 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。答案和评论。
原回答:
我尝试使用与您提供的代码和编译命令完全相同的代码和编译命令重现您的问题,但似乎 main
和 XOR
都使用 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
所以我们看到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 [=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
部分 - 你调用了什么命令以及它们产生了什么输出。
最近在学习动态链接,试了一下:
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。答案和评论。
原回答:
我尝试使用与您提供的代码和编译命令完全相同的代码和编译命令重现您的问题,但似乎 main
和 XOR
都使用 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
所以我们看到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 [=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>
在您的机器上它可能只显示地址和数据,没有“global_variable
位于内存中地址 0x7ffff7fc4020 下。现在我们可以在 GDB 中执行 info proc mappings
来找出这个地址属于哪个地址范围。我的输出很长,但在列出的所有范围中,有一个是我们感兴趣的:
0x7ffff7fc4000 0x7ffff7fc5000 0x1000 0x3000 /home/user/test_got/dynamic.so
该地址在该内存区域内部,GDB 告诉我们它来自 dynamic.so
库。
如果上述命令的任何输出对您来说不同(更改值是可以的 - 我的意思是根本区别,例如不属于特定地址范围的地址等),请提供更多有关确切原因的信息你得出的结论是 global_variable
存储在 .data
部分 - 你调用了什么命令以及它们产生了什么输出。