关于Unity5中的一个BUG的研究

某日在群里有人悬赏解决一个Unity 5.2~5.3的BUG(目前已上报过,也试图在github讨论过,不知道5.4会不会解决):

在Unity 5.2~5.3中,Cloth类有一个solverFrequency属性,此属性本应是uint类型,但却被错误地定义为bool,导致代码中无法正常使用。

然而有趣的是,在Unity的编辑器中却可以正确地修改这个值(恐怕这也是Unity官方未能注意到此处有bug的原因之一):

通过对UnityEngine.dll的观察分析,可以看出此.NET类库的绝大部分都只是C++的一个Wrapper,没有实际内容,是在运行时由mono的一个方法mono_add_internal_call绑定到C++中的对应的包装方法,再由这个包装方法调用真正的C++中的对象的内部方法。而至少在这个UnityEngine.dll即.NET部分中,此属性的定义是错误地定义成bool的。

那么我们可以尝试一下修改这个定义。将UnityEngine.dll中此属性的get和set方法修改成uint类型,然后替换Unity中各个UnityEngine.dll(不同的平台都有不同的UnityEngine.dll,但基本相同,只有少数方法会缺失)。结果确实代码中能够设置和修改此属性了,但出现了非常明显的问题:修改范围最高只能到255,尝试设为256得到的值是1,也就是说效果上类型是一个byte,或者unsigned int8。

然而在Unity编辑器里,别说256,设成几千都没问题(但是会很卡)。为何Unity编辑器不会出问题呢?

原因是:相较于Unity的执行引擎,比如PC版的Player,Unity编辑器是一个加强版的UnityPlayer,它通过一种Proxy代理通信方式直接序列化C++中的真正对象,拿到和修改其中的值。可以参考UnityEditor中的SerializedObject类。由于整个对象的来回传输都是通过序列化,因此总能正确反映真实的情况。问题是真正部署时的执行引擎是没有提供这些序列化功能的,因此无法直接使用序列化的方法来解决此问题。(但执行引擎中也有Proxy代理相关的类,但并不知道如何才能逆向利用)

以下代码演示了如何通过SerializedObject实现正确无数值限制地修改此属性。但是由于部署时无法引用UnityEditor命名空间(因为Player中没有这些功能),所以并不能实际解决此BUG。

public static class ClothExtension
{
    public static uint GetSolverFrequency(this Cloth c)
    {
        SerializedObject sObj = new SerializedObject(c);
        return (uint)sObj.FindProperty("m_SolverFrequency").longValue;
    }
    public static void SetSolverFrequency(this Cloth c, uint freq)
    {
        SerializedObject sObj = new SerializedObject(c);
        sObj.FindProperty("m_SolverFrequency").longValue = freq;
        sObj.ApplyModifiedPropertiesWithoutUndo();
    }
}

接下来探讨:为何修改dll后设置效果仍仅限8位?

通过逆向分析PC版的执行引擎Player.exe,我们可以在某内部包装方法(即用于和.NET中set_solverFrequency(bool value)函数绑定的方法)的实现中看到它主动“缩短”传入参数为8位的过程:

在OD调试过程中,将movzx edx,dl一句Patch掉,结果代码的表现确实变得正常(可以设置256以上的值)了,虽然取值时取到的值仍然不正确,因为没有对应地Patch一下get方法。这是一个确实有效的紧急修复方案,但是目前仅限于PC平台——因为我对ARM汇编实在不甚了解,看了半天,并没有发现ARM汇编是在哪里“缩短”传入参数的,但至少不是在对应的那个包装方法中。

这样的表现意味着C++的包装部分也和.NET包装一样设计成了bool类型,但由于C++中的bool是8位的(而C#中为32位),所以在C++中把传入参数给缩短了。表现如此一致的bug,显然两者是采用了某种代码生成技术同时生成导致的。通过查看古老版本的Unity源码,可看到确实如此。对一个或几个类的包装定义是使用一个txt文件来描述的,格式大概类似于下面这样:

	// A random, external acceleration applied to the cloth.
	AUTO_PROP Vector3 randomAcceleration GetRandomAcceleration SetRandomAcceleration
	
	// Should gravity affect the cloth simulation?
	AUTO_PROP bool useGravity GetUseGravity SetUseGravity

显然出了问题的就是某个txt(通过多处逆向研究可推测名字大概为DynamicsBindings.txt)中对Cloth类的solverFrequency的定义出错了。

//目前错误的定义
AUTO_PROP bool solverFrequency

//正确的定义
AUTO_PROP uint solverFrequency

只要Unity工作人员修改这一行,再重新编译,想必就能解决此问题。我研究了这么几天,结果还是只能解决一下PC端的问题,或者在1~255之间有限地修改。由于求助者的目标平台是iOS,因此目前此问题我还是没能解决。

 

如果要求不高,只需要在1~255之间修改的话,要修改和替换所有的UnityEngine.dll显得过于麻烦。事实上由于在C#中bool值其实也是int数值(true其实是1,false其实是0),所以我们可以通过类型转换解决“向bool参数传入一个uint”这个听起来奇葩但此处却真有用的问题。

为此我做了一个ClothExt.dll,就几行代码,效果如下图。只需要将此dll放入Unity项目的Plugins文件夹下,在代码中引入此库,即可通过这两个扩展方法取得和设置值,而不需要修改任何Unity的dll。当然,范围限制在1~255之间。(估计受众太小了,就不上传了,实在有需要者可邮件联系我)

这代码也许对于C#的初学者来说显得有些不可思议。是的,其实C#不支持这个bool和uint之间的类型转换语法。但是也只是C#不支持而已,CLR其实是支持的(因为bool、int、uint在CLR内部都是i4——4字节整数,并没有什么区别),在IL中看,这个过程根本就没有“转换类型”。

题外话:提到CLR,其实目前国内绝大多数Unity开发者恐怕都不懂CLR、Mono、.NET、C#之间是什么关系,在群里经常能看到“这就是mono和C#的区别了吧”之类的话,令人汗颜。不过毕竟大部分人其实并没想学好C#,只想用好Unity;追求更高一点的人只是想做好游戏。所以我也倒可以理解,不过上面这种发言恐怕还是会误导更多的C#初学者吧。

添加评论

Loading