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 .
\\ 这里对前面的三步说明一下: ESP EBP 寄存器是堆栈专用的。堆栈基址指针 (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 来打印的,所以只会再取栈顶
\\ 0x 0012FF28 指向的内容 ;所以打印出来的就是上面压栈的常量 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