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

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

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

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

0x02: 变量与常量

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

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

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

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

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

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

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

0x03: 指针操作的本质

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

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

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

  代码示例:

  1. const int a=;
  2. int *pa=(int*)&a;
  3. *pa=;
  4. printf("*pa=%d,a=%d",*pa,a);
  5.  
  6. /*输出:
  7. *pa=99,a=10
  8. */

常量修改

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

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

  我们再深入看看:

  1. const int a=;
  2. int *pa=(int*)&a;
  3. printf("*pa=%d,a=%d,pa=%p,&a=%p\n",*pa,a,pa,&a);
  4. *pa=;
  5. printf("*pa=%d,a=%d,pa=%p,&a=%p\n",*pa,a,pa,&a);
  6.  
  7. /*输出:
  8. *pa=10,a=10,pa=0019FF3C,&a=0019FF3C
  9. *pa=99,a=10,pa=0019FF3C,&a=0019FF3C
  10. */

常量地址

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

  这时候我们使用 '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. : {
  5. push ebp
  6. mov ebp,esp
  7. sub esp,48h
  8. push ebx
  9. push esi
  10. push edi
  11. lea edi,[ebp-48h]
  12. 0040101C mov ecx,12h
  13. mov eax,0CCCCCCCCh
  14. rep stos dword ptr [edi]
  15. : const int a=;
  16. mov dword ptr [ebp-],0Ah
  17. : int *pa=(int*)&a;
  18. 0040102F lea eax,[ebp-]
  19. mov dword ptr [ebp-],eax
  20. : *pa=;
  21. mov ecx,dword ptr [ebp-]
  22. mov dword ptr [ecx],63h
  23. : printf("*pa=%d,a=%d,pa=%p,&a=%p\n",*pa,a,pa,&a);
  24. 0040103E lea edx,[ebp-]
  25. push edx
  26. mov eax,dword ptr [ebp-]
  27. push eax
  28. push 0Ah
  29. mov ecx,dword ptr [ebp-]
  30. 0040104B mov edx,dword ptr [ecx]
  31. 0040104D push edx
  32. 0040104E push offset string "*pa=%d,a=%d,pa=%p,&a=%p\n" (0042201c)
  33. call printf ()
  34. add esp,14h
  35. : }
  36. 0040105B pop edi
  37. 0040105C pop esi
  38. 0040105D pop ebx
  39. 0040105E add esp,48h
  40. cmp ebp,esp
  41. call __chkesp ()
  42. mov esp,ebp
  43. 0040106A pop ebp
  44. 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时就碰到了这个问题,当时问老师说没遇到过我这么用的,就没有和这个问题刚到底(也不会这些技术);技术的话可能还是有一些地方描述得有问题,望大佬不吝赐教,同时也希望这篇文章中的东西能对同学们有哪怕一丝用处

C语言之修改常量的更多相关文章

  1. C/C++修改常量的值

    C/C++中常量修饰const可以用来保证一些确定的量不会被一不小心改变,比如PI,一直是3.14159...... 但是不排除有时候也会需要修改常量的值,通过直接修改是不能达到目的. 比如: #in ...

  2. Swift语言指南(一)--语言基础之常量和变量

    原文:Swift语言指南(一)--语言基础之常量和变量 Swift 是开发 iOS 及 OS X 应用的一门新编程语言,然而,它的开发体验与 C 或 Objective-C 有很多相似之处. Swif ...

  3. Go语言变量和常量

    一.变量相关 1.变量声明 C# : int a; Go : var a int; 需要在前面加一个var关键字,后面定义类型 可以使用 var( a int; b string;)减少var 2.变 ...

  4. Go 语言基础——变量常量的定义

    go语言不支持隐式类型转换,别名和原有类型也不能进行隐式类型转换 go语言不支持隐式转换 变量 变量声明 var v1 int var v2 string var v3 [10]int // 数组 v ...

  5. C语言中字符串常量到底存在哪了?

    常量存储总结局部变量.静态局部变量.全局变量.全局静态变量.字符串常量以及动态申请的内存区 1.局部变量存储在栈中2.全局变量.静态变量(全局和局部静态变量)存储在静态存储区3.new申请的内存是在堆 ...

  6. C语言实现修改文本文件中的特定行

         最近由于项目需要实现修改文件的功能,所以,博主认真查阅了一些资料,但是,很遗憾,并没有太多的收获. 好的,首先我先叙述下功能要求: 其实很简单,就是Shell中sed命令的C语言实现,实现定 ...

  7. [C语言]变量VS常量

    -------------------------------------------------------------------------------------------- 1. 固定不变 ...

  8. Linux语言设置修改乱码

    1.system-config-language 命令语言改成英文.(安装yum install  system-config-language) 如何系统安装后,使用的语言不是自己想要的.但是在图形 ...

  9. Go语言中的常量

    1 概述 常量,一经定义不可更改的量.功能角度看,当出现不需要被更改的数据时,应该使用常量进行存储,例如圆周率.从语法的角度看,使用常量可以保证数据,在整个运行期间内,不会被更改.例如当前处理器的架构 ...

随机推荐

  1. java迭代器 常用

    19 //使用迭代器遍历ArrayList集合 20 Iterator<String> listIt = list.iterator(); 21 while(listIt.hasNext( ...

  2. hydra的使用

    hydra参数详解 -R 继续从上一次进度接着破解 -S 大写,采用SSL链接 -s 小写,可通过这个参数指定非默认端口 -l 指定破解的用户,对特定用户破解 -L 指定用户名字典 -p 小写,指定密 ...

  3. Web信息搜集

    文件是转载原文https://www.freebuf.com/articles/web/204883.html  如有侵权 请联系 对一个网站挖掘的深浅来说,信息收集是非常的重要的,这篇文章主要分享本 ...

  4. 学习笔记39_EF的DAL层(精)

    通用的分页查询 public IQueryable<UserInfo> GetPage<T>(int pageSize,int pageIndex,out int total, ...

  5. 基于 Jenkins Pipeline 自动化部署

    最近在公司推行Docker Swarm集群的过程中,需要用到Jenkins来做自动化部署,Jenkins实现自动化部署有很多种方案,可以直接在jenkins页面写Job,把一些操作和脚本都通过页面设置 ...

  6. 来探讨一下最近面试问的ThreadLocal问题

    中高级阶段开发者出去面试,应该躲不开ThreadLocal相关问题,本文就常见问题做出一些解答,欢迎留言探讨. ThreadLocal为java并发提供了一个新的思路, 它用来存储Thread的局部变 ...

  7. PHP路径指定web路径的方法

    PHP路径指定web路径的方法直接在/前面加.就是代表web路径了 不是按照文件路径来算了 <pre>./Public/uploads/suolutu/' . $suijishu . '_ ...

  8. Java升级那么快,多个版本如何灵活切换和管理?

    前言 近两年,Java 版本升级频繁,感觉刚刚掌握 Java8,写本文时,已听到 java14 的消息,无论是尝鲜新特性(Java12 中 Collectors.teeing 超强功能使用),还是由于 ...

  9. java编程思想第四版第六章总结

    1. 代码重构 为什么f要代码重构 第一次代码不一定是完美的, 总会发现更优雅的写法. 代码重构需要考虑的问题 类库的修改不会破坏客户端程序员的代码. 源程序方便扩展和优化 2. 包 创建一个独一无二 ...

  10. shodan 文档学习笔记

    Table of Contents 1. Introduction 1.1. All About the Data 1.2. Data Collection 1.3. SSL in Depth 1.3 ...