Hoodlum1980 (fafa)'s Technological Blog

Languages mainly using and digging: C / CPP, ASM, C#, Python. Other languages:Java.

博客园 首页 新随笔 联系 订阅 管理

【补充声明】此文完成于几年前回答 BCCN 论坛的网友提问,就问题本身而言,对于这个问题似乎是没必要深究的,因为这种代码在读取一个变量的值的过程中反复尝试修改它的值,其结果依赖编辑器的实现。这种代码当然也是不可能在现实应用中出现的。不过作为一个问题,如果他一定要问,某编译器为什么会给出这样的结果,那就必须了解编译器对这个代码的编译结果细节,这就是本文所论述的东西。本文只涉及到了 TC2, VC6, VC2005 几种编译器。而且后两者应该使用的 WIN32 DEBUG,通常 Release 版本和 Debug 版本是一种在运行结果表现上的等效关系,对这个具体问题在当时我并未有精力再去细分。此次更新编辑顺便修改了原文中的个别错别字。原文中的术语“堆栈”修改为“栈”。原文中的一些中间结论不够准确,但暂未删除,已经用删除线做了处理。

    ---- hoodlum1980 ,2012年9月10日。


 

首先我们来看一下这个问题的提出,来自于一个网友的提问:
http://bbs.bccn.net/thread-200774-1-1.html
----------------------------------------------------------------------------------------------------------

求教大家,简单问题,但为什么是这样的结果?(vc6.0)

很简单的程序
void main()
{
     int i=8;
     printf("%d,%d,%d,%d\n", ++i, --i, i++, i--);
}
但是结果为(8 7 8 8)无论是从左到右顺序求值还是从右到左顺序求值都不应该是这个结果吧?
我觉得从左到右应该是(9 8 8 9 )从右到左是(8 7 7 8),
是我的错还是编译器的原因?如果是从右到左顺序求值,为什么结果不是(8 7 7 8)而是(8 7 8 8)
请大家指点一下!
[ 本帖最后由 默默无纹 于 2008-2-24 21:04 编辑 ]
-----------------------------------------------------------------------------------------------------------
    在这里我使用了VS.NET2005编译的结果是:8,8,7,8。用TC2.0编译的结果是:8,7,7,8。VC6.0我没有安装,所以没有试过,也没办法分析。
    这里我们可以看到,由不同的编译器产生了不同结果,可见这个问题是依赖编译器的理解和实现的。换句话说,对于 i++ 和 ++i 的处理在这里是有歧义的,当然在自己应用中我相信也不会有任何人写出这样的代码。但是作为一个问题,我们有必要分析一下不同编译器究竟如何理解i++和++i操作符的。

        我们在学习C的时候,应该已经大概知道了 i++ 和 ++i 两者的区别,即“++”符号在 i 之前还是之后,决定了 i 自增操作和他的语句的执行顺序的关系。即i++,理解为i在其语句中取原始值,++i在其语句中取自增后的新值。这一点是毫无疑义的。但是问题在于,网友的问题中又涉及到了 i++,++i 在作为参数时候的处理,所以这时候我们就会感到困惑,i++ 和 ++i 在作为参数的时候,和进入栈的顺序之间有何关系呢?根据前面的实验,可见TC2.0和VS.net2005的处理不同,可见两者对其处理不同,那么造成这种不同的结果的原因是什么呢?我们从代码上无法看到差异,因此我们必须看汇编语言才能知晓,编译器到底把我们的代码翻译成了什么样子。下面我采用IDA反汇编编译器把生成的.exe文件,结果如下:
 
VS.NET 2005的代码

    可见,++i和--i执行的时候直接改变了i的值,而i++和i--必须在所在的这个语句执行后才能改变i的值,所以i++作为参数时,实际上是这样的过程,
         printf("%d",i++);


    相当于下面的语句:
         int temp=i;
           i = (i+1);
         printf("%d",temp);

 

    因此上面的代码可以翻译为:
        int i=8;
          printf("%d,%d,%d,%d",++i,--i,i++,i--);

 

    因此可以翻译为下面的等效代码:
    i=8;
    temp0=i; //temp0=8;
    i--;     //7
    temp1=i; //temp1=7
    i++;     //8
    --i;     //7
    ++i;     //i=8
    printf("%d,%d,%d,%d",i,i,temp1,temp0);   


    所以打印结果是8,8,7,8

