我无法理解通过我编写的编译器生成的以下汇编代码的(错误)输出。
这是我编译的伪代码:
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
来电而被破坏?
答案 0 :(得分:4)
您没有指定您使用的是哪个x86-64 ABI,但是通过使用%rdi
/ %rsi
进行arg传递,我假设您的目标是SysV ABI(一切除了窗户)。有关文档和内容的链接,请参阅x86 wiki。
...从前两个sidefx()调用中回复返回值...为了解决这个问题,我需要将返回值存储在堆栈中的某个位置,或者存储在被调用者保存的寄存器中。
这是对的。 gcc更喜欢使用调用保留的regs,因为当你在调用之间推送或弹出时,你不必摆弄堆栈对齐。
为什么最终的printf返回
one: 1, two: 2147483641, three: 3
?打印的第一个数字是否也应该像第二个数字那样由于后续的sidefx
调用而被破坏?
当您致电%rdi=1
时threeargs()
只是巧合。如果您单步执行代码,您可能会发现在printf
返回时恰好具有该值。它不是来自保存/恢复,因为在调用printf之前,movq $.string1, %rdi
会破坏原始值。恰好1
在寄存器中找到了常见的东西。
最佳猜测:1
是write(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中short
,int
和long
的大小。
避免enter
指令,除非仅关心代码大小,而不是速度。 It's horribly slow。 leave
没问题,可能比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
。您的版本不需要这样做。
另请注意,没有一个函数浪费指令制作堆栈帧。