为什么%rdi仍然具有正确的价值,在我破坏它并调用printf之后?

时间:2016-04-05 18:32:55

标签: gcc assembly x86 x86-64

我无法理解通过我编写的编译器生成的以下汇编代码的(错误)输出。

这是我编译的伪代码:

int sidefx ( ) {
     a = a + 1;
     printf("side effect: a is %d\n", a);
     return a;
}

void threeargs ( int one, int two, int three ) {
     printf("three arguments. one: %d, two: %d, three: %d\n", one, two, three);
}

void main ( ) {
     a = 0;
     threeargs(sidefx(), sidefx(), sidefx());
}

这是我生成的汇编代码:

.section .rodata
.comm global_a, 8, 8
.string0:
.string "a is %d\n"
.string1:
.string "one: %d, two: %d, three: %d\n"
.globl main

sidefx:                /* Function sidefx() */
enter $(8*0),$0        /* Enter a new stack frame */
movq global_a, %r10    /* Store the value in .global_a in %r10 */
movq $1, %r11          /* Store immediate 1 into %r11 */
addq %r10,%r11         /* Add %r10 and %r11 */
movq %r11, global_a    /* Store the result in .global_a */
movq global_a, %rsi    /* Put the value of .global_a into second paramater register */
movq $.string0, %rdi   /* Move .string0 to first parameter register */
movq $0, %rax        
call printf            /* Call printf */
movq global_a, %rax    /* Return the new value of .global_a */
leave                  /* Restore old %rsp, %rbp values */
ret                    /* Pop the return address */

threeargs:             /* Function threeargs() */
enter $(8*0),$0        /* Enter a new stack frame */
movq %rdx, %rcx        /* Move 3rd parameter register value into 4th parameter register */
movq %rsi, %rdx        /* move 2nd parameter register value into 3th parameter register */ 
movq %rdi, %rsi        /* Move 1st parameter register value into 2nd parameter register */
movq $.string1, %rdi   /* Move .string1 to 1st parameter register */
movq $0, %rax
call printf            /* call printf */
leave                  /* Restore old %rsp, %rbp values */
ret                    /* Pop the return address */

main:
enter $(8*0),$0        /* Enter a new stack frame */
movq $0, global_a      /* Set .global_a to 0 */
movq $0, %rax          
call sidefx            /* Call sidefx() */
movq %rax,%rdi         /* Store value in %rdi, our first parameter register */
movq $0, %rax
call sidefx            /* Call sidefx() */
movq %rax,%rsi         /* Store value in %rsi, our second parameter register */
movq $0, %rax
call sidefx            /* Call sidefx() */
movq %rax,%rdx         /* Store value in %rdx, our third parameter register */
movq $0, %rax
call threeargs         /* Call threeargs() */

main_return:
leave
ret

现在,我不明白。编译时(gcc file.s -o code && ./code)的程序输出如下:

dmlittle$ gcc file.s -o code && ./code 
a is 1
a is 2
a is 3
one: 1, two: 2147483641, three: 3

汇编代码的问题在于我将sidefx()调用的值最终作为threeargs()的参数存储到函数寄存器中,但是后续调用了{2} { {1}}会覆盖sidefx()%rdi的值,以便调用%rsi。为了解决这个问题,我需要将返回值存储在堆栈中的某个位置,或者存储在被调用者保存的寄存器中。

为什么最终printf返回printf?打印的第一个号码是否也应该像第二个号码那样由于后续的one: 1, two: 2147483641, three: 3 来电而被破坏?

1 个答案:

答案 0 :(得分:4)

您没有指定您使用的是哪个x86-64 ABI,但是通过使用%rdi / %rsi进行arg传递,我假设您的目标是SysV ABI(一切除了窗户)。有关文档和内容的链接,请参阅 wiki。

  

...从前两个sidefx()调用中回复返回值...为了解决这个问题,我需要将返回值存储在堆栈中的某个位置,或者存储在被调用者保存的寄存器中。