---------------------------------------------------------------------------------------------------------
    我们再看在TC2.0下的反汇编代码:
TC2.0下面的反汇编代码

可见,上面的行为可以翻译为下面的等效代码(TC2.0):
i=8;
temp0=i;        //这时8已经入栈,实际上通过ax寄存器直接压栈里了~~~,不存在temp0)
i--;                //i=7
temp1=i;        //这时7已经入栈,实际上通过ax寄存器直接压栈里了~~~,不存在temp1)
i++;               //i=8
--i;                 //i=7     
temp2=i;        //7已经入栈
++i;               //i=8
temp3=i;        //8已经入栈
printf("%d,%d,%d,%d",temp3,temp2,temp1,temp0);

输出结果是:8,7,7,8

----------------------------------------------------------------------------------------------------------------------
【注意】:下文中用删除线标记的文本属于最初结论,但这些结论不够准确,这些文字保留仅为了保留此文的初貌,因此请略过。本文的最终结论是下面的绿色文字补充部分。
      ——hoodlum1980 于2012年9月10日。
----------------------------------------------------------------------------------------------------------------------

下面我们将总结和分析两种编译器的处理之间有何不同:

请注意两者的区别主要是,他们是一边处理自增自减并一边随时入栈,还是先处理完所有自增自减之后再最后统一一次性的入栈。

 

----------------------------------------------------------

【补充】:在这里这样总结他们的区别是不完善的,实际上涉及到(1)缓存 i 的值,(2)++/--运算符的执行,(3)push i 或 缓存值。

 这三者之间的顺序问题。以上三者之间的顺序的微小差别都能对结果产生关键影响。请参考本文最后的补充。

    --hoodlum1980 @ 2011年10月12日

----------------------------------------------------------


(1)在TC下面属于前者,每执行一个语句,就把i通过ax寄存器马上入栈了,所以参数入栈和i++等语句是交叉交替性进行的。这里的i++和++i的主要区别在于压栈是在i自增之前还是之后。
i++相当于:先入栈,再自增。
++i相当于,先自增,再入栈。
所以我们看到下面的参数:从右到左:
i--: 入栈8,i=7
i++:入栈7,i=8
--i:i=7,入栈7
++i:i=8,入栈8
所以导致栈里面的参数是8,7,7,8,所以打印结果是8,7,7,8.

 

(2)在上面的VC.net2005中属于后者,是先为i++和i--保存值,然后执行完所有的自增和自减,最后一次性的把所有参数入栈。在这里i++和++i的区别
主要是是否把i的值保存到另一个位置:而且最大不同点在于这里不马上入栈,而是等所有参数处理后统一入栈。
i++:先缓存i的原始值,然后i自增。最后入栈时,用i的原始值入栈。
++i:i自增,不缓存i的原始值。最后入栈时,是更新后的i。
所以我们看到在VC2005.NET中的顺序是:
i--: 缓存8,i=7
i++:缓存7,i=8
--i:i=7
++i:i=8
参数一次性依次入栈:第一个缓存值8,第二个缓存值7,i的当前值8,i的当前值8。
所以这时候栈的数据是:8,8,7,8.(从左到右)。
所以打印结果是:8,8,7,8.

 

------------------------------------------------------------

【补充】对 VC6.0 的分析(如提问者所述:结果是 8,7,8,8)
           --hoodlum1980 @ 2011-10-12
------------------------------------------------------------
 
    现在补充下对 VC6.0 的debug版本的分析。本题目本质上是分析编译器如何处理入栈和++等操作符之间的关系。也就是说结果取决于编译器如何将参数push到栈中。VC6.0 的结果打印出来是 8,7,8,8。其汇编代码如下:
