------------------------------------------
转载请注明出处:http://lullaby2005.cublog.cn/
------------------------------------------
昨天下午luke让我试下linux上的sin结果,我顺手在linux上试了下这个例子:
[root@localhost home]# cat a.c
#include
#include
void foo (void)
{
printf("f = %f\n", 10 / 3);
}
int main ()
{
foo();
return 0;
}
[root@localhost home]# gcc -o a.out a.c
[root@localhost home]# objdump -D a.out > a.out.objdump
[root@localhost home]#
[root@localhost home]# ./a.out
f = -1.995216
[root@localhost home]# ./a.out
f = -1.994850
[root@localhost home]# ./a.out
f = -1.998390
[root@localhost home]# ./a.out
f = -1.994728
[root@localhost home]# ./a.out
f = -1.998268
[root@localhost home]# ./a.out
f = -1.994362
[root@localhost home]#
每次运行的结果都不一样。why?
分析:
反汇编并查看:
[root@localhost home]# objdump -D a.out > a.out.objdump
08048328 :
8048328: 55 push %ebp
8048329: 89 e5 mov %esp,%ebp
804832b: 83 ec 08 sub $0x8,%esp
804832e: 83 ec 08 sub $0x8,%esp
8048331: 6a 03 push $0x3
8048333: 68 0c 84 04 08 push $0x804840c
8048338: e8 2b ff ff ff call 8048268 <_init+0x38>
804833d: 83 c4 10 add $0x10,%esp
8048340: c9 leave
8048341: c3 ret
注意这句代码: push $0x3
很明显,compiler已经算出了常数运算"10/3",并截断为整型,作为printf的%f参数压入了栈中。
在printf的内部实现中,由于scan到了%f,那么自然会把栈上的空间作为浮点格式来解析(参考YangYi讲过的ieee7xxx文档)。由于整数格式与浮点数格式大
相径庭,那么必然会导致printf结果错误。这点大家应该很好理解。
那么,为什么每次结果都不一样呢?这个就要从C标准说起了。
如果你泡过技术论坛也许会有这样的经验:某菜鸟问一些C语言的基本问题,某高手很鄙视的扔一句话出来:请参考C标准。偶以前一直以为谭浩强爷爷的书就
是C标准,后来才知道其实不是。。。也许你听过C89,C99这些词,是的,这些就是C标准。
一个最简单的例子:刚学C语言时,变量定义必须放在函数的最前面,否则就编译不过。这件事是C89定义的。
后来工作了,发现公司的代码可以把变量定义在函数中间,最明显的就是for (int i = 0; i < size; i++)(虽然我们不推荐这样做)。这件事是C99定义的
。
C99除了定义这些看上去“鸡毛蒜皮”的小事,还会定义C lib的一些标准,其中,printf对可变参的解析,就属于其中之一。
我们看下对lib printf的描述:
7.19.6.3 The printf function
Synopsis
1 #include
int printf(const char * restrict format, ...);
Description
2 The printf function is equivalent to fprintf with the argument stdout interposed
before the arguments to printf.
Returns
3 The printf function returns the number of characters transmitted, or a negative value if
an output or encoding error occurred.
写的很简单,直接让我们参考fprintf。fprintf的描述很多,我抽出了对我们这个问题有用的那部分:
7.19.6.1 The fprintf function
Synopsis
1 #include
int fprintf(FILE * restrict stream,
const char * restrict format, ...);
f,F A double argument representing a floating-point number is converted to
decimal notation in the style [?]ddd.ddd, where the number of digits after
the decimal-point character is equal to the precision specification. If the
precision is missing, it is taken as 6; if the precision is zero and the # flag is
not specified, no decimal-point character appears. If a decimal-point
character appears, at least one digit appears before it. The value is rounded to
the appropriate number of digits.
A double argument representing an infinity is converted in one of the styles
[-]inf or [-]infinity — which style is implementation-defined. A
double argument representing a NaN is converted in one of the styles
[-]nan or [-]nan(n-char-sequence) — which style, and the meaning of
any n-char-sequence, is implementation-defined. The F conversion specifier
produces INF, INFINITY, or NAN instead of inf, infinity, or nan,
respectively.243)
看,这个%f,让printf内部去解析double型的参数。 读这个标准之前,我一直都以为%f是解析float型的参数,
看来以前论坛上“高手”的话,还是有道理的。。。
OK,那么这个就可以解释为啥每次./a.out都是乱码了,为了方便我再贴一次反汇编的内容上来:
[root@localhost home]# objdump -D a.out > a.out.objdump
08048328 :
8048328: 55 push %ebp
8048329: 89 e5 mov %esp,%ebp
804832b: 83 ec 08 sub $0x8,%esp
804832e: 83 ec 08 sub $0x8,%esp
8048331: 6a 03 push $0x3
8048333: 68 0c 84 04 08 push $0x804840c
8048338: e8 2b ff ff ff call 8048268 <_init+0x38>
804833d: 83 c4 10 add $0x10,%esp
8048340: c9 leave
8048341: c3 ret
栈上分配了0x16个字节的空间,用于溢出保护(现在靠谱的编译器都会干这事,好像以前老的TC不会这样做),然后压了4个字节(0x3)作为参数传给
printf. 插一句,call 8048268 <_init+0x38>是调用printf,因为"gcc -o a.out a.c"是用的动态链接(linux上用gcc默认是动态链接),8048268是plt
段里的地址(plt是现代动态链接的一项关键技术--延迟绑定,学过C++的人应该会有点印象,延迟绑定让动态链接大大提升了速度,这里就不扯开说了)。我
们可以看到,栈上分配的0x16个字节的空间是没有初始化的,也就意味着里面的值是随机的。 到了printf里,只要这个lib是遵从C标准的,那么它只要看到
了%f,就一定会去解析栈上的sizeof(double)=8个字节。 显然会解析到前面提到的16个字节中的低4个字节的随机值。
所以,这就说明了为啥每次都是不一样的值。
看到这里,也许你会有这样的疑问:既然printf里解析%f是用double型,那如果我传给printf是一个float,那是不是printf也应该打印出乱码?
恩,good question。答案是:编译器帮你做了精度转换了。这个谭爷爷的书里貌似是有讲过的。。。。
看下这个代码以及它的反汇编就清楚了:
[root@localhost home]# cat c.c
#include
#include
void foo()
{
float f = 2.2;
printf("f = %f\n", f);
return;
}
int main ()
{
foo();
return 0;
}
[root@localhost home]# gcc -o c.out c.c
[root@localhost home]# objdump -D c.out > c.out.objdump
[root@localhost home]#
查看foo的反汇编代码:
08048328 :
8048328: 55 push %ebp
8048329: 89 e5 mov %esp,%ebp
804832b: 83 ec 08 sub $0x8,%esp
804832e: c7 45 fc cd cc 0c 40 movl $0x400ccccd,0xfffffffc(%ebp)
8048335: 83 ec 04 sub $0x4,%esp
8048338: d9 45 fc flds 0xfffffffc(%ebp)
804833b: 8d 64 24 f8 lea 0xfffffff8(%esp,1),%esp
804833f: dd 1c 24 fstpl (%esp,1)
8048342: 68 1c 84 04 08 push $0x804841c
8048347: e8 1c ff ff ff call 8048268 <_init+0x38>
804834c: 83 c4 10 add $0x10,%esp
804834f: c9 leave
8048350: c3 ret
flds到X87 float register stack里的是单精度,但你看后面的fstpl,把X87 stack上你刚压进去的单精度又转化成了双精度存到内存stack中了。你有兴趣
可以画一下stack的变化情况,就非常清楚了。
所以这种情况下,printf得到的值是正确的,不会出现前面说到的随机值。
至于vxworks里怎样处理这个问题,我相信读完了前面的内容,再去看vxworks,应该很容易了。
文章写到这里,这个问题看似差不多了,其实不然:
刚才我们说到的是linux的动态链接的结果,那静态链接呢?
[root@localhost home]# cat a.c
#include
#include
void foo (void)
{
printf("f = %f\n", 10 / 3);
}
int main ()
{
foo();
return 0;
}
[root@localhost home]# gcc -static -o a1.out a.c
[root@localhost home]# objdump -D a1.out > a1.out.objdump
[root@localhost home]# ./a1.out
f = 0.000000
[root@localhost home]# ./a1.out
f = 0.000000
[root@localhost home]# ./a1.out
f = 0.000000
[root@localhost home]# ./a1.out
f = 0.000000
[root@localhost home]# ./a1.out
f = 0.000000
奇怪了吧?大家有兴趣可以讨论讨论。。。。也许要从静态库的printf的实现入手了
阅读(1240) | 评论(0) | 转发(0) |