热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

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

C常见的三种内存破坏场景和分析以下文章来源于一个程序员的修炼之路,作者河边一枝柳一个程序员的修炼之路主要分享Windows开发与调试,Linux,CC,以及后端开发技

C++常见的三种内存破坏场景和分析
以下文章来源于一个程序员的修炼之路 ,作者河边一枝柳
一个程序员的修炼之路
主要分享Windows开发与调试, Linux, C/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!" <};int main()
{DemoClass testObj;testObj.DoSomething();return 0;
}

Do Task!这个字符串会不会打印出来呢? 可以发现这段程序在VC6中可以打印出来&#xff0c;但是在VS2017中却打印不出来了。那是因为如下原因:
函数原型time_t time( time_t *destTime );&#xff0c;在VC6中time_t默认是32位&#xff0c;而在VS2017中默认是64位。早期程序以为32位中表达最大的时间是2038年&#xff0c;那时候完全够用&#xff0c;但随着计算机本身的发展64位逐渐成为主流time_t在最新的编译器中也默认采用64位&#xff0c;这样时间完全够用以亿年为单位了&#xff0c;那时候计算机发展超出我们想象了。
程序的问题所在m_tRecordTime采用的是int类型&#xff0c;默认为32位&#xff0c;那么其地址作为time_t time( time_t *destTime );函数实参后&#xff0c;在VC6中time_t本身为32位自然也不会出错&#xff0c;但是在VS2017中因为time_t为64位&#xff0c;则time((time_t *)(&m_tRecordTime));后写入了一个64位的值。结合下图&#xff0c;看下这个对象的内存布局&#xff0c;m_bInit的值将会被覆盖&#xff0c;而这里原先的m_bInit的值为1&#xff0c;被覆盖为0&#xff0c;从而导致内存破坏&#xff0c;导致程序执行意想不到的结果。这里只是不输出&#xff0c;那在真实程序中&#xff0c;可能会导致某个逻辑错乱&#xff0c;发生严重的问题。

这个问题修改自然比较简单&#xff0c;将m_tRecordTime定义为time_t类型就可以了。如果有类似的问题发生的时候&#xff0c;比如这个变量的可疑的发生了不该有的变化的时候&#xff0c;你可以查看下这个变量定义的附近是否有内存的操作可能产生溢出&#xff0c;找到问题所在。因为内存上溢的比较多&#xff0c;一般可以查看下定义在当前出现问题的变量的低地址出的变量操作&#xff0c;是否存在可疑的地方。最后&#xff0c;针对这种场景&#xff0c;我们是不是也可以得到一些收获呢&#xff0c;个人总结如下两点:
在定义类型的时候&#xff0c;尽量和原始类型一致&#xff0c;比如这里的time_t有些程序员可能惯性的认为就是32位&#xff0c;那就定义一个时间戳的时候就定义为int了&#xff0c;而我们要做的应该是和原始类型匹配&#xff08;也就是函数的输入类型&#xff09;&#xff0c;将其定义为time_t&#xff0c;于此类似的还有size_t等&#xff0c;这样可以避免未来在数据集变化或者做平台迁移的时候造成不必要的麻烦。
在有一些复杂的场景的下&#xff0c;也许你不得不做类型转换&#xff0c;而这个时候就格外的需要注意或者了解清楚&#xff0c;转换带来的情况和后果&#xff0c;保持警惕&#xff0c;否则就可能是一个潜在的bug。这和开车一样&#xff0c;当你开车的时候如果看到前方车辆忽然产生一个不合常理的变道行为&#xff0c;首先要做的不是喷那辆车&#xff0c;而是集中注意力&#xff0c;看看是否更前方有障碍物或者事故放生&#xff0c;做出相应的反应。
2. 字符串拷贝溢出
这种情况应该是最常见了&#xff0c;我们来看一看样例程序:

#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 <};int main()
{DemoClass testObj;testObj.DoSomething();return 0;
}

这种情况下肉眼可以分析的&#xff0c;输出结果为:

