以下的代码很有意思,在相同时刻,相同的内存地址,数据居然会不一样。

#include <iostream>

int main(void)
{
const int const_val = 3; int *nomal_pot = (int*)&const_val;
*nomal_pot = 9; printf("const_val: 0x%p -> %d\n", &const_val, const_val);
printf("nomal_pot: 0x%p -> %d\n", nomal_pot, *nomal_pot); return 0;
}

运行结果:

const_val: 0x002DF7B0 -> 3
nomal_pot: 0x002DF7B0 -> 9

造成如此神奇的现象,原因是因为 C++ 常量折叠的特性(C没有),这跟 C++ 编译器相关,在编译阶段,会将代码中所有引用该常量的地方,都会替换为常量的初始值,就像宏定义一样,那么上述代码中的第一句代码:

printf("const_val: 0x%p -> %d\n", &const_val, const_val);

会被编译器修改等同为以下代码(实际以汇编体现):

printf("const_val: 0x%p -> %d\n", &const_val, 3);

与宏定义不同的是,编译会为常量 const_val 分配内存空间,并且该内存空间处于栈空间,并不是只读的,因此在运行时仍可以通过以下代码修改该内存空间的数据:

int *nomal_pot = (int*)&const_val;
*nomal_pot = 9;

这就出现了同一时间,在相同的 “内存地址” 中输出了不同的值。

以上两点可以通过VS2017查看反汇编的汇编代码即可确认:

#include <iostream>

int main(void)
{
00EA18C0 push ebp
00EA18C1 mov ebp,esp
00EA18C3 sub esp,0DCh
00EA18C9 push ebx
00EA18CA push esi
00EA18CB push edi
00EA18CC lea edi,[ebp-0DCh]
00EA18D2 mov ecx,37h
00EA18D7 mov eax,0CCCCCCCCh
00EA18DC rep stos dword ptr es:[edi]
00EA18DE mov eax,dword ptr [__security_cookie (0EAA004h)]
00EA18E3 xor eax,ebp
00EA18E5 mov dword ptr [ebp-4],eax
00EA18E8 mov ecx,offset _2EA97ABB_consoleapplicationtest@cpp (0EAC027h)
00EA18ED call @__CheckForDebuggerJustMyCode@4 (0EA121Ch)
const int const_val = 3;
00EA18F2 mov dword ptr [const_val],3 // 常量赋值为初始值 3 int *nomal_pot = (int*)&const_val;
00EA18F9 lea eax,[const_val] // 提取常量所在的内存地址
00EA18FC mov dword ptr [nomal_pot],eax // 赋给 nomal_pot 指针 *nomal_pot = 9;
00EA18FF mov eax,dword ptr [nomal_pot] // 提取 nomal_pot 指向的内存地址
00EA1902 mov dword ptr [eax],9 // 往该内存地址写入值 9 (即 const_val 的地址) printf("const_val: 0x%p -> %d\n", &const_val, const_val);
00EA1908 push 3 // 这里可以看到 const_val 直接被替换为初始值 3,并未从地址取值
00EA190A lea eax,[const_val]
00EA190D push eax
00EA190E push offset string "const_val: 0x%p -> %d\n" (0EA7B30h)
00EA1913 call _printf (0EA104Bh)
00EA1918 add esp,0Ch printf("nomal_pot: 0x%p -> %d\n", nomal_pot, *nomal_pot);
00EA191B mov eax,dword ptr [nomal_pot] // 提取 nomal_pot 指向的内存地址
00EA191E mov ecx,dword ptr [eax] // 从该内存地址提取内容,即 9 而非初始值 3
00EA1920 push ecx
00EA1921 mov edx,dword ptr [nomal_pot]
00EA1924 push edx
00EA1925 push offset string "nomal_pot: 0x%p -> %d\n" (0EA7C04h)
00EA192A call _printf (0EA104Bh)
00EA192F add esp,0Ch return 0;
00EA1932 xor eax,eax
}

那么以下代码,你们觉得 add_val 输出的结果会是什么呢?

#include <iostream>

int main(void)
{
const int const_val = 3; int *nomal_pot = (int*)&const_val;
*nomal_pot = 9; printf("const_val: 0x%p -> %d\n", &const_val, const_val);
printf("nomal_pot: 0x%p -> %d\n", nomal_pot, *nomal_pot); int add_val = const_val + *nomal_pot;
printf("add_val:%d\n", add_val); return 0;
}

按照上述分析,源码中所有对 const_val 的引用都会在编译期间替换为初始值,而 const_val 所在的内存空间是可以在运行时被修改,因此正确的答案应为 3 + 9 = 12

