为什么printf使用float和integer格式说明符打印随机值
我在64位机器上写了一个简单的代码
int main() { printf("%d", 2.443); }
所以,这就是编译器的行为方式。 它将识别第二个参数为double,因此它将在堆栈上推送8个字节,或者可能只是在调用之间使用寄存器来访问变量。 %d
需要一个4字节的整数值,因此它会输出一些垃圾值。
有趣的是,每次执行此程序时,打印的值都会发生变化。 那么发生了什么? 我希望它每次打印相同的垃圾值,而不是每次都不同。
当然,它是未定义的行为,传递与格式不对应的参数,因此语言无法告诉我们输出更改的原因。 我们必须查看实现,它生成的代码,以及可能的操作系统。
我的设置与你的不同,
Linux 3.1.10-1.16-desktop x86_64 GNU / Linux(openSuSE 12.1)
用gcc-4.6.2。 但它足够相似,怀疑相同的机制是合理的。
查看生成的程序集( -O3
,出于习惯),相关部分( main
)是
.cfi_startproc subq $8, %rsp # adjust stack pointer .cfi_def_cfa_offset 16 movl $.LC1, %edi # move format string to edi movl $1, %eax # move 1 to eax, seems to be the number of double arguments movsd .LC0(%rip), %xmm0 # move the double to the floating point register call printf xorl %eax, %eax # clear eax (return 0) addq $8, %rsp # adjust stack pointer .cfi_def_cfa_offset 8 ret # return
如果不是double
,我传递一个int
,没有太大的变化,但是那个显着
movl $47, %esi # move int to esi movl $.LC0, %edi # format string xorl %eax, %eax # clear eax call printf
我已经查看了生成的代码,其中包含许多类型的变体和传递给printf
的参数计数,并且一致地说,第一个double
(或提升的float
)参数在xmmN
, N = 0, 1, 2
和整数( int
)中传递, char
, long
,无论签名)都在esi
, edx
, ecx
, r8d
, r9d
,然后是堆栈。
因此,我冒昧地猜测printf
在esi
查找已宣布的int
,并打印出任何正常情况。
当主要的东西没有被移动时, esi
的内容是否以任何方式可预测,以及它们可能意味着什么,我不知道。
这个答案试图解决一些变异的来源。 这是Daniel Fischer的回答和一些评论的后续行动。
由于我不使用Linux,我无法给出明确的答案。 对于稍后在大型应用程序中的printf
,会有无数的潜在变化来源。 这个早期的小应用程序,应该只有少数几个。
地址空间布局随机化(ASLR)是一个:操作系统故意随机重新排列一些内存,以防止恶意软件知道要使用的地址。 我不知道Linux 3.4.4-2是否有此function。
另一个是环境变量。 您的shell环境变量将复制到它生成的进程中(并可通过getenv
例程访问)。 其中一些可能会自动更改,因此它们的值会略有不同。 这不太可能直接影响printf
在尝试使用缺少的整数参数时看到的内容,但可能存在级联效应。
可能有一个共享库加载器在调用main
之前或调用printf
之前运行。 例如,如果printf
位于共享库中,而不是内置到可执行文件中,那么对printf
的调用实际上可能会导致调用调用加载器的存根例程。 加载器查找共享库,找到包含printf
的模块,将该模块加载到进程的地址空间,更改存根以便将来直接调用新加载的printf
(而不是调用加载器),并调用printf
。 可以想象,这可能是一个相当广泛的过程,除其他外,涉及在磁盘上查找和读取文件(所有目录都要到达共享库和共享库)。 可以想象,系统上的某些缓存或文件操作会导致加载程序中的行为略有不同。
到目前为止,我赞成ASLR最有可能成为上述人选。 后两者可能相当稳定; 所涉及的价值通常会偶尔变化,而不是经常变化。 ASLR每次都会改变,只需在寄存器中留一个地址就足以解释printf
行为。
这是一个实验:在初始printf
,使用以下代码插入另一个printf
:
printf("%d\n", 2.443); int a; printf("%p\n", (void *) &a);
第二个printf打印可能在堆栈中的a
的地址。 运行程序两到三次,并计算第一个printf
打印的值与第二个printf
打印的值之间的差值。 (第二个printf
可能以hex打印,因此将第一个更改为“%x”以使其成为hex也可能很方便。)如果第二个printf
打印的值因运行而异,那么您的程序是体验ASLR。 如果值在运行之间变化但它们之间的差异保持不变,则printf
在第一个printf
发生的值是进程中的某个地址,该地址在程序初始化后仍然存在。
如果更改的地址但差异不会保持不变,您可以尝试更改int a;
to static int a;
看看将第一个值与地址空间的不同部分进行比较是否会产生更好的结果。
当然,这对编写可靠的程序都没有用; 关于程序加载和初始化如何工作,它只是教育性的。