c语言存储于常量区的修改,C语言之修改常量

前言:指针!菜鸟的终点,高手的起点。漫谈一些进阶之路上的趣事;记录一些语言本身的特性以及思想,没有STL,也没有API!

0x01: 程序内存中的存储划分

对于程序在内存中是如何分布的,网上有多个解释的版本(解释为3、4、5、6个区的都有),这里我也不赘述了,反正该有的都有,只是看个人怎么理解

建议自己搜来看看温习一下(主要是栈区、常量区、代码段),看懵了就不要说我描述有问题了......

0x22: 变量与常量

程序的运行过程(屏蔽一些较为底层的东西):

① 将物理内存(磁盘等存储介质)中的程序文件装入运行内存中,程序中的内存指的是运行内存

② CPU从内存中的指定位置读取到指令加以运行,这里的指令最终都是对于内存的操作

程序中定义的操作存储于内存 - 代码段,操作指C代码指令编译的结果,例如赋值操作、比较运算等;CPU读取指令的位置

程序中定义的局部变量存储于内存 - 栈区,这是我们最常使用的存储区域;这些变量在同一作用范围内时我们可以随意改变其值

程序中定义的常量存储于内存 - 数据区,数据区中全局变量、静态变量、常量的存储区不同,我们通常使用 'const' 定义的常量是存储在常量区的,常量区的数据根据规定是不可改变的

思考:程序加载到内存中的绝对位置是由操作系统决定的,程序可以加载到的内存(除系统保留区)也是平等的,为什么存储在栈区的变量可以改变而存储在代码区和常量区的数据不可改变;理论上来说该程序可以操作的内存(也就是系统加载该程序时分配的内存地址范围)都是可以被改变的,所以这里可以推测为程序做了权限的限制

0x32: 指针操作的本质

指针操作是可以直接作用于内存的,使用指针操作时只有两个限制,一个是定义指针时规定的对于变量本身的限制,一个是该程序的寻址空间限制;在某些情况下这两个限制都可以突破,这里不作论述

指针的强大之处在于它能修改所有能寻址到的内存中的值;对应程序在内存中的分配,理论上可以使用指针操作栈区堆区(常用),那么同样可以操作数据区和代码区;语言限制中不允许修改操作的区域为代码区和数据区中的常量区,这里我们可以将指针指向这两个区域,这样就能达到修改代码和常量的目的

0x42: 通过指针操作常量区

代码示例:

img?u=aHR0cHM6Ly9pbWFnZXMuY25ibG9ncy5jb20vT3V0bGluaW5nSW5kaWNhdG9ycy9Db250cmFjdGVkQmxvY2suZ2lm

img?u=aHR0cHM6Ly9pbWFnZXMuY25ibG9ncy5jb20vT3V0bGluaW5nSW5kaWNhdG9ycy9FeHBhbmRlZEJsb2NrU3RhcnQuZ2lm

const int a=10;int *pa=(int*)&a;*pa=99;

printf("*pa=%d,a=%d",*pa,a);/*输出:

*pa=99,a=10*/

常量修改

示例中第二行必需使用强转,C中认为 'const' 是更加广泛的类型限制

输出结果是不是有点奇怪?理论上来说定义的 'const' 常量存储的常量区也在内存中,为什么 'a' 和 '*pa' 的值不一样呢?难道说使用这两个名的时候不是寻址的同一块内存?或者是程序寻址的时候使用相同的地址实际地址是不同的区域(a在常量区,对pa赋值时在栈区生成了新的*pa内存)?

我们再深入看看:

img?u=aHR0cHM6Ly9pbWFnZXMuY25ibG9ncy5jb20vT3V0bGluaW5nSW5kaWNhdG9ycy9Db250cmFjdGVkQmxvY2suZ2lm

img?u=aHR0cHM6Ly9pbWFnZXMuY25ibG9ncy5jb20vT3V0bGluaW5nSW5kaWNhdG9ycy9FeHBhbmRlZEJsb2NrU3RhcnQuZ2lm

const int a=10;int *pa=(int*)&a;

printf("*pa=%d,a=%d,pa=%p,&a=%p\n",*pa,a,pa,&a);*pa=99;

printf("*pa=%d,a=%d,pa=%p,&a=%p\n",*pa,a,pa,&a);/*输出:

*pa=10,a=10,pa=0019FF3C,&a=0019FF3C

*pa=99,a=10,pa=0019FF3C,&a=0019FF3C*/

常量地址

这里分别输出了赋值之前两者的值和地址、赋值之后两者的值和地址,这里我们可以知道地址是相同的,但值就是不同???我去 哪有这么怪的事,同一块地址的值同一时间取怎么就不同了?

这时候我们使用 'F10' 单步调试大法进行变量跟踪(VC++6.0),打开变量池、内存,跟踪过程(需要一点点的调试能力):

① 第一行定义 'const' 变量,查看变量 'a' 的值(=10)、查看 'a' 的地址 '&a' (=0x0019FF3C)

② 第二行定义 'int' 指针指向 'a',查看 '*pa' 的值(=10)、查看 'pa' 的值(=0x0019FF3C)

