对const声明变量的奇异行为的探讨

The information in this article applies to:
- C/C++
----------------------------------------------------------------
我把这个试验的源代码列出来:
int main(int argc, char* argv[])
{
       const int x=10000;
       int *y=0;
       y=(int*)&x;
       *y=10;
       printf("%d/n", x);
       printf("%d/n", *y);
       return 0;
}
首先我们声明了一个const变量x,初始化为10000。然后让一个int指针y指向x。通过给*y赋值,从而改变了x的实际值!
虽然在Watch窗口中你明明看到x的值确实是10,但是printf出来的x的值却偏偏是10000!!
可是,这个已经被彻底抹去的10000,又是从哪里被找回来的呢?
 
我的解释:
这样的代码经过VC编译器的Debug版本的编译,最后生成的完整的汇编代码为(我做了注释,可以参考一下):
11:   int main(int argc, char* argv[])
12:   {
00401250   push        ebp
// 第一步,将基址寄存器(EBP) 压入堆栈
00401251   mov         ebp,esp         
// 第二步,把当前的栈顶指针(ESP)拷贝到EBP,做为新的基地址
00401253   sub         esp,48h
// 第三步,ESP减去一个数值,用来为本地变量留出一定空间。这里减去48h,也就是
// 72 .
// 这里对前面的三步说明一下:ESPEBP寄存器是堆栈专用的。堆栈基址指针(EBP)
// 存器确定堆栈帧的起始位置,而堆栈指针(ESP)寄存器执行当前堆栈顶。在函数的入口处,
// 当前堆栈基址指针被压到了堆栈中,并且当前堆栈指针成为新的堆栈基址指针。局部变 
// 量的存储空间、函数使用的各种需要保存的寄存器的存储空间在函数入口处也被预留出
// 来。
// 所以也就有了下面的三个压栈行为。
                    
// 下面是连续三个压栈,4步:
00401256   push        ebx
// ebx寄存器压栈;EBX寄存器是段寄存器的一种,为基址 DS 数据段;
00401257   push        esi
// esi寄存器压栈;ESI寄存器是指针寄存器的一种。是内存移动和比较操作的源地址寄
// 存器;
00401258   push        edi
// edi寄存器压栈;EDI寄存器是指针寄存器的一种。是内存移动和比较操作的目标地址
// 寄存器
// 以上四步执行完之后,函数入口处的堆栈帧结构如下所示:
 
 
 
 
// 值得注意的是,上面所说的对于Debug版本才是正确的,对于Release版本可不一定对。
// Release 版本也许已经把堆栈基址指针优化掉了。
 
00401259   lea         edi,[ebp-48h]
// 5步,lea指令装入有效地址,用来得到局部变量和函数参数的指针。这里[ebp-48h]就是基地址再向下偏移48h,就是前面说的为本地变量留出的空间的起始地址;将这个值装载入edi寄存器,从而得到局部变量的地址;
 
// 下面的这第六步可是非常的重要,请记住:
// 第六步,给段寄存器预先赋值:
0040125C   mov         ecx,12h
// ECX寄存器是段寄存器的一种,为计数器 SS 堆栈段。设为12h
00401261   mov         eax,0CCCCCCCCh
// EAX寄存器是段寄存器的一种,为累加器 CS 代码段;设为0CCCCCCCCh
 
00401266   rep stos    dword ptr [edi]
// 这句话是干吗的?
 
// 下面开始我们的代码了:
13:       const int x=10000;
00401268   mov         dword ptr [ebp-4], 2710h
// 第一步,在基地址向下偏移4个字节所指向的地址,将10000这个DWORD数值放进//去;
// 可以看出的是,对于一个普通的int z = 10000;汇编代码依然是这个样子。说明从这句
// 话是无法分清楚局部const变量的初始化和普通变量的初始化的!这一点很重要!就
// 是说编译器从表面上是无法分清楚一个局部const变量和一个普通变量的。
 
14:       int *y=0;
0040126F   mov         dword ptr [ebp-8],0
 
15:       y=(int*)&x;
00401276   lea         eax,[ebp-4]
00401279   mov         dword ptr [ebp-8],eax
// 2步,将x的地址装载到EAX寄存器;
// 3步,再把这个地址作为一个数值导到y的地址,这样y就指向了x
// 这是局部const变量声明的情况!
// 而对于全局const变量声明的情况,这句y=(int*)&x;的汇编却是:
// 00401276   mov         dword ptr [ebp-8],offset x (0043101c)
// 一个很显著的区别!
 
16:       *y=10;
0040127C   mov         ecx,dword ptr [ebp-8]
0040127F   mov         dword ptr [ecx],0Ah
// 4步,通过ECX寄存器倒手,将y所指向的地址的数值修改为0Ah,也就是10
// 编译器之所以允许这种修改const变量值的非法情况,是因为编译器并不知道这//是一个
// const变量,它实在是和普通的变量太像了!
 
17:
18:       printf("%d/n", x);
00401285   push        2710h
// 5步,将10000数值压栈!按照惯例,这个2710h会被存在当前栈顶指针前4个字节
// 处。原来ESP指向0012FF2C,所以现在指向0012FF28了。
// 编译器为什么会直接push一个常量入栈呢?
// 我觉得可能是这样:制定C++编译器规则的人想反正都是const变量了,它的值肯定不
// 能变。printf一个普通变量是倒手两个寄存器后把EAX寄存器的内容压栈,多影响效率
// 还不如直接将这个const变量的值压栈呢。
 
0040128A   push        offset string "%d/n" (0042f01c)
// 再把格式化压栈;
// 这样,printf函数将取栈顶的内容打印,当然是按照%d/n来打印的,所以只会再取栈顶
// 0x0012FF28指向的内容;所以打印出来的就是上面压栈的常量2710h
// 这就是我给出的解释。请高手们指正。
 
0040128F   call        printf (004082f0)
00401294   add         esp,8
19:
20:       printf("%d/n", *y);
00401297   mov         edx,dword ptr [ebp-8]
0040129A   mov         eax,dword ptr [edx]
0040129C   push        eax
// 看,对于一个普通变量的printf
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值