记录一下 BDS 有关 BlockEventDispatcherToken::unregister 的一个崩服问题
# 起因
当初有人反馈,大型服务器在长期运行的时候,在关服的时候会出现崩服问题 (0xc0000005 错误), 而崩服很显然,会导致地图损失等问题,再加上当初闲得无聊 ,就着手开展了对这个的研究.
# 研究
当时的崩服Log:
1 2 3 4 5 6 7 8 ...... [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] ......
很明显的看到和 BlockEventDispatcherToken 有关,所以显而易见的得去 ida 里看这个函数的问题
但是由于一系列问题,最初是并没有定位到出问题的本质的 (菜)
后续,在多次定位后无果,尝试去探寻一下这个的复现方法,于是在调用堆栈里发现这个的调用和 BlockBreakSensorComponent 有关,而这个又和数据驱动的 minecraft:block_sensor 有关.
查阅可知,这个组件和蜜蜂,猪灵的攻击仇恨有关,具体就是蜂箱和金块之类的方块被破坏时,蜜蜂这些拥有该组件的实体会对破坏者产生敌意,并攻击破坏者.
那这个为什么又会导致崩服呢?问题来到了实体上面.
根据经验,这种情况一般是实体销毁导致的问题,于是,笔者便开始了测试,尝试在本地复现该问题.
经过多次测试,发现利用蜂蜜快速死亡 + 反复生成的情况下,stop 可以导致崩服,而且复现概率很高.
在查阅 ida 的反汇编后,得到 BlockEventDispatcherToken::unregister 的逻辑如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 void __fastcall BlockEventDispatcherToken::unregister (BlockEventDispatcherToken *this ) { unsigned int v1; _QWORD *v3; __int64 v4; __int64 *v5; __int64 v6; __int64 v7; unsigned int v8; v1 = *(_DWORD *)this ; if ( *(_DWORD *)this == -1 ) return ; v3 = (_QWORD *)*((_QWORD *)this + 1 ); v8 = *(_DWORD *)this ; v4 = v3[1 ]; v5 = (__int64 *)(v3[3 ] + 16 * ((0x100000001B3 i64 * (((unsigned __int64)v1 >> 24 ) ^ (0x100000001B3 i64 * (BYTE2 (v1) ^ (0x100000001B3 i64 * ((0x100000001B3 i64 * ((unsigned __int8)v1 ^ 0xCBF29CE484222325 ui64)) ^ 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 = 0 i64; } 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 ; }
而问题出现在
于是,很显然,这与 v3 有关
# 修复
在恢复了 BlockEventDispatcherToken 的结构后,发现其拥有两个成员
1 2 3 4 class BlockEventDispatcherToken { ListenerHandle handle; BlockEventDispatcher *dispatcher; }
而 BlockEventDispatcher 也有一个成员
1 2 3 class BlockEventDispatcher { std::unordered_map<int , std::unique_ptr<ListenerInfo>> listeners; }
很明显,从 v3 上分析,崩服问题和 umap 逃不了关系.
于是,在 unregister 伪代码的研究下,我们得到了正常情况下,BlockEventDispatcherToken::unregister 的大致逻辑
1 2 3 4 5 6 7 8 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 出现问题.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 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 的内存管理方面有了更深的认识,也算是有点收获了.