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