范文健康探索娱乐情感热点
投稿投诉
热点动态
科技财经
情感日志
励志美文
娱乐时尚
游戏搞笑
探索旅游
历史星座
健康养生
美丽育儿
范文作文
教案论文

C常见的三种内存破坏场景和分析

  有一定 C++  开发经验的同学大多数踩过内存破坏的坑,有这么几种现象:比如某个变量整形,在程序中只可能初始化或者赋值为 1  或者2  , 但是在使用的时候却发现其为0  或者其他的情况。对于其他类型,比如字符串等,可能出现了一种出乎意料  的值!程序在堆上申请内存或者释放内存的时候,在内存充足的情况下,居然出现了堆错误。
  当出现以上场景的时候,你该思考一下,是不是出现了内存破坏的情况了。而本文主要通过展示和分析常见的三种内存破坏导致覆盖相邻变量的场景,让读者在碰到类似的场景,不至于束手无策。而对于堆上的内存破坏,很常见并且棘手的场景,本人将在后续的文章和大家分享。 1. 内存破坏之强制类型转换
  大家都知道不匹配的类型强制转换会带来一些 bug  ,比如int  和unsigned int  互相转换,又或者int  和__int64  强行转换。是不是每次当读起这类文章起来如雷贯耳,但是当自己去写代码的时候还是容易犯错?这也就是为什么C++  容易写出坑  的原因,明知可能有错,还难以避免。这往往是因为真实的项目中复杂程度,往往让人容易忽略这些细节。
  不少老的工程代码还是采用 VC6  编译,为了安全问题或者使用C++新特性需要将VC6  升级到更新的Visual Studio  。接下来要介绍的一个样例程序,就是隐藏于代码中的一个问题,如果从VC6  升级到VS2017  的时候会带来问题吗?可以先找找看:#include  #include   class DemoClass { public:   DemoClass() : m_bInit(true), m_tRecordTime(0)   {      time((time_t *)(&m_tRecordTime));   };    void DoSomething() {     if (m_bInit)       std::cout << "Do Task!" << std::endl;   }  private:   int     m_tRecordTime;   bool   m_bInit; };  int main() {   DemoClass testObj;   testObj.DoSomething();   return 0; }
  Do Task!  这个字符串会不会打印出来呢? 可以发现这段程序在VC6  中可以打印出来,但是在VS2017  中却打印不出来了。那是因为如下原因:函数原型 time_t time( time_t *destTime );  ,在VC6  中time_t  默认是32位,而在VS2017  中默认是64位。早期程序以为32位中表达最大的时间是2038  年,那时候完全够用,但随着计算机本身的发展64位  逐渐成为主流time_t  在最新的编译器中也默认采用64位  ,这样时间完全够用以亿年  为单位了,那时候计算机发展超出我们想象了。程序的问题所在 m_tRecordTime  采用的是int  类型,默认为32位,那么其地址作为time_t time( time_t *destTime );  函数实参后,在VC6  中time_t  本身为32位自然也不会出错,但是在VS2017  中因为time_t  为64位,则time((time_t *)(&m_tRecordTime));  后写入了一个64位  的值。结合下图,看下这个对象的内存布局,m_bInit  的值将会被覆盖,而这里原先的m_bInit  的值为1  ,被覆盖为0  ,从而导致内存破坏,导致程序执行意想不到的结果。这里只是不输出,那在真实程序中,可能会导致某个逻辑错乱,发生严重的问题。
  这个问题修改自然比较简单,将 m_tRecordTime  定义为time_t  类型就可以了。如果有类似的问题发生的时候,比如这个变量的可疑的发生了不该有的变化的时候,你可以查看下这个变量定义的附近是否有内存的操作可能产生溢出,找到问题所在。因为内存上溢的比较多,一般可以查看下定义在当前出现问题的变量的低地址出的变量操作,是否存在可疑的地方。最后,针对这种场景,我们是不是也可以得到一些收获呢,个人总结如下两点:在定义类型的时候,尽量和原始类型一致,比如这里的 time_t  有些程序员可能惯性的认为就是32位  ,那就定义一个时间戳的时候就定义为int  了,而我们要做的应该是和原始类型匹配(也就是函数的输入类型),将其定义为time_t  ,于此类似的还有size_t  等,这样可以避免未来在数据集变化或者做平台迁移的时候造成不必要的麻烦。在有一些复杂的场景的下,也许你不得不做类型转换,而这个时候就格外的需要注意或者了解清楚,转换带来的情况和后果,保持警惕,否则就可能是一个潜在的 bug  。这和开车一样,当你开车的时候如果看到前方车辆忽然产生一个不合常理的变道行为,首先要做的不是喷那辆车,而是集中注意力,看看是否更前方有障碍物或者事故放生,做出相应的反应。2. 字符串拷贝溢出
  这种情况应该是最常见了,我们来看一看样例程序: #include  #define BUFER_SIZE_STR_1 5 #define BUFER_SIZE_STR_2 8 class DemoClass { public:   void DoSomething() {     strcpy(m_str1, "Hi Coder!");     std::cout << m_str1 << std::endl;     std::cout << m_str2 << std::endl;   }  private:   char m_str1[BUFER_SIZE_STR_1] = { 0 };   char m_str2[BUFER_SIZE_STR_2] = { 0 }; };  int main() {   DemoClass testObj;   testObj.DoSomething();   return 0; }
  这种情况下肉眼可以分析的,输出结果为:
  在 m_str1  的空间为5  ,但是Hi Coder!  包含  是10  个字符,在调用strcpy(m_str1, "Hi Coder!");  的时候超过了m_str1  的空间,于是覆盖了m_str2  的内存,从而导致内存破坏。内存溢出这种尤其字符串溢出,程序崩溃可能是小事儿,如果是一个广为流传的软件,那么就很有可能会被黑客所利用。
  这种字符串场景如何分析呢,如果程序崩溃了,可以收集 Dump  先看看被覆盖的地方是什么样的字符串,然后联想看看自己的程序哪里有可能对这个字符串的操作,从而找到原因。别小看这种方法,简单粗暴很有用,曾经就用这种方式分析过Linux驱动模块的内存泄露问题。
  那如果还找不到问题呢?如果问题还能重现,那还是有调试手法的,下一节将会进行讲解。
  当然最差最差的还是不要放弃代码审查。尤其在这个内存被破坏的附近的逻辑。对于这种场景的建议,比较简单就是使用微软安全函数 strcpy_s  ,注意这里虽然列出了返回值errno_t  不过对于微软的实现来说,如果是目标内存空间不够的情况下,在Relase  版本下会调用TerminateProcess  , 并且要注意的是这个时候抓Dump有时候并不是完整的Dump。
  至于微软为什么要这样做,有可能是安全的考虑比崩溃优先级更高,于是在内存溢出不够的时候,直接让程序结束。 errno_t strcpy_s(   char *dest,   rsize_t dest_size,   const char *src);3. 随机性的内存被修改
  这一个一听都快崩溃了, C++  的坑能不能少一点呢。但是确实是会有各种各样的场景让你落入坑内。上一节的程序我稍作修改:#include  #define BUFER_SIZE_STR_1 5 #define BUFER_SIZE_STR_2 8  class DemoClass { public:   void DoSomething() {     strcpy_s(m_str2, BUFER_SIZE_STR_2, "Coder");     strcpy_s(m_str1, BUFER_SIZE_STR_1, "Test");          //Notice this line:     m_str1[BUFER_SIZE_STR_2 - 1] = "";      std::cout << m_str1 << std::endl;     std::cout << m_str2 << std::endl;   }  private:   char m_str1[BUFER_SIZE_STR_1] = { 0 };   char m_str2[BUFER_SIZE_STR_2] = { 0 }; };  int main() {   DemoClass testObj;   testObj.DoSomething();   return 0; }
  程序本意是 m_str2  赋值为Coder  , m_str1  赋值为Test  , 在编程中很多字符串拷贝或者操作中有些是在字符串末尾补  有的可能不补  , 而在本例中实际上strcpy_s  会自动补0,但是有的程序员防止万一,字符串靠背后,在数组的最后一位设置为’’。这种有时候就变成了好心办坏事。
  比如这里的 m_str1[BUFER_SIZE_STR_2 - 1] = "";   ,大家注意到没,这里应该改写为m_str1[BUFER_SIZE_STR_1 - 1] = "";   ,也就是说程序员可能拷贝代码或者不小心写错了BUFER_SIZE_STR_2  和BUFER_SIZE_STR_1  因为两者宏差不多。只要是人写代码,就有可能会犯这种错误。这个程序的输出变为:
  这个程序是比较简单,一目了然,但是在大型程序中呢,这个数组的位置跳跃的访问到了其他变量的位置,你首先得判断这个被跳跃式修改的变量,是不是程序本意造成的,因为混合了这么多的猜想,可能会导致分析变的异常复杂。那么有什么好的方法吗?只要程序能偶尔重现这个问题,那就是有方法的。
  通过Windbg调试命令 ba  可以在指定的内存地址做操作的时候进入断点。假设目前已经知道m_str2  的第四个字符,总是被某个地方误写,那么我们可以在这个地址处设置一个ba  命令: 当写的这个内存地址的时候进入断点。不过这样还是有个问题,那就是程序中有可能有很多次对这块内存的写操作,有时候是正常的写操作,如果一直进入断点,人工分析将会非常累,不现实。
  这个时候有个方法,同时也是一个 workaround  ,就是当你还没找到程序出错的根本原因的时候在被误踩的内存前面加上一个足够大的不使用的空间。比如下面的代码, m_str2  总是被误写,于是在m_str2  的前面加上一个100个字节的不使用的内存m_strUnused  (因为一般程序内存溢出是上溢,当然也可以在m_str2  的后面同样加上)。
  这样我们被踩的内存就很容易落在 m_strUnused  空间里面了,这个时候我们在其空间里设置写内存操作的断点,就容易捕获到问题所在了。#include  #define BUFER_SIZE_STR_1 5 #define BUFER_SIZE_STR_2 8 #define BUFFER_SIZE_UNUSED 100 class DemoClass { public:   void DoSomething() {     strcpy_s(m_str2, BUFER_SIZE_STR_2, "Coder");     strcpy_s(m_str1, BUFER_SIZE_STR_1, "Test");          //Notice this line:     m_str1[BUFER_SIZE_STR_2 - 1] = "";      std::cout << m_str1 << std::endl;     std::cout << m_str2 << std::endl;   }  private:   char m_str1[BUFER_SIZE_STR_1] = { 0 };   char m_strUnused[BUFFER_SIZE_UNUSED] = { 0 };   char m_str2[BUFER_SIZE_STR_2] = { 0 }; };  int main() {   DemoClass testObj;   testObj.DoSomething();   return 0; }
  下面完整的展示一下分析过程:
  第一步   用Windbg  启动(有的情况下可能是Attach,根据情况而定)到调试进程,设置main  的断点0:000> bp ObjectMemberBufferOverFllow!main *** WARNING: Unable to verify checksum for ObjectMemberBufferOverFllow.exe 0:000> g Breakpoint 0 hit eax=010964c0 ebx=00e66000 ecx=00000000 edx=00000000 esi=75aae0b0 edi=0109b390 eip=003a1700 esp=00defa00 ebp=00defa44 iopl=0         nv up ei pl nz na pe nc cs=0023  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00000206 ObjectMemberBufferOverFllow!main: 003a1700 55              push    ebp
  第二步   使用p  命令单步执行代码到testObj.DoSomething();
  第三步   找到testObj  的地址为00def984  0:000> dv /t /v 00def984          class DemoClass testObj = class DemoClass
  第四步   设置断点到testObj  相对偏移的位置,这个位置即&m_str1+BUFER_SIZE_STR_2 - 1   = &m_str1+7  。并且继续执行代码:0:000> ba w1 00def984+7 0:000> g
  第五步   你会发现程序运行进入断点,这个时候查看对应的函数调用栈即可。这个断点不一定在一个非常精确的位置,但是当你按照函数调用栈去阅读附近的代码,便比较容易找出问题所在了。0:000> k  # ChildEBP RetAddr   00 00def97c 003a1720 ObjectMemberBufferOverFllow!DemoClass::DoSomething+0x41 [......strcpybufferoverflow.cpp @ 16] 01 00def9fc 003a1906 ObjectMemberBufferOverFllow!main+0x20 [......strcpybufferoverflow.cpp @ 30] 02 (Inline) -------- ObjectMemberBufferOverFllow!invoke_main+0x1c [d:agent_work3ssrcvctoolscrtvcstartupsrcstartupexe_common.inl @ 78] 03 00defa44 75818494 ObjectMemberBufferOverFllow!__scrt_common_main_seh+0xfa [d:agent_work3ssrcvctoolscrtvcstartupsrcstartupexe_common.inl @ 288] 04 00defa58 770a40e8 KERNEL32!BaseThreadInitThunk+0x24 05 00defaa0 770a40b8 ntdll!__RtlUserThreadStart+0x2f 06 00defab0 00000000 ntdll!_RtlUserThreadStart+0x1b总结
  以上对三种内存破坏场景做了分析,在实际应用中将会变的更加复杂。在写代码的时候要注意避开其中的坑,有个叫做墨菲定律,你感觉可能会出问题的地方,那它一定会在某个时刻出现,当你对某个地方有所疑虑的时候一定要多加考虑,否则这个坑可能查找的时间,比写代码的时间要长的许多,更可怕的是可能会带来意想不到的后果。同样的分析问题要保持足够的耐心,相信真相总会出现,这样的底气也是来自于自己平时不断的学习和实践。
  内存破坏问题不区分栈上还是堆上,我们在产品中离不开使用堆开间,而且由多个模块核心功能模块组成,而这些模块通常是公用一个进程默认堆的。所以也有人推荐在这些关键模块中,各自创建一个独立的堆,从而降低一个堆内存的使用对另一个堆中内存的影响。虽然不是完全隔离,但是也是一个聊胜于无的操作了。

