如何在 x86-64 Linux 上的 GAS 程序集中通过 GOT 访问 C 全局变量?

How to access a C global variable through GOT in GAS assembly on x86-64 Linux?

我的问题

我正在尝试编写一个共享库(不是 executable,所以请不要告诉我使用 -no-pie),在单独的文件中使用汇编和 C(不是内联汇编)。

而且我想在汇编代码中通过全局偏移Table访问一个C全局变量,因为调用的函数可能在任何其他共享库中定义。

我知道 PLT/GOT 的东西,但我不确定如何告诉编译器为 linker 正确生成重定位信息(语法是什么),以及如何告诉linker 使用该信息实际重新定位我的代码(linker 选项是什么)。

我的代码编译时出现 linking 错误

/bin/ld: tracer.o: relocation R_X86_64_PC32 against
/bin/ld: final link failed: bad value

此外,如果有人能分享一些关于GAS组件的关于搬迁的详细文档就更好了。例如,关于如何使用 GNU 汇编器在 C 和汇编之间进行插值的详尽列表。

源代码

将 C 和汇编代码与 link 编译成一个共享库。

# Makefile
liba.so: tracer2.S target2.c
    gcc -shared -g -o liba.so tracer2.S target2.c
// target2.c
// NOTE: This is a variable, not a function.
int (*read_original)(int fd, void *data, unsigned long size) = 0;
// tracer2.S
.text
    // external symbol declarition
    .global read_original
read:
  lea read_original(%rip), %rax
  mov (%rax), %rax
  jmp *%rax

期望与结果

我希望 link 人能愉快地 link 我的目标文件,但它说

g++ -shared -g -o liba.so tracer2.o target2.c -ldl
/bin/ld: tracer.o: relocation R_X86_64_PC32 against
/bin/ld: final link failed: bad value
collect2: error: ld returned 1 exit status
make: *** [Makefile:2: liba.so] Error 1

并注释掉行

// lea read_original(%rip), %rax

使错误消失。

解决方案。

    lea read_original@GOTPCREL(%rip), %rax

关键字GOTPCREL 将告诉编译器这是对GOT table 的PC 相对重定位。 linker 将计算从当前 rip 到目标 GOT table 条目的偏移量。

您可以通过

验证
$ objdump -d liba.so
    10e9:       48 8d 05 f8 2e 00 00    lea    0x2ef8(%rip),%rax        # 3fe8 <read_original@@Base-0x40>
    10f0:       48 8b 00                mov    (%rax),%rax
    10f3:       ff e0                   jmpq   *%rax

感谢彼得。

一些可能相关或不相关的信息

1.我可以调用C函数
  call read@plt

objdump 表明它调用了正确的 PLT 条目。

$ objdump -d liba.so
...
0000000000001109 <read1>:
    1109:       e8 22 ff ff ff          callq  1030 <read@plt>
    110e:       ff e0                   jmpq   *%rax
2.我可以lea一个正确的PLT入口地址

0xffffff23 是 -0xdd,0x1109 - 0xdd = 102c

0000000000001020 <.plt>:
    1020:       ff 35 e2 2f 00 00       pushq  0x2fe2(%rip)        # 4008 <_GLOBAL_OFFSET_TABLE_+0x8>
    1026:       ff 25 e4 2f 00 00       jmpq   *0x2fe4(%rip)        # 4010 <_GLOBAL_OFFSET_TABLE_+0x10>
    102c:       0f 1f 40 00             nopl   0x0(%rax)

0000000000001030 <read@plt>:
    1030:       ff 25 e2 2f 00 00       jmpq   *0x2fe2(%rip)        # 4018 <read@GLIBC_2.2.5>
    1036:       68 00 00 00 00          pushq  [=21=]x0
    103b:       e9 e0 ff ff ff          jmpq   1020 <.plt>

0000000000001109 <read1>:
    1109:       48 8d 04 25 23 ff ff    lea    0xffffffffffffff23,%rax
    1110:       ff
    1111:       ff e0                   jmpq   *%rax

环境

$ uname -a
Linux alex-arch 5.2.6-arch1-1-ARCH #1 SMP PREEMPT Sun Aug 4 14:58:49 UTC 2019 x86_64 GNU/Linux
$ gcc -v
Using built-in specs.
COLLECT_GCC=/bin/gcc
COLLECT_LTO_WRAPPER=/usr/lib/gcc/x86_64-pc-linux-gnu/9.1.0/lto-wrapper
Target: x86_64-pc-linux-gnu
Configured with: /build/gcc/src/gcc/configure --prefix=/usr --libdir=/usr/lib --libexecdir=/usr/lib --mandir=/usr/share/man --infodir=/usr/share/info --with-bugurl=https://bugs.archlinux.org/ --enable-languages=c,c++,ada,fortran,go,lto,objc,obj-c++ --enable-shared --enable-threads=posix --with-system-zlib --with-isl --enable-__cxa_atexit --disable-libunwind-exceptions --enable-clocale=gnu --disable-libstdcxx-pch --disable-libssp --enable-gnu-unique-object --enable-linker-build-id --enable-lto --enable-plugin --enable-install-libiberty --with-linker-hash-style=gnu --enable-gnu-indirect-function --enable-multilib --disable-werror --enable-checking=release --enable-default-pie --enable-default-ssp --enable-cet=auto
Thread model: posix
gcc version 9.1.0 (GCC)
$ ld --version
GNU ld (GNU Binutils) 2.32
Copyright (C) 2019 Free Software Foundation, Inc.
This program is free software; you may redistribute it under the terms of
the GNU General Public License version 3 or (at your option) a later version.
This program has absolutely no warranty.

显然,链接器对 ELF 共享对象中的符号强制执行全局可见性和隐藏可见性,不允许 "back door" 访问参与符号插入的符号(因此可能超过 2GB。)

要使用正常的 RIP 相对寻址从同一共享对象中的其他代码直接访问它,请通过设置符号的 ELF 可见性使符号 隐藏 。 (另请参阅 https://www.macieira.org/blog/2012/01/sorry-state-of-dynamic-libraries-on-linux/ and Ulrich Drepper's How to Write Shared Libraries

__attribute__ ((visibility("hidden")))
 int (*read_original)(int fd, void *data, unsigned long size) = 0;

然后 gcc -save-temps tracer2.S target2.c -shared -fPIC compiles/assembles + 链接共享库。 GCC 也有像 -fvisibility=hidden 这样的选项,这使得它成为默认值,需要你 do 想要导出的符号的显式属性以进行动态链接。如果您在库中使用了任何全局变量,那么这是一个很好的主意,可以让编译器发出高效的代码来使用它们。它还可以保护您免受与其他库的全局名称冲突。 GCC 手册强烈推荐它。

它也适用于 g++; C++ name mangling 仅适用于函数名称,不适用于变量(包括 function-pointers)。但是一般不要用C++编译器编译.c文件。


如果要支持符号插入,需要使用GOT;显然你可以看看编译器是如何做到的:

int glob;                 // with default visibility = default
int foo() { return glob; }

compiles to this asm with GCC -O3 -fPIC(没有任何可见性选项,因此全局符号完全全局可见:从共享对象导出并参与符号插入)。

foo:
        movq    glob@GOTPCREL(%rip), %rax
        movl    (%rax), %eax
        ret

显然这比 mov glob(%rip), %eax 效率低,所以更喜欢将全局变量限制在库范围内(隐藏),而不是真正的全局变量。

您可以使用弱别名执行一些技巧,让您导出只有该库定义的符号,并通过 "hidden" 别名有效地访问该定义。