Chinaunix首页 | 论坛 | 博客
  • 博客访问: 986761
  • 博文数量: 633
  • 博客积分: 30780
  • 博客等级: 大将
  • 技术积分: 7532
  • 用 户 组: 普通用户
  • 注册时间: 2007-05-12 21:07
文章分类

全部博文(633)

文章存档

2011年(10)

2010年(500)

2009年(47)

2008年(76)

我的朋友

分类: C/C++

2011-04-21 00:22:16

------------------------------------------
转载请注明出处: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的实现入手了


阅读(1235) | 评论(0) | 转发(0) |
0

上一篇:看到11.5

下一篇:[原创]IA cache学习笔记

给主人留下些什么吧!~~