热门标签 | 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;但是也是一个聊胜于无的操作了。


推荐阅读
  • 本文介绍了P1651题目的描述和要求,以及计算能搭建的塔的最大高度的方法。通过动态规划和状压技术,将问题转化为求解差值的问题,并定义了相应的状态。最终得出了计算最大高度的解法。 ... [详细]
  • 2018年人工智能大数据的爆发,学Java还是Python?
    本文介绍了2018年人工智能大数据的爆发以及学习Java和Python的相关知识。在人工智能和大数据时代,Java和Python这两门编程语言都很优秀且火爆。选择学习哪门语言要根据个人兴趣爱好来决定。Python是一门拥有简洁语法的高级编程语言,容易上手。其特色之一是强制使用空白符作为语句缩进,使得新手可以快速上手。目前,Python在人工智能领域有着广泛的应用。如果对Java、Python或大数据感兴趣,欢迎加入qq群458345782。 ... [详细]
  • 开发笔记:加密&json&StringIO模块&BytesIO模块
    篇首语:本文由编程笔记#小编为大家整理,主要介绍了加密&json&StringIO模块&BytesIO模块相关的知识,希望对你有一定的参考价值。一、加密加密 ... [详细]
  • 本文介绍了C#中生成随机数的三种方法,并分析了其中存在的问题。首先介绍了使用Random类生成随机数的默认方法,但在高并发情况下可能会出现重复的情况。接着通过循环生成了一系列随机数,进一步突显了这个问题。文章指出,随机数生成在任何编程语言中都是必备的功能,但Random类生成的随机数并不可靠。最后,提出了需要寻找其他可靠的随机数生成方法的建议。 ... [详细]
  • 本文主要解析了Open judge C16H问题中涉及到的Magical Balls的快速幂和逆元算法,并给出了问题的解析和解决方法。详细介绍了问题的背景和规则,并给出了相应的算法解析和实现步骤。通过本文的解析,读者可以更好地理解和解决Open judge C16H问题中的Magical Balls部分。 ... [详细]
  • Android Studio Bumblebee | 2021.1.1(大黄蜂版本使用介绍)
    本文介绍了Android Studio Bumblebee | 2021.1.1(大黄蜂版本)的使用方法和相关知识,包括Gradle的介绍、设备管理器的配置、无线调试、新版本问题等内容。同时还提供了更新版本的下载地址和启动页面截图。 ... [详细]
  • 解决Cydia数据库错误:could not open file /var/lib/dpkg/status 的方法
    本文介绍了解决iOS系统中Cydia数据库错误的方法。通过使用苹果电脑上的Impactor工具和NewTerm软件,以及ifunbox工具和终端命令,可以解决该问题。具体步骤包括下载所需工具、连接手机到电脑、安装NewTerm、下载ifunbox并注册Dropbox账号、下载并解压lib.zip文件、将lib文件夹拖入Books文件夹中,并将lib文件夹拷贝到/var/目录下。以上方法适用于已经越狱且出现Cydia数据库错误的iPhone手机。 ... [详细]
  • 计算机存储系统的层次结构及其优势
    本文介绍了计算机存储系统的层次结构,包括高速缓存、主存储器和辅助存储器三个层次。通过分层存储数据可以提高程序的执行效率。计算机存储系统的层次结构将各种不同存储容量、存取速度和价格的存储器有机组合成整体,形成可寻址存储空间比主存储器空间大得多的存储整体。由于辅助存储器容量大、价格低,使得整体存储系统的平均价格降低。同时,高速缓存的存取速度可以和CPU的工作速度相匹配,进一步提高程序执行效率。 ... [详细]
  • 开发笔记:计网局域网:NAT 是如何工作的?
    篇首语:本文由编程笔记#小编为大家整理,主要介绍了计网-局域网:NAT是如何工作的?相关的知识,希望对你有一定的参考价值。 ... [详细]
  • 前景:当UI一个查询条件为多项选择,或录入多个条件的时候,比如查询所有名称里面包含以下动态条件,需要模糊查询里面每一项时比如是这样一个数组条件:newstring[]{兴业银行, ... [详细]
  • 本文讨论了clone的fork与pthread_create创建线程的不同之处。进程是一个指令执行流及其执行环境,其执行环境是一个系统资源的集合。在调用系统调用fork创建一个进程时,子进程只是完全复制父进程的资源,这样得到的子进程独立于父进程,具有良好的并发性。但是二者之间的通讯需要通过专门的通讯机制,另外通过fork创建子进程系统开销很大。因此,在某些情况下,使用clone或pthread_create创建线程可能更加高效。 ... [详细]
  • 本文详细介绍了如何使用MySQL来显示SQL语句的执行时间,并通过MySQL Query Profiler获取CPU和内存使用量以及系统锁和表锁的时间。同时介绍了效能分析的三种方法:瓶颈分析、工作负载分析和基于比率的分析。 ... [详细]
  • 本文讨论了在手机移动端如何使用HTML5和JavaScript实现视频上传并压缩视频质量,或者降低手机摄像头拍摄质量的问题。作者指出HTML5和JavaScript无法直接压缩视频,只能通过将视频传送到服务器端由后端进行压缩。对于控制相机拍摄质量,只有使用JAVA编写Android客户端才能实现压缩。此外,作者还解释了在交作业时使用zip格式压缩包导致CSS文件和图片音乐丢失的原因,并提供了解决方法。最后,作者还介绍了一个用于处理图片的类,可以实现图片剪裁处理和生成缩略图的功能。 ... [详细]
  • 预备知识可参考我整理的博客Windows编程之线程:https:www.cnblogs.comZhuSenlinp16662075.htmlWindows编程之线程同步:https ... [详细]
  • 本文讨论了一个数列求和问题,该数列按照一定规律生成。通过观察数列的规律,我们可以得出求解该问题的算法。具体算法为计算前n项i*f[i]的和,其中f[i]表示数列中有i个数字。根据参考的思路,我们可以将算法的时间复杂度控制在O(n),即计算到5e5即可满足1e9的要求。 ... [详细]
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社区 版权所有