x86/C++ - 指向指针:编译器是否违反了常量?

9
我正在为我的学生项目的游戏引擎编写一个共享指针(称为Handle)实现,我们遇到了一个无法解释的错误。由于某种原因,在我们的工厂中的某个特定点,一个无效的内部指针通过一个句柄被传递到一个工厂中,这导致了在原型加载期间的崩溃。我们调试了数小时的过程,并将任何复杂语句分解成最简单的版本以便于调试。最终我把问题隔离到了Handle类的复制构造函数。但是,似乎还有一些中间步骤会释放内部指针。我阅读了每篇我能找到的关于可能导致此问题的文章,但没有发现任何有用的信息。最后,我决定查看汇编代码并尝试找出发生了什么。以下是没有汇编代码的复制构造函数。
template <class TYPE>
Handle<TYPE>::Handle(const Handle<TYPE>& rhs, bool weak) : m_weak(weak), m_ref_count(rhs.m_ref_count), m_ptr(rhs.m_ptr)
{
  if(!m_weak)
    ++(*m_ref_count);
}

这是带有反汇编的复制构造函数。
template <class TYPE>
Handle<TYPE>::Handle(const Handle<TYPE>& rhs, bool weak) : m_weak(weak), m_ref_count(rhs.m_ref_count), m_ptr(rhs.m_ptr)
{
013FFF50 push         ebp
013FFF51 mov          ebp,esp
013FFF53 sub          esp,0CCh
013FFF59 push         ebx
013FFF5A push         esi
013FFF5B  push        edi  
013FFF5C  push        ecx  
013FFF5D  lea         edi,[ebp-0CCh]  
013FFF63  mov         ecx,33h  
013FFF68  mov         eax,0CCCCCCCCh  
013FFF6D  rep stos    dword ptr es:[edi]  
013FFF6F  pop         ecx  
013FFF70  mov         dword ptr [this],ecx  
013FFF73  mov         eax,dword ptr [this]  
013FFF76  mov         cl,byte ptr [weak]  
013FFF79  mov         byte ptr [eax],cl  
013FFF7B  mov         eax,dword ptr [this]  
013FFF7E  mov         ecx,dword ptr [rhs]  
013FFF81  mov         edx,dword ptr [ecx+4]  
013FFF84  mov         dword ptr [eax+4],edx  
013FFF87  mov         eax,dword ptr [this]  
013FFF8A  mov         ecx,dword ptr [rhs]  
013FFF8D  mov         edx,dword ptr [ecx+8]  
013FFF90  mov         dword ptr [eax+8],edx  
  if(!m_weak)
013FFF93  mov         eax,dword ptr [this]  
013FFF96  movzx       ecx,byte ptr [eax]  
013FFF99  test        ecx,ecx  
013FFF9B  jne         Handle<Component>::Handle<Component>+60h (013FFFB0h)  
    ++(*m_ref_count);
013FFF9D  mov         eax,dword ptr [this]  
013FFFA0  mov         ecx,dword ptr [eax+4]  
013FFFA3  mov         edx,dword ptr [ecx]  
013FFFA5  add         edx,1  
013FFFA8  mov         eax,dword ptr [this]  
013FFFAB  mov         ecx,dword ptr [eax+4]  
013FFFAE  mov         dword ptr [ecx],edx  
}

(据我所知)问题出在这里:
013FFF6D  rep stos    dword ptr es:[edi]

众所周知,这个指令用于快速清除内存。然而,这个指令清除的是rhs Handle内部指针所指向的指针,而且它违反了const规定,因为传入的Handle是一个const引用。我不确定是不是VS的问题,还是我们构建设置中某种标志导致发生了这种情况,但是复制构造函数在尝试使用它创建原型时工作正常。
非常感谢任何帮助!
编辑1:在一些评论提到我可能在代码中返回临时变量后,我进一步研究了这个问题。我没有找到任何表明我确实这样做了的东西,但是我浏览了更多的汇编代码以查找任何提示。这是我们的CreateGenericArchetype函数,在此处问题出现。
Handle<Object> ObjectFactory::CreateGenericArchetype(const std::wstring &ArchetypeName)
{
  /* Create The Object */
  auto archetype = mComponentFactoryMap.find(std::wstring(L"Object"));
  Handle<Component> created_component = archetype->second->CreateArchetypeComponent();
  Handle<Object> retVal = static_handle_cast<Object>(created_component);
  retVal->mArchetypeName = ArchetypeName;
  mArchetypeMap.insert(std::pair<std::wstring, Handle<Object>>(ArchetypeName, retVal));

  return Handle<Object>(retVal, true);
}

目前导致所有问题的代码行是这个:

Handle<Component> created_component = archetype->second->CreateArchetypeComponent();

自然地,我也将我的调试扩展到了CreateArchetypeComponent,因此这里也有相关内容:
Handle<Component> ComponentTypeFactory<Object>::CreateArchetypeComponent() const
{
  Object* created_object = new Object;
  Component* casted_object = static_cast<Component*>(created_object);
  Handle<Component> newComponent(casted_object);
  newComponent->mType = mType;
  newComponent->mID = GetNewID();
  return newComponent;
}

对象继承自组件,因此将对象的 static_cast 降级为组件是有效的,并且构造函数成功地从 Component* 构造了 Handle。问题出现在我们尝试返回在函数内部构造的 Handle 时。以下是该交互的反汇编代码:

