背景
- UE4引擎时不时要魔改编译。可能大一点的项目是难以避免的吧 ┓( ´∀` )┏
- 工程C++会自动编译,有持续集成平台做统一的编译和分发。这样可以不用每个人都编译引擎和工程C++了,代码安全性和开发效率能得到保障;
- 每当要调试别人发的Dump,就要满世界找dll,找符号,找代码;
目的
搭建符号服务器,调试分析Crash Dump。
如果你也有跟我一样的痛点,那接下来就来看看怎么解决吧。
接下来,我会先介绍怎么用,最后再说说原理和一些技巧。
搭建符号服务器(如已有请略过)
- 在编译机上安装WinDbg,后面需要用到其中的
symstore
和agestore
。
Download Debugging Tools for Windows - WinDbg - Windows drivers
注意安装这个版本:(这里才有我们需要的symstore
和agestore
)
安装好之后,应该可以在这里找到这两个工具,请根据机器的架构选用合适的版本。
2. 准备一个脚本,用symstore
把Binaries和PDB存入符号服务器,比如这样:
@echo off
rem 脚本传入参数(按顺序):
setlocal EnableDelayedExpansion
rem echo BIN_DIR:%1rem SYMSTOREPATH 符号服务器地址
rem PRODUCT 你的产品名
rem VERSION 你产品的版本
rem COMMENT 你想加的注释
rem TIMEOUT_DAYS 清理多少天前的符号
rem SYMSTORE_EXE symstore的文件地址
rem AGESTORE_EXE agestore的文件地址
set SYMSTOREPATH=D:SymbolStore
set PRODUCT="sakura"
set VERSION="0.0.0"
set COMMENT="none"
set TIMEOUT_DAYS=60
set SYMSTORE_EXE="C:Program Files (x86)Windows Kits10Debuggersx64symstore.exe"
set AGESTORE_EXE="C:Program Files (x86)Windows Kits10Debuggersx64agestore.exe" -yset BIN_DIR=%~f1%SYMSTORE_EXE% > nul 2> nul
if [%ERRORLEVEL%] EQU [9009] (echo "symstore.exe 没找到,请包含它所在文件夹到PATH"exit /b 0
)echo ============== 清除旧符号 ==============
echo 清除%TIMEOUT_DAYS%天前的符号
%AGESTORE_EXE% -days=%TIMEOUT_DAYS% %SYMSTOREPATH%
echo.echo =============== 上传符号 ===============
echo ++ 本地上传目录: %BIN_DIR%
echo ++ 符号服务器: %SYMSTOREPATH%
echo.echo ++++++++
echo ++ 上传exe到符号服务器
%SYMSTORE_EXE% add /r /f %BIN_DIR%*.exe /s %SYMSTOREPATH% /t %PRODUCT% /v %VERSION% /c %COMMENT%
echo.echo ++++++++
echo ++ 上传dll到符号服务器
%SYMSTORE_EXE% add /r /f %BIN_DIR%*.dll /s %SYMSTOREPATH% /t %PRODUCT% /v %VERSION% /c %COMMENT%
echo.echo ++++++++
echo ++ 上传pdb到符号服务器
%SYMSTORE_EXE% add /r /f %BIN_DIR%*.pdb /s %SYMSTOREPATH% /t %PRODUCT% /v %VERSION% /c %COMMENT%
echo.echo ++ 上传成功!!!!
echo.
在每次编译完之后,运行。
bat脚本会用agestore
自动清理过期的符号。
> upload_symbols.bat ...UE4EpicEngineBinaries
> upload_symbols.bat ...UE4EpicEnginePlugins
3. 设置好符号服务器的文件服务器,可以简单地用Samba协议(Windows共享文件夹)或者用IIS或者Apache弄一个。
在VS中设置符号服务器
以VS2019为例,在 工具->选项->调试->符号 把刚才的服务器地址(确保有访问权限)设置好:
小技巧:勾选Load only specified modules,可以显著加速调试载入module的时间。在需要查看符号时,再右键加载所需的module:
就能看到符号名字了:
参考官方文档:
https://docs.microsoft.com/en-us/windows/win32/debug/using-symstore
源码匹配
此时很可能因为pdb中的源码路径和你本地的不一致,导致找不到源码,如下图:
此时除了一个个地去找源码路径之外,有两种方法可以让VS自动定位到所需的源码:
- 在Solution设置中配置Debug Source Files查找路径。
这种方法简单但有局限,能应付大部分情况。如下图:
2. Source Server(推荐)
假如产生这个Dump的Binaries已经很旧了,源码已经被改的天翻地覆,除了在本地通过版本控制系统把代码手动还原回去,这种比较笨的办法之外,还有没有更好的做法呢?
又假如要调试的程序本地并没有全部代码,但想大致看一下问题出在哪(比如在QA机器上出现的Crash想准确分发给对应的工程师),有没有更好的做法呢?
有的,答案是Source Server。
Source Server配置方法
编译时:Source Indexing
概要:(以SVN为例)在编译完成后,上传符号之前,用svnindex.cmd
工具将版本控制信息写入PDB。
运行要求:
- Debugging Tools for Windows(在上面搭建符号服务器时已安装)
- ActiveState Perl(Indexing所需的工具,比如:svn.pm,用Perl运行)
- 准备svn.exe,并加入到PATH
在PDB生成出来后,上传符号前,执行下面的操作:
svnindex.cmd /source= /symbols= /debug
此时应该会看到PDB被修改了。
可以用pdbstr.exe检查被写入的内容:
pdbstr.exe -r -p: -s:srcsrv
此时的PDB将比编译生成的PDB多出一些信息。
PDB就准备完成了。
注意svnindex.cmd的source路径只能是SVN根目录。
调试时:从版本控制服务器自动下载源码
- 准备svn.exe,并加入到PATH;
- 在VS中启用Source Server的支持;
设置完成。
在调试Dump时,会提取PDB中的指令。这可能会带来代码注入风险。比如你调试一个未知来源的程序,自带PDB,这个PDB中是可以嵌入任意代码的。
VS会弹窗提醒。解决方法可以是:
修改svn.pm文件中的SVN_EXTRACT_CMD,将"cmd /c"删掉,直接执行svn.exe,而不是通过cmd.exe。
修改VS安装目录下的srcsrv.ini,将svn.exe加入[trusted command]:
[trusted commands]
svn.exe
至此,代码就可以自动从版本控制服务器上获取了。(^-^)V
代码会缓存到AppData下,不会自动清理,时间长了请自行手动清理。
参考资料
Source Server
Using a Source Server
Enable source server support
Source Server + Subversion = Easy Assembly Debugging
原理介绍
通过前面的操作,也基本能猜出个大概了。这篇文章写得挺详细的,我就不赘述了,只简单讲讲自己的理解:
搭建自己的符号服务器
PDB:
PDB里面记录了一系列的调试辅助信息,与Build的Binary一一对应。比如:
- publics and exports
- global symbols
- local symbols
- type data
- source files
- line numbers
最关键的,无非就是代码段地址,对应的源码路径、函数签名和行号。这样在调试的时候,就知道当前的代码地址,对应哪个代码的哪个函数的哪一行了。
DLL和PDB中会有相同的GUID,通过GUID在符号服务器上组织目录存放文件,调试时根据GUID来找对应的PDB下载。
Source Server重建索引时(重新执行`svnindex.cmd`)PDB文件会被修改(哪怕对应的Binary一点都不变,PDB中的索引日期也会变),但GUID会保持不变。
更新后的PDB会被symstore重新上传符号服务器(而不是相同跳过),会覆盖掉符号服务器上GUID相同的旧PDB,不会因此导致硬盘空间不足,但需要注意IO和流量问题。
Symbol Files - Win32 apps
Crash Dump:
Dump简单来说就是进程的内存镜像。把这个进程的虚拟内存(代码段、数据段、堆、栈、全局段等等)部分或者全部转储到磁盘文件,以便进行死后调试。
https://medium.com/@shoheiyokoyama/understanding-memory-layout-4ef452c2e709
User-Mode Dump Files - Windows drivers
其中,又分为Full Dump和Mini Dump,两者最大的区别在于Heap是否做转储。
因为Heap通常非常大,在UE4下动辄10GB以上。而其他内存段相对小很多,比如线程的Stack一般就是几MB。
所以,在Mini Dump下,因为Heap都被剔除了,所以在调试Dump时,看到的是???内存不可访问,比如UE4的FString内部是一个TArray,其字符串Data就是在Heap上的,所以调试Dump时都不可见。
那问题又来了,这样的Mini Dump有什么用呢?如果是因为资源问题导致的Crash,怎么知道是哪个资源呢?
小技巧:可以用在UE4程序后加命令行参数-fullcrashdump来告诉引擎,崩溃时生成Full Dump。这样就可以勉强弥补Mini Dump所带来的不足。
生成Dump
UE4程序Crash的时候会自动生成Dump,因为引擎中提供了Crash Handler:
但假如我想随意生成Dump呢?比如没有Crash而是假死了,怎么知道问题出在哪呢?
有很多种方式可以做到,我个人比较常用的是:
- 用VS Attach上去,然后Debug->Break All->Save Dump As。可以创建Mini Dump或者Full Dump;
- 用任务管理器,右键对应的进程->Create dump file。只能创建Full Dump;
其他的方法可以参考这里:
https://www.wintellect.com/how-to-capture-a-minidump-let-me-count-the-ways/
结束
感谢阅读!希望大家能有所收获,不正之处还请指教。