const_val: 0x0036FDC4 -> 3
nomal_pot: 0x0036FDC4 -> 9
add_val:12

对应的汇编代码:

#include <iostream>

int main(void)
{
001218C0 push ebp
001218C1 mov ebp,esp
001218C3 sub esp,0E8h
001218C9 push ebx
001218CA push esi
001218CB push edi
001218CC lea edi,[ebp-0E8h]
001218D2 mov ecx,3Ah
001218D7 mov eax,0CCCCCCCCh
001218DC rep stos dword ptr es:[edi]
001218DE mov eax,dword ptr [__security_cookie (012A004h)]
001218E3 xor eax,ebp
001218E5 mov dword ptr [ebp-4],eax
001218E8 mov ecx,offset _2EA97ABB_consoleapplicationtest@cpp (012C027h)
001218ED call @__CheckForDebuggerJustMyCode@4 (012121Ch)
const int const_val = 3;
001218F2 mov dword ptr [const_val],3 int *nomal_pot = (int*)&const_val;
001218F9 lea eax,[const_val]
001218FC mov dword ptr [nomal_pot],eax
*nomal_pot = 9;
001218FF mov eax,dword ptr [nomal_pot]
00121902 mov dword ptr [eax],9 printf("const_val: 0x%p -> %d\n", &const_val, const_val);
00121908 push 3
0012190A lea eax,[const_val]
0012190D push eax
0012190E push offset string "const_val: 0x%p -> %d\n" (0127B30h)
00121913 call _printf (012104Bh)
00121918 add esp,0Ch
printf("nomal_pot: 0x%p -> %d\n", nomal_pot, *nomal_pot);
0012191B mov eax,dword ptr [nomal_pot]
0012191E mov ecx,dword ptr [eax]
00121920 push ecx
00121921 mov edx,dword ptr [nomal_pot]
00121924 push edx
00121925 push offset string "nomal_pot: 0x%p -> %d\n" (0127C04h)
0012192A call _printf (012104Bh)
0012192F add esp,0Ch int add_val = const_val + *nomal_pot;
00121932 mov eax,dword ptr [nomal_pot] // 提取 nomal_pot 所指向的内存地址
00121935 mov ecx,dword ptr [eax] // 从该内存地址中提取数值(即9)
00121937 add ecx,3 // 将该数值与 3 相加(即 9 + 3)
0012193A mov dword ptr [add_val],ecx // 将结果写入 add_val (即 12)
printf("add_val:%d\n", add_val);
0012193D mov eax,dword ptr [add_val]
00121940 push eax
00121941 push offset string "add_val:%d\n" (0127B48h)
00121946 call _printf (012104Bh)
0012194B add esp,8 return 0;
0012194E xor eax,eax
}

那再略微修改一下,const 增加 volatile 修饰符,输出结果会是一样吗?

#include <iostream>

int main(void)
{
volatile const int const_val = 3; // 增加 volatile 修饰符 int *nomal_pot = (int*)&const_val;
*nomal_pot = 9; printf("const_val: 0x%p -> %d\n", &const_val, const_val);
printf("nomal_pot: 0x%p -> %d\n", nomal_pot, *nomal_pot); int add_val = const_val + *nomal_pot;
printf("add_val:%d\n", add_val); return 0;
}

输出结果:

const_val: 0x0039F8F8 -> 9
nomal_pot: 0x0039F8F8 -> 9
add_val:18

原因是因为经过 volatile 修饰的变量会明确让编译器编译代码时,每次都要求生成的汇编语句都是从该变量的地址中取出数据,而不是用常量值替代,这个可以通过反汇编证实这一点:

差异的点:


// 原来的 ------------------------------------------------------------------------
printf("const_val: 0x%p -> %d\n", &const_val, const_val);
00121908 push 3 // 这里可以看到 const_val 直接被替换为初始值 3,并未从地址取值
0012190A lea eax,[const_val]
0012190D push eax
0012190E push offset string "const_val: 0x%p -> %d\n" (0127B30h)
00121913 call _printf (012104Bh)
00121918 add esp,0Ch ...... int add_val = const_val + *nomal_pot;
00121932 mov eax,dword ptr [nomal_pot] // 提取 nomal_pot 所指向的内存地址
00121935 mov ecx,dword ptr [eax] // 从该内存地址中提取数值(即9)
00121937 add ecx,3 // 将该数值与 3 相加(即 9 + 3)
0012193A mov dword ptr [add_val],ecx // 将结果写入 add_val (即 12) // 现在的 ------------------------------------------------------------------------
printf("const_val: 0x%p -> %d\n", &const_val, const_val);
01151908 mov eax,dword ptr [const_val] // 注意,这里改从该内存地址取值
0115190B push eax
0115190C lea ecx,[const_val]
0115190F push ecx
01151910 push offset string "const_val: 0x%p -> %d\n" (01157B30h)
01151915 call _printf (0115104Bh)
0115191A add esp,0Ch ...... int add_val = const_val + *nomal_pot;
01151934 mov eax,dword ptr [const_val] // 提取 const_val 所指向的内存地址
01151937 mov ecx,dword ptr [nomal_pot] // 提取 nomal_pot 所指向的内存地址
0115193A add eax,dword ptr [ecx] // 注意,将两者数据相加(即 9 + 9)
0115193C mov dword ptr [add_val],eax // 将结果写入到 add_val (即 18)