③ 运行并查看输出,没问题

④ 使用 '*pa' 对这块内存赋值,查看 'a'、'&a'、'*pa'、'pa' 的值,其中 'a' 的值和 '*pa' 的值变成了 '99',正常

⑤ 运行并查看输出,得到输出中的最后一行 '*pa=99,a=10,pa=0019FF3C,&a=0019FF3C'

???啥意思???④中得到了 'a' 的值明明为 '99',⑤这输出咋回事儿啊?

再使用内存view查看地址为 '0x0019FF3C' 地址内的值,为 '63 00 00 00',小端存储的十六进制,63H==99D;可以得到的结论为:使用这两个名进行寻址的是同一块内存,同一程序中寻址方式唯一

问题就在于这一块内存的原值在赋值之后已经被新的值覆盖掉了,读取到的 'a' 的值是从哪来的,'a' 的值一定在内存中的某个位置

接下来再进一步跟踪程序运行过程,查询程序中间步骤,单步调试汇编语句,打开寄存器、汇编文件(需要再多一点点调试能力,只解释相关语句):

img?u=aHR0cHM6Ly9pbWFnZXMuY25ibG9ncy5jb20vT3V0bGluaW5nSW5kaWNhdG9ycy9Db250cmFjdGVkQmxvY2suZ2lm

img?u=aHR0cHM6Ly9pbWFnZXMuY25ibG9ncy5jb20vT3V0bGluaW5nSW5kaWNhdG9ycy9FeHBhbmRlZEJsb2NrU3RhcnQuZ2lm

1: #include 2:3: void main(void)4: {00401010 pushebp00401011 movebp,esp00401013 subesp,48h00401016 pushebx00401017 pushesi00401018 pushedi00401019 leaedi,[ebp-48h]0040101C movecx,12h00401021 moveax,0CCCCCCCCh00401026 repstos dword ptr [edi]5: const int a=10;00401028 mov dword ptr [ebp-4],0Ah6: int *pa=(int*)&a;0040102F lea eax,[ebp-4]00401032 mov dword ptr [ebp-8],eax7: *pa=99;00401035 mov ecx,dword ptr [ebp-8]00401038 movdword ptr [ecx],63h8: printf("*pa=%d,a=%d,pa=%p,&a=%p\n",*pa,a,pa,&a);0040103E lea edx,[ebp-4]00401041 pushedx00401042 mov eax,dword ptr [ebp-8]00401045 pusheax00401046 push0Ah00401048 mov ecx,dword ptr [ebp-8]0040104B movedx,dword ptr [ecx]0040104D pushedx0040104E push offset string "*pa=%d,a=%d,pa=%p,&a=%p\n" (0042201c)00401053 call printf (00401090)00401058 addesp,14h9: }0040105B popedi0040105C popesi0040105D popebx0040105E addesp,48h00401061 cmpebp,esp00401063 call __chkesp (00401110)00401068 movesp,ebp0040106A popebp0040106B ret

汇编文件

以上汇编代码中的注释代码为C的源代码,其余汇编语句只做重要点的讲解

① 4-5 行之间是做初始化、入栈一类的操作,略过

② 5-6 将 0xA 存到 'a'

③ 6-7 取 'a' 的地址存到 'pa'

④ 7-8 取 'pa' 值对应的地址,存入 0x63

⑤ 8-9 输出:将变量压栈、字符串压栈调用 'printf()' 库函数

⑥ 9-最后 出栈、释放空间、返回等操作

其中 ① ⑥ 我也不太懂,②-④步是比较简单的操作,关键在第⑤步

可以看到 'printf()' 函数的调用过程:依次使用 'push' 压入4个需要串化的参数、压入原字符串,最后 'call printf()',压入参数的顺序为从右至左:

执行第一个 'push' 时查得 'edx' 值为 0x0019FF3C,是 '&a' 的值

第二个 'push' 时 'eax' 值为 0x0019FF3C,是 'pa' 的值

第三个 'push' 的值为 0x0A,是 'a' 的值

第四个 'push' 时 'edx' 值为 0x63,是 '*pa' 的值

问题就在于第三个 'push' 目标直接为值 0x0A 而不是取 '&a' 这个地址内的值,根据之前的推断即使是常量也需要从其存储位置取值,而实际情况却是在编译时就进行了类似 '#define' 之类的直接替换......

章结:

一个无聊的实验,如何修改常量;得出的结论:使用指针操作常量区是没有任何问题的,但有时即使修改了常量区的值也对运行结果没有影响,编译器会优化在使用常量时不去常量的存储位置取值,而是编译阶段直接将值写入到代码区

另:即使写入到代码区的值也可以修改,通过某种神奇的方法找到编译后代码的位置,将逻辑修改为从内存寻值;或者暴力点内嵌汇编......

写在最后:

这是一个困扰了我三年的问题(有点丢人),初学C时就碰到了这个问题,当时问老师说没遇到过我这么用的,就没有和这个问题刚到底(也不会这些技术);技术的话可能还是有一些地方描述得有问题,望大佬不吝赐教,同时也希望这篇文章中的东西能对同学们有哪怕一丝用处

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值