源程序
int main()
{
int i = 0;
printf("%d,%d,%d\n",i++,--i,i++);
return 0;
}
按照我们平时对自增自减运算符表达式的理解,我们预期的结果应该是:0,0,0
具体的理解过程是:函数参数的压栈是从右向左压的,所以先把i++产生的临时量0先压入栈,i的值变成1,再经过–i使i变成0,然后将0再压入栈,最后又将i++的临时量i=0压入栈,最后i的值是1,这样压入的结果就是0,0,0了。这种压栈过程其实是每计算出一个参数的值就压入栈。
但是实际结果却是:0,1,0
这是为什么呢?
原因是我们在调用标准库函数printf之前,对所传入的参数进行了遍历,将计算之后的结果保存在寄存器中,并当做实参压入栈中,由于CPU中寄存器的数量是有限的,所以在运算过程中使用了临时量,暂时保留变量的值。其运算的过程可以通过汇编指令来分析。
汇编程序
int i = 0;
0030526E mov dword ptr [i],0
printf("%d,%d,%d\n",i++,--i,i++);//0,1,0
00305275 mov eax,dword ptr [i]
00305278 mov dword ptr [ebp-0D0h],eax
0030527E mov ecx,dword ptr [i]
00305281 add ecx,1
00305284 mov dword ptr [i],ecx
00305287 mov edx,dword ptr [i]
0030528A sub edx,1
0030528D mov dword ptr [i],edx
00305290 mov eax,dword ptr [i]
00305293 mov dword ptr [ebp-0D4h],eax
00305299 mov ecx,dword ptr [i]
0030529C add ecx,1
0030529F mov dword ptr [i],ecx
003052A2 mov esi,esp
003052A4 mov edx,dword ptr [ebp-0D0h]
003052AA push edx
003052AB mov eax,dword ptr [i]
003052AE push eax
003052AF mov ecx,dword ptr [ebp-0D4h]
003052B5 push ecx
003052B6 push offset string "%d,%d,%d\n" (30AC44h)
003052BB call dword ptr [__imp__printf (30E43Ch)]
003052C1 add esp,10h
003052C4 cmp esi,esp
003052C6 call @ILT+695(__RTC_CheckEsp) (3012BCh)
代码解析
1、先将0存放到变量i中,再把i的值存放到寄存器eax中。
0030526E mov dword ptr [i],0
00305275 mov eax,dword ptr [i]
2、将寄存器的值存放到地址为ebp-0D0h的临时量中,此临时量保存的值是0。
00305278 mov dword ptr [ebp-0D0h],eax
3、又将i的值存放到寄存器ecx中,并对寄存器ecx的值进行+1操作。将寄存器ecx的值重新写到变量i中。即修改i变量的值为1。
0030527E mov ecx,dword ptr [i]
00305281 add ecx,1
00305284 mov dword ptr [i],ecx
4、将i的值取出来放到寄存器edx中,并对edx寄存器进行-1操作。将运算结果重新放到变量i中,此时i的值为0。
00305287 mov edx,dword ptr [i]
0030528A sub edx,1
0030528D mov dword ptr [i],edx
5、把i的值放到寄存器eax中。然后将eax的值存放到地址为ebp-0D4h的内存中,此临时量的值是0。
00305290 mov eax,dword ptr [i]
00305293 mov dword ptr [ebp-0D4h],eax
6、将i的值放到寄存器ecx中,并对寄存器ecx进行+1操作。将ecx的值重新写回变量i中,最后i的值是1。
00305299 mov ecx,dword ptr [i]
0030529C add ecx,1
0030529F mov dword ptr [i],ecx
7、将寄存器esp(栈顶指针)的值放到esi中。
003052A2 mov esi,esp
8、将临时量ebp-0D0h的值放到edx中,并将edx的值压入栈中。
003052A4 mov edx,dword ptr [ebp-0D0h]
003052AA push edx
9、将i的值取出来放入寄存器eax中,并将eax的值压入栈中。
003052AB mov eax,dword ptr [i]
003052AE push eax
10、将临时量ebp-0D4h的值放到寄存器ecx中,并将ecx的值压入栈中。
003052AF mov ecx,dword ptr [ebp-0D4h]
003052B5 push ecx
11、压入字符串参数。
003052B6 push offset string "%d,%d,%d\n" (30AC44h)
12、保存下一条指令的地址,开始执行printf函数。
003052BB call dword ptr [__imp__printf (30E43Ch)]
13、执行完printf函数之后,继续执行下面的指令。
003052C1 add esp,10h
003052C4 cmp esi,esp
003052C6 call @ILT+695(__RTC_CheckEsp) (3012BCh)
总结:在调用printf函数时,变量的压参是从右向左开始的,但先需要遍历函数参数,计算出每个参数具体数值。而在遍历参数时,分别进行了两次后置++和一次前置–运算,后置++会产生临时量。遍历完之后,计算出来i的值是1,但在此过程中首先将两次i的值先保存到了临时量中。所以最后实参的值有两次是从临时空间中取值,一次从i中取值。将取到的值分别存放到寄存器,再通过寄存器压入到栈中。后置++压入的参数值是临时量中的值,前置–压入的参数值是最后i的值。因此最后传入的参数分别是0,1,0,打印的结果也是如此。
可以看到函数压入的参数是字面值常量,如果有多个参数传递时,首先会遍历每个参数,计算并取出数值(有的保存在临时量中,有的则是在变量中),然后才是从右向左进行压栈。从汇编程序的执行过程中,可以看到参数的计算过成主要在第2、5、6步,参数的压栈过程是在第8 、9、10步。