【问题标题】:Why does this movq instruction work on linux and not osx?为什么这条 movq 指令适用于 linux 而不是 osx?
【发布时间】:2014-11-06 02:48:38
【问题描述】:

以下汇编代码在 OSX 10.9.4 上运行 as 时出错,但在 Linux (Debian 7.6) 上成功运行。特别是 movq 指令似乎不喜欢 label 参数。

$ cat test.S
.globl _main
_main:
    movq $_main, %rax
    ret

这是错误:

$ as -o test.o test.S 
test.S:3:32-bit absolute addressing is not supported for x86-64
test.S:3:cannot do signed 4 byte relocation

将第 3 行中的 $_main 更改为像 $10 这样的文字可以正常工作。

代码必须以一种非常小的方式进行修改才能在 Linux 上运行 - 只需从标签中删除下划线即可。

$ cat test.S
.globl main
main:
    movq $main, %rax
    ret

独立验证代码是否可以在 Linux 上运行非常容易:

$ as -o test.o test.S
$ gcc -o test.out test.o
$ ./test.out

请忽略该代码实际上并没有做任何事情,我故意将其尽可能地精简以显示错误。

我对使用 LEA(加载有效地址)进行了相当多的研究,但在进行更改之前,我想了解其中的区别 - 为什么它适用于 Linux 而不是 OSX?

【问题讨论】:

  • +1 个写得很好的问题,带有一个非常好的最小、完整、可验证的代码示例

标签: linux macos x86-64


【解决方案1】:

movq 指令无法引用绝对地址是正确的。这部分是由于OS X ABI Mach-O 格式对符号使用可重定位寻址。

编译为与位置无关的可执行文件 (PIE) 的程序通常不能像 movq $_main, %rax 那样引用绝对虚拟地址。相反,Global Offset Tables 被调用,它允许位置相对代码 (PC-rel) 和位置无关代码 (PIC) 在其当前绝对地址位置提取全局符号。如下所示,GOTPCREL(%rip)创建了对lea rdi, _msg的解释:

PC-rel 代码引用它的全局偏移表:

.globl _main
_main:

    movq     _main@GOTPCREL(%rip), %rax
    sub      $8, %rsp
    mov      $0, %rax
    movq     _msg@GOTPCREL(%rip), %rdi
    call     _printf
    add      $8, %rsp
    ret

.cstring
_msg:

    .ascii  "Hello, world\n"

Mac OS X Mach-O 汇编器:

$ as -o test.o test.asm

Apple 版本的 GCC:

$ gcc -o test.out test.o

输出:

$ ./test.out
Hello, world

【讨论】:

  • 这非常有用,我肯定会接受答案。但是,我想知道您能否解释一下 Linux 上发生了什么?虽然我猜答案很简单:它使用绝对寻址?然而(也许我很困惑,也许这值得它自己的后续问题),我认为可重定位寻址是 x86-64 中必需的和/或基础的。
  • Linux 示例可能看起来使用绝对寻址,但是,$main, %rax 等指令会自动转换为 GOTPCREL 并利用其各自的全局偏移表。过程链接表(PLT)——在运行时确定函数绝对地址的机制也有所不同。 ELF PLT 条目被组织并分组为由它们自己的plt 部分标识的块。使用 Mach-O,PLT 被拼接到多个部分,称为 stub helpers,并且需要一个额外的步骤来找到相应的符号 id。
  • 在适用于 Linux 和 OS X 的程序中始终使用GOTPCREL 的好处在于它可以节省大量时间;因为所有需要更改的只是为符号添加下划线或删除它们。 This example 使用此原理演示了几乎相同的 Linux/OS X 代码。
  • @joshcough:你永远不需要使用 GOT 来访问你自己的符号,只需 lea _msg(%rip), %rdi 就像编译器在 Linux 上使用 -fPIE-fPIC 一样。 godbolt.org/z/WKPTYv不能假设_printf 可以通过call rel32 访问,因为它在共享库中;这实际上需要间接。链接器将通过使用 GOT 的存根将call _printf 转换为调用。在 Linux 上,gcc -fno-plt 将使用 call *puts@GOTPCREL(%rip) 通过 PLT 绕过该存根,或直接使用 GOT 进行间接调用。
  • 您的答案有效,但这是一种效率低下且过于复杂的处理方式,这也为动态链接器创建了更多工作。 (在进程启动时填写那些 GOT 条目。)它也错过了解释 x86-64 具有良好的 RIP 相对寻址能力的机会,这使得高效的 PIC 代码成为可能,而无需动态链接器的太多帮助。我不建议任何人以这种方式编写代码。这就是我投反对票的原因。
猜你喜欢
  • 1970-01-01
  • 2012-06-23
  • 1970-01-01
  • 2020-09-07
  • 2015-04-23
  • 1970-01-01
  • 1970-01-01
  • 2014-11-28
  • 1970-01-01
相关资源
最近更新 更多