WinDbg查看Unity C#脚本堆栈
WinDbg查看Unity C#脚本堆栈
最近项目遇到了两次卡死的问题,一次是因为场景里的Collider未正确设置导致脚本里碰撞检测的代码进入了死循环,另一次也是因为while循环跳出条件判断不严谨。
Unity里找到这类问题的原因并不简单,因为卡死既有可能发生在C#脚本层,也有可能发生在Unity Native层。使用MonoDevelopment或UnityVS Attach到卡死进程上,只能看到C#脚本层的堆栈,若卡死发生在Native层则什么也看不到;使用WinDbg虽能看到全部堆栈,但中间C#脚本层的托管代码调用堆栈仅是一堆没有符号的地址也很难看出问题,如下图。
相比VS的调试功能WinDbg更为强大,因此本文探索了在WinDbg中查看Unity C#脚本堆栈的方法。
考虑到C#是遵循CLS跨平台的,mono CLR的实现可能和MS .NET的差别不大,我们首先尝试了WinDbg里调试托管代码的方法——使用SOS调试扩展。然而通过lm列出加载模块我们发现Unity进程并未加载MS .NET CLR,而是加载了mono.dll,因此这种方法不行。
再考虑到Windows上地址与符号的匹配是通过PDB文件实现的,于是想找到Unity编译生成的脚本dll的PDB文件,但我们在Library/noxssAssembliesxsstag目录下只能找到MDB文件。这是因为mono将PDB文件格式转为了自己的MDB文件格式,Unity支持使用外部的编译器来编译脚本dll并导入工程,但若要支持调试还需要通过mono提供的pdb2mdb.exe来将PDB转成MDB的调试符号信息,参考:Managed Plugins。
以上方法不行只能从mono本身入手了。我们从mono官方文档的Debugging一节中找到了一些信息:使用GDB调试mono脚本时是调用了mono本身的一个导出函数来获取给定地址处的函数名,这个导出函数名为mono_pmip(或mono_print_method_from_ip)。那么mono的Windows版本是否也存在一个这样的导出函数呢?通过ln mono!mono_pmip我们发现是存在的,并且mono_pmip和mono_print_method_from_ip是同一函数。
既然导出函数存在,接下来要考虑如何调用它。通过分析可以得出mono_pmip的函数原型应该是char* __cdecl mono_pmip(void* address),而WinDbg的.call命令可以让目标进程执行一个函数,我们尝试了一下,如下图。
这是因为Unity仅提供了自身的Unity.pdb,而未提供mono.dll的PDB文件,导致WinDbg无法判断mono_pmip的函数原型,也就无法生成要执行的指令序了。既然我们已经分析出它的函数原型,那就我们自己处理吧:
1)将参数(待获取函数名的地址)压栈
r esp=esp-0x4
ed esp 0x158e21b4
2)将返回地址压栈(执行完我们要求插入执行的mono_pmip后要将执行绪还给原流程)
r esp=esp-0x4
ed esp eip
3)对当前IP下断(用于查看执行完mono_pmip后的返回结果),然后将IP改为mono_pmip
bp eip
r eip=0x09d6a0f0
4)继续运行,断下后恢复堆栈,查看mono_pmip返回结果
g
r esp=esp+0x4
da /c100 eax
我们用WinDbg Script封装一下:
WinDbg执行:
对比测试源码,可以看到能正确反应问题所在了: