我在 64 位机器上写了一个简单的代码
int main() {
printf("%d", 2.443);
}
所以,这就是编译器的行为方式。它会将第二个参数识别为双精度,因此它将在堆栈上压入 8 个字节,或者可能只是在调用之间使用寄存器来访问变量。%d
需要一个 4 字节的整数值,因此它会打印一些垃圾值。
有趣的是,每次我执行这个程序时,打印的值都会发生变化。那么发生了什么?我希望它每次都打印相同的垃圾值,而不是每次都打印不同的垃圾值。
我在 64 位机器上写了一个简单的代码
int main() {
printf("%d", 2.443);
}
所以,这就是编译器的行为方式。它会将第二个参数识别为双精度,因此它将在堆栈上压入 8 个字节,或者可能只是在调用之间使用寄存器来访问变量。%d
需要一个 4 字节的整数值,因此它会打印一些垃圾值。
有趣的是,每次我执行这个程序时,打印的值都会发生变化。那么发生了什么?我希望它每次都打印相同的垃圾值,而不是每次都打印不同的垃圾值。
当然,传递与格式不对应的参数是未定义的行为,因此语言无法告诉我们为什么输出会发生变化。我们必须查看实现、它产生的代码,以及可能的操作系统。
我的设置和你的不一样
Linux 3.1.10-1.16-桌面 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
寻找 in 中宣布int
的esi
内容,并打印发生的任何事情。
esi
当没有任何东西移入时,的内容是否可以以任何方式预测main
,以及它们可能意味着什么,我不知道。
这个答案试图解决一些变化的来源。这是 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;
查看将第一个值与地址空间的不同部分进行比较是否会产生更好的结果。
当然,这些对于编写可靠的程序都没有用。它只是关于程序加载和初始化如何工作的教育。