【问题标题】:How do i get rid of call __x86.get_pc_thunk.ax我如何摆脱呼叫 __x86.get_pc_thunk.ax
【发布时间】:2018-10-10 20:21:11
【问题描述】:

我试图编译一个非常简单的 C 程序并将其转换为汇编语言。

我使用的是 Ubuntu,操作系统类型是 64 位。

这是 C 程序。

void add();

int main() { 
add();
return 0;
}

如果我使用 gcc -S -m32 -fno-asynchronous-unwind-tables -o simple.S simple.c 这就是我的汇编源代码文件的样子:

.file   "main1.c"
.text
.globl main
.type   main, @function
main:
pushl   %ebp
movl    %esp, %ebp
andl    $-16, %esp
call    add
movl    $0, %eax
movl    %ebp, %esp
popl    %ebp
ret
.size   main, .-main
.ident  "GCC: (Debian 4.4.5-8) 4.4.5" // this part should say Ubuntu instead of Debian
.section    .note.GNU-stack,"",@progbits

但它看起来像这样:

.file   "main0.c"
.text
.globl  main
.type   main, @function
main:
leal    4(%esp), %ecx
andl    $-16, %esp
pushl   -4(%ecx)
pushl   %ebp
movl    %esp, %ebp
pushl   %ebx
pushl   %ecx
call    __x86.get_pc_thunk.ax
addl    $_GLOBAL_OFFSET_TABLE_, %eax
movl    %eax, %ebx
call    add@PLT
movl    $0, %eax
popl    %ecx
popl    %ebx
popl    %ebp
leal    -4(%ecx), %esp
ret
.size   main, .-main
.section        

.text.__x86.get_pc_thunk.ax,"axG",@progbits,__x86.get_pc_thunk.ax,comdat
.globl  __x86.get_pc_thunk.ax
.hidden __x86.get_pc_thunk.ax
.type   __x86.get_pc_thunk.ax, @function
__x86.get_pc_thunk.ax:
movl    (%esp), %eax
ret
.ident  "GCC: (Ubuntu 6.3.0-12ubuntu2) 6.3.0 20170406"
.section    .note.GNU-stack,"",@progbits

在我的大学,如果我使用 64 位 Linux 版本,他们告诉我使用标志 -m32。有人可以告诉我我做错了什么吗? 我是否使用了正确的标志?

在 -fno-pie 之后编辑

.file   "main0.c"
.text
.globl  main
.type   main, @function
main:
leal    4(%esp), %ecx
andl    $-16, %esp
pushl   -4(%ecx)
pushl   %ebp
movl    %esp, %ebp
pushl   %ecx
subl    $4, %esp
call    add
movl    $0, %eax
addl    $4, %esp
popl    %ecx
popl    %ebp
leal    -4(%ecx), %esp
ret
.size   main, .-main
.ident  "GCC: (Ubuntu 6.3.0-12ubuntu2) 6.3.0 20170406"
.section    .note.GNU-stack,"",@progbits

看起来更好,但并不完全相同。 例如,leal 是什么意思?

