找回密码
 registerss
搜索
查看: 47|回复: 1

[文档笔记] TLBB 服务端 ASan 诊断版使用与编译教程

[复制链接]
Waylee 发表于 2026-9-1 18:38 | 显示全部楼层 |阅读模式 | Google Chrome | Windows 10

马上注册,查看网站隐藏内容!!

您需要 登录 才可以下载或查看,没有账号?registerss

×

一、ASan 是什么

ASan 全称是 AddressSanitizer,是编译器提供的内存错误检测工具。它会在程序运行时检查:

  • 数组或缓冲区越界;
  • 空指针和非法地址访问;
  • 释放后继续使用;
  • 重复释放内存;
  • 堆内存被提前破坏。

普通 Release 版本经常是在内存已经被破坏后才崩溃,调用栈只能看到最后出错的位置。ASan 通常可以更早捕获第一次非法访问,并显示源码文件、行号和调用栈。

ASan 不会改变游戏业务逻辑,只是在程序中加入额外的内存检查代码。

二、为什么不能用于线上

ASan 版本只用于测试环境,不能直接替换线上 Server

  • 内存占用通常增加约 2 倍;
  • 执行速度会变慢;
  • 会改变线程执行时序;
  • 长时间运行可能产生较多诊断日志;
  • 不能检测所有类型的问题。

线上仍然使用普通 Release 版本,ASan 只部署到独立测试目录,使用测试数据库和测试角色。

三、编译环境要求

Linux 编译机需要安装:

cmake
ninja
g++
clang

Ubuntu 可以执行:

sudo apt update
sudo apt install cmake ninja-build build-essential clang llvm

确认工具可用:

cmake --version
ninja --version
g++ --version

四、配置 ASan 构建目录

当前工程使用独立构建目录:

/opt/tlbb-build/v0-85-8130/build/game-asan

进入 Linux 编译机后执行:

cmake \
  -S /opt/tlbb-build/v0-85-8130/GameCode/Server/Server/GameServer \
  -B /opt/tlbb-build/v0-85-8130/build/game-asan \
  -G Ninja \
  -DCMAKE_BUILD_TYPE=RelWithDebInfo \
  -DTLBB_BUILD_GAME_SERVER=ON \
  -DTLBB_BUILD_BILLING=OFF \
  -DTLBB_BUILD_LOGIN=OFF \
  -DTLBB_BUILD_SHARE_MEMORY=OFF \
  -DTLBB_BUILD_WORLD=OFF \
  -DTLBB_ENABLE_WINDOWS_UPDATE_FILE_CHECK=ON \
  -DCMAKE_CXX_FLAGS="-fsanitize=address -fno-omit-frame-pointer" \
  -DCMAKE_EXE_LINKER_FLAGS="-fsanitize=address" \
  -DCMAKE_SHARED_LINKER_FLAGS="-fsanitize=address"

参数说明:

-fsanitize=address       启用 ASan 检查
-fno-omit-frame-pointer  保留帧指针,方便生成完整调用栈
-O2                      保持接近 Release 的优化级别
-g                       保留调试符号
RelWithDebInfo           优化代码,同时保留调试信息

五、编译 Server

cmake --build \
  /opt/tlbb-build/v0-85-8130/build/game-asan \
  --target Server \
  --parallel 8

成功时会看到:

[1353/1353] Linking CXX executable bin/Server

生成文件:

/opt/tlbb-build/v0-85-8130/build/game-asan/bin/Server

六、确认确实启用了 ASan

检查编译参数:

ninja -C /opt/tlbb-build/v0-85-8130/build/game-asan \
  -t commands Server | grep -oE \
  -- '-fsanitize=address|-fno-omit-frame-pointer|-O2|-g|-DNDEBUG' | sort -u

应看到:

-fsanitize=address
-fno-omit-frame-pointer
-O2
-g
-DNDEBUG

检查程序:

file /opt/tlbb-build/v0-85-8130/build/game-asan/bin/Server
ldd /opt/tlbb-build/v0-85-8130/build/game-asan/bin/Server | grep asan

程序应包含:

with debug_info
not stripped

并能看到 libasan.so

七、运行诊断版

建议在测试副本目录运行,不要使用线上目录:

cd /opt/tlbb-build/v0-85-8130/build/game-asan/bin

设置 ASan 参数:

export ASAN_OPTIONS="halt_on_error=1:abort_on_error=1:detect_leaks=0:fast_unwind_on_fatal=0:symbolize=1:log_path=${LOG_DIR}/asan"

启动:

./Server -loadscriptonce

其中:

abort_on_error=1       发现错误后立即终止
halt_on_error=1        停止继续执行损坏路径
detect_leaks=0         长时间运行服务端时关闭泄漏统计
fast_unwind_on_fatal=0 使用更完整的调用栈
log_path=...           将诊断结果写入指定文件

