C++堆内存错误:C运行时库检测到向堆内存头部写入了内容

1、问题描述

       最近测试发现一个掩藏很深的bug,这个只有在特定的操作场景下才会出现,和测试数据及场景有强相关性。好在我们找到了复现的办法,直接在Debug调试下复现了问题。复现问题后,弹出了如下的报错提示框:

图片[1]-C++堆内存错误:C运行时库检测到向堆内存头部写入了内容-卡核

       根据提示信息,检测到了堆内存被破坏,程序向堆内存前面的内存区域写入了内容。系统在分配堆内存时,会在给用户使用的堆内存前后加上头信息和尾信息,用来维护和管理这些堆内存,这些堆内存的头部内存和尾部内存区域,应用程序是不能写入的,是系统来维护和管理的。

2、详细分析

       本例是堆内存越界到内存的头部内存区域,而一般情况是越界到内存尾部的,越界到头部则是比较少见的。当然这不排除内存越界的比较长,直接越到另一段堆内存的头部内存区域的。

       VS检测到异常后中断下来,发现是崩溃在对堆内存的delete释放代码上:

图片[2]-C++堆内存错误:C运行时库检测到向堆内存头部写入了内容-卡核

按讲释放一段申请的堆内存是不应该有问题的,如果在new或者delete时报错,排除对堆内存delete两次的场景,那基本可以断定是堆内存被破坏了,导致new或delete出现异常。

       至于到底是什么情况,需要去结合代码去分析了。于是去查看出问题函数的代码上下文,查看相关变量的值,发现我们在new出一段堆内存后,使用数组下标去访问该段堆内存,结果在当前的场景下,数组下标出现了-1的情况,对该-1下标对应的内存区域进行了赋值,所以就越界到了堆内存的头部区域,然后被系统检测到了,系统产生了异常。相关代码如下所示:

void CRenderEngine::DrawText( HDC hDC, CPaintManagerUI* pManager, RECT& rc, LPCTSTR pstrText, DWORD dwTextColor, int iFont, UINT uStyle )
{
    ASSERT(::GetObjectType(hDC) == OBJ_DC || ::GetObjectType(hDC) == OBJ_MEMDC);
    if (pstrText == NULL || pManager == NULL) return;
    ::SetBkMode(hDC, TRANSPARENT);
    ::SetTextColor(hDC, RGB(GetBValue(dwTextColor), GetGValue(dwTextColor), GetRValue(dwTextColor)));
    HFONT hOldFont = (HFONT)::SelectObject(hDC, pManager->GetFont(iFont));

        // 检查一下待绘制的字符串中是否有换行符,如果有换行符,则不能使用下面处理省略号的算法
        bool bHasCarriageReturn = false;
        CStdString strTmp = pstrText;
        if ( strTmp.Find( _T("\n") ) != -1 )
        {
                bHasCarriageReturn = true;
        }
        else if ( strTmp.Find( _T("\r\n") ) != -1 )
        {
                bHasCarriageReturn = true;
        }
        
        if ( (uStyle & DT_WORDBREAK) || bHasCarriageReturn )
        {
                ::DrawText(hDC, pstrText, -1, &rc, uStyle | DT_NOPREFIX );
        }
        else if( uStyle & (DT_END_ELLIPSIS | DT_MODIFYSTRING) )
        {
                // 为了解决endellipsis的省略号显示不全问题:
                // 1)有些情况下省略号的三个点显示不全;
                // 2)win7下省略号显示正常,win10下不显示省略号
                // 现做如下处理,参考
                // https://www.codeproject.com/articles/7079/implementing-the-dt-end-ellipsis-flag-on-the-pocke
                // 注意:下面的代码只能处理单行文字显示时的结尾省略号的问题
                int    nCount       = 0;
                int    nWidth       = rc.right - rc.left;
                SIZE   szStr        = { 0, 0 };
                LPTSTR lpStr        = NULL;        

                // 计算需绘制的字符串字符个数
                nCount = _tcslen(pstrText);
                if( nCount == 0 || nCount == -1 )
                {
                        // 0是没有字符需要绘制,直接返回,-1是计算字符串长度出错
                        ::SelectObject(hDC, hOldFont);
                        return;
                }

                // 复制一份文字内容
                lpStr = new TCHAR[nCount+1];
                ZeroMemory( lpStr , (nCount+1)*sizeof(TCHAR) );
                _tcscpy_s( lpStr, nCount+1, pstrText );
                lpStr[nCount] = _T('\0');

                // 检查如果字符串长度大于绘制区域宽度
                GetTextExtentPoint32( hDC, lpStr, nCount, &szStr );
                if( szStr.cx > nWidth  && nWidth > 0 )
                {
                        // 估算出绘制区域能够显示的字符数量
                        // 估算公式:nWidth/nEstimate = szStr.cx/nCount
                        // 该公式假定字符串中的字符宽度是相等的,实际上数字或字母与中文的宽度是不同的,此处
                        // 只是粗略的估计
                        int nEstimate = (nCount * nWidth) / szStr.cx + 1;
                        if(nEstimate < nCount)
                                nCount = nEstimate;

                        const DWORD dwEllipsisUnicode = 0x2026;

                        // 0x2026对应省略号三个点的Unicode编码
                        lpStr[nCount-1] = (TCHAR)dwEllipsisUnicode;
                        GetTextExtentPoint32( hDC, lpStr, nCount, &szStr );

                        // 当延伸的内容大于绘制区域的宽度,移除这个字符
                        while(szStr.cx > nWidth && nCount > 1)
                        {
                                lpStr[--nCount] = 0;        // 移除字符
                                lpStr[nCount-1] = (TCHAR)dwEllipsisUnicode; // 用...代替

                                GetTextExtentPoint32(hDC, lpStr, nCount, &szStr);
                        }

                        ::DrawText(hDC, lpStr, nCount, &rc, uStyle | DT_NOPREFIX );

                        delete[] lpStr;
                        lpStr = NULL;
                }
                else
                {
                        ::DrawText(hDC, pstrText, -1, &rc, uStyle | DT_NOPREFIX );
                }
        }
        else
        {
                ::DrawText(hDC, pstrText, -1, &rc, uStyle | DT_NOPREFIX );
        }

    ::SelectObject(hDC, hOldFont);
}

3、最后

        我们一定要注意,在使用表达式作为数组下标去访问内存时,一定要注意对数组下标值做保护,放置出现异常的值,比如负值或者超过数组长度的值,我们在此处添加一个保护即可。

© 版权声明
THE END
喜欢就支持一下吧
点赞7339赞赏 分享
评论 抢沙发
头像
欢迎您留下宝贵的见解!
提交
头像

昵称

取消
昵称表情代码图片

    暂无评论内容