在m_str1的空间为5&#xff0c;但是Hi Coder!包含是10个字符&#xff0c;在调用strcpy(m_str1, “Hi Coder!”);的时候超过了m_str1的空间&#xff0c;于是覆盖了m_str2的内存&#xff0c;从而导致内存破坏。内存溢出这种尤其字符串溢出&#xff0c;程序崩溃可能是小事儿&#xff0c;如果是一个广为流传的软件&#xff0c;那么就很有可能会被黑客所利用。

这种字符串场景如何分析呢&#xff0c;如果程序崩溃了&#xff0c;可以收集Dump先看看被覆盖的地方是什么样的字符串&#xff0c;然后联想看看自己的程序哪里有可能对这个字符串的操作&#xff0c;从而找到原因。别小看这种方法&#xff0c;简单粗暴很有用&#xff0c;曾经就用这种方式分析过Linux驱动模块的内存泄露问题。

那如果还找不到问题呢&#xff1f;如果问题还能重现&#xff0c;那还是有调试手法的&#xff0c;下一节将会进行讲解。

当然最差最差的还是不要放弃代码审查。尤其在这个内存被破坏的附近的逻辑。对于这种场景的建议&#xff0c;比较简单就是使用微软安全函数strcpy_s&#xff0c;注意这里虽然列出了返回值errno_t不过对于微软的实现来说&#xff0c;如果是目标内存空间不够的情况下&#xff0c;在Relase版本下会调用TerminateProcess, 并且要注意的是这个时候抓Dump有时候并不是完整的Dump。
至于微软为什么要这样做&#xff0c;有可能是安全的考虑比崩溃优先级更高&#xff0c;于是在内存溢出不够的时候&#xff0c;直接让程序结束。
errno_t strcpy_s( char *dest, rsize_t dest_size, const char *src);
3. 随机性的内存被修改
这一个一听都快崩溃了&#xff0c;C&#43;&#43;的坑能不能少一点呢。但是确实是会有各种各样的场景让你落入坑内。上一节的程序我稍作修改:

#include
#define BUFER_SIZE_STR_1 5
#define BUFER_SIZE_STR_2 8class 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] &#61; &#39;&#39;;std::cout <};int main()
{DemoClass testObj;testObj.DoSomething();return 0;
}

程序本意是m_str2赋值为Coder, m_str1赋值为Test, 在编程中很多字符串拷贝或者操作中有些是在字符串末尾补有的可能不补, 而在本例中实际上strcpy_s会自动补0&#xff0c;但是有的程序员防止万一&#xff0c;字符串靠背后&#xff0c;在数组的最后一位设置为’’。这种有时候就变成了好心办坏事。
比如这里的m_str1[BUFER_SIZE_STR_2 - 1] &#61; ‘’; &#xff0c;大家注意到没&#xff0c;这里应该改写为m_str1[BUFER_SIZE_STR_1 - 1] &#61; ‘’; &#xff0c;也就是说程序员可能拷贝代码或者不小心写错了BUFER_SIZE_STR_2和BUFER_SIZE_STR_1因为两者宏差不多。只要是人写代码&#xff0c;就有可能会犯这种错误。这个程序的输出变为:

这个程序是比较简单&#xff0c;一目了然&#xff0c;但是在大型程序中呢&#xff0c;这个数组的位置跳跃的访问到了其他变量的位置&#xff0c;你首先得判断这个被跳跃式修改的变量&#xff0c;是不是程序本意造成的&#xff0c;因为混合了这么多的猜想&#xff0c;可能会导致分析变的异常复杂。那么有什么好的方法吗&#xff1f;只要程序能偶尔重现这个问题&#xff0c;那就是有方法的。

通过Windbg调试命令ba可以在指定的内存地址做操作的时候进入断点。假设目前已经知道m_str2的第四个字符&#xff0c;总是被某个地方误写&#xff0c;那么我们可以在这个地址处设置一个ba命令: 当写的这个内存地址的时候进入断点。不过这样还是有个问题&#xff0c;那就是程序中有可能有很多次对这块内存的写操作&#xff0c;有时候是正常的写操作&#xff0c;如果一直进入断点&#xff0c;人工分析将会非常累&#xff0c;不现实。
这个时候有个方法&#xff0c;同时也是一个workaround&#xff0c;就是当你还没找到程序出错的根本原因的时候在被误踩的内存前面加上一个足够大的不使用的空间。比如下面的代码, m_str2总是被误写&#xff0c;于是在m_str2的前面加上一个100个字节的不使用的内存m_strUnused&#xff08;因为一般程序内存溢出是上溢&#xff0c;当然也可以在m_str2的后面同样加上&#xff09;。
这样我们被踩的内存就很容易落在m_strUnused空间里面了&#xff0c;这个时候我们在其空间里设置写内存操作的断点&#xff0c;就容易捕获到问题所在了。