如需保留 core 文件,可以先执行:

ulimit -c unlimited

八、如何看 ASan 日志

典型日志如下:

ERROR: AddressSanitizer: heap-buffer-overflow
READ of size 4
    #0 0x... in SomeFunction source.cpp:123
    #1 0x... in OtherFunction other.cpp:456

重点查看第一个属于 TLBB 源码的调用帧:

#0 ...

不要只看最后的 SIGSEGVabort。最后的崩溃位置可能只是内存破坏被发现的地方,真正的问题通常在更早的 READWRITE

如果没有自动显示源码行号,可以使用:

llvm-symbolizer

或者:

addr2line -Cfipe \
  /opt/tlbb-build/v0-85-8130/build/game-asan/bin/Server \
  0x程序地址

九、推荐的复现流程

  1. 复制一份测试服配置、脚本和数据库。
  2. 使用 ASan 版 Server,不要替换线上 Release 文件。
  3. 记录复现前的角色、场景和操作步骤。
  4. 重放触发崩溃的封包或正常操作。
  5. 优先查看 ASan 报出的第一处非法读写。
  6. 修复源码后重新编译并重复测试。
  7. 确认正常玩家流程没有异常后,再编译正式 Release。

十、正式版本与 ASan 版本必须分开

正式版本:

/opt/tlbb-build/v0-85-8130/build/game-release/bin/Server

ASan 版本:

/opt/tlbb-build/v0-85-8130/build/game-asan/bin/Server

不要把 ASan 版复制到:

/home/tlbb/Server/Server

ASan 适合定位“哪里第一次写坏了内存”,正式代码修复仍然需要增加服务端边界检查、空指针检查和非法封包拦截。

 楼主| Waylee 发表于 2026-9-1 18:51 | 显示全部楼层 | Google Chrome | Windows 10

