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

Gcc 为什么在我重击%rdi并调用printf之后,它仍然具有正确的值?,gcc,assembly,x86,x86-64,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",

我很难理解通过我正在编写的编译器生成的以下汇编代码的(错误)输出

这是我正在编译的伪代码:

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()
的参数,但随后对
sidefx()的两个调用
将覆盖
%rdi
%rsi
的值,以便调用
printf
。为了解决这个问题,我需要将返回值存储在堆栈中的某个位置,或者可能存储在被调用方保存的寄存器中

为什么最后的
printf
返回
one:1,two:2147483641,three:3

?由于随后的
sidefx
调用,打印的第一个数字不也应该像第二个数字一样被损坏吗?

您没有指定正在使用哪个x86-64 ABI,但是从您对arg传递使用的
%rdi
/
%rsi
来看,我假设您的目标是SysV ABI(除了windows之外的所有东西)。查看wiki以获取指向文档和内容的链接


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

没错。gcc更喜欢使用保留调用的regs,因为这样在调用之间推送或弹出时就不必处理堆栈对齐

为什么最终的printf返回
1:1,2:2147483641,3:3
?打印的第一个号码不也应该像第二个号码由于后续的
sidefx
调用而发生的那样被弄乱吗

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

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


您的C与您的实现不匹配在asm中,
全局_a
是8个字节,但在C中,您将其视为4个字节的整数(使用
%d
打印,而不是
%ld
)。你的C根本没有声明它。我打算在一个声明中对问题进行编辑,但您应该自己解决歧义(在
long global\u a=0;
int global\u a=0;
之间)。AMD64 SysV ABI指定
long
为8字节。不过,无论何时编写便携式C,都要使用
int64\t
。在与asm进行交互操作时,编写
int64\t
没有什么害处,即使您碰巧知道正在使用的ABI中
short
int
long
的大小


避免使用
输入
指令
,除非您只关心代码大小而不关心速度<代码>离开是可以的,可能比
mov%rbp,%rsp
/
pop%rbp
慢,但通常你只需要
pop%rbp
,因为你要么没有修改
%rsp
,要么你需要用
添加$something恢复rsp,%rsp
,然后弹出在
%rbp
之后保存的其他一些寄存器

使用异或%eax,%eax对64位寄存器进行归零(2字节)


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

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

不管怎样,这是您在gcc 5.3-O3的
上的代码

threeargs
使用
jmp
跟踪调用printf,而不是call/ret

main
中的显著区别在于正确保存
sidefx
中的返回值。请注意,main中的
a=0
是不需要的,因为它已经在BSS中初始化为零,但是使用
-fwhole程序
,gcc无法对其进行优化。(构造函数可以在运行
main
之前修改
A
,也可以在链接具有不同初始值设定项的
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
添加一个并存储在不同的dest中。您的版本将立即
1
移动到寄存器中,这很愚蠢。使用立即数操作数和
lea
。CPU设计人员已经支付了x86税(额外的设计复杂性以支持CISC i)