这些机构因拒收现金被央行处罚,你还习惯使用现金吗?近年来,移动支付逐渐得到普及,越来越多的人出门只需要带一部手机即可,从而也导致了现金的使用频率的确越来越少,同时,很多商户也不愿意接受现金,只能使用手机中的移动支付。上月,人民银行700元微星B460主板业界标杆!变身Z490破解超频4千4上个文章提及的ASUS开始借鉴并学习MSI的策略,这说明了,MSI真的成功了。微星B460M迫击炮主板,基本上成为了B460主板的业界标杆,最顶端的巅峰存在,只因为那强悍的供电,能为打工人打造最接地气的超市购物节2020年,中国与世界遭遇到了前所未有的艰难,每一个人都见证了这个历史。而正是因为社会中无数平凡的付出者们挺身而出,他们当中聚集着医护人员军人建筑工人,甚至快递小哥等各行各业的劳动涨价到2000元!1660S6G还有性价比吗?鲁大师20万虽然近期显卡市场非常痛苦,折磨的消费者很是难过。但是魔改君还是想办法从市场上搞到了一些好玩的东西1,影驰1660S6G乞丐骁将版2,影驰1660S6G金属大师OC版3,映众RTX3300元处理器装迷你电脑跑分30万!超频4。3G功耗160W本文是一篇作业帖主角是一台QL3XH110RX5804G的12L迷你电脑主机,这个尺寸相对正统的ITX机箱尺寸显然有点大了,但是对比先马趣造那类的机箱,却几乎小了一半。同样可以放在蓝宝石5804G满血版跑分20万!垃圾佬心中永远的神显卡诚然,大家现在手里都没有靠的显卡,魔改君手上也恰了一些好货,但星星之火怎可燎原?影驰1650SUPER大将OC,二手黄鱼价格1500,魔改君1300找了一些,影驰1660SUPER69元国产精品小电源干翻美商!12L体积ITX机箱年底大涨最近魔改君也玩了很多ITX的机箱,总结了一些特点1,SFX电源比较舒服,1U电源比较麻烦2,容积12L以下的体积,并且能兼容MATX主板(非全尺寸)的机箱,能大幅度节约主板投资3,国际排名五年提升11位,这家支付公司的底气何来全球座次创新高,银联商务位列第7关于中国获得支付业务牌照的第三方支付机构,很多人都能随口说出几个,但是如果要问到哪家第三方支付机构位列全球前十名,恐怕能说出来的就寥寥无几了。最近全出厂又灰烬!华硕玩家国度ITX主板AMD锐龙5代超4400说起内存超频,就不得不提及ITX主板,因为主板体积小,内存走线直接,距离更短,所以ITX主板的内存信号是最强的存在,所以,大部分内存超频记录,反而都是由ITX主板得到,而并非是AT500元英特尔主板被坑!260元金士顿内存被骗!黄鱼明目张胆本文算是一篇随笔吧作为一名有素质,有技术,有情操的垃圾佬,魔改君和各位粉丝小伙伴们一样,常年游走于大黄鱼,寻找着捡漏的机会。当年,捡不到也没什么,货真价实也不亏。但随着时间的推移,INTEL10代处理器真的可以上9代主板?单核500分魔改U要么说自媒体是一个神奇的地方么,今天收到一个神秘组织的信息,说给我发了两种笔记本魔改处理器1,大家熟知的QNCT,正常叫法应该是I78850HES版本?2,型号未知,告知是9代U,
声姿丨小米电视6OLED,极致亲民的爆款选择报告显示,仅2021上半年全球OLED电视出货量就达到了270万台,同比增长133。3。这一数据接近持平2020年全年市场,并超过此前任何一年OLED电视的全球全年销量。OLED电声姿XiaomiWatchColor2,你喜欢的都在这里9月27日,小米召开以天生好看为主题的新品发布会,正式发布了潮流运动的XiaomiWatchColor2。作为一款高颜值运动手表,XiaomiWatchColor2搭配简约彩色表带2021全球智博会,思必驰AI场景协创未来版图作为苏州市获批国家新一代人工智能创新发展试验区后的首个人工智能产业盛会,9月16日,2021全球人工智能产品应用博览会开幕,众多知名企业前沿AI技术与产品惊艳亮相。思必驰作为人工智会议魔方M1实时字幕所见即所听报告显示,占比高达50。1的新生代农民工朝着信息传输软件和信息服务业聚集。这群科技弄潮儿,拥有极具创造力的思维方式,生活上,他们热衷智能穿戴智能家居等黑科技产品工作上,他们喜欢翻译9月VR圈以硬件技术消息为主,元宇宙热度居高不下(VRPinea10月13日讯)2021年第3季度末,伴随着温度的居高不下,元宇宙也被国内外大厂纷纷追捧。除了Facebook将用5000万美元基金,来开发元宇宙项目Horizon彭博社泄露MetaMilan智能手表苹果ARVR头显将支持WiFi6E(VRPinea11月2日讯)今日重点新闻彭博社泄露了代号为Milan的Meta智能手表。这款智能手表采用了大屏幕显示屏可能支持视频通话的摄像头苹果分析师郭明錤称,苹果ARVR头显域乎上榜2020年度最具成长力区块链企业12月24日,域乎CEO曹胜虎受邀出席由浙商产业区块链促进联盟宏链财经举办的第三届数字经济与区块链应用峰会暨2020年度区块链行业颁奖盛典。会议现场颁布了2020年度区块链行业榜单喜报!域乎一项目入选综合能源服务优秀项目案例集近日,中国综合能源服务产业发展联盟和北极星电力网联合发布了第一期综合能源服务优秀项目案例集,域乎科技的YHChain项目,经过初选专家评审等层层筛选,成功入选该案例集。这也是继日前域乎科技荣获2020年IT企业社会责任贡献奖近日,2021上海数聚智理产业峰会暨上海市信息化企业家协会会员大会在沪顺利召开。会议深入探讨了数字化转型大背景下IT企业面临的机遇与挑战,并聚焦城市全面数字化转型议题,探索超大城市牛年开门红!苏州域乎荣膺相城区2020年度高质量发展创新奖近日,苏州相城区2020年度高质量发展综合考核表彰大会在苏州召开。大会通报了6大名单,现场进行了2020年度苏州市相城区高质量发展名单的重点表彰,苏州域乎荣幸入选了该名单并获得新经喜讯!苏州域乎入围苏州头雁培育企业名单近日,苏州市工信局公示了2020年苏州市软件和信息服务业推动数字产业化发展头雁企业名单和头雁培育企业。其中苏州域乎区块链科技有限公司(以下简称苏州域乎)入围区块链领域头雁培育企业名