完整汇编:

#include <iostream>

int main(void)
{
011518C0 push ebp
011518C1 mov ebp,esp
011518C3 sub esp,0E8h
011518C9 push ebx
011518CA push esi
011518CB push edi
011518CC lea edi,[ebp-0E8h]
011518D2 mov ecx,3Ah
011518D7 mov eax,0CCCCCCCCh
011518DC rep stos dword ptr es:[edi]
011518DE mov eax,dword ptr [__security_cookie (0115A004h)]
011518E3 xor eax,ebp
011518E5 mov dword ptr [ebp-4],eax
011518E8 mov ecx,offset _2EA97ABB_consoleapplicationtest@cpp (0115C027h)
011518ED call @__CheckForDebuggerJustMyCode@4 (0115121Ch)
volatile const int const_val = 3;
011518F2 mov dword ptr [const_val],3 int *nomal_pot = (int*)&const_val;
011518F9 lea eax,[const_val]
011518FC mov dword ptr [nomal_pot],eax
*nomal_pot = 9;
011518FF mov eax,dword ptr [nomal_pot]
01151902 mov dword ptr [eax],9 printf("const_val: 0x%p -> %d\n", &const_val, const_val);
01151908 mov eax,dword ptr [const_val]
0115190B push eax
0115190C lea ecx,[const_val]
0115190F push ecx
01151910 push offset string "const_val: 0x%p -> %d\n" (01157B30h)
01151915 call _printf (0115104Bh)
0115191A add esp,0Ch
printf("nomal_pot: 0x%p -> %d\n", nomal_pot, *nomal_pot);
0115191D mov eax,dword ptr [nomal_pot]
01151920 mov ecx,dword ptr [eax]
01151922 push ecx
01151923 mov edx,dword ptr [nomal_pot]
01151926 push edx
01151927 push offset string "nomal_pot: 0x%p -> %d\n" (01157C04h)
0115192C call _printf (0115104Bh)
01151931 add esp,0Ch int add_val = const_val + *nomal_pot;
01151934 mov eax,dword ptr [const_val]
01151937 mov ecx,dword ptr [nomal_pot]
0115193A add eax,dword ptr [ecx]
0115193C mov dword ptr [add_val],eax
printf("add_val:%d\n", add_val);
0115193F mov eax,dword ptr [add_val]
01151942 push eax
01151943 push offset string "add_val:%d\n" (01157B48h)
01151948 call _printf (0115104Bh)
0115194D add esp,8 return 0;
01151950 xor eax,eax
}

