图文理解printf遇到float类型提升且类型不匹配时产生的地址错位
看下面的代码:
#include <stdio.h>int main(){ int x = 5; float y = 3.1f; printf("%d\n%f\n%f\n%d\n",x,x,y,y); getchar(); return 0;}/*32位平台的输出:5-2.000000-2.0000001074318540*/
为什么会有这样的诡异输出?首先需要了解以下知识点:
1 函数调用约定
C默认的调用方式是__cdecl的调用方式,这个调用方式由主调函数负责堆栈管理(包括堆栈平衡),该种调用约定支持变长参数(数量不确定的函数参数)。主调函数主调变参列表的参数数量。另外,参数由右至左压入栈帧。其它函数调用约定由被调函数负责堆栈管理
关于函数调用约定和变参函数的细节,请参考:
C/C++|图文深入理解函数调用的5种约定
C|图文深入理解实现变参函数的4个宏和栈帧机制
2 函数栈帧按字长(32位平台4字节)对齐,数据类型不够4个字节的按4字节压栈,多余的字节填充,超过4个字节的double使用两个字节。
3 浮点数的数据处理使用一个由8个浮点寄存器循环构造的浮点栈来处理。浮点数的处理会有一个精度的问题。传参时,当有float和double之间的隐式类型转换时,会用到浮点栈。通常,float类型在数据处理时(非存储时)会提升为double类型。
4 数据存储大小端的问题,intel CPU通常是小端存储。
5 变参函数以第一个参数为基准,按%后面字符指示的数据类型的长度进行偏移来访问其它参数。
现在我们再来分析上面的三行代码:
int x = 5; float y = 3.1f; printf("%d\n%f\n%f\n%d\n",x,x,y,y);
看下面的汇编代码,先是压入局部变量:
4: int x = 5;00411188 mov dword ptr [ebp-4],55: float y = 3.1f;0041118F mov dword ptr [ebp-8],40466666h
然后要压入参数:
6: printf("%d\n%f\n%f\n%d\n",x,x,y,y);00411196 fld dword ptr [ebp-8]00411199 sub esp,80041119C fstp qword ptr [esp]0041119F fld dword ptr [ebp-8]004111A2 sub esp,8004111A5 fstp qword ptr [esp]004111A8 mov eax,dword ptr [ebp-4]004111AB push eax004111AC mov ecx,dword ptr [ebp-4]004111AF push ecx004111B0 push offset string "%d\n%f\n%f\n%d\n" (0042701c)
float要提升到double,用到了浮点栈,压入栈帧的不是4字节的float,而是转换后的8字节的double。
变参函数的机制首先会让一个指针基于第1个参数指向第2个参数,由第1个参数(格式化字符串,其“%”后的字符指明了数据类型)来控制其它参数的相对偏移位置(地址),并控制指针的强制类型转换和移动:
上面浮点数的在浮点栈的处理时有精点丢失的问题,在printf显示时也会有精度丢失的问题,上面因为类型的不匹配形成了指针偏移时的错位,结合到一起,形成了上述诡异的输出结果。
这也是变参函数容易出错的问题所在。
-End-