我如何摆脱调用__x86.get_pc_thunk.ax

时间:2018-04-30 17:41:12

标签: c function assembly call flags

我尝试编译并将一个非常简单的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版本,他们告诉我使用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

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

2 个答案:

答案 0 :(得分:5)

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

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

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

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

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

的原因
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来解决所有问题。添加了PC相对寻址以及x86架构的64位扩展。同样,您的原始程序,在64位位置无关模式下,只会说

    call    add@PLT

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

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

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

    call 1f
1:  pop  %ebx

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

答案 1 :(得分:0)

您看到的额外垃圾是由于您的GCC特殊套管main的版本,以补偿可能损坏的入口点代码,以未对齐的堆栈启动它。我不知道如何禁用它或者它是否可能,但是将函数重命名为main以外的函数会为了你的阅读而禁止它。

重命名为xmain后,我得到:

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