为什么printf使用float和integer格式说明符打印随机值

时间:2012-10-12 02:30:03

标签: c floating-point x86-64

我在64位机器上写了一个简单的代码

int main() {
    printf("%d", 2.443);
}

所以,这就是编译器的行为方式。它将第二个参数标识为double,因此它将在堆栈上推送8个字节,或者可能只是在调用之间使用寄存器来访问变量。 %d期望一个4字节的整数值,因此它会输出一些垃圾值。

有趣的是,每次执行此程序时,打印的值都会更改。那么发生了什么?我希望它每次打印相同的垃圾值,而不是每次都不同。

2 个答案:

答案 0 :(得分:6)

当然,未定义的行为是传递与格式不对应的参数,因此语言无法告诉我们输出更改的原因。我们必须查看实现,它生成的代码,以及可能的操作系统。

我的设置与您的不同,

  

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)参数xmmNN = 0, 1, 2和整数(intcharlong,无论签名如何)均在esi中传递,{{1 }},edxecxr8d,然后是堆栈。

所以我冒昧地猜测r9d寻找printf中宣布的int,然后打印那里发生的任何事情。

esi中没有移动任何内容时esi的内容是否可以预测,以及它们可能表示什么,我不知道。

答案 1 :(得分:0)

这个答案试图解决一些变异的来源。这是Daniel Fischer的回答以及对它的一些评论的后续行动。

由于我不使用Linux,我无法给出明确的答案。对于稍后在大型应用程序中的printf,会有无数的潜在变化来源。这在一个小应用程序的早期,应该只有少数几个。

地址空间布局随机化(ASLR)之一:操作系统故意随机重新安排一些内存,以防止恶意软件知道要使用的地址。我不知道Linux 3.4.4-2是否有这个。

另一个是环境变量。您的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可能以十六进制打印,因此将第一个更改为“%x”以使其成为十六进制可能更方便。)如果第二个printf打印的值不同于运行运行,然后您的程序正在经历ASLR。如果值在运行之间变化但它们之间的差异保持不变,则printf在第一个printf中发生的值是您的进程中的某个地址,该地址在程序初始化后留下来

如果a的地址发生变化但差异不会保持不变,您可以尝试将int a;更改为static int a;,看看是否将第一个值与地址空间的不同部分进行比较产生更好的结果。

当然,这对编写可靠的程序都没有用;关于程序加载和初始化的工作方式,它只是教育性的。