分享
 
 
 

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

王朝vc·作者佚名  2006-01-08
窄屏简体版  字體: |||超大  

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

Article last modified on 2002-7-25

----------------------------------------------------------------

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来打印的,所以只会再取栈顶的

\\ 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,就不一样了!

0040129D push offset string "%d\n" (0042f01c)

004012A2 call printf (004082f0)

004012A7 add esp,8

21:

22:

23: return 0;

004012AA xor eax,eax

24: }

004012AC pop edi

004012AD pop esi

004012AE pop ebx

\\ 函数入口处连着三个把寄存器压栈,这里一个一个地弹出来;

004012AF add esp,48h

\\ ESP寄存器再加回去;

004012B2 cmp ebp,esp

\\ 比较EBP和ESP

004012B4 call __chkesp (00408190)

004012B9 mov esp,ebp

\\ 将EBP中保存的栈顶指针再拷回ESP寄存器;

004012BB pop ebp

\\ 将EBP弹出堆栈;

004012BC ret

还有一个问题,为什么将const int x=100;这句代码放在全局声明会发生访问禁止呢?

访问禁止是发生在:

16: *y=10;

00401276 mov eax,dword ptr [ebp-4]

00401279 mov dword ptr [eax],0Ah

时。

为什么呢?

原因是,这里将0Ah存入EAX所指向的地址时,发生了0xC0000005: Access Violation。

那为什么局部const声明时没有这个问题呢?

局部const声明时,则这句*y = 10;的汇编为:

16: *y=10;

0040127C mov ecx,dword ptr [ebp-8]

0040127F mov dword ptr [ecx],0Ah

,我们可以看出寄存器用的不一样!

前者为EAX,

后者为ECX。

有什么区别吗?

是这样子:

EAX寄存器在函数入口处,她被预先设为0CCCCCCCCh。这一点很关键!

ECX寄存器却被预先设为12h。

这样,当全局声明const变量时,我们企图将一个DWORD值写入一个0CCCCCCCCh指向的未分配的内存中,所以遭遇到访问禁止!!

而局部声明const变量时,是将一个DWORD值写入一个00000012h指向的内存中,这是可以的!!

Written by zhengyun@tomosoft.com

 
 
 
免责声明:本文为网络用户发布,其观点仅代表作者个人观点,与本站无关,本站仅提供信息存储服务。文中陈述内容未经本站证实,其真实性、完整性、及时性本站不作任何保证或承诺,请读者仅作参考,并请自行核实相关内容。
2023年上半年GDP全球前十五强
 百态   2023-10-24
美众议院议长启动对拜登的弹劾调查
 百态   2023-09-13
上海、济南、武汉等多地出现不明坠落物
 探索   2023-09-06
印度或要将国名改为“巴拉特”
 百态   2023-09-06
男子为女友送行,买票不登机被捕
 百态   2023-08-20
手机地震预警功能怎么开?
 干货   2023-08-06
女子4年卖2套房花700多万做美容:不但没变美脸,面部还出现变形
 百态   2023-08-04
住户一楼被水淹 还冲来8头猪
 百态   2023-07-31
女子体内爬出大量瓜子状活虫
 百态   2023-07-25
地球连续35年收到神秘规律性信号,网友:不要回答!
 探索   2023-07-21
全球镓价格本周大涨27%
 探索   2023-07-09
钱都流向了那些不缺钱的人,苦都留给了能吃苦的人
 探索   2023-07-02
倩女手游刀客魅者强控制(强混乱强眩晕强睡眠)和对应控制抗性的关系
 百态   2020-08-20
美国5月9日最新疫情:美国确诊人数突破131万
 百态   2020-05-09
荷兰政府宣布将集体辞职
 干货   2020-04-30
倩女幽魂手游师徒任务情义春秋猜成语答案逍遥观:鹏程万里
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案神机营:射石饮羽
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案昆仑山:拔刀相助
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案天工阁:鬼斧神工
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案丝路古道:单枪匹马
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案镇郊荒野:与虎谋皮
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案镇郊荒野:李代桃僵
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案镇郊荒野:指鹿为马
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案金陵:小鸟依人
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案金陵:千金买邻
 干货   2019-11-12
 
推荐阅读
 
 
 
>>返回首頁<<
 
靜靜地坐在廢墟上,四周的荒凉一望無際,忽然覺得,淒涼也很美
© 2005- 王朝網路 版權所有