记录一下 BDS 有关 BlockEventDispatcherToken::unregister 的一个崩服问题

# 起因

当初有人反馈,大型服务器在长期运行的时候,在关服的时候会出现崩服问题 (0xc0000005 错误), 而崩服很显然,会导致地图损失等问题,再加上当初闲得无聊,就着手开展了对这个的研究.

# 研究

<details>
<summary> 当时的崩服 Log: </summary>

h
......
[2023-06-07 06:00:49.138] [info] 
[2023-06-07 06:00:49.138] [info] Last Assembly: 
[2023-06-07 06:00:49.138] [info]   0x7FF7CCAA92BE --> mov rax, [rcx+0x08]
[2023-06-07 06:00:49.138] [info] 
[2023-06-07 06:00:49.138] [info] Stacktrace: 
[2023-06-07 06:00:49.169] [info]   #0 at pc 0x7FF7CCAA9230 bedrock_server_mod.exe -> BlockEventDispatcherToken::unregister+0x8E
......

</details>

很明显的看到和 BlockEventDispatcherToken 有关,所以显而易见的得去 ida 里看这个函数的问题
但是由于一系列问题,最初是并没有定位到出问题的本质的 (菜)

后续,在多次定位后无果,尝试去探寻一下这个的复现方法,于是在调用堆栈里发现这个的调用和 BlockBreakSensorComponent 有关,而这个又和数据驱动的 minecraft:block_sensor 有关.
查阅可知,这个组件和蜜蜂,猪灵的攻击仇恨有关,具体就是蜂箱和金块之类的方块被破坏时,蜜蜂这些拥有该组件的实体会对破坏者产生敌意,并攻击破坏者.
那这个为什么又会导致崩服呢?问题来到了实体上面.
根据经验,这种情况一般是实体销毁导致的问题,于是,笔者便开始了测试,尝试在本地复现该问题.
经过多次测试,发现利用蜂蜜快速死亡 + 反复生成的情况下,stop 可以导致崩服,而且复现概率很高.

在查阅 ida 的反汇编后,得到 BlockEventDispatcherToken::unregister 的逻辑如下:

p
void __fastcall BlockEventDispatcherToken::unregister(BlockEventDispatcherToken *this)
{
  unsigned int v1; // r8d
  _QWORD *v3; // r9
  __int64 v4; // rdx
  __int64 *v5; // rcx
  __int64 v6; // rax
  __int64 v7; // rcx
  unsigned int v8; // [rsp+20h] [rbp-18h] BYREF
  v1 = *(_DWORD *)this;
  if ( *(_DWORD *)this == -1 )
    return;
  v3 = (_QWORD *)*((_QWORD *)this + 1);
  v8 = *(_DWORD *)this;
  v4 = v3[1];
  v5 = (__int64 *)(v3[3]
                 + 16
                 * ((0x100000001B3i64
                   * (((unsigned __int64)v1 >> 24) ^ (0x100000001B3i64
                                                    * (BYTE2(v1) ^ (0x100000001B3i64
                                                                  * ((0x100000001B3i64
                                                                    * ((unsigned __int8)v1 ^ 0xCBF29CE484222325ui64)) ^ BYTE1(v1))))))) & v3[6]));
  v6 = v5[1];
  if ( v6 == v4 )
    goto LABEL_7;
  v7 = *v5;
  if ( v1 != *(_DWORD *)(v6 + 16) )
  {
    while ( v6 != v7 )
    {
      v6 = *(_QWORD *)(v6 + 8);
      if ( v1 == *(_DWORD *)(v6 + 16) )
        goto LABEL_8;
    }
LABEL_7:
    v6 = 0i64;
  }
LABEL_8:
  if ( !v6 )
    v6 = v3[1];
  if ( v6 != v4 )
    std::_Hash<std::_Umap_traits<int,std::unique_ptr<ListenerInfo>,std::_Uhash_compare<int,std::hash<int>,std::equal_to<int>>,std::allocator<std::pair<int const,std::unique_ptr<ListenerInfo>>>,0>>::_Erase<int>(
      v3,
      &v8);
  *(_DWORD *)this = -1;
}

而问题出现在

p
v6 = v3[1]

于是,很显然,这与 v3 有关

# 修复

在恢复了 BlockEventDispatcherToken 的结构后,发现其拥有两个成员

p
class BlockEventDispatcherToken {
  ListenerHandle handle;  
  BlockEventDispatcher *dispatcher;
}

而 BlockEventDispatcher 也有一个成员

p
class BlockEventDispatcher {
  std::unordered_map<int, std::unique_ptr<ListenerInfo>> listeners;
}

很明显,从 v3 上分析,崩服问题和 umap 逃不了关系.
于是,在 unregister 伪代码的研究下,我们得到了正常情况下,BlockEventDispatcherToken::unregister 的大致逻辑

p
void BlockEventDispatcherToken::unregister() {
  if (this->handle != -1){
    auto it = this->dispatcher->listeners.find(this->handle);
    if (it != this->dispatcher->listeners.end())
        this->dispatcher->listeners.erase(it);
    this->handle = -1;
  }
}

于是,我们大致知道了问题的所在,便可以着手修复工作了.

由于该问题的特殊性,引起该问题的本身就是内存回收和销毁的问题,而笔者并没有权限对源码进行阅读和查看,基于 Windows 平台本身在线程死亡后,便会自行清理,于是该问题的最容易解决的方案也就很明显了.
笔者采用了直接对 BlockEventDispatcherToken::unregister 进行 hook, 自行实现了上面的伪代码中的清理部分的功能,并且为了方便和稳定,在关服状态下便不主动对 listeners 进行回收,防止因为关服回收内存时候 dispatcher 出现问题.

p
TInstanceHook(void,"?unregister@BlockEventDispatcherToken@@QEAAXXZ",BlockEventDispatcherToken){
  if (this->handle != -1)
  {
    if(ll::globalRuntimeConfig.serverStatus == ll::LLServerStatus::Stopping){
      logger.debug("BlockEventDispatcherToken::unregister ignore unregister when server stopping");
      this->handle = -1;
      return;
    }
    auto it = this->dispatcher->listeners.find(this->handle);
    if (it != this->dispatcher->listeners.end())
        this->dispatcher->listeners.erase(it);
    this->handle = -1;
    return;
  }
}

# 总结

该问题的出现和修复其实并没有那么快速,前期因为经验不足而走错了方向.
但是,该问题的出现也让笔者对 BDS 的内存管理方面有了更深的认识,也算是有点收获了.