我如何摆脱通话__x86.get_pc_thunk.ax

0 c assembly flags function call

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

我正在使用Ubuntu,操作系统类型为64位。

这是C程序。

void add();

int main() { 
add();
return 0;
}
Run Code Online (Sandbox Code Playgroud)

如果我使用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
Run Code Online (Sandbox Code Playgroud)

但是它看起来像这样:

.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
Run Code Online (Sandbox Code Playgroud)

在我的大学,他们告诉我如果我使用的是64位Linux版本,请使用Flag -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
Run Code Online (Sandbox Code Playgroud)

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

zwo*_*wol 8

通常,您不能期望两个不同的编译器为相同的输入生成相同的汇编代码,即使它们具有相同的版本号也是如此。他们可以在代码生成中添加任意数量的额外“补丁”。只要可观察到的行为是相同的,任何事情都会发生。

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

您还应该知道,该main功能通常需要执行其他功能不需要的额外设置和拆卸。该说明leal 4(%esp),%ecx是该额外设置的一部分。如果您只想了解与编写的代码相对应的机器代码,而不是ABI的详细信息,请给您的测试函数命名,而不是main

(正如注释中指出的那样,设置代码并未像可能的那样进行严格的调整,但通常并不重要,因为它在程序的生命周期中仅执行一次。)


现在,要回答字面上的问题,出现

call __x86.get_pc_thunk.ax
Run Code Online (Sandbox Code Playgroud)

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

main:
        leal    4(%esp), %ecx
        andl    $-16, %esp
        pushl   -4(%ecx)
        pushl   %ebp
        movl    %esp, %ebp
        pushl   %ebx
        pushl   %ecx
Run Code Online (Sandbox Code Playgroud)

这只是设置main的堆栈框架并保存需要保存的寄存器。您可以忽略它。

        call    __x86.get_pc_thunk.bx
        addl    $_GLOBAL_OFFSET_TABLE_, %ebx
Run Code Online (Sandbox Code Playgroud)

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

        call    add@PLT
Run Code Online (Sandbox Code Playgroud)

现在,我们调用add@PLT,这意味着调用add,但是跳过“过程链接表”以执行此操作。PLT负责add在共享库而不是主可执行文件中定义的可能性。PLT中的代码使用全局偏移表,并假定您已经在调用@PLT符号之前将EBX设置为指向它。这就是为什么main即使没有使用它也必须设置EBX 的原因。如果您改为写类似

 extern int number;
 int main(void) { return number; }
Run Code Online (Sandbox Code Playgroud)

那么您会看到直接使用GOT,例如

    call    __x86.get_pc_thunk.bx
    addl    $_GLOBAL_OFFSET_TABLE_, %ebx
    movl    number@GOT(%ebx), %eax
    movl    (%eax), %eax
Run Code Online (Sandbox Code Playgroud)

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

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

    movl    number(%rip), %eax
Run Code Online (Sandbox Code Playgroud)

不用用GOT来解决所有这些问题,我们只需number从程序计数器的固定偏移量加载即可。与PC相关的寻址以及对x86体系结构的64位扩展被添加。同样,您的原始程序在64位位置独立模式下只会说

    call    add@PLT
Run Code Online (Sandbox Code Playgroud)

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


__x86.get_pc_thunk.bx和之间的唯一区别__x86.get_pc_thunk.ax是它们将返回地址存储在哪个寄存器中:EBX for .bx,EAX for .ax。我还看到了GCC生成.cx.dx变体。只是要使用哪个寄存器作为全局指针的问题-如果要通过PLT进行调用,则必须为EBX,但是如果没有,则可以使用任何寄存器,因此它尝试使用选择其他不需要的东西。


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

    call 1f
1:  pop  %ebx
Run Code Online (Sandbox Code Playgroud)

但这搞砸了返回地址的预测,因此,如今的编译器遇到了一些额外的麻烦,以确保每一个call都与配对ret

  • 很好的答案。太棒了@zwol。 (2认同)