【问题标题】:Trying to understand the gcc assembly output for printf()试图了解 printf() 的 gcc 程序集输出
【发布时间】:2020-11-14 15:16:21
【问题描述】:

我正在努力学习如何理解汇编代码,所以我一直在研究 GCC 的汇编输出,以了解一些愚蠢的程序。其中之一就是int i = 0;,我现在或多或少完全理解了它的代码(最大的困难是理解散布在各处的 GAS 指令)。无论如何,我向前迈了一步,并添加了printf("%d\n", i);,看看我是否能理解这一点,然后代码突然变得更加混乱。

    .file   "helloworld.c"
    .text
    .section    .rodata.str1.1,"aMS",@progbits,1
.LC0:
    .string "%d\n"
    .section    .text.startup,"ax",@progbits
    .p2align 4
    .globl  main
    .type   main, @function
main:
    subq    $8, %rsp
    xorl    %edx, %edx
    leaq    .LC0(%rip), %rsi
    xorl    %eax, %eax
    movl    $1, %edi
    call    __printf_chk@PLT
    xorl    %eax, %eax
    addq    $8, %rsp
    ret
    .size   main, .-main
    .ident  "GCC: (Gentoo 10.2.0-r3 p4) 10.2.0"
    .section    .note.GNU-stack,"",@progbits

我正在用gcc -S -O3 -fno-asynchronous-unwind-tables 编译它以删除.cfi 指令,但是-O2 产生相同的代码,所以-O3 是多余的。我对汇编的理解非常有限,但在我看来,编译器在这里做了很多不必要的事情。为什么要在rsp 上减去然后加上 8?为什么它执行这么多xors?只有一个变量。 movl $1, %edi 在做什么?我想也许编译器在尝试优化时做了一些愚蠢的事情,但正如我所说,它没有优化超出-O2,即使在-O1 上它也执行所有这些操作。老实说,我根本不了解未优化的代码,所以我认为它效率低下。

唯一想到的是对printf 的调用使用了这些寄存器,否则它们没有被使用并且没有任何用处。真的是这样吗?如果有,怎么可能知道?

提前致谢。我现在正在阅读一本关于编译器设计的书,并且我已经阅读了大部分 GCC 手册(我阅读了关于优化的整章)并且我已经阅读了一些介绍性的 x86_64 asm 材料,如果有人可以指出我其他的学习更多资源(除了 Intel x86 手册)我也将不胜感激。

【问题讨论】:

  • 这个程序集似乎与您应该编译的代码不匹配。如果您调用printf(1, "%d\n"),这就是我希望看到的,并且从godbolt 判断,这实际上非常接近您调用printf(1, "%d\n") 时得到的程序集。
  • @Aplet123 这是代码:#include <stdio.h> main() { int i = 0; printf("%d\n", i); }
  • 查看stackoverflow.com/tags/x86/info 以获得许多有用的链接。 ABI 可能特别受关注。

标签: c assembly x86-64


【解决方案1】:

对于您正在使用的编译器,它看起来像 printf(...) 映射到 __printf_chk(1, ...)

要了解代码,you need to understand the parameter passing conventions for the platform (part of the ABI)。一旦知道在 %rdi、%rsi、%rdx、%rcx 中最多传递了 4 个参数,您就可以了解大部分情况了:

subq    $8, %rsp             ; allocate 8 bytes of stack
xorl    %edx, %edx           ; i = 0 ; put it in the 3rd parameter for __printf_chk
leaq    .LC0(%rip), %rsi     ; 2nd parameter for __printf_chk.  The: "%d\n"
xorl    %eax, %eax           ; 0 variadic fp params
movl    $1, %edi             ; 1st parameter for __printf_chk
call    __printf_chk@PLT     ; call the runtime loader wrapper for __printf_chk
xorl    %eax, %eax           ; return 0 from main
addq    $8, %rsp             ; deallocate 8 bytes of stack.
ret

Nate 在 cmets 中指出,ABI 中的第 3.5.7 节解释了 %eax = 0(无浮点可变参数。)

【讨论】:

  • 我明白了!阅读 Linux x86 调用约定在我的阅读清单上,我想它比我意识到的更重要。回复:__printf_chk,编译器直接在 -O0 调用 printf(),但在打开优化时使用 __printf_chk。我没有想到另一个 0 可能是返回值,我的 main() 函数有一个隐式返回值,在 main() { int i = 0; } 的情况下没有返回值,只有一个 return 语句。现在它是有道理的。感谢您的帮助。
  • 对于像printf这样的可变参数函数,需要将%al设置为用于传递浮点参数的向量(%xmm)寄存器的数量。参见ABI 的3.5.7 这里没有传递浮点参数,所以%al 必须为零。编译器将所有%eax 清零(实际上将所有%rax 清零),因为为什么不(可能是为了避免一些部分寄存器依赖性)。
  • 另外,sub $8, %rsp 本身并不是真正分配 8 个字节的堆栈(该函数不使用该空间做任何事情),而是确保 stack alignment 按要求ABI(见 3.2.2)。
  • @NateEldredge,不需要 8 字节堆栈对齐吗?如果是这样,减去另外 8 个字节对对齐没有任何作用。 __printf_chk 没有隐式使用这个堆栈存储(如果它调用任何东西,返回地址不是保存在调用者分配的堆栈存储中吗?)
  • 不,需要的对齐是 16 个字节。在调用 main 之前,堆栈已对齐到 16 个字节,将控制权转移到 maincall 指令推送了 8 个字节,因此我们必须再减去 8 以返回到对 @ 的调用的 16 字节对齐987654339@。返回地址被调用指令压入栈槽下面 rsp。 sub $8, %rsp“分配”的 8 个字节实际上从未被任何人读取或写入。如果您愿意,请单步执行代码,您会看到。
猜你喜欢
  • 2018-05-13
  • 2011-10-12
  • 2013-02-23
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2020-09-03
相关资源
最近更新 更多