您现在的位置是:首页 > 学无止境 > C语言网站首页C语言 printf多个自增自减表达式的底层实现原理

printf多个自增自减表达式的底层实现原理

  • 莫愁
  • C语言
  • 2019-09-20
简介提起i++(i--)和++i(--i),相信大家非常熟悉。两者的区别是:前者是先赋值,然后再自增或自减;后者是先自增或自减,后赋值。但是当printf函数与多个自增自减表达式结合起来,编译器实现i++(i--)和++i(--i)的原理你真的了解吗?
字数 1812.5

提起i++(i--)和++i(--i),相信大家非常熟悉。两者的区别是:前者是先赋值,然后再自增或自减;后者是先自增或自减,后赋值。但是当printf函数与多个自增自减表达式结合起来,编译器实现i++(i--)和++i(--i)的原理你真的了解吗?今天,我们从一道有意思的题目揭开printf多个自增自减表达式底层实现的面纱......

源程序

 

 

按照我们平时对自增自减运算符表达式的理解,我们预期的结果应该是:0,0,0

具体的理解过程是:函数参数的压栈是从右向左压的,所以先把i++产生的临时量0先压入栈,i的值变成1,再经过–i使i变成0,然后将0再压入栈,最后又将i++的临时量i=0压入栈,最后i的值是1,这样压入的结果就是0,0,0了。这种压栈过程其实是每计算出一个参数的值就压入栈。

但是实际结果却是:0,1,0

这是为什么呢?

原因是我们在调用标准库函数printf之前,对所传入的参数进行了遍历,将计算之后的结果保存在寄存器中,并当做实参压入栈中,由于CPU中寄存器的数量是有限的,所以在运算过程中使用了临时量,暂时保留变量的值。其运算的过程可以通过汇编指令来分析。

 

汇编指令

 

汇编指令的分析

(1)先将0,放入到变量i中。

00EA13BE  mov         dword ptr [i],0

(2)将i的值0,放入到寄存器eax,再将eax的值放入到一个地址为[ebp-0D0h]的临时变量中,临时变量中的值为0。接着又把变量i的值0,放入到寄存器ecx中,ecx加1,值为1,最后将ecx的值放入到变量i中,此时i值为1。

00EA13C5  mov         eax,dword ptr [i] 
00EA13C8  mov         dword ptr [ebp-0D0h],eax 
00EA13CE  mov         ecx,dword ptr [i] 
00EA13D1  add         ecx,1 
00EA13D4  mov         dword ptr [i],ecx 

(3)将i值放到寄存器edx中,此时edx的值为1,再将edx的值减1,edx值为0,再将edx的值放入变量i中,此时i值为0。

00EA13D7  mov         edx,dword ptr [i] 
00EA13DA  sub         edx,1 
00EA13DD  mov         dword ptr [i],edx 

(4)将变量i的值放入寄存器eax中,此时eax的值为0,将eax的值放入到地址为[edp-0D4h]的临时量中,此时临时量的值为0。接着又把变量i的值,放入到寄存器ecx中,ecx的值为0,再将ecx的值加1,放到变量i中。

00EA13E0  mov         eax,dword ptr [i] 
00EA13E3  mov         dword ptr [ebp-0D4h],eax 
00EA13E9  mov         ecx,dword ptr [i] 
00EA13EC  add         ecx,1 
00EA13EF  mov         dword ptr [i],ecx 

(5)将寄存器esp的值放入esi中

00EA13F2  mov         esi,esp 

(6)将地址为[ebp-0D0h]临时量的值0,放入到寄存器edx中,再将edx的值0压入栈中。

00EA13F4  mov         edx,dword ptr [ebp-0D0h] 
00EA13FA  push        edx  

(7)将变量i的值1,放入寄存器eax中,再将eax的值1压入栈中。

00EA13FB  mov         eax,dword ptr [i] 
00EA13FE  push        eax  

(8)将地址为[ebp-0D4h]临时量的值0,放入到寄存器ecx中,再将edx的值0压入栈中。

00EA13FF  mov         ecx,dword ptr [ebp-0D4h] 
00EA1405  push        ecx

(9)压入字符串参数,保存下一条指令的地址,进入printf函数。执行完printf函数之后,继续执行下面的指令。

00EA1406  push        offset string "%d %d %d\n" (0EA573Ch) 
00EA140B  call        dword ptr [__imp__printf (0EA82BCh)] 
00EA1411  add         esp,10h 
00EA1414  cmp         esi,esp 
00EA1416  call        @ILT+310(__RTC_CheckEsp) (0EA113Bh) 

看完汇编代码,我们的问题就解决了!

 

总结

原来在调用printf函数时,先自右向左遍历所有参数,同时进行参数的计算,最终自右向左压参入栈。在上面汇编指令分析的过程,(2)(3)(4)步完成参数的遍历和计算的操作,(6)(7)(8)步完成自右向左压参入栈的操作。所以对于i++(i--),编译器必须先将变量i保存到一个临时量中,保证不受参数运算的影响,再进行自加/自减操作,最终将临时量的值压入栈中。对于--i(++i),对变量i直接进行自加/自减操作,无需保存到临时量中,最终直接去i的内存中取值进行压栈。值得注意的是:如果在--i之后对i进一步进行别的操作,那么最终压栈的数据很可能不是我们在--i或者++i时存入i的内存的值。正如本题中,i++之后,i的值变为1,在进行--i时,i内存的值变为0,之后又对i进行i++,i内存的值变为1,所以最终--i压入栈中的参数是1,并非我们所想的0。

 

启示

当代码分析结果与实际运行结果不一致时,善用汇编代码去分析底层实现原理。


转载: 感谢您对莫愁个人博客网站平台的认可,非常欢迎各位朋友分享到个人站长或者朋友圈,但转载请说明文章出处“来源莫愁个人博客 https://www.mochoublog.com/study/348.html”。

文章评论

    • 评论
    人参与,条评论

技术在线

服务时间

周一至周日 12:00-22:00

关闭下雪
关闭背景特效