这是我使用
puts()
的 C 程序:
#include <stdio.h>
int main(void){
puts("testing");
}
使用
gcc -S -o sample.s sample.c
将其编译成Assembly后,这就是我得到的:
.file "sample.c"
.section .rodata
.LC0:
.string "testing"
.text
.globl main
.type main, @function
main:
pushl %ebp
movl %esp, %ebp
andl $-16, %esp
subl $16, %esp
movl $.LC0, (%esp)
call puts
leave
ret
.size main, .-main
.ident "GCC: (GNU) 4.4.5 20110214 (Red Hat 4.4.5-6)"
.section .note.GNU-stack,"",@progbits
我以同样的方式重复了一遍,但这次我使用
printf()
而不是 puts()
,这就是我得到的:
.file "sample.c"
.section .rodata
.LC0:
.string "testing"
.text
.globl main
.type main, @function
main:
pushl %ebp
movl %esp, %ebp
andl $-16, %esp
subl $16, %esp
movl $.LC0, %eax //this is the difference
movl %eax, (%esp)
call printf
leave
ret
.size main, .-main
.ident "GCC: (GNU) 4.4.5 20110214 (Red Hat 4.4.5-6)"
.section .note.GNU-stack,"",@progbits
这是我不明白的地方:在
printf()
版本中,他们mov $.LC0
到%eax
,然后是mov %eax
到(%esp)
,但在puts()
版本中,他们mov %.LC0
直接到(%esp)
.在汇编级别,这两个函数之间的最大区别在于
puts()
函数仅采用一个参数(指向要显示的字符串的指针),而 printf()
函数将采用一个参数(指向要显示的字符串的指针)。格式字符串),然后是堆栈中任意数量的参数(printf()
是一个可变参数函数)。
请注意,绝对不会检查参数的数量,它仅取决于在格式字符串中遇到字符
%
的次数。例如,这种特殊性用于格式字符串格式错误利用方法,以交互方式探索进程堆栈的内容。
所以,基本上,区别在于
puts()
只有一个参数,而 printf()
是一个可变参数函数。
如果你想更好地理解这个区别,请尝试编译:
#include <stdio.h>
int main(void) {
printf("testing %d", 10);
}
(我发布此内容是为了回答帖子中提出的有关 arg-passing code-gen 的实际问题。关于
printf
和 puts
之间的差异,人们可能仅根据标题就想知道,而不是更一般的问题,如果这在他们的搜索中出现。)
没有任何原因,这只是错过了优化,并且您在禁用优化的情况下编译(默认为
-O0
,编译快但不好),所以GCC甚至没有尝试。 只是 GCC 内部的一个怪癖。
此外,我无法使用 GCC 4.4.7 或我尝试过的任何其他版本重现
mov
-to-EAX https://godbolt.org/z/d511GWs59,无论有或没有 -maccumulate-outgoing-args
来制作较新的 GCC 仍然为参数保留空间并使用 mov
存储它们,而不是使用 push
。 (这是默认设置,但在 Pentium M 和更新版本变得普遍后发生了变化,因为它们有一个“堆栈引擎”,可以使推送/弹出单微操作。)int printf(const char*, ...)
是可变参数,因此它在 GCC 程序逻辑的内部表示(GIMPLE 然后 RTL)中的表示方式可能与调用具有固定数量参数的函数不同?
此外,如果要打印的文本是编译时常量并以换行符结尾(
printf
隐式附加,与 puts
不同,GCC 会尝试将 puts
优化为 fputs
。)GCC 甚至会这样做-O0
,与其他编译器不同,除非您使用-fno-builtin-printf
。 因此这可能会影响 GCC 内部的表示方式。