一个 printf 引发的基础复习
先看一下引发我追究一下 printf 和栈桢等相关知识的一段简单的程序:
1 |
|
初看时,想当然了一下觉得输出就是1 1.00
,后来编译出来运行一下,屏幕上却赫然是-1717986918 1.60
。
在脑中干想了良久,其时的疑惑主要有两点:
1.6 转换为整形怎么就变成了负数。
1 转换为浮点数怎么就变成了 1.60。
现在看来当时的理解中存在着一个很大的误区,就是觉得 printf 是将参数根据格式化字符串进行强制类型转换之后再进行输出的,即编译器会自动将程序变换成如下模样:
1 |
|
但是第一段程序的输出已经打脸了,那么想想办法找找合理的解释。
分析
面对这类问题,现象诡异程序简单,能想到的最有效的方法之一就是看汇编。
使用g++ -S
编译出第一段程序的汇编如下:
1 | .file "demo.cpp" |
第一个 printf 结果的解释
一眼望去,有没有发现一个熟悉的数?没错,我们程序的第一个输出 -1717986918 赫然在目。由此产生的猜想:
LC0 对应的两个。long 合起来是 double 类型的 8.0/5,而对其低位 4 字节进行截取后对应的整数为 -1717986918。
来把相关的数转换成二进制验证一下(IEEE 浮点数表示法相关知识见附:IEEE 754 浮点数表示法):
-1717986918 转换成十六进制为 -0x66666666,对应的二进制为:
1 | 1110 0110 0110 0110 0110 0110 0110 |
因为负数在内存中使用补码存储,故将如上二进制转换为补码才是它在内存中的样子:
1 | 1001 1001 1001 1001 1001 1001 1010 |
1073322393 转换成十六进制为 0x3ff99999,对应的二进制为:
1 | 0011 1111 1111 1001 1001 1001 1001 |
将这两个数合起来,1073322393 作为高位就是:
1 | 0011 1111 1111 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1010 |
转换成浮点数恰恰就是 1.6000000000000001,可以认为与 8.0/5 的结果相符。所以第一个 printf 输出结果的推论:
给 printf 传递的是参数的原始类型,而不是根据格式化字符串进行强制转换后的类型。
比如
printf("%d ", 8.0/5);
就会传 double 类型的 8.0/5,而不是根据 %d 强制转换成整型后再传参。printf 在根据格式化字符串组成输出的时候,会直接在对应参数的起始地址读取一个格式指定的类型出来。
比如
printf("%d ", 8.0/5);
就会在 double 类型的 8.0/5 的位置读取一个整型数出来,而小端模式下是高位高地址,低位低地址,所以这里是将 double 的低位 4 字节按 int 类型读取。1
2
3
4
5+--------------+
| double low | --> 把低位 4 字节当作 int 读取
+--------------+
| double high |
+--------------+
第二次 printf 结果的解释
在上面的汇编代码中对第二次 printf 的调用部分如下:
1 | movl $1, 4(%esp) |
可以看到传参确实传的整数 1 进去的,但是输出就变成了 1.60,结合我们对第一个输出的推论,则是会在整型 1 的位置读取一个 double 类型的数,并将内存中的整型 1 作为 double 的低位部分。为什么这里偏偏这么巧会是 1.60 而不是其它的什么值呢?结合上一次调用 printf 时传的参是 8.0/5 的情况,猜想:
受上一次调用后栈上残留数据的影响。
即:
1 | +--------------+ |
于是将第一次调用的传参修改一下将残留数据变化一下,即:
1 |
|
果然如预料第二个 printf 的输出变成了 1.80。这又一次印证了对第一个输出分析后的两个结论。来复习一下基础,引自《深入理解计算机系统》里的一段话:
假设过程 P(调用者)调用过程 Q(被调用者),则 Q 的参数放在 P 的栈帧中。
即 printf 的参数是放在 main 函数的栈帧中的,那么两次调用call _printf
前的堆栈情况应该是这样的:
1 | +-------------+ +-------------+ |
这里面补充的关键知识点:
- 被调用函数的参数存放在调用函数的栈帧中。
IEEE-754
1 | +---+-----+----------+ |
S:符号位
Exp:指数偏差
Mantissa:尾数
单精度(32 位)
S:1 位
Exp:8 位,二进制科学计数法中的指数加 127(2^(8-1)-1)
Mantissa:23 位,二进制科学计数法中的小数部分
双精度(64 位)
S:1 位
Exp:11 位,二进制科学计数法中的指数加 1023(2^(11-1)-1)
Mantissa:52 位,二进制科学计数法中的小数部分