昨天遇到了一个 C# DLL 动态载入后调试信息缺失的问题,今天上午解决后记录一下,以便遇到这个问题的同学可以参考。
(注)此文中的截图内文字偏小,可以 Ctrl + 鼠标滚轮放大查看。
问题描述
我们知道,Unity 中的 Debug.Log() 系列函数不仅能输出用户内容,而且能通过类似 StackTraceUtility.ExtractStackTrace() 这样的机制把该输出对应的堆栈打出来;当用户代码出现未捕获异常时,Unity 也会利用该机制输出异常及相关的完整堆栈信息。
如这个函数:
void PrintStacktraceOrdinary()
{
Debug.LogFormat("Stacktrace (ordinary): n{0}",
Environment.StackTrace);
}
会输出下面的结果:
注意,此图中 StackTrace 上的每个函数都有完整的调试信息(函数,源文件,行号)。
但是,当调用链中有一部分位于一个外部 DLL 中时,位于外部 DLL 中的这一部分函数调用,是无法像正常的函数那样显示堆栈的。
如下面位于单独 DLL 的函数:
namespace test_stacktrace_dll
{
public class Foo
{
public static string GetStacktraceInDLL()
{
return Environment.StackTrace;
}
}
}
在 Unity 工程中像下面这样使用
void PrintStacktraceInsideUserDLL()
{
Debug.LogFormat("Stacktrace (inside user dll): n{0}",
test_stacktrace_dll.Foo.GetStacktraceInDLL());
}
会输出下面的结果:
注意,此图中 Stacktrace 的第二行 GetStacktraceInDLL(),也就是外部 DLL 内的函数,尾部是不显示文件和行号信息的。
这样的话,如果项目的 C# 代码放在外部的 DLL 里(一般有更好的代码组织和模块化,更好的 VS IL 代码生成,方便代码热更新等原因),调试的时候就会缺失不少辅助信息。
问题解决
我们知道,每一个 VS 编译出来的 C# DLL 都带有一个 pdb 存储着调试相关的信息,如果能让 Unity 项目在运行时定位到这个 pdb 理论上就可以获取对应的信息了。对于一个常规的 C# 程序,只要把某个 DLL 对应的 .pdb 文件拷到 .exe 所在目录就可以了,但我试了一下把 .pdb 拷到项目 Assets 所在目录或 Unity.exe 所在目录,发现仍然无效,此时我开始推测是 Unity 所用的 mono 与 VS 生成的 pdb 之间的兼容性问题。
随即想到,既然 Unity 项目自己生成的 Assembly 能顺利找到调试信息,那么这些 mono 生成的调试信息一定是存在项目工程内的某个地方,于是开始翻项目目录,最终在 Library 下的子目录里找到了这两个文件:
看起来 Unity/mono 生成的调试文件名叫 .mdb,也就是说只要把 VS 生成的 pdb 文件转成 mdb 就可以了。很快找到方法:
使用这个生成 mdb 后放入 Assets 目录,顺利得到额外的调试信息:
动态载入 & 未捕获的异常
如果这个 DLL 是动态载入的呢?
使用下面的代码动态载入这个 DLL 并调用上面的 GetStacktraceInDLL() 函数:
string localAssmlyPath = "Assets/test_stacktrace_dll.bin";
byte[] src = File.ReadAllBytes(localAssmlyPath);
string localSymbolPath = "Assets/test_stacktrace_dll.bin.mdb";
byte[] symbolBytes = File.ReadAllBytes(localSymbolPath);
Assembly assembly = Assembly.Load(src, symbolBytes);
Type type = assembly.GetType("test_stacktrace_dll.Foo");
MethodInfo getStacktrace = type.GetMethod("GetStacktraceInDLL",
BindingFlags.Public | BindingFlags.Static);
Debug.LogFormat("Stacktrace (dynamically): n{0}",
getStacktrace.Invoke(null, null));
载入时一同载入对应的 mdb 文件,可以得到同样的结果。
顺便测试一下未捕获的异常,手动在 DLL 中制造一个,一样能看到完整的调试信息:
多说一句,这里若干操作均未处理错误,实际项目里至少要检查 Assembly 是否加载成功,处理获取的函数是否有效等错误。