【学习笔记】C++ 常量折叠原理和验证的更多相关文章

  1. Java IO学习笔记:概念与原理

    Java IO学习笔记:概念与原理   一.概念   Java中对文件的操作是以流的方式进行的.流是Java内存中的一组有序数据序列.Java将数据从源(文件.内存.键盘.网络)读入到内存 中,形成了 ...

  2. golang学习笔记10 beego api 用jwt验证auth2 token 获取解码信息

    golang学习笔记10 beego api 用jwt验证auth2 token 获取解码信息 Json web token (JWT), 是为了在网络应用环境间传递声明而执行的一种基于JSON的开放 ...

  3. tensorflow学习笔记——模型持久化的原理,将CKPT转为pb文件,使用pb模型预测

    由题目就可以看出,本节内容分为三部分,第一部分就是如何将训练好的模型持久化,并学习模型持久化的原理,第二部分就是如何将CKPT转化为pb文件,第三部分就是如何使用pb模型进行预测. 一,模型持久化 为 ...

  4. [Firefly引擎][学习笔记一][已完结]带用户验证的聊天室

    原地址:http://bbs.9miao.com/thread-44571-1-1.html 前言:早在群里看到大鸡蛋分享他们团队的Firefly引擎,但一直没有时间去仔细看看,恰好最近需要开发一个棋 ...

  5. 强化学习-学习笔记7 | Sarsa算法原理与推导

    Sarsa算法 是 TD算法的一种,之前没有严谨推导过 TD 算法,这一篇就来从数学的角度推导一下 Sarsa 算法.注意,这部分属于 TD算法的延申. 7. Sarsa算法 7.1 推导 TD ta ...

  6. 机器学习实战(Machine Learning in Action)学习笔记————10.奇异值分解(SVD)原理、基于协同过滤的推荐引擎、数据降维

    关键字:SVD.奇异值分解.降维.基于协同过滤的推荐引擎作者:米仓山下时间:2018-11-3机器学习实战(Machine Learning in Action,@author: Peter Harr ...

  7. Android(java)学习笔记95:Android原理揭秘系列之View、ViewGroup

    作过Android 应用开发的朋友都知道,Android的UI界面都是由View和ViewGroup及其派生类组合而成的.其中,View是所有UI组件的基类,而ViewGroup是容纳这些组件的容器, ...

  8. java学习笔记 (2) —— Struts2类型转换、数据验证重要知识点

    1.*Action.conversion-properties 如(point=com.test.Converter.PointListConverter) 具体操作类的配置文件 2.*Action. ...

  9. Android(java)学习笔记34:Android原理揭秘系列之View、ViewGroup

    1. 作过Android 应用开发的朋友都知道,Android的UI界面都是由View和ViewGroup及其派生类组合而成的.其中,View是所有UI组件的基类,而ViewGroup是容纳这些组件的 ...

  10. Shiro 学习笔记(二)——shiro身份验证

    身份验证: 在应用中证明他就是他本人.一般上用身份证.用户/密码 来证明. 在shiro中,用户需要提供principals (身份)和credentials(证明)给shiro,从而应用能验证用户身 ...

随机推荐

  1. JS逆向实战7-- 某省在线审批网站params 随机生成

    参数分析 我们首先通过抓包 发现这个就是我们所需要的数据 然后我们通过fidder 发起请求 结果: 通过我们反复测试 发现这个params的参数是每次请求中都会变化的 断点查找 我们通过 这个t参数 ...

  2. 小程序利用canvas 绘制图案 (生成海报, 生成有特色的头像)

    小程序利用canvas 绘制图案 (生成海报, 生成有特色的头像) 微信小程序生成特色头像,海报等是比较常见的.下面我来介绍下实现该类小程序的过程. 首先选择前端来通过 canvas 绘制.这样比较节 ...

  3. 聊聊kafka

    两个月因为忙于工作毫无输出了,最近想给团队小伙伴分享下kafka的相关知识,于是就想着利用博客来做个提前的准备工作了:接下来会对kafka做一个简单的介绍,包括利用akf原则来解析单机下kafk的各个 ...

  4. XTDrone和PX4学习期间问题记录(一)

    XTDrone和PX4学习期间问题记录(一) Written By PiscesAlpaca 前言: 出现问题可以去官方网站http://ceres-solver.org/index.html查看文档 ...

  5. JavaScript代码是怎么在浏览器里面运行起来的?

    JavaScript代码是怎么在浏览器里面运行的?下面简单探索一下 浏览器内核 浏览器内核(Rendering Engine),常见的叫法如:排版引擎.解释引擎.渲染引擎,现在流行称为浏览器内核. 浏 ...

  6. 简单的sql注入2

    尝试 1 1' 1" 发现1'还是会报错,所以注入口还是1' 再试试1' and '1'='1发现报出SQLi detected! 取消空格试试1'and'1'='1 似乎可以进入,应该就是 ...

  7. 【SQL进阶】Day05:窗口函数

    〇.概述 一.专用窗口函数 1.每类试卷得分前3名 自己写出来的部分 SELECT tag AS tid, uid AS uid, Rank AS ranking -- 如何确定排名 FROM exa ...

  8. 【每日一题】【小根堆&边出队边入队后续节点&注意判空】23. 合并K个升序链表-211128/220213

    给你一个链表数组,每个链表都已经按升序排列. 请你将所有链表合并到一个升序链表中,返回合并后的链表. 答案1(参数是数组): /** * Definition for singly-linked li ...

  9. kali2021.4a安装angr(使用virtualenv)

    在Linux中安装各种依赖python的软件时,最头疼的问题之一就是各个软件的python版本不匹配的问题,angr依赖python3,因此考虑使用virtualenv来安装angr Virtuale ...

  10. 《HTTP权威指南》– 1.HTTP概述

    HTTP的概念 HTTP : Hypertext Transfer Protocol 超文本传输协议 因特网上有数千种不同的数据类型,HTTP仔细地给每种要通过Web传输的对象都打上了名为MIME类型 ...