【问题讨论】:

  • 尝试在命令行中添加-fno-pie。如果可行,我会解释。
  • @zwol thx 我试过了,但结果并不完全相同。我在上面发布了结果
  • lea 是 gcc 将 esp 对齐 16 的非常笨拙代码的一部分。它仅在 32 位代码中的 main 中执行此操作,或者如果它需要超过 16 字节其他功能中的本地人对齐。 gcc 将返回地址复制到帧指针的正上方,并无缘无故地使用ecx 进行过度复杂的操作,保存/恢复指向%esp 上方4 个字节的指针。 LEA is not complicated
  • 编译器可以生成它想要的任何代码,只要它是正确的,你的代码是正确的,不能保证你会得到完全相同的代码。为此,您还需要使用相同版本的编译器和编译选项(并且 - 编译器结果的二进制稳定性 - 实际上是 gcc 和许多其他编译器的功能,编译器每次都可能产生不同的输出,并且它仍然可以用作编译器(但为编译器创建者本身调试它会是一场噩梦),所以不要认为这是理所当然的,这实际上是努力工作的结果。
  • 我专门针对对齐序言提出了一个新问题:stackoverflow.com/q/50106490/379897

标签: c function assembly call flags


【解决方案1】:

作为一般规则,您不能期望两个不同的编译器为相同的输入生成相同的汇编代码,即使它们具有相同的版本号;他们可以有任意数量的额外“补丁”来生成代码。只要可观察到的行为相同,一切都会发生。

您还应该知道,GCC 在其默认的-O0 模式下会生成故意错误代码。它的调整是为了便于调试和编译速度,而不是为了生成代码的清晰度或效率。 gcc -O1生成的代码往往比gcc -O0生成的代码更容易理解。

您还应该知道main 函数通常需要进行其他函数不需要做的额外设置和拆卸。指令leal 4(%esp),%ecx 是该额外设置的一部分。如果您只想了解 编写的代码对应的机器代码,而不是 ABI 的详细信息,请将您的测试函数命名为 main 以外的名称。

(正如 cmets 中所指出的,设置代码并没有像它可能的那样紧密调整,但这通常并不重要,因为它在程序的生命周期中只执行一次。)


现在,回答字面意思的问题,出现的原因

call __x86.get_pc_thunk.ax

是因为您的编译器默认生成“与位置无关”的可执行文件。位置无关意味着操作系统可以将程序的机器代码加载到(虚拟)内存中的任何地址并且它仍然可以工作。这允许address space layout randomization 之类的东西,但要使其工作,您必须采取特殊步骤在每个访问全局变量或调用另一个函数(有一些例外)的函数的开头设置一个“全局指针”。如果打开优化,实际上更容易解释生成的代码:

main:
        leal    4(%esp), %ecx
        andl    $-16, %esp
        pushl   -4(%ecx)
        pushl   %ebp
        movl    %esp, %ebp
        pushl   %ebx
        pushl   %ecx

这一切只是设置main的堆栈帧和保存需要保存的寄存器。你可以忽略它。

        call    __x86.get_pc_thunk.bx
        addl    $_GLOBAL_OFFSET_TABLE_, %ebx

特殊函数__x86.get_pc_thunk.bx 将其返回地址——即紧随其后的addl 指令的地址——加载到EBX 寄存器中。然后我们将魔术常量_GLOBAL_OFFSET_TABLE_ 的值添加到该地址,在与位置无关的代码中,它是使用_GLOBAL_OFFSET_TABLE_ 的指令的地址与global offset table。因此,EBX 现在指向全局偏移表。

        call    add@PLT

现在我们调用add@PLT,也就是调用add,但是要跳过“过程联动表”来做。 PLT 处理add 定义在共享库而不是主可执行文件中的可能性。 PLT 中的代码使用全局偏移表,并假定您在调用@PLT 符号之前已经将EBX 设置为指向它。这就是为什么main 必须设置 EBX,即使似乎没有任何东西使用它。如果你写了类似的东西

 extern int number;
 int main(void) { return number; }

然后你会看到直接使用 GOT,类似于

    call    __x86.get_pc_thunk.bx
    addl    $_GLOBAL_OFFSET_TABLE_, %ebx
    movl    number@GOT(%ebx), %eax
    movl    (%eax), %eax

我们用 GOT 的地址加载 EBX,然后我们可以从 GOT 加载全局变量 number 的地址,然后我们实际上取消引用地址以获得值number.

如果您改为编译 64 位代码,您会看到一些不同且简单得多的东西:

    movl    number(%rip), %eax

我们可以从程序计数器的固定偏移处加载number,而不是在 GOT 中搞这些。与 x86 架构的 64 位扩展一起添加了 PC 相对寻址。同样,您的原始程序在 64 位位置无关模式下只会说

    call    add@PLT

无需先设置 EBX。调用仍需通过 PLT,但 PLT 本身使用 PC 相对寻址,不需要调用者的任何帮助。


__x86.get_pc_thunk.bx__x86.get_pc_thunk.ax 之间的唯一区别是它们将返回地址存储在哪个寄存器中:EBX 代表.bx,EAX 代表.ax。我还看到 GCC 生成 .cx.dx 变体。这只是它想为全局指针使用哪个寄存器的问题——如果要通过 PLT 进行调用,它必须是 EBX,但如果没有,那么它可以使用任何寄存器,所以它尝试选择一个其他任何事情都不需要的。


为什么要调用函数来获取返回地址?旧的编译器会这样做:

    call 1f
1:  pop  %ebx

但是这搞砸了return-address prediction,所以现在编译器会花一点额外的麻烦来确保每个call都与ret配对。

【讨论】:

  • 我真的很喜欢阅读您的回答,这对我目前正在学习的内容非常有用。非常感谢。
  • 优秀的答案。太棒了@zwol。
【解决方案2】:

您看到的额外垃圾是由于您的 GCC 特殊外壳版本main 以补偿可能以未对齐堆栈开头的入口点代码。我不确定如何禁用它,或者它是否可能,但是为了您的阅读,将函数重命名为 main 以外的其他东西会抑制它。

重命名为xmain后我得到:

xmain:
        pushl   %ebp
        movl    %esp, %ebp
        subl    $8, %esp
        call    add
        movl    $0, %eax
        leave
        ret

【讨论】:

  • 在我的情况下,重命名 main 不起作用。但是 GCC 的 -fno-pic 标志删除了 PIC 代码。
猜你喜欢
  • 1970-01-01
  • 2019-06-11
  • 2015-09-28
  • 1970-01-01
  • 1970-01-01
  • 2020-12-19
  • 2023-03-31
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多