为什么GCC在printf调用之前插入看似不重要的指令?

为什么GCC在printf调用之前插入看似不重要的指令?,gcc,assembly,x86,Gcc,Assembly,X86,我试图自己学习x86,我决定剖析一个简单的c程序,看看GCC输出什么。计划如下: #include <stdio.h> int main() { printf("%s","Hello World"); return 0; } 现在,除了main下面的前两个之外,上面代码中的几乎所有内容对我来说都是有意义的。虽然这是我不用剥掉东西就能得到的 .LC0: .string "%s" .LC1: .string "Hello World" .text .

我试图自己学习x86,我决定剖析一个简单的c程序,看看GCC输出什么。计划如下:

#include <stdio.h>
int main() {
  printf("%s","Hello World");
  return 0;
}
现在,除了main下面的前两个之外,上面代码中的几乎所有内容对我来说都是有意义的。虽然这是我不用剥掉东西就能得到的

.LC0:
    .string "%s"

.LC1:
    .string "Hello World"
    .text

.globl main
    .type   main, @function

main:

.LFB0:
    pushq   %rbp        # push what was in base pointer onto stack
    movq    %rsp, %rbp  # move stack pointer to base pointer

  # prepare arguments for printf
    movl    $.LC0, %eax # put arg into %eax
    movl    $.LC1, %esi # put second arg into %esi
    movq    %rax, %rdi  # move value in %rax to %rdi ???? ( why not just put $.LCO into %rax directly )
    movl    $0, %eax    # clear out %eax ???? ( why do we need to clear it out )
    call    printf      
    movl    $0, %eax    # return 0
    leave
    ret

.LFE0:
    .size   main, .-main
    .ident  "GCC: (Ubuntu/Linaro 4.5.2-8ubuntu4) 4.5.2"
    .section    .note.GNU-stack,"",@progbits
我已经标记了两个说明????我不明白

第一条指令是将%rax中的内容移动到%rdi中,以准备printf调用。这一切都很好,只是我们刚刚将$.LC0(字符串“%s”)移动到了%eax中。这似乎没有必要,为什么我们不先将$.LC0移动到%rdi,而不是将其移动到%eax,然后再移动到%rdi


第二条指令是清除%eax,我理解它是函数的返回值。但是,如果函数将以任何方式对其进行重击,那么GCC为什么要清除它呢?

您是在查看优化的输出,还是未优化的输出(这基本上是将C代码转换为汇编程序的简单过程)?这会产生巨大的差异,因为优化器通常很擅长应用与您描述的相同类型的规则。

您是在查看优化的输出,还是未优化的输出(这基本上是将C代码转换为汇编代码的简单过程)?这会产生巨大的差异,因为优化器通常非常擅长应用与您描述的相同类型的规则。

因为GCC是一个编译器,而编译器是哑的


您可以使用-O2使GCC更智能。它开始使用优化技巧并减少冗余指令。

因为GCC是一个编译器,而编译器是哑的


您可以使用-O2使GCC更智能。它开始使用优化技巧并减少冗余指令。

一些经验法则:

  • 如果您关心的是高效的代码,那么就不要麻烦查看未优化的输出
  • 始终测量,不要假设您在汇编语言级别的“改进”会提高性能
  • 即使在优化的代码中,您也可能会看到一些看似不必要的指令,例如“xor%eax,%eax”,而在功能上不需要对寄存器进行填充。这些指令通过通知管道在该点之外不存在该寄存器的数据依赖性,起到了特殊的作用。在现代无序处理器中,内核的管道在当前EIP之前推测性地执行许多指令。以这种方式显式地切割数据依赖关系有助于推测机制,尤其可以提高紧循环中的性能

    在其他情况下,编译器显然可能会采取一种迂回的方法,而实际上它正试图将手头的工作与目标内核管道中可用的并行执行单元相匹配。并行调度的更多指令通常比序列化的更少指令完成得更快


    如果您真的想挤出性能的最后一点,请在代码块前后使用rdtsc指令来测量花费的时钟数。要小心一点,因为rdtsc并没有严格按照周围的指令进行订购,但实际上测量它对于1000时钟范围内的任何东西都非常准确。

    一些经验法则:

  • 如果您关心的是高效的代码,那么就不要麻烦查看未优化的输出
  • 始终测量,不要假设您在汇编语言级别的“改进”会提高性能
  • 即使在优化的代码中,您也可能会看到一些看似不必要的指令,例如“xor%eax,%eax”,而在功能上不需要对寄存器进行填充。这些指令通过通知管道在该点之外不存在该寄存器的数据依赖性,起到了特殊的作用。在现代无序处理器中,内核的管道在当前EIP之前推测性地执行许多指令。以这种方式显式地切割数据依赖关系有助于推测机制,尤其可以提高紧循环中的性能

    在其他情况下,编译器显然可能会采取一种迂回的方法,而实际上它正试图将手头的工作与目标内核管道中可用的并行执行单元相匹配。并行调度的更多指令通常比序列化的更少指令完成得更快

    如果您真的想挤出性能的最后一点,请在代码块前后使用rdtsc指令来测量花费的时钟数。要小心一点,因为rdtsc并没有严格按照周围的指示进行订购,但在实际测量中,它对于1000时钟范围内的任何东西都非常精确

    第一条指令是将
    %rax
    中的内容移动到
    %rdi
    中,以准备
    printf
    调用。除了我们刚刚将
    $.LC0
    (即字符串
    %s“
    )移动到
    %eax
    )之外,一切都很好。这似乎没有必要,为什么我们不先将
    $.LC0
    移动到
    %rdi
    中,而不是将其移动到
    %eax
    中,然后再移动到
    %rdi

    这可能是因为编译时没有优化。当我在Mac OS X v10.6.8上使用GCC 4.2.1编译您的示例时,我得到以下输出:

    .globl _main
    _main:
    LFB3:
        pushq   %rbp
    LCFI0:
        movq    %rsp, %rbp
    LCFI1:
        leaq    LC0(%rip), %rsi
        leaq    LC1(%rip), %rdi
        movl    $0, %eax
        call    _printf
        movl    $0, %eax
        leave
        ret
    
    如您所见,参数直接存储在
    %rsi
    %rdi

    第二条指令是清除
    %eax
    ,我理解它是函数的返回值。但是,如果函数将以任何方式对其进行重击,那么GCC为什么要清除它呢

    因为x86_64 ABI指定,如果函数采用变量参数,则
    AL
    (属于
    %eax
    )将保存用于该函数调用参数的向量寄存器的数量。因为调用
    printf()
    时没有指定浮点参数,所以没有使用向量寄存器,所以.globl _main _main: LFB3: pushq %rbp LCFI0: movq %rsp, %rbp LCFI1: leaq LC0(%rip), %rsi leaq LC1(%rip), %rdi movl $0, %eax call _printf movl $0, %eax leave ret