很显然,UdonSharp和大多数编程语言一样,VRChat做了类似的事情
为了简称,以下开始 U# 代表 UdonSharp。
我在写这篇文章的时候非常随意,可能存在错误,不严谨的地方,如果你发现有任何问题或者有疑问请及时在 issue 提出。
UdonAssembly 几乎是 1 对 1 直接编译生成的,它将调用的函数,变量赋值等操作直接编译为等效的 UdonAssembly 指令,毫无优化可言(根本就是没有)。
官方对此的说明:
https://udonsharp.docs.vrchat.com/random-tips-&-performance-pointers#udon-is-slow
让我们打个比方,下面有一段看着非常糟糕的代码
bool somebool = false;
bool IsSafe()
{
if(somebool)
{
return true;
}
else{
return false;
}
}
对于这样的代码,U#的编译器不会进行任何优化,直接编译,结果如下:
.data_start
__refl_const_intnl_udonTypeID: %SystemInt64, null
__refl_const_intnl_udonTypeName: %SystemString, null
somebool: %SystemBoolean, null
__1_const_intnl_SystemBoolean: %SystemBoolean, null
__0_const_intnl_SystemBoolean: %SystemBoolean, null
__0_const_intnl_SystemUInt32: %SystemUInt32, null
__0_intnl_returnValSymbol_Boolean: %SystemBoolean, null
__0_intnl_returnTarget_UInt32: %SystemUInt32, null
.data_end
# using UdonSharp;
# using UnityEngine;
# using VRC.SDKBase;
# using VRC.Udon;
# public class GameObject3 : UdonSharpBehaviour
.code_start
# bool somebool = false;
# void Start()
.export _start
_start:
PUSH, __0_const_intnl_SystemUInt32
# {
PUSH, __0_intnl_returnTarget_UInt32 # Function epilogue
COPY
JUMP_INDIRECT, __0_intnl_returnTarget_UInt32
# bool IsSafe()
IsSafe:
PUSH, __0_const_intnl_SystemUInt32
# {
# if(somebool)
PUSH, somebool
JUMP_IF_FALSE, 0x00000064
# {
# return true;
PUSH, __0_const_intnl_SystemBoolean
PUSH, __0_intnl_returnValSymbol_Boolean
COPY
PUSH, __0_intnl_returnTarget_UInt32 # Explicit return sequence
COPY
JUMP_INDIRECT, __0_intnl_returnTarget_UInt32
JUMP, 0x0000008C
# else{
# return false;
PUSH, __1_const_intnl_SystemBoolean
PUSH, __0_intnl_returnValSymbol_Boolean
COPY
PUSH, __0_intnl_returnTarget_UInt32 # Explicit return sequence
COPY
JUMP_INDIRECT, __0_intnl_returnTarget_UInt32
PUSH, __0_intnl_returnTarget_UInt32 # Function epilogue
COPY
JUMP_INDIRECT, __0_intnl_returnTarget_UInt32
.code_end
官方反编译:
0x00000000: PUSH, 0x00000005
0x00000008: PUSH, 0x00000007
0x00000010: COPY
0x00000014: JUMP_INDIRECT, __0_intnl_returnTarget_UInt32
0x0000001C: PUSH, 0x00000005
0x00000024: PUSH, 0x00000002
0x0000002C: JUMP_IF_FALSE, 0x00000064
0x00000034: PUSH, 0x00000004
0x0000003C: PUSH, 0x00000006
0x00000044: COPY
0x00000048: PUSH, 0x00000007
0x00000050: COPY
0x00000054: JUMP_INDIRECT, __0_intnl_returnTarget_UInt32
0x0000005C: JUMP, 0x0000008C
0x00000064: PUSH, 0x00000003
0x0000006C: PUSH, 0x00000006
0x00000074: COPY
0x00000078: PUSH, 0x00000007
0x00000080: COPY
0x00000084: JUMP_INDIRECT, __0_intnl_returnTarget_UInt32
0x0000008C: PUSH, 0x00000007
0x00000094: COPY
0x00000098: JUMP_INDIRECT, __0_intnl_returnTarget_UInt32
官方甚至贴心的帮你标注好源码对照...
在你看不见的地方(也可能看得见),创建了大量的 重复
变量,这些变量主要用于代替赋值,我认为这是他们的设计缺陷。
参阅官方文档:
https://creators.vrchat.com/worlds/udon/vm-and-assembly/#overview-of-the-udon-vm
譬如:
这里有一段我从别的地方转储出的反汇编
0x0000000000000000 PUSH 0x000000000000000A(Obj[UnityEngine.GameObject[]])
0x0000000000000008 PUSH 0x0000000000000009(__instance_0[UnityEngine.GameObject[]])
0x0000000000000010 COPY
0x0000000000000014 PUSH 0x0000000000000009(__instance_0[UnityEngine.GameObject[]])
0x000000000000001C PUSH 0x0000000000000007(__end_0[System.Int32])
0x0000000000000024 EXTERN "UnityEngineGameObjectArray.__get_Length__SystemInt32"
你说说,这为什么要创建一个新变量 __instance_0
再把Obj
给赋值过去?直接用Obj
不行吗?
然而,这只是一小段代码,想想看这样的东西成千上万的存在于你的代码生成的UdonAssembly中,多可怕啊。
我认为,这是一个可以证明 U# 是一个只有栈(Stack)
而没有堆(Heap)
的虚拟机语言
U#的调用非常随便,像是...上一秒还在和你说话的人下一秒直接消失了。
所有的函数调用都是把变量推入堆栈然后直接跳走的,我相信设计UdonAssembly的人可能多少受到了一些x86_64指令集的启发,但是无论如何,他们没有 CALL
指令
下面我们来看一段代码:
.func__start_0x3B8
0x00000000000003B8 PUSH 0x000000000000006F(__const_SystemUInt32_0[System.UInt32])
0x00000000000003C0 PUSH 0x000000000000007F(__gintnl_SystemUInt32_0[System.UInt32])
0x00000000000003C8 PUSH 0x0000000000000081(__const_SystemBoolean_0[System.Boolean])
0x00000000000003D0 PUSH 0x0000000000000080(__0_active__param[System.Boolean])
0x00000000000003D8 COPY
0x00000000000003DC JMP 0x0000000000007C24
0x00000000000003E4 PUSH 0x0000000000000081(__const_SystemBoolean_0[System.Boolean])
0x00000000000003EC PUSH 0x000000000000000B(_isBusy[System.Boolean])
0x00000000000003F4 COPY
JMP
这个指令,除了用作带else
的if
的分支跳转以外,还被用作for
和while
的循环执行。
...当然,还用作调用函数!
我们可以清楚的看到,在0x3DC
处,直接跳走到0x7C24
,您认为函数会有这么大吗?
很显然,这是一个函数调用,让我们来看看 0x7C24
处
0x0000000000007C1C PUSH 0x000000000000006F(__const_SystemUInt32_0[System.UInt32])
0x0000000000007C24 PUSH 0x0000000000000080(__0_active__param[System.Boolean])
0x0000000000007C2C JNE 0x0000000000007D60
0x0000000000007C34 PUSH 0x00000000000000AA(__const_SystemInt32_1[System.Int32])
0x0000000000007C3C PUSH 0x000000000000049E(__lcl_i_SystemInt32_11[System.Int32])
0x0000000000007C44 COPY
0x0000000000007C48 PUSH 0x000000000000027D(__gintnl_SystemUInt32_272[System.UInt32])
0x0000000000007C50 PUSH 0x000000000000027E(__gintnl_SystemUInt32_273[System.UInt32])
根据指令的特征来看,这是一个标准的U#函数头,PUSH __const_SystemUInt32_0
可以用作判断依据。
在 x86_64
下,调用的返回值一般都会存放在 eax(rax)
下,但是由于 U# 不存在寄存器,因此,他们只能被迫将返回值使用一个新的临时变量保存。[1]
这意味着任何可以在游戏内调查U#对象的人,都可以轻易的得知上一次调用函数的返回值是什么。
[1]
不总是会产生临时变量用于保存返回值,它取决于 U# 的编译器版本。
在 U# 中,每个指令都是 4字节 长的,并且做的事情都特别简单,这也导致了特别低效。
几乎所有的 U# 运算操作都是由调用外部函数来完成的,你可以参阅这里 和 这里 来查看
这基本上意味着每进行一次简单的运算都会产生一次调用(在这些函数包装的背后还有更大的开销)的开销,缺少多样化且低效。
参阅官方文档:
https://creators.vrchat.com/worlds/udon/vm-and-assembly/#udon-opcodes
综合上面的种种说明,我们现在知道了,U# 一种简单,且不高效的语言。
在你吐槽之前,先等等,我知道这里是手册,而不是专门批判U#的地方。但是恰恰相反,正是因为U#的种种缺陷和其简单性,其对逆向工程非常友好,而且你几乎可以得到源代码级别的U#汇编代码。