EOL:3C33613C,20,256635195,7420,432,81.86,56.10, (2018818160)(T0=2026-9-1_18:41:56 T1=312.9910)
CGCharUseSkillHandler: S:15000 D:25 Skill:521 (-1.000000,-1.000000) DIR = -1.000000 (2018818160)(T0=2026-9-1_18:41:56 T1=313.1830)
CGCharUseSkillHandler: S:15000 D:25 Skill:521 (-1.000000,-1.000000) DIR = -1.000000 (2018818160)(T0=2026-9-1_18:41:56 T1=313.1830)
CGCharUseSkillHandler: S:15000 D:25 Skill:521 (-1.000000,-1.000000) DIR = -1.000000 (2018818160)(T0=2026-9-1_18:41:56 T1=313.3840)
CGCharUseSkillHandler: S:15000 D:25 Skill:521 (-1.000000,-1.000000) DIR = -1.000000 (2018818160)(T0=2026-9-1_18:41:56 T1=313.3840)
CGAskItemInfoHandler: GUID=3C336140 BagIndex=14 (120=================================================================
==2708230==ERROR: AddressSanitizer: global-buffer-overflow on address 0x59a76e1f2b9c at pc 0x59a755fe0e06 bp 0x7f7e339e2880 sp 0x7f7e339e2870
READ of size 4 at 0x59a76e1f2b9c thread T62
    #0 0x59a755fe0e05 in AI_Pet::ProcessSkillOfAttackState(Combat_Module::Skill_Module::SkillTemplateData_T const*, int) /opt/tlbb-build/v0-85-8130/GameCode/Server/Server/GameServer/Server/AI/AI_Pet.cpp:769
    #1 0x59a755fe1708 in AI_Pet::ProcessSkillInCache(int) /opt/tlbb-build/v0-85-8130/GameCode/Server/Server/GameServer/Server/AI/AI_Pet.cpp:909
    #2 0x59a755fe2504 in AI_Pet::AI_Logic_Combat(unsigned int) /opt/tlbb-build/v0-85-8130/GameCode/Server/Server/GameServer/Server/AI/AI_Pet.cpp:133
    #3 0x59a755ff7933 in State::AI_Logic_Combat(AI_Character*, unsigned int) /opt/tlbb-build/v0-85-8130/GameCode/Server/Server/GameServer/Server/AI/State.cpp:306
    #4 0x59a755ff7933 in CombatState::StateLogic(AI_Character*, unsigned int) /opt/tlbb-build/v0-85-8130/GameCode/Server/Server/GameServer/Server/AI/State.cpp:480
    #5 0x59a755ff8536 in State::Logic(AI_Character*, unsigned int) /opt/tlbb-build/v0-85-8130/GameCode/Server/Server/GameServer/Server/AI/State.cpp:242
    #6 0x59a755b10a4d in AI_Character::Logic(unsigned int) /opt/tlbb-build/v0-85-8130/GameCode/Server/Server/GameServer/Server/AI/AI_Character.h:57
    #7 0x59a755b10a4d in Obj_Character::HeartBeat(unsigned int) /opt/tlbb-build/v0-85-8130/GameCode/Server/Server/GameServer/Server/Obj/Obj_Character.cpp:242
    #8 0x59a755c16fc2 in Obj_Pet::HeartBeat(unsigned int) /opt/tlbb-build/v0-85-8130/GameCode/Server/Server/GameServer/Server/Obj/Obj_Pet.cpp:263
    #9 0x59a755c4888d in PetManager::HeartBeat(unsigned int) /opt/tlbb-build/v0-85-8130/GameCode/Server/Server/GameServer/Server/Obj/PetManager.cpp:728
    #10 0x59a75606ec59 in Scene::HeartBeat() /opt/tlbb-build/v0-85-8130/GameCode/Server/Server/GameServer/Server/Scene/Scene_Core.cpp:1229
    #11 0x59a75606fad1 in Scene::Tick() /opt/tlbb-build/v0-85-8130/GameCode/Server/Server/GameServer/Server/Scene/Scene_Core.cpp:951
    #12 0x59a756089aa7 in SceneThread::run() /opt/tlbb-build/v0-85-8130/GameCode/Server/Server/GameServer/Server/Scene/SceneThread.cpp:117
    #13 0x59a755c91004 in Thread::MyThreadProcess(void*) /opt/tlbb-build/v0-85-8130/GameCode/Common/ServerBase/Thread.cpp:557
    #14 0x7f7ec1a5ea41 in asan_thread_start ../../../../src/libsanitizer/asan/asan_interceptors.cpp:234
    #15 0x7f7ec129cb83 in start_thread nptl/pthread_create.c:447
    #16 0x7f7ec1329d6b in clone3 ../sysdeps/unix/sysv/linux/x86_64/clone3.S:78

0x59a76e1f2b9c is located 4 bytes before global variable 'g_PetAIStrategyTble' defined in '/opt/tlbb-build/v0-85-8130/GameCode/Server/Server/GameServer/Server/Main/GameTable.cpp:82:12' (0x59a76e1f2ba0) of size 100
0x59a76e1f2b9c is located 32 bytes after global variable 'g_IsEnterCombatAiTable' defined in '/opt/tlbb-build/v0-85-8130/GameCode/Server/Server/GameServer/Server/Main/GameTable.cpp:83:23' (0x59a76e1f2b40) of size 60
SUMMARY: AddressSanitizer: global-buffer-overflow /opt/tlbb-build/v0-85-8130/GameCode/Server/Server/GameServer/Server/AI/AI_Pet.cpp:769 in AI_Pet::ProcessSkillOfAttackState(Combat_Module::Skill_Module::SkillTemplateData_T const*, int)
Shadow bytes around the buggy address:
  0x59a76e1f2900: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x59a76e1f2980: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x59a76e1f2a00: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x59a76e1f2a80: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x59a76e1f2b00: 04 f9 f9 f9 f9 f9 f9 f9 00 00 00 00 00 00 00 04
=>0x59a76e1f2b80: f9 f9 f9[f9]00 00 00 00 00 00 00 00 00 00 00 00
  0x59a76e1f2c00: 04 f9 f9 f9 f9 f9 f9 f9 00 00 00 00 00 00 00 00
  0x59a76e1f2c80: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x59a76e1f2d00: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x59a76e1f2d80: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x59a76e1f2e00: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
Shadow byte legend (one shadow byte represents 8 application bytes):
  Addressable:           00
  Partially addressable: 01 02 03 04 05 06 07 
  Heap left redzone:       fa
  Freed heap region:       fd
  Stack left redzone:      f1
  Stack mid redzone:       f2
  Stack right redzone:     f3
  Stack after return:      f5
  Stack use after scope:   f8
  Global redzone:          f9
  Global init order:       f6
  Poisoned by user:        f7
  Container overflow:      fc
  Array cookie:            ac
  Intra object redzone:    bb
  ASan internal:           fe
  Left alloca redzone:     ca
  Right alloca redzone:    cb
Thread T62 created by T0 here:
    #0 0x7f7ec1af51f9 in pthread_create ../../../../src/libsanitizer/asan/asan_interceptors.cpp:245
    #1 0x59a755c91107 in Thread::start() /opt/tlbb-build/v0-85-8130/GameCode/Common/ServerBase/Thread.cpp:476
    #2 0x59a755cd849d in ThreadPool::Start() /opt/tlbb-build/v0-85-8130/GameCode/Server/Server/GameServer/Server/Main/ThreadPool.cpp:116
    #3 0x59a755cd7dbc in ThreadManager::Start() /opt/tlbb-build/v0-85-8130/GameCode/Server/Server/GameServer/Server/Main/ThreadManager.cpp:115
    #4 0x59a755ccfd9a in Server::Loop() /opt/tlbb-build/v0-85-8130/GameCode/Server/Server/GameServer/Server/Main/Server.cpp:366
    #5 0x59a7559d9257 in main /opt/tlbb-build/v0-85-8130/GameCode/Server/Server/GameServer/Server/Main/Server.cpp:251
    #6 0x7f7ec122a1c9 in __libc_start_call_main ../sysdeps/nptl/libc_start_call_main.h:58
    #7 0x7f7ec122a28a in __libc_start_main_impl ../csu/libc-start.c:360
    #8 0x59a7559e10d4 in _start (/home/tlbb/Server/Server+0x1b20d4) (BuildId: 363a1cba6b79c8db46546d2b11f6c8c14e1aadef)

==2708230==ABORTING

宠物 AI 策略表越界崩溃修复教程

一、崩溃现象

ASan 日志显示崩溃发生在:

Server/AI/AI_Pet.cpp

崩溃位置访问了全局数组:

g_PetAIStrategyTble[nTypeOfSkill][nAIType]

其中一个数组下标来自宠物技能配置,另一个来自宠物道具数据。当技能类型为 -1,或 AI 类型超出有效范围时,Release 版本仍会继续访问数组,最终触发越界崩溃。

二、根本原因

原代码主要依赖 Assert

Assert(nAIType >= 0 && nAIType < PET_AITYPE);

FLOAT rate = g_PetAIStrategyTble[TypeOfSkill][nAIType];

Release 编译时 Assert 不会阻止后续执行,因此非法配置仍然会进入数组访问。

IDA 对照后确认,参考程序同样缺少有效的 Release 边界保护。因此本次保持正常技能逻辑不变,只为非法数据增加安全返回。

三、修复方法

在访问策略表之前检查技能类型和宠物 AI 类型:

ID_t const nTypeOfSkill = pSkillData_T->GetTypeOfPetSkill();
INT const nAIType = pPet->GetAIType();

if (nTypeOfSkill < 0 || nTypeOfSkill >= PET_SKILL_TYPE ||
    nAIType < 0 || nAIType >= PET_AITYPE)
{
    return;
}

INT nUseSkillRate = Float2Int(
    nRateOfSkill * g_PetAIStrategyTble[nTypeOfSkill][nAIType]);

同时,只有自动策略技能才读取策略表:

if (PET_SKILL_OPERATE_AISTRATEGY != nOperateMode)
{
    return;
}

四、补充空指针保护

宠物 AI 类型读取过程中,宠物容器或宠物道具可能为空。修复后先验证对象:

ItemContainer* pPetContainer = GetCreator()->GetPetContain();

if (pPetContainer == NULL)
{
    return _PET_DB_LOAD::PET_AI_INVALID;
}

Item* pPetItem = pPetContainer->GetItem(m_nIndex);

if (pPetItem == NULL)
{
    return _PET_DB_LOAD::PET_AI_INVALID;
}

BUFF 概率计算中也增加了宠物容器和物品指针检查,避免 Assert 被禁用后继续解引用空指针。

五、修改文件

Server/Server/GameServer/Server/AI/AI_Pet.cpp
Server/Server/GameServer/Server/Obj/Obj_Pet.cpp
Server/Server/GameServer/Server/Obj/Obj_Pet.inl

六、修复后的行为

  • 合法宠物技能的概率和效果不变;
  • 普通技能不会错误读取 AI 策略表;
  • 非法技能类型、非法 AI 类型直接忽略;
  • 宠物容器或宠物道具为空时安全返回;
  • 不修改协议、配置格式或 Obj_Human 结构大小;
  • 不依赖 Release 版本的 Assert 进行安全防护。

七、验证结果

Linux Release 和 ASan 版本均已重新编译:

/opt/tlbb-build/v0-85-8130/build/game-release/bin/Server
/opt/tlbb-build/v0-85-8130/build/game-asan/bin/Server

ASan 版本确认保留:

-fsanitize=address
-fno-omit-frame-pointer
-O2 -g

并且未执行 strip


您需要登录后才可以回帖 登录 | registerss

本版积分规则

QQ|手机版|雪舞知识库 ( 浙ICP备15015590号-1 | 萌ICP备20232229号|浙公网安备33048102000118号 )

GMT+8, 2026-9-21 16:04 , Processed in 0.073291 second(s), 25 queries .

Powered by Discuz! X5.0

© 2001-2026 Discuz! Team.

快速回复 返回顶部 返回列表