昨天遇到了一个 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

Comments
Write a Comment

Tags

随笔   游戏开发   Programming   C/C++   优化   Unity   C++   知乎   游戏设计   比特币   Unity3D   区块链   软件开发   Bitcoin   引擎设计   系统架构   Production   idtech   中国文化   加密货币   项目管理   游戏评论   资源管理   资源流水线   效率   道德经   网络   方法论   模板编程   Blockchain   Lua   Blockchain Computing   Oculus   GDC   渲染   VR   PerfAssist   Unity MemoryProfiler   BCH   经济学   信息过载   行业报告   字体   Productivity   图形   网络编程   Dice   协程   EMC   Premake   测试   中间件   Game Engine   新手引导   区块链游戏   Methodology   CI   命令行解析   goroutine   ndk   Ethereum   nanomsg   自动化   Scripting   摘录   Debugging   同步技术   cppcon   C++模板   DOOM3   技术评估   Unity GC   C++11   学习方法   Surface Pro 3   Engine Evaluation   CRT   文化   笔记   golang   图形编程   多线程   ETH   Bitcoin Cash   cppcon14   Bitcoin SV   Visual Studio   Unity Coroutine   跨语言可变参数列表   团队协作   货币   Deployment   Visual Assist   工程改进   Michael Abrash   exp   开放世界   量子计算   域名   虚拟现实   系统重构   slua   遮挡剔除   完美转发   协作式调度   Modern C++   类型推导   Memory Debugging   个人成长   小故事   BTC   暴雪   产品   历史   错误处理   Unity Profiler   MOD  

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