C语言之修改常量
前言:指针!菜鸟的终点,高手的起点。漫谈一些进阶之路上的趣事;记录一些语言本身的特性以及思想,没有STL,也没有API!
0x01: 程序内存中的存储划分
对于程序在内存中是如何分布的,网上有多个解释的版本(解释为3、4、5、6个区的都有),这里我也不赘述了,反正该有的都有,只是看个人怎么理解
建议自己搜来看看温习一下(主要是栈区、常量区、代码段),看懵了就不要说我描述有问题了……
0x22: 变量与常量
程序的运行过程(屏蔽一些较为底层的东西):
① 将物理内存(磁盘等存储介质)中的程序文件装入运行内存中,程序中的内存指的是运行内存
② CPU从内存中的指定位置读取到指令加以运行,这里的指令最终都是对于内存的操作
程序中定义的操作存储于内存 – 代码段,操作指C代码指令编译的结果,例如赋值操作、比较运算等;CPU读取指令的位置
程序中定义的局部变量存储于内存 – 栈区,这是我们最常使用的存储区域;这些变量在同一作用范围内时我们可以随意改变其值
程序中定义的常量存储于内存 – 数据区,数据区中全局变量、静态变量、常量的存储区不同,我们通常使用 ‘const’ 定义的常量是存储在常量区的,常量区的数据根据规定是不可改变的
思考:程序加载到内存中的绝对位置是由操作系统决定的,程序可以加载到的内存(除系统保留区)也是平等的,为什么存储在栈区的变量可以改变而存储在代码区和常量区的数据不可改变;理论上来说该程序可以操作的内存(也就是系统加载该程序时分配的内存地址范围)都是可以被改变的,所以这里可以推测为程序做了权限的限制
0x32: 指针操作的本质
指针操作是可以直接作用于内存的,使用指针操作时只有两个限制,一个是定义指针时规定的对于变量本身的限制,一个是该程序的寻址空间限制;在某些情况下这两个限制都可以突破,这里不作论述
指针的强大之处在于它能修改所有能寻址到的内存中的值;对应程序在内存中的分配,理论上可以使用指针操作栈区堆区(常用),那么同样可以操作数据区和代码区;语言限制中不允许修改操作的区域为代码区和数据区中的常量区,这里我们可以将指针指向这两个区域,这样就能达到修改代码和常量的目的
0x42: 通过指针操作常量区
代码示例:
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内存)?
我们再深入看看:
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’ 的值一定在内存中的某个位置
接下来再进一步跟踪程序运行过程,查询程序中间步骤,单步调试汇编语句,打开寄存器、汇编文件(需要再多一点点调试能力,只解释相关语句):
1: #include <stdio.h> 2: 3: void main(void) 4: { 00401010 push ebp 00401011 mov ebp,esp 00401013 sub esp,48h 00401016 push ebx 00401017 push esi 00401018 push edi 00401019 lea edi,[ebp-48h] 0040101C mov ecx,12h 00401021 mov eax,0CCCCCCCCh 00401026 rep stos dword ptr [edi] 5: const int a=10; 00401028 mov dword ptr [ebp-4],0Ah 6: int *pa=(int*)&a; 0040102F lea eax,[ebp-4] 00401032 mov dword ptr [ebp-8],eax 7: *pa=99; 00401035 mov ecx,dword ptr [ebp-8] 00401038 mov dword ptr [ecx],63h 8: printf("*pa=%d,a=%d,pa=%p,&a=%p\n",*pa,a,pa,&a); 0040103E lea edx,[ebp-4] 00401041 push edx 00401042 mov eax,dword ptr [ebp-8] 00401045 push eax 00401046 push 0Ah 00401048 mov ecx,dword ptr [ebp-8] 0040104B mov edx,dword ptr [ecx] 0040104D push edx 0040104E push offset string "*pa=%d,a=%d,pa=%p,&a=%p\n" (0042201c) 00401053 call printf (00401090) 00401058 add esp,14h 9: } 0040105B pop edi 0040105C pop esi 0040105D pop ebx 0040105E add esp,48h 00401061 cmp ebp,esp 00401063 call __chkesp (00401110) 00401068 mov esp,ebp 0040106A pop ebp 0040106B 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时就碰到了这个问题,当时问老师说没遇到过我这么用的,就没有和这个问题刚到底(也不会这些技术);技术的话可能还是有一些地方描述得有问题,望大佬不吝赐教,同时也希望这篇文章中的东西能对同学们有哪怕一丝用处