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


推荐阅读
  • 本文详细介绍如何使用Python进行配置文件的读写操作,涵盖常见的配置文件格式(如INI、JSON、TOML和YAML),并提供具体的代码示例。 ... [详细]
  • UNP 第9章:主机名与地址转换
    本章探讨了用于在主机名和数值地址之间进行转换的函数,如gethostbyname和gethostbyaddr。此外,还介绍了getservbyname和getservbyport函数,用于在服务器名和端口号之间进行转换。 ... [详细]
  • 从 .NET 转 Java 的自学之路:IO 流基础篇
    本文详细介绍了 Java 中的 IO 流,包括字节流和字符流的基本概念及其操作方式。探讨了如何处理不同类型的文件数据,并结合编码机制确保字符数据的正确读写。同时,文中还涵盖了装饰设计模式的应用,以及多种常见的 IO 操作实例。 ... [详细]
  • 使用Vultr云服务器和Namesilo域名搭建个人网站
    本文详细介绍了如何通过Vultr云服务器和Namesilo域名搭建一个功能齐全的个人网站,包括购买、配置服务器以及绑定域名的具体步骤。文章还提供了详细的命令行操作指南,帮助读者顺利完成建站过程。 ... [详细]
  • 题目Link题目学习link1题目学习link2题目学习link3%%%受益匪浅!-----&# ... [详细]
  • 本文详细介绍了如何在Linux系统上安装和配置Smokeping,以实现对网络链路质量的实时监控。通过详细的步骤和必要的依赖包安装,确保用户能够顺利完成部署并优化其网络性能监控。 ... [详细]
  • 题目描述:给定n个半开区间[a, b),要求使用两个互不重叠的记录器,求最多可以记录多少个区间。解决方案采用贪心算法,通过排序和遍历实现最优解。 ... [详细]
  • 本文详细介绍了 Dockerfile 的编写方法及其在网络配置中的应用,涵盖基础指令、镜像构建与发布流程,并深入探讨了 Docker 的默认网络、容器互联及自定义网络的实现。 ... [详细]
  • 使用 Azure Service Principal 和 Microsoft Graph API 获取 AAD 用户列表
    本文介绍了一段通用代码示例,该代码不仅能够操作 Azure Active Directory (AAD),还可以通过 Azure Service Principal 的授权访问和管理 Azure 订阅资源。Azure 的架构可以分为两个层级:AAD 和 Subscription。 ... [详细]
  • 前言--页数多了以后需要指定到某一页(只做了功能,样式没有细调)html ... [详细]
  • 在金融和会计领域,准确无误地填写票据和结算凭证至关重要。这些文件不仅是支付结算和现金收付的重要依据,还直接关系到交易的安全性和准确性。本文介绍了一种使用C语言实现小写金额转换为大写金额的方法,确保数据的标准化和规范化。 ... [详细]
  • 本文详细介绍了如何构建一个高效的UI管理系统,集中处理UI页面的打开、关闭、层级管理和页面跳转等问题。通过UIManager统一管理外部切换逻辑,实现功能逻辑分散化和代码复用,支持多人协作开发。 ... [详细]
  • ImmutableX Poised to Pioneer Web3 Gaming Revolution
    ImmutableX is set to spearhead the evolution of Web3 gaming, with its innovative technologies and strategic partnerships driving significant advancements in the industry. ... [详细]
  • 本文详细分析了Hive在启动过程中遇到的权限拒绝错误,并提供了多种解决方案,包括调整文件权限、用户组设置以及环境变量配置等。 ... [详细]
  • 本文详细探讨了VxWorks操作系统中双向链表和环形缓冲区的实现原理及使用方法,通过具体示例代码加深理解。 ... [详细]
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社区 版权所有