【问题标题】:Impossible stacktrace? Access Violation with this=nullptr although checked for不可能的堆栈跟踪?尽管检查了 this=nullptr 的访问冲突
【发布时间】:2020-06-04 17:45:41
【问题描述】:

我在这里分析一个非常复杂的项目中罕见但一致的崩溃,它是这些“不可能的堆栈跟踪”之一,这意味着:乍一看,一切看起来都很好。

崩溃本身是“抛出未处理的异常:读取访问冲突。这是 nullptr。”。

堆栈当前帧的代码如下所示:

FT6VehiclePathCheckpoint AT6Building::GetVehicleCheckpoint() const
{
    FT6VehiclePathCheckpoint targetCheckpoint = FT6VehiclePathCheckpoint();
    if (GetBusStopComponent() != nullptr)
    {
        ...

崩溃发生在“if (GetBusStopComponent() != nullptr)”这一行。函数“GetBusStopComponent()”本身是对象成员变量的内联 getter,因此它不会显示在跟踪中。此外,在内存转储中,“this”确实为 0。到目前为止一切顺利。

但是,它正上方的堆栈帧如下所示:

const AT6Building* portalBuildingByLocation = pathFinder != nullptr ? pathFinder->GetPortalBuildingByLocation(startLocation) : GetNearestDrivewayBuilding();

if (portalBuildingByLocation != nullptr)
{
    startCheckpoint = portalBuildingByLocation->GetVehicleCheckpoint();
}

(指向“portalBuildingByLocation->GetVehicleCheckpoint”行)。很明显,在调用函数之前有 一个 nullptr-check。那是第一眼^^。

接下来我怀疑“GetVehicleCheckpoint”中的内存损坏导致堆栈跟踪混乱?但那里实际上什么都没有。 “FT6VehiclePathCheckpoint”初始化甚至没有构造函数——只有一堆直接初始化的字段(它们都像 nullptr 和整数文字)..

代码是用 cl.exe 编译的,开启了优化,所以我怀疑我在某个地方得到了一些 UB,编译器依赖于某些东西......

因此,虽然我在阅读汇编代码方面相当菜鸟,但我尝试过。这是 GetVehicleCheckpoint 函数的拆解后的第一部分:

FT6VehiclePathCheckpoint AT6Building::GetVehicleCheckpoint() const
{
00007FF7304714B0  mov         qword ptr [rsp+8],rbx  
00007FF7304714B5  mov         qword ptr [rsp+10h],rsi  
00007FF7304714BA  push        rdi  
00007FF7304714BB  sub         rsp,40h  
    FT6VehiclePathCheckpoint targetCheckpoint = FT6VehiclePathCheckpoint();
00007FF7304714BF  xor         eax,eax  
00007FF7304714C1  mov         qword ptr [rsp+28h],0FFFFFFFFFFFFFFFFh  
00007FF7304714CA  mov         qword ptr [rsp+30h],rax  
00007FF7304714CF  mov         rsi,rcx  

    if (GetBusStopComponent() != nullptr)
00007FF7304714D2  mov         rcx,qword ptr [rcx+5F8h]  

在最后一行出现崩溃。好的,如果我理解正确,它试图从 rcx+5F8h 读取一个 qword 并且可能 rcx 为 0? (但如果是这样,错误不应该是“访问冲突,尝试从 0x5f8 读取”吗?好吧,也许 Visual Studio 想要更有帮助..)。

另外,我验证过,是的:BusStopComponent 确实位于类的偏移量 0x5F8..

好的,所以我尝试将 rcx 跟踪到倒数第二个堆栈帧:

            const AT6Building* portalBuildingByLocation = pathFinder != nullptr ? pathFinder->GetPortalBuildingByLocation(startLocation) : GetNearestDrivewayBuilding();
00007FF730410F21  lea         rdx,[rbp-29h]  
00007FF730410F25  mov         rcx,r12  
00007FF730410F28  call        AT6AStarPathfinder::GetPortalBuildingByLocation (07FF73059CD30h)  
00007FF730410F2D  jmp         UT6Agent::HandleAgentArrivedToUseCar+34Ah (07FF730410F4Ah)  
00007FF730410F2F  test        r12,r12  
00007FF730410F32  je          UT6Agent::HandleAgentArrivedToUseCar+342h (07FF730410F42h)  
00007FF730410F34  lea         rdx,[rbp-29h]  
00007FF730410F38  mov         rcx,r12  
00007FF730410F3B  call        AT6AStarPathfinder::GetPortalBuildingByLocation (07FF73059CD30h)  
00007FF730410F40  jmp         UT6Agent::HandleAgentArrivedToUseCar+34Ah (07FF730410F4Ah)  
00007FF730410F42  mov         rcx,rsi  
00007FF730410F45  call        UT6Agent::GetNearestDrivewayBuilding (07FF73040DA50h)  

            if (portalBuildingByLocation != nullptr)
00007FF730410F4A  test        rax,rax  
00007FF730410F4D  je          UT6Agent::HandleAgentArrivedToUseCar+36Ch (07FF730410F6Ch)  
            {
                startCheckpoint = portalBuildingByLocation->GetVehicleCheckpoint();
00007FF730410F4F  mov         rcx,rax  
00007FF730410F52  lea         rdx,[rbp-9]  
00007FF730410F56  call        AT6Building::GetVehicleCheckpoint (07FF7304714B0h)  
00007FF730410F5B  movups      xmm0,xmmword ptr [rax]  

再次:我对解释程序集不是很有信心,但仅凭目测,我发现 rcx 被设置为 rax 中的任何内容,并且 rax 被正确测试为 0。所以没有花哨的“编译器删除了 nullptr 检查作为优化”。

任何可能导致访问冲突的见解?有什么可疑的东西可以给我线索吗?

干杯,艾米。

【问题讨论】:

  • hm.. 理论:可能是对 GetNearestDrivewayBuilding 的调用扰乱了其堆栈帧的返回地址,并“希望”通过 nullptr-check 恰好调用 GetVehicleCheckpoint?
  • 如果这是 Visual Studio,您应该能够看到寄存器值(它们有一个调试窗口)。 rcx 和 rsi 可能很有趣。为地址 rcx 或 rcx+5F8h 打开内存窗口也可能提供信息,因为您可以检查该类的所有成员变量。一个一个地检查它们可能没有效率(考虑到有 5f8h 字节的价值),但是看看它是否看起来一般正确,尤其是在 5f8h 偏移附近。
  • RCX 确实为 0。在第二个堆栈帧中,RSI 为 0x229239886E0(在最低堆栈帧中,在“mov rsi,rcx”之后,RSI 也是 0)。但是这个内存位置似乎不是故障转储的一部分:( - 我得到了所有?? 当直接在内存调试中查看它时(我认为这是因为外部“this”位于堆上并且它是一个“小型故障转储” “?)。或者 RSI 中的值是其他值的偏移量?
  • 例如,如果RelatedPathBuildings[CurrentPath.CurrentSegmentIndex] 为空怎么办?虽然我从未这样做过,但您似乎应该能够在 GetVehicleCheckpoint 内执行if (this == nullptr) __debugbreak();。及早发现问题可能会让您在事情被覆盖之前看到更多。
  • @DavidWohlferd:IIRC,在nullptr 上调用成员函数不幸的是 UB,因此允许编译器假定它是非 null 并优化该检查,并在实践中这样做。不过,在未优化的构建中,您可能会因为调试而侥幸。

标签: c++ assembly stack-trace access-violation


【解决方案1】:

明白了! (或者更确切地说是 David Wohlferd 发现的)。

“直接在上面的堆栈帧”具有误导性,因为代码在同一个函数中多次重复使用此调用(在不同的 if 分支中)。在查看局部变量时,那里的分支路径实际上是不可能的,我将问题确定为另一个数组中的 nullptr。

感谢大卫沃尔弗德!死机! :)

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2014-11-24
    • 2019-11-07
    • 2020-11-15
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2023-04-02
    • 1970-01-01
    相关资源
    最近更新 更多