昨天遇到了一个 C# DLL 动态载入后调试信息缺失的问题,今天上午解决后记录一下,以便遇到这个问题的同学可以参考。

(注)此文中的截图内文字偏小,可以 Ctrl + 鼠标滚轮放大查看。


问题描述

我们知道,Unity 中的 Debug.Log() 系列函数不仅能输出用户内容,而且能通过类似 StackTraceUtility.ExtractStackTrace() 这样的机制把该输出对应的堆栈打出来;当用户代码出现未捕获异常时,Unity 也会利用该机制输出异常及相关的完整堆栈信息。

如这个函数:

void PrintStacktraceOrdinary()
{
    Debug.LogFormat("Stacktrace (ordinary): \n{0}", 
        Environment.StackTrace);
}

会输出下面的结果:

1

注意,此图中 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());
}

会输出下面的结果:

2

注意,此图中 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 下的子目录里找到了这两个文件:

3

看起来 Unity/mono 生成的调试文件名叫 .mdb,也就是说只要把 VS 生成的 pdb 文件转成 mdb 就可以了。很快找到方法:

<unity_root>\Unity-5.3.6f1\Editor\Data\Mono\lib\mono\2.0\pdb2mdb.exe <target_assembly>.dll

使用这个生成 mdb 后放入 Assets 目录,顺利得到额外的调试信息:

4


动态载入 & 未捕获的异常

如果这个 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 中制造一个,一样能看到完整的调试信息:

5

多说一句,这里若干操作均未处理错误,实际项目里至少要检查 Assembly 是否加载成功,处理获取的函数是否有效等错误。

示例代码

本文中所有的示例代码可以在这里找到:

Gu Lu
2016-08-30

Comments
Write a Comment

知识共享许可协议
本作品由Gu Lu创作,采用知识共享Attribution-NonCommercial-NoDerivatives 4.0 国际许可协议进行许可。