.text:00401028                 mov     [ebp+var_4], 8  ; int i = 8;
.text:0040102F mov eax, [ebp+var_4]
.text:00401032 mov [ebp+var_8], eax ; int tmp1 = i;
.text:00401035 mov ecx, [ebp+var_8]
.text:00401038 push ecx ; push tmp1; (8)
.text:00401039 mov edx, [ebp+var_4] ; int tmp2 = i;
.text:0040103C mov [ebp+var_C], edx
.text:0040103F mov eax, [ebp+var_C]
.text:00401042 push eax ; push tmp2; (8)
.text:00401043 mov ecx, [ebp+var_4]
.text:00401046 sub ecx, 1
.text:00401049 mov [ebp+var_4], ecx ; --i; (之后i = 7)
.text:0040104C mov edx, [ebp+var_4]
.text:0040104F push edx ; push i; (7)
.text:00401050 mov eax, [ebp+var_4]
.text:00401053 add eax, 1
.text:00401056 mov [ebp+var_4], eax ; ++i; (之后i=8)
.text:00401059 mov ecx, [ebp+var_4]
.text:0040105C push ecx ; push i; (8)
.text:0040105D push offset ??_C@_0N@KFBM@?$CFd?0?$CFd?0?$CFd?0?$CFd?6?$AA@ ; "%d,%d,%d,%d\n"
.text:00401062 mov edx, [ebp+var_4]
.text:00401065 add edx, 1
.text:00401068 mov [ebp+var_4], edx ; i++; (之后i=9)
.text:0040106B mov eax, [ebp+var_4]
.text:0040106E sub eax, 1
.text:00401071 mov [ebp+var_4], eax ; i--; (之后i=8)
.text:00401074 call printf
.text:00401079 add esp, 14h

    
    我们翻译上面的汇编代码的等效代码如下:
 
int i = 8;
int tmp1 = i;
int tmp2 = i; //临时变量 tmp1 和 tmp2 是实际上存在!
--i;
int tmp3 = i; //临时变量 tmp3 和 tmp4 是实际上不存在。本质是push i。
++i;
int tmp4 = i;


i++; //此处是处理语句中的两个后置操作符。
i--;

printf("%d,%d,%d,%d\n", tmp4, tmp3, tmp2, tmp1);

    这里我们注意到,VC6 本质上也是一边入栈一边处理前置运算符。VC6是先处理两个前置的运算符(++i和--i),并在此过程中就入栈参数,当参数入栈完毕后最后处理两个后置运算符(i++和i--)。同时 VC6 和 Vs2005的处理过程非常类似,但是不同在于处理顺序不同。VC6先处理前置运算符(--i 和 ++i),然后最后处理后置运算符(i++,i--),注意这个顺序是依照了参数入栈顺序(从右到左)。而VS2005和VC6的处理顺序相反。
 

     【总结】
 
    尽管我们分析了上面的汇编代码,但是实际上还是有些云里雾里。不同编译器产生不同的结果的本质原因在于:在处理++,--运算符和入栈之间的顺序关系,同时还需要了解的是,前置的运算符一般在执行本语句之前就执行,因此无需缓存值。而后置运算符通常的处理则通常需要缓存值。由于本问题中是对同一个变量多次使用++,--运算符,且对其入栈,因此运算符和入栈之间的顺序对结果有着关键影响。通过下表可以进行直观的对比:
 

int i = 8;

printf ( "%d,%d,%d,%d\n", ++i, --i, i++, i-- );

 

VC6.0

VS2005

TC2.0

tmp1 = i

push tmp1

tmp2 = i

push tmp2

--i

push i

++i

push i

i++

i--

 

 

tmp1 = i

i—

tmp2 = i

i++

--i

++i

push tmp1

push tmp2

push i

push i

tmp1 = i

i—

push tmp1

tmp2 = i

i++

push tmp2

--i

push i

++i

push i

8,7,8,8

8,8,7,8

8,7,7,8

 
    备注:上面的 tmp1,tmp2 在 VC6和 VS2005中是栈上的实际临时变量(它们是由于两个后置运算符而产生的),在TC2.0中是使用AX寄存器中转。
 
    仔细观察上面的表格中可以发现,push 了 4 个 i 的顺序是没有疑问的,由于有后置运算符,所以需要两个临时变量来缓存当前值。结果不同的原因在于,不同编译器,(1)缓存 i 的当前值,(2)处理 ++,--运算符 和 (3)push 语句之间的顺序不同,因此导致结果不同。
 
 
posted on 2008-02-25 06:18  hoodlum1980  阅读(3767)  评论(5编辑  收藏  举报