你好,我叫Steven Toscano,我是Visual C++团队的SDET技术领头羊。 我目前在IDE小组工作,致力于改进我们的测试方法和扩展我们的工具库。 在我7年的职业生涯中,我一直是Visual C++中的许多团队的成员,包括前端、后端优化器、库(MFC、ATL、CRT)和现在IDE。
我从工作中得到的许多乐趣之一就是能够诊断问题并发现其根本原因。 我喜欢尽可能深入地挖掘东西,直到asm盯着我的脸,暴露出那些讨厌的小虫子。 有时候没必要这么深入,有时候你必须对如何诊断问题有创意。 在处理复杂的多线程错误时尤其如此。
我调查的一个特别的窃听器变成了为期三天的调查。 错误的净效果如下所示:
1.打开devenv
2、加载包含两个C++项目的解决方案
3.转到“选项”对话框,将“最大并行项目生成数”设置为2(如果您在双处理器机器上安装了VS,我认为这是默认值)
4.构建解决方案
结果是在处发生访问冲突 VCProjectEngine.dll!CBldFileRegEntry::ReleaseFRHRef()
没错,IDE崩溃了! 客户可能遇到的最糟糕的行为(除了数据丢失)。 更妙的是,这种崩溃并不总是可以重现的;它可能在100次中有1次发生,或者根本不发生。
因此,典型的过程是获得精确的复制步骤,在AV命中时获得完整的调用堆栈,并将转储附加到bug。 在我们的bug数据库中快速搜索之后,我发现了一个bug,这个bug有一个类似的调用堆栈,是我们团队以外的人在一个月前记录的。 我有点担心,因为这个错误被解决为没有复制,并包括完整的转储信息!
因为我在我的多进程机器上遇到了这个问题,所以我决定更深入地研究一下。 首先,AV的调用堆栈看起来有点奇怪。 请注意堆栈顶部的函数。
> 0996f4f3()
VCProjectEngine.dll!CBldFileRegEntry::ReleaseFRHRef
VCProjectEngine.dll!CBldFileRegistry::LookupFile文件
VCProjectEngine.dll!CVCBuildRegistryManipulator::查找文件
VCProjectEngine.dll!GetFileFullPathI文件
VCProjectEngine.dll!获取文件完整路径
VCProjectEngine.dll!cblIncludeEntry::FindFile
VCProjectEngine.dll!CVCToolImpl::ResolveIncludeDirectivesI
VCProjectEngine.dll!CVCToolImpl::ResolveIncludeDirectivesToPath
VCProjectEngine.dll!炉渣库助手::PickUpDeps
VCProjectEngine.dll!clinkerLibraryHelper::DoGetDependencies
VCProjectEngine.dll!ClinkerLibraryHelper::DoGetAdditionalDependencies内部
VCProjectEngine.dll!CVCLinkerTool::GetAdditionalDependenciesInternal
VCProjectEngine.dll!CVCToolImpl::GetCommandLineOptions
VCProjectEngine.dll!CVCToolImpl::GetCommandLineOptions
VCProjectEngine.dll!CBldToolWrapper::GetCommandLineOptions
VCProjectEngine.dll!CBldAction::刷新命令选项
VCProjectEngine.dll!CBldFileDepGraph::EnumerateBuildActionsI
VCProjectEngine.dll!CBldFileDepGraph::RetrieveBuildActions
VCProjectEngine.dll!CDynamicBuildEngine::DoBuild
VCProjectEngine.dll!CDynamicBuildEngine::DoBuild
VCProjectEngine.dll!C配置::DoPrepareBuild
VCProjectEngine.dll!C配置::TopLevelBuild
VCProjectEngine.dll!CVCBuildThread::构建线程
kernel32.dll!基本线程开始
无法解析为符号名,并且在内存窗口中进行检查后可以确定该地址指向外部空间。 以下是此时的相关源代码:
无效 CBldFileRegEntry::ReleaseFRHRef()
{
//阻止其他线程访问注册表。
CritSectionT cs(gu sectionFileMap);
如果 (mu nRefCount<=0)
返回 ;
mèn计数–;
如果 (mu nRefCount==0)
SafeDelete();
}
调用SafeDelete函数时发生AV。 我遇到过一次这个错误,并在我的机器上打开了整整三天的调试会话。 考虑到调试的工作环境,这会使我的机器在日常工作中变慢,但我不得不离开它,以防万一我不能重新编程。
在一个调用站点钻研这个神秘的AV,我认为它是一个糟糕的代码生成器,下面是ReleaseFRHRef函数的相关asm:
如果(mu nRefCount<=0)
5B00B14A型 压敏电阻 eax,dword ptr[edi+18h]
5B00B14D型 测试 eax,eax
返回;
5B00B14F型 jbe公司 CBldFileRegEntry::ReleaseFRHRef+32h(5B00B15Eh)
mèn计数–;
5B00B151型 12月 eax公司
5B00B152型 压敏电阻 dword ptr[edi+18h],eax公司
如果(mu nRefCount==0)
5B00B155型 jne公司 CBldFileRegEntry::ReleaseFRHRef+32h(5B00B15Eh)
SafeDelete();
5B00B157型 压敏电阻 eax,dword ptr[电子数据交换]
5B00B159型 压敏电阻 ecx、edi
5B00B15B型 呼叫 dword ptr[eax+8]
最后三条指令是编译器为通过vtable的调用生成的代码。 当然,在调试代码SafeDelete之后,这是我们在这个类中开始执行代码以来进行的第一个虚拟函数调用。 EDI是this指针的源代码,它取消了对vtable的引用。 vtable上8的偏移量应该会返回SafeDelete虚拟函数的地址(因为它是第三个vfunction)。 但正如我们所看到的,间接寻址会导致访问冲突。 所以有人把我们的东西,包括vtable,都扔了。
现在的问题是谁毁了我们的东西? 这听起来像是一个裁判计数的问题。 我开始通过查看CBldFileRegEntry类的实现和引用位置来了解它是如何被使用的。 这个类被重新安装以允许通过不同的线程同时访问。 这是通过共享引用或句柄完成的这个句柄的使用受到贯穿整个代码的关键部分的保护。
埃弗雷特出货后,VisualC++团队开始为WHDIDY产品并行工程建设(参见此) 博客 详细信息)。 如果您有一个包含许多叶节点项目(即没有内部依赖关系的项目)的大型解决方案,则此功能非常适合多处理器机器。 此调用堆栈上的类集是为在Whidbey中启用此功能而修改的一些类。
所有这些很酷的功能都是随机崩溃的吗? 我必须更深入地了解发生了什么。 参考计数理论开始有意义了。 如果一个线程过早地减少了引用计数,那么另一个线程可能会认为没有对共享对象的引用,并将其删除。 当另一个线程仍在使用该对象时,下一次访问该对象的内容会导致代码中间出现一个AV。 事实上,我们看到一个AV是一件好事- Visual C++ 2005会弄脏内存,如果你删除一个对象(用0xFEEEE FEEE序列填充)想象一下,如果不是这样的话,你就不会看到任何崩溃只是随机错误行为。
对于bug中的信息,您无能为力——如果它不容易复制,您就无法重新运行场景并遍历代码。 在垃圾堆里你只能看到 *结束* 结果–造成损坏的螺纹早就不见了。
所以我采取了不同的方法,将相关的代码片段提取到我自己的模拟中。 这很简单,因为类是非常独立的,我删除了对不重要的东西的任何引用或依赖。 然后我为代码创建了一个外壳——直接从这个MSDN上取下 文章 . 它是一个通过CreateThread()Win32 API创建线程的简单应用程序。 我把线程数设为30。 在我的ThreadProc中,我添加了访问共享资源的代码,方法与CBldFileRegEntry代码相同。 当然,我每隔一次都会打一次AV。 所以现在我有一个非常一致的批评!
有了这个小设置,我就可以开始用二进制搜索技术接近这个bug了。 我在代码的前半部分放了一个大的关键部分块,其余部分保持原样。 我运行了这个场景,但它没有重新编译(因为整个代码都阻塞了所有线程!)。 因为它没有击中我尝试了下半场,它开始重新开始。 现在我有了正确的一半,我继续这个过程,直到我降到一个功能级别。 以下是可疑函数:
BldFileRegHandle CBldFileRegFile::GetFileHandle(LPCOLESTR szFileName,BOOL bVerifyCase)
{
CPathW路径;
LPCOLESTR szKey公司;