目录
1、0xcccccccc、0xcdcdcdcd和0xfeeefeee等常见异常值的说明
2、由0xcdcdcdcd 异常值引发的内存访问违例问题说明
2.1、用户态内存地址与内核态用户地址
2.2、根据0xcdcdcdcd异常值初步估计出引发问题的原因
3、详细分析与问题解决
4、变量未初始化在Debug和Release下的差异
5、最后
VC++常用功能开发汇总(专栏文章列表,欢迎订阅,持续更新...)https://blog.csdn.net/chenlycly/article/details/124272585C++软件异常排查从入门到精通系列教程(专栏文章列表,欢迎订阅,持续更新...)https://blog.csdn.net/chenlycly/article/details/125529931 以前在总结Visual Studio高效调试手段与技巧时,特别提到了对0xcccccccc、0xcdcdcdcd和0xfeeefeee等常见异常值的辨识度,当时有些人可能对这点不以为然。今天我们就来通过一个问题实例来讲述一下这点的重要性。如果在调试过程中遇到这些异常值,通过这些特殊值的具体含义,也许就能找到排查问题的线索,并最终定位问题。
1、0xcccccccc、0xcdcdcdcd和0xfeeefeee等常见异常值的说明
0xcccccccc 、0xcdcdcdcd 和0xfeeefeee 等常见异常值的说明如下所示:
* 0xcccccccc : Used by Microsoft's C++ debugging runtime library to mark uninitialised stack memory
* 0xcdcdcdcd : Used by Microsoft's C++ debugging runtime library to mark uninitialised heap memory
* 0xfeeefeee : Used by Microsoft's HeapFree() to mark freed heap memory
* 0xabababab : Used by Microsoft's HeapAlloc() to mark "no man's land" guard bytes after allocated heap memory
* 0xabadcafe : A startup to this value to initialize all free memory to catch errant pointers
* 0xbaadf00d : Used by Microsoft's LocalAlloc(LMEM_FIXED) to mark uninitialised allocated heap memory
* 0xbadcab1e : Error Code returned to the Microsoft eVC debugger when connection is severed to the debugger
* 0xbeefcace : Used by Microsoft .NET as a magic number in resource files
对于0xcccccccc 和0xcdcdcdcd,在 Debug 模式下,VS会把未初始化的栈内存全部填充成0xcccccccc ,当成字符串看就是“烫烫烫烫……”;VS会把未初始化的堆内存全部填充成 0xcdcdcdcd ,当成字符串看就是 “屯屯屯屯……”。这两类特殊的字符串,很多人应该都见到过。
那么调试器为什么要这么做呢?VS在Debug下把未初始化的内存自动填充成0xcccccccc 或0xcdcdcdcd ,而不是就让取随机值,是为了方便我们快速识别出问题,如果内存中出现这两个值,很可能就是因为变量没有初始化导致的。如果内存中的值是随机的,那么每次调试程序时就可能出现不一样的结果,比如这次程序崩掉,下次却能正常运行,这样显然对我们解bug是非常不利的。
如果在Debug调试时遇到有变量的值为0xcccccccc 或0xcdcdcdcd ,一般是变量没有初始化引起的。没有初始化的栈变量(在栈上分配内存的变量)的内存会被初始化为0xcccccccc ,没有初始化的堆变量(在堆上分配内存的变量)的内存会被初始化为0xcdcdcdcd 。
对于0xfeeefeee ,是用来标记堆上已经释放掉的内存,即已经释放的堆内存会被填充成0xfeeefeee 。
注意,如果指针指向的内存被释放了,指针变量本身的地址是没做改动的,还是之前指向的内存的地址,只是指向的堆内存会被填充成0xfeeefeee 。
如果该指针是一个类的成员变量,并且该类对象是在堆上分配内存的,则该类对象的堆内存被释放后(对于C++类,通常是执行delete操作),类对象中的指针变量就会被赋值为0xfeeefeee 。如果在调试代码过程中,发现指针的值为0xfeeefeee ,就说明该指针所在类对象的内存被释放掉了,如果继续使用该指针变量,可能就会出问题了。我们在日常调试时会经常遇到0xfeeefeee 这个异常值的。
2、由0xcdcdcdcd 异常值引发的内存访问违例问题说明
新人在VS中对代码进行Debug调试,调试某个新功能时会时不时遇到崩溃,相关信息如下所示:
这个崩溃不是必现的,但复现的概率很大。
2.1、用户态内存地址与内核态用户地址
根据提示信息得知,是0xcdcdcdcd 异常值引发的内存访问违例的崩溃,崩溃处的类对象的this值为0xcdcdcdcd。这个地址值对于32位程序来说,其大于0x80000000,属于内核态的地址范围,用户态的代码是禁止访问内核态内存地址的,所以触发了内存访问违例。
一般情况下,软件中的业务模块都是运行在用户态中的,进程中加载的系统内核模块是运行进程的内核态中的,比如驱动程序就是运行在内核态的。
那到底哪些地址范围是属于用户态的,哪些内存地址范围是属于内核态的呢?以Windows中的32位程序程序为例,系统会给32程序进程分配4GB的虚拟地址空间,一般情况下,32位程序进程的内核态和用户态各占一半(各占2GB),即用户态的内存地址范围为:0x00000000 - 0x7FFFFFFF,内核态的内存地址范围为:0x80000000 - 0xFFFFFFFF。
关于不同系统模式下的内核态和用户态内存的划分,可以参加《Windows核心编程》一书的第13章关于Windows虚拟地址空间如何分区一节中的说明:
在Linux系统中,32位程序的4GB总的虚拟内存空间,用户态占3GB,内核态占1GB。
2.2、根据0xcdcdcdcd异常值初步估计出引发问题的原因
对于0xcdcdcdcd ,其含义为: Used by Microsoft's C++ debugging runtime library to mark uninitialised heap memory,即微软C++调试运行时库会将没有初始化的堆内存都置为0xcdcdcdcd 。说明该处的CWindowWnd指针是没有初始化的,然后到call stack窗口中查看函数调用堆栈,看看是哪个函数使用了没有初始化的CWindowWnd*指针的。
3、详细分析与问题解决
上述崩溃问题,是在VS中调试代码时遇到的,于是切换到call stack函数调用堆栈页面,查看到如下的函数调用堆栈:
从函数调用堆栈中可以看出,是CRtcFullVideoWndUI::OnUpdateBigVideoCursor函数调用了发生异常的代码,在CRtcFullVideoWndUI::OnUpdateBigVideoCursor函数中用到了CSmallVideoWndUI* m_pBigVideoChildWnd成员变量,其中CSmallVideoWndUI类是继承于CWindowWnd的。
上述发生崩溃的C++对象就是m_pBigVideoChildWnd指针变量指向的对象。
根据0xcdcdcdcd异常值的含义, 可以推测,在CRtcFullVideoWndUI::OnUpdateBigVideoCursor函数中使用到CSmallVideoWndUI* m_pBigVideoChildWnd变量时,该变量没有初始化。
给CSmallVideoWndUI* m_pBigVideoChildWnd变量赋值的操作位于CRtcFullVideoWndUI::UpdateRtcBigVideo函数中,如下所示:
void CRtcFullVideoWndUI::UpdateRtcBigVideo()
{
::SetParent(m_pToolBarWnd->GetHWND(), m_hWnd);
::SetParent(m_pStatusBarWnd->GetHWND(), m_hWnd);
::DestroyWindow(m_pBigVideoChildWnd->GetHWND());
m_pBigVideoChildLay->SetAutoDestroy(false);
m_pBigVideoChildLay->RemoveAt(0);
m_pBigVideoChildLay->SetAutoDestroy(true);
m_pBigVideoChildWnd = new CSmallVideoWndUI();
m_pBigVideoChildWnd->SetBigVideo(true);
m_pBigVideoChildWnd->SetWndColor(0xFF000000);
m_pBigVideoChildWnd->SetFixedHeight(m_pRtcVideoParentLay->GetBigVideoHeight());
m_pBigVideoChildWnd->SetFixedWidth(m_pRtcVideoParentLay->GetBigVideoWidth());
m_pBigVideoChildWnd->SetName(_T("SmallVideoWnd"));
}
在本例的崩溃场景中,运行到CRtcFullVideoWndUI::OnUpdateBigVideoCursor函数中时,上述给CSmallVideoWndUI* m_pBigVideoChildWnd变量赋值的函数CRtcFullVideoWndUI::UpdateRtcBigVideo还没执行到,即CRtcFullVideoWndUI::OnUpdateBigVideoCursor函数先于CRtcFullVideoWndUI::UpdateRtcBigVideo函数执行。
这个地方牵涉到一个老生常谈的问题,定义变量时一定要给变量初始化。很多人没有初始化变量的习惯,结合以往的问题排查案例,不少问题都是由于变量未初始化引起的。
为了解决本问题,需要先将CSmallVideoWndUI* m_pBigVideoChildWnd变量的初值赋为NULL,放在CSmallVideoWndUI的构造函数中处理即可;然后在CRtcFullVideoWndUI::OnUpdateBigVideoCursor函数中使用CSmallVideoWndUI* m_pBigVideoChildWnd指针变量之前,判断该指针变量是否为空,代码如下所示:
bool CRtcFullVideoWndUI::OnUpdateBigVideoCursor(WPARAM wParam, LPARAM lParam, bool & bHandled)
{
if ( m_pBigVideoChildWnd != NULL )
{
::PostMessage(m_pBigVideoChildWnd->GetHWND(), WM_LBUTTONUP, 0, 0);
::PostMessage(m_pBigVideoChildWnd->GetHWND(), WM_MOUSELEAVE, 0, 0);
}
bHandled = true;
return true;
}
4、变量未初始化在Debug和Release下的差异
对于Visual C++编译器,Debug下会将栈内存自动初始化为0xcccccccc ,会将堆内存自动初始化为0xcdcdcdcd。但Release下不会对栈内存和堆内存做任何的初始化,此时未初始化的变量的内存中的内容是分配内存时分配到的内存的“残留值”,这样未初始化的变量的值就是随机的。
所以因为变量值未初始化导致的问题,在Debug和Release下的现象可能是不同的,比如Debug下运行没问题但Release下运行有问题或者Release下运行没问题但Debug下运行有问题,这样的问题我们在项目中遇到过不少次了。所以大家平时写代码时,一定要养成初始化变量的好习惯!
5、最后
一个不起眼的或者难度不大的问题,有时候也值得去推敲和思考的,小问题也能整理出一些有价值的细节点。积小流以成江海,积跬步以至千里!大家在日常工作中一定要多思考多总结,思考总结的多了,经验也就越多了!