如何摆脱call __x86.get_pc_thunk.ax?

9

我尝试将一个非常简单的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”是什么意思?

1
尝试在命令行中添加“-fno-pie”。如果可以的话,我会解释。 - zwol
@zwol 谢谢,我试过了,但结果并不完全相同。我已经在上面发布了结果。 - user9585531
lea 是 gcc 用于将 esp 对齐到 16 的非常笨重的代码的一部分。它仅在 32 位代码中的 main 中执行此操作,或者如果其他函数的本地变量需要超过 16 字节的对齐方式。gcc 将返回地址复制到帧指针正上方,并且出于无意义的原因对 ecx 进行了过于复杂的处理,在进入时保存/恢复了一个指向 %esp 上方 4 个字节的指针。[LEA 不是复杂的] (//stackoverflow.com/a/46597375) - Peter Cordes
1
编译器可以生成任何它想要的代码,只要它是正确的,你的代码也是正确的,但并不能保证你会得到完全相同的代码。为了达到这个目的,你需要使用相同版本的编译器和编译选项(而且二进制稳定性是gcc和许多其他编译器的特性,一个编译器每次可能产生不同的输出,但仍然可用作编译器(但对于编译器创建者本身来说,调试将是一场噩梦),所以不要认为这是理所当然的,实际上这是辛勤工作的结果)。 - Ped7g
我已经开了一个新的问题,专门讨论对齐序言:https://dev59.com/Q6vka4cB1Zd3GeqPmgWX - R.. GitHub STOP HELPING ICE
2个回答

24
作为一般规则,即使它们具有相同的版本号,您也不能指望两个不同的编译器为相同的输入生成相同的汇编代码;它们可能有任意数量的额外“补丁”来生成它们的代码。只要可观察到的行为相同,任何事情都可以发生。
您还应该知道,GCC在其默认的-O0模式下,会生成故意糟糕的代码。它被调整为易于调试和编译速度,而不是为了生成的代码的清晰度或效率。通常比理解由gcc -O0生成的代码更容易理解由gcc -O1生成的代码。
您还应该知道,main函数通常需要执行其他函数不需要执行的额外设置和拆卸。指令leal 4(%esp),%ecx是其中的一部分额外设置。如果您只想理解与您编写的代码对应的机器代码,而不是ABI的琐碎细节,请将测试函数命名为除main之外的其他名称。
(正如评论中指出的那样,该设置代码并不是非常紧密地调整过,但通常并不重要,因为它只在程序的生命周期中执行一次。)
现在,回答字面上提出的问题,出现这种情况的原因是什么。
call __x86.get_pc_thunk.ax

这是因为您的编译器默认生成“位置无关”的可执行文件。位置无关意味着操作系统可以将程序的机器代码加载到(虚拟)内存中的任何地址,它仍然可以正常工作。这允许使用地址空间布局随机化等功能,但要使其起作用,您必须采取特殊步骤,在每个访问全局变量或调用另一个函数(有一些例外情况)的函数开头设置“全局指针”。如果您打开优化选项,实际上更容易解释生成的代码。
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_指令的地址和全局偏移表地址之间的。因此,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

不必再与全局偏移表(GOT)纠缠不清,我们可以从程序计数器的固定偏移处加载number。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

但这会破坏返回地址预测,所以现在编译器会多花点功夫来确保每个call都与一个ret配对。

1
我非常喜欢阅读你的回答,对我目前正在学习的内容非常有帮助。非常感谢。 - John H
2
优秀的回答。干得好 @zwol。 - rustyMagnet

1
你看到的额外信息是由于你的版本的GCC为了补偿可能由于错误的入口点代码以错误的堆栈对齐启动而特别针对main进行特殊处理。我不确定如何禁用它或是否可能禁用它,但将函数重命名为非main名称将抑制它以便阅读。

重命名为xmain后,我得到:

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

2
在我的情况下,重命名main函数并没有起作用。但是使用GCC的“-fno-pic”标志可以去除PIC代码。 - Ashfaqur Rahaman

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接