这是对的。 gcc更喜欢使用调用保留的regs,因为当你在调用之间推送或弹出时,你不必摆弄堆栈对齐。

  

为什么最终的printf返回one: 1, two: 2147483641, three: 3?打印的第一个数字是否也应该像第二个数字那样由于后续的sidefx调用而被破坏?

当您致电%rdi=1threeargs() 只是巧合。如果您单步执行代码,您可能会发现在printf返回时恰好具有该值。它不是来自保存/恢复,因为在调用printf之前,movq $.string1, %rdi会破坏原始值。恰好1在寄存器中找到了常见的东西。

最佳猜测:1write(2)系统调用的文件描述符arg ,这是printf在返回之前需要做的最后一件事。 (因为stdout是行缓冲的)。

您的C与您的实施不符。 在asm中,global_a为8个字节,但在C中,您将其视为4字节整数(使用%d打印,而不是%ld)。你的C根本没有声明它。我打算在问题的声明中进行编辑,但您应该自己解决歧义(long global_a = 0;int global_a = 0;之间)。 AMD64 SysV ABI指定long为8个字节。但是,无论何时编写可移植C,都要使用int64_t。在与asm进行互操作时写int64_t没有任何害处,即使您确实知道您正在使用的ABI中shortintlong的大小。

避免enter指令,除非关心代码大小,而不是速度。 It's horribly slowleave没问题,可能比mov %rbp, %rsp / pop %rbp慢,但通常你只需要pop %rbp,因为你要么没有修改%rsp,要么你需要在弹出add $something, %rsp之后保存的其他寄存器之前,请先使用%rbp恢复rsp。

使用xor %eax,%eax(2字节)has many advantages beyond code-size over mov $0, %rax (7 bytes: mov $sign-extended-imm32, r64)将64位寄存器归零。

将您的代码与编译器输出进行比较:gcc -fverbose-asm -O3 -fno-inline实际上会从您的C生成代码;您只需要a的声明,并使main返回int,它就可以编译为C11。当然,它主要使用32位操作数大小,因为你使用了int,但数据移动(哪个属于哪个寄存器)是相同的。

此外,未指定参数列表的评估顺序,因此threeargs(sidefx(), sidefx(), sidefx())未定义的行为。您有多个具有副作用的表达式,没有序列点将它们分开。我想这就是为什么你把它称为伪代码,而不是C,但这是表达你的意思的糟糕方式。

无论如何,这是来自gcc 5.3 -O3的Godbolt Compiler Explorer 上的代码

threeargs使用jmp尾部调用printf,而不是call / ret。

main的重大差异就是正确保存sidefx的返回值。请注意,不需要main中的a=0,因为它已经在BSS中初始化为零,但是对于-fwhole-program,gcc无法对其进行优化。 (构造函数可以在a运行之前修改main,或者在链接之后可以使用具有不同初始值设定项的a的不同定义。)

sidefx的实施明显比你的实施更严格:

sidefx:
    subq    $8, %rsp        #     aligns the stack for another function call
    movl    a(%rip), %eax   # a, tmp94      # load `a`
    movl    $.LC0, %edi     #,              # the format string
    leal    1(%rax), %esi   #, D.2311       # esi = a+1
    xorl    %eax, %eax      #               # only needed because printf is a varargs function.  Your `main` is doing this unnecessarily.
    movl    %esi, a(%rip)   # D.2311, a     # store back to the global
    call    printf  #
    movl    a(%rip), %eax   # a,            # reload a
    addq    $8, %rsp        #,
    ret

IDK为什么gcc首先没有加载到%esi,而inc %esi而不是使用lea添加一个并存储在不同的目标中。您的版本立即将1移动到寄存器中,这很愚蠢。使用立即操作数和lea。 CPU设计人员已经支付了x86税(额外的设计复杂性以支持CISC指令集),确保通过充分利用lea和即时操作数来获得您的资金。

请注意,在调用printf之前,它不会存储/重新加载a 。您的版本不需要这样做。

另请注意,没有一个函数浪费指令制作堆栈帧。