#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] &#61; &#39;&#39;;std::cout <};int main()
{DemoClass testObj;testObj.DoSomething();return 0;
}

下面完整的展示一下分析过程&#xff1a;
第一步 用Windbg启动(有的情况下可能是Attach&#xff0c;根据情况而定)到调试进程&#xff0c;设置main的断点
0:000> bp ObjectMemberBufferOverFllow!main
*** WARNING: Unable to verify checksum for ObjectMemberBufferOverFllow.exe
0:000> g
Breakpoint 0 hit
eax&#61;010964c0 ebx&#61;00e66000 ecx&#61;00000000 edx&#61;00000000 esi&#61;75aae0b0 edi&#61;0109b390
eip&#61;003a1700 esp&#61;00defa00 ebp&#61;00defa44 iopl&#61;0 nv up ei pl nz na pe nc
cs&#61;0023 ss&#61;002b ds&#61;002b es&#61;002b fs&#61;0053 gs&#61;002b efl&#61;00000206
ObjectMemberBufferOverFllow!main:
003a1700 55 push ebp
第二步 使用p命令单步执行代码到testObj.DoSomething();
第三步 找到testObj的地址为00def984
0:000> dv /t /v
00def984 class DemoClass testObj &#61; class DemoClass
第四步 设置断点到testObj相对偏移的位置&#xff0c;这个位置即&m_str1&#43;BUFER_SIZE_STR_2 - 1 &#61; &m_str1&#43;7。并且继续执行代码:
0:000> ba w1 00def984&#43;7
0:000> g
第五步 你会发现程序运行进入断点&#xff0c;这个时候查看对应的函数调用栈即可。这个断点不一定在一个非常精确的位置&#xff0c;但是当你按照函数调用栈去阅读附近的代码&#xff0c;便比较容易找出问题所在了。
0:000> k


ChildEBP RetAddr

00 00def97c 003a1720 ObjectMemberBufferOverFllow!DemoClass::DoSomething&#43;0x41 […strcpybufferoverflow.cpp &#64; 16]
01 00def9fc 003a1906 ObjectMemberBufferOverFllow!main&#43;0x20 […strcpybufferoverflow.cpp &#64; 30]
02 (Inline) -------- ObjectMemberBufferOverFllow!invoke_main&#43;0x1c [d:agent_workssrcctoolscrtcstartupsrcstartupexe_common.inl &#64; 78]
03 00defa44 75818494 ObjectMemberBufferOverFllow!__scrt_common_main_seh&#43;0xfa [d:agent_workssrcctoolscrtcstartupsrcstartupexe_common.inl &#64; 288]
04 00defa58 770a40e8 KERNEL32!BaseThreadInitThunk&#43;0x24
05 00defaa0 770a40b8 ntdll!__RtlUserThreadStart&#43;0x2f
06 00defab0 00000000 ntdll!_RtlUserThreadStart&#43;0x1b
总结
以上对三种内存破坏场景做了分析&#xff0c;在实际应用中将会变的更加复杂。在写代码的时候要注意避开其中的坑&#xff0c;有个叫做墨菲定律&#xff0c;你感觉可能会出问题的地方&#xff0c;那它一定会在某个时刻出现&#xff0c;当你对某个地方有所疑虑的时候一定要多加考虑&#xff0c;否则这个坑可能查找的时间&#xff0c;比写代码的时间要长的许多&#xff0c;更可怕的是可能会带来意想不到的后果。同样的分析问题要保持足够的耐心&#xff0c;相信真相总会出现&#xff0c;这样的底气也是来自于自己平时不断的学习和实践。
内存破坏问题不区分栈上还是堆上&#xff0c;我们在产品中离不开使用堆开间&#xff0c;而且由多个模块核心功能模块组成&#xff0c;而这些模块通常是公用一个进程默认堆的。所以也有人推荐在这些关键模块中&#xff0c;各自创建一个独立的堆&#xff0c;从而降低一个堆内存的使用对另一个堆中内存的影响。虽然不是完全隔离&#xff0c;但是也是一个聊胜于无的操作了。


推荐阅读
  • 首先说一下,这是我在CSDN上的第一个文章,其实这个账号早在几年前就申请了,不过当时只是为了下载一个资源,而且也不怎么懂信息技术相关的领域,后来就再也没怎么动过,直到今天我才开始使用这个账号 ... [详细]
  • 应用程序配置详解
    本文介绍了配置文件的关键特性及其在不同场景下的应用,重点探讨了Machine.Config和Web.Config两种主要配置文件的用途和配置方法。文章还详细解释了如何利用XML格式的配置文件来调整应用程序的行为,包括自定义配置、错误处理、身份验证和授权设置。 ... [详细]
  • 手把手教你构建简易JSON解析器
    本文将带你深入了解JSON解析器的构建过程,通过实践掌握JSON解析的基本原理。适合所有对数据解析感兴趣的开发者。 ... [详细]
  • 本文档详细介绍了服务器与应用系统迁移的策略与实施步骤。迁移不仅涉及数据的转移,还包括环境配置、应用兼容性测试等多个方面,旨在确保迁移过程的顺利进行及迁移后的系统稳定运行。 ... [详细]
  • 本文详细探讨了在Windows Server 2003环境下遇到MySQL连接失败(错误代码10061)的解决方案,包括通过卸载特定的Windows更新和调整系统注册表设置的方法。 ... [详细]
  • 本文探讨了数据挖掘技术的发展及其在大数据环境下的应用流程,重点介绍了统计学、在线分析处理、信息检索、机器学习、专家系统和模式识别等领域的最新进展。 ... [详细]
  • 学习目的:1.了解android线程的使用2.了解主线程与子线程区别3.解析异步处理机制主线程与子线程:所谓主线程,在Windows窗体应用程序中一般指UI线程,这个是程序启动的时 ... [详细]
  • Web安全入门:MySQL基础操作与SQL注入防范
    本文详细介绍了MySQL数据库的基础操作命令,包括数据库和表的基本管理,以及数据的增删查改等常用操作。同时,针对Web安全领域常见的SQL注入问题,提供了初步的理解和防范措施。 ... [详细]
  • mysql 分库分表策略_【数据库】分库分表策略
    关系型数据库本身比较容易成为系统瓶颈,单机存储容量、连接数、处理能力都有限。当单表的数据量达到1000W或100G以后,由于查询维度较多, ... [详细]
  • 在现代多线程编程中,Lock接口提供的灵活性和控制力超越了传统的synchronized关键字。Lock接口不仅使锁成为一个独立的对象,还提供了更细粒度的锁定机制,例如读写锁(ReadWriteLock)。本文将探讨如何利用ReentrantReadWriteLock提高并发性能。 ... [详细]
  • C语言实现句子中单词顺序反转
    本题源自《C语言程序设计——现代方法》第八章编程练习14,要求编写一个程序,能够接收用户输入的一句话,并将其中的单词顺序进行反转。 ... [详细]
  • 本文深入探讨了企业级开发框架NHibernate和Spring.NET的关键特性之一——面向方面编程(AOP)。文章不仅介绍了AOP的基本概念及其如何增强面向对象编程(OOP),还详细说明了Spring.NET中AOP的具体应用,包括事务管理和自定义方面的实现。 ... [详细]
  • Python作为一种广泛使用的高级编程语言,以其简洁的语法、强大的功能和丰富的库支持著称。本文将详细介绍Python的主要特点及其在现代软件开发中的应用。 ... [详细]
  • java锁策略
    文章目录锁的分类一、乐观锁VS悲观锁二、读写锁三、可重入锁VS不可重入锁四、重量级锁VS轻量级锁五、公平锁VS非公平锁六、自旋锁VS挂起等待锁七、锁升级策略1、无锁: ... [详细]
  • 本文详细介绍了MySQL中关于员工数据库的基础知识、操作技巧以及常见问题的解决方案,适合初学者和有一定基础的用户阅读。 ... [详细]
author-avatar
玩偶0-0
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有