return newComponent;
005602AD  push        0  
005602AF  lea         eax,[newComponent]  
005602B2  push        eax  
005602B3  mov         ecx,dword ptr [ebp+8]  
005602B6  call        Handle<Component>::Handle<Component> (04EA050h)  
005602BB  mov         ecx,dword ptr [ebp-110h]  
005602C1  or          ecx,1  
005602C4  mov         dword ptr [ebp-110h],ecx  
005602CA  mov         byte ptr [ebp-4],0  
005602CE  lea         ecx,[newComponent]  
005602D1  call        Handle<Component>::~Handle<Component> (04F2E7Bh)  
005602D6  mov         eax,dword ptr [ebp+8]  
}
00EB02D9  push        edx  
00EB02DA  mov         ecx,ebp  
00EB02DC  push        eax  
00EB02DD  lea         edx,ds:[0EB0318h]  
00EB02E3  call        @_RTC_CheckStackVars@8 (0E4BA9Eh)  
00EB02E8  pop         eax  
00EB02E9  pop         edx  
00EB02EA  mov         ecx,dword ptr [ebp-0Ch]  
00EB02ED  mov         dword ptr fs:[0],ecx  
00EB02F4  pop         ecx  
00EB02F5  pop         edi  
00EB02F6  pop         esi  
00EB02F7  pop         ebx  
00EB02F8  mov         ecx,dword ptr [ebp-10h]  
00EB02FB  xor         ecx,ebp  
00EB02FD  call        @__security_check_cookie@4 (0E4E9BAh)  
00EB0302  add         esp,130h  
00EB0308  cmp         ebp,esp  
00EB030A  call        __RTC_CheckEsp (0E41C3Dh)  
00EB030F  mov         esp,ebp  
00EB0311  pop         ebp  
00EB0312  ret         4  

*******back to CreateGenericArchetype*******

005C2639  call        __RTC_CheckEsp (04F1C3Dh)  
005C263E  mov         dword ptr [ebp-1D4h],eax  
005C2644  mov         ecx,dword ptr [ebp-1D4h]  
005C264A  mov         dword ptr [ebp-1D8h],ecx  
005C2650  mov         byte ptr [ebp-4],4  
005C2654  mov         edx,dword ptr [ebp-1D8h]  
005C265A  push        edx  
005C265B  lea         ecx,[created_component]  
005C265E  call        Handle<Component>::Handle<Component> (04EA050h)

从上面的内容中复制构造函数反汇编:

005C2663  mov         byte ptr [ebp-4],6  
005C2667  lea         ecx,[ebp-174h]  
005C266D  call        Handle<Component>::~Handle<Component> (04F2E7Bh)  
  Handle<Object> retVal = static_handle_cast<Object>(created_component);

除非将我的复制构造函数作为“created_component”作为rhs句柄调用被视为返回本地变量,否则我无法看出可能出错的地方,除非是因为从CreateArchetypeComponent返回的句柄在传递给复制构造函数时位于堆栈上,然后被清除。


3
看起来 rep 指令只是在清空堆栈,没有触及到任何对象。 - Captain Obvlious
2
点赞那个罕见的物品:一个体面、经过深入研究的问题! - user1864610
3
我同意这似乎只是用0xCC清除本地变量的标准函数入口代码。如果它没有正常工作,那么绝对什么都不会正常运行。因此,问题很可能不在清除代码中。另外,清除代码之所以存在,仅是因为您通过编译器选项告诉编译器要将其放在那里。尝试删除此选项,您应该会得到相同的行为。但是,如果您没有得到相同的行为,则仍然可能不是清除代码有问题;您可能正在访问某个未初始化的变量。 - Mike Nakis
2
你正在使用 /GZ 或者 /RTCs 编译选项编译你的代码。你的 rep stos 操作码用一个已知模式(0xCC)初始化未初始化的堆栈空间(从 ebp-0xCC 到 ebp)。它不会影响传递给构造函数的任何变量,这些变量是在 ebp 之上传递的。 - IInspectable
2
你误解了我的评论。我只是解释了有问题的代码做了什么,以及哪些编译器开关负责它。@Michael已经解释了为什么你可能会有一个指向被清除数据的无效指针:返回本地变量的地址。你应该保留运行时检查,并将编译器警告级别提高到4。这可能会揭示根本原因。 - IInspectable
显示剩余15条评论
1个回答

0

感谢您在此问题上提供的所有帮助,但我们已经找出了问题所在。发生的情况是,在某些 Handle 构造函数中,我们需要为 Handle 的内部指针-指针指向的指针分配内存。因此,发生的情况是 Handle 本身使用的是存储在堆栈上的指针,当调用 rep stos 操作码时,它会清除该指针。


有点像……被指向的对象是在工厂中新建的,因此它位于堆上。然而,Handle本身使用了指向指针的指针,并且保存Handle需要指向的地址的数据位于堆栈上。当rep stos执行时,它清除了堆栈中的地址。因此,指向指针的指针指向的是堆栈上的一个位置,该位置本身又指向堆上的一个位置。 - Blake A. L. Richardson
实际上,这是因为它包含在我们从CreateArchetypeComponent函数返回的Handle中。 - Blake A. L. Richardson
1
希望你通过让Handle(T* obj)构造函数分配一个new T*{obj}来解决它,当引用计数归零时会被删除。顺便说一句,你的Handle类似乎没有存储足够的元数据来支持static_handle_cast的正确实现。或者为了支持弱指针,你需要计算所有句柄(包括弱句柄)的数量,以决定何时删除元数据,这与控制目标生命周期的强句柄的计数是分开的。 - Ben Voigt
1
啊,老问题了,“新项目向导设置调试生成调试信息并使用缓慢的调试库和发布生成使用快速库并优化您的代码以便您无法进行调试”。我建议熟悉项目选项并使用库的发布版本,但在自己的代码中启用调试信息和不进行优化。 - Ben Voigt
你的 HandleRegistrar 听起来像是重新发明了 enable_shared_from_this... 在这里找到了一个解释,看起来相当不错:http://aldrin.co/esft.html - Ben Voigt
显示剩余4条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接