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)