我相信只要有 x86_64 汇编基础的人都能够很轻易的理解指令集。
以下是一些在他们编译器(或汇编器?)中找到的指令集:
NOP
什么都不做的指令。 [2]PUSH
将一个地址推入栈。POP
将栈中一条数据弹出。 [1]JUMP_IF_FALSE
如果结果为FALSE
则跳转,并弹出1次栈。JMP
无条件跳转,同时承载了调用函数的作用。EXTERN
调用外部函数,并弹出参数个数
的栈。ANNOTATION
调用外部函数,并弹出参数个数
的栈。(注意:尽管官方文档中声称这是一个什么都不做的长 NOP 但是定义与 EXTERN 类似,也许只是复制粘贴了 EXTERN 的声明) [2]JUMP_INDIRECT
无条件跳转至取消引用的地址,相当于x86_64下的jmp [eax]
。 [1]COPY
将堆栈中的栈顶中第二个值
赋值给堆栈顶部的值
,并弹出2次栈。
[1]
未证实的内容,仅通过猜测得出的结论。
[2]
这条指令一般不会出现在正常编译后的 UdonSharp 汇编中。
另可参阅:https://creators.vrchat.com/worlds/udon/vm-and-assembly/#udon-opcodes
以下所有出现的汇编都由本组织下的UdonSharpDisassmebler生成。
并且需要说明的是,所有指令都会同时介绍,这是因为这些指令之间非常依赖栈且UdonSharp不具有寄存器
PUSH
是UdonSharp中出现最频繁的一条指令,大部分其它指令的运转都离不开它。
PUSH
指令如何和 COPY
指令配合:
0x0000000000000000 PUSH 0x0000000000000016(N1[UnityEngine.GameObject])
0x0000000000000008 PUSH 0x0000000000000001(__instance_0[UnityEngine.GameObject])
0x0000000000000010 COPY
首先 PUSH
指令将 N1[UnityEngine.GameObject]
推入栈中,此时栈的情况如下:
[0] -> N1[UnityEngine.GameObject]
然后 PUSH
指令将 __instance_0[UnityEngine.GameObject]
推入栈中,此时栈的情况如下:
[0] -> __instance_0[UnityEngine.GameObject]
[1] -> N1[UnityEngine.GameObject]
接着调用 COPY
指令,将堆栈中的栈顶中第二个值
赋值给堆栈顶部的值
,并弹出2次栈。
此时堆栈为空,然后执行了以下操作 __instance_0 = N1
。
PUSH
指令如何 JUMP_IF_FALSE
配合:
0x000000000000002C PUSH 0x0000000000000000(__Boolean_0[System.Boolean])
0x0000000000000034 JNE 0x000000000000025C
首先 PUSH
指令将 __Boolean_0[System.Boolean]
推入栈中,此时堆栈情况如下:
[0] -> __Boolean_0[System.Boolean]
然后调用 JUMP_IF_FALSE
指令,JUMP_IF_FALSE
将检测栈顶中的 bool
变量是否为 FALSE
,并弹出一次栈,如果是,则跳转至 0x000000000000025C
,否则继续执行。
注:JUMP_IF_FALSE
会根据 if
语句是否带有 else
产生变化
如果在原代码中的 if
带有 else
,在 JUMP_IF_FALSE
的目标地址的上一条指令必定是 JMP
[1],用于从 if
的 true
分支出口,例如:
0x0000000000000254 JMP 0x000000000000025C
0x000000000000025C JMP 0x00000000FFFFFFFC
在上方的示例中, JUMP_IF_FALSE
的目标地址为 0x000000000000025C
,因此上一条指令必定是 JMP
,但是由于在该 if
的 else
处不存在任何代码,因此直接生成了一个 JMP 0x000000000000025C
。
反之,如果原代码中的 if
没有 else
,该 JUMP_IF_FLASE
的目标地址有可能会在非常远的地方。
注:如果其中的一个分支是空的,很可能是因为没有反向检测指令导致的,例如如果存在 JUMP_IF_TRUE
则这种情况可能会改善。
[1]
这个说法也许不正确,它取决于UdonSharp编译器的版本。
PUSH
指令如何和 EXTERN
指令配合:
0x0000000000000014 PUSH 0x0000000000000001(__instance_0[UnityEngine.GameObject])
0x000000000000001C PUSH 0x0000000000000000(__Boolean_0[System.Boolean])
0x0000000000000024 EXTERN "UnityEngineGameObject.__get_activeSelf__SystemBoolean"
由于在前面的小节中我已经介绍了两次 PUSH
产生的作用和效果,因此在本例子中,将不再逐条解析。
EXTERN
指令非常多变
,它对栈中的个数要求非常随便,有可能是2,有可能是5,有可能是3。
当然,我只是在瞎说,EXTERN
指令对栈的要求主要是基于被调用函数的属性决定的,在本例子中的代码片段,PUSH
指令出现了两次。
在开始解读之前,我需要介绍一下如何读懂 EXTERN 调用的函数名中包含什么信息。
除了本文以外,官方文档对此做出了一些额外解释:https://creators.vrchat.com/worlds/udon/vm-and-assembly/#externs-reference
我们现在有这样一串文本 UnityEngineGameObject.__get_activeSelf__SystemBoolean
,.
是一个分隔符,在分隔符的左边是命名空间+类名的组合
,在右边是函数名+参数个数+返回值类型
。
由于左边没有任何多余的可解读信息,我们从右边开始,一个完整的调用信息有以下规则:
- 每个分段总是以
__
起头。 - 第一个分段总是为
被调用的函数名
,例如__get_activeSelf
。 - 第二个分段总是为
被调用函数的所有参数类型
,例如__SystemInt32
。 - 第三个分段总是为
被调用函数的返回值类型
,例如__SystemBoolean
。 - 一个调用信息至少有两个分段,一个分段为
被调用的函数名
,另外一个分段为被调用函数的返回值类型
。 被调用的函数的所有参数类型
可以被省略。被调用的函数的所有参数类型
的参数类型依次以_
分割,如果只有一个参数则不存在分隔符。- 如果
被调用的函数名
中以op_
起头,则意味着这个函数属于运算符函数
,可以缩写为对应操作符。
根据以上规则,我们来解读一下 __get_activeSelf__SystemBoolean
。
- 根据
1
,我们知道函数名是get_activeSelf
。 - 根据
5
,我们知道这个函数实际上没有参数。 - 根据
3
,我们知道返回值类型为SystemBoolean
。
现在另外有一个函数名 SystemInt32.__op_GreaterThan__SystemInt32_SystemInt32__SystemBoolean
,根据规则再解读一次
- 根据
1
,我们知道函数名是get_activeSelf
。 - 根据
2
,我们知道这个函数带有参数。 - 根据
6
,我们知道这个函数的参数一
的类型为SystemInt32
,参数二
的类型为SystemInt32
。 - 根据
3
,我们知道返回值类型为SystemBoolean
。 - 根据
7
,我们可以将这个函数缩写为>
,例如a = 1 > 2
。
恭喜!你已经能够解读UdonSharp的函数调用了!
让我们回到之前那个案例,我们还需要继续解读本示例的代码片段
在UdonSharp中,调用栈的顺序和x86_64
是完全相反的,在x86_64
中,第一个调用参数总是在最后才被推入栈,然而,UdonSharp完全相反,第一个调用参数总是第一个被推入栈。
在上述的代码片段中,由于 U# 是基于 C# 的,而 C# 是一个面向对象
语言,因此在调用此类函数的时候需要传递一个实例(instance)
,下面是两种调用类型
类函数调用
需要传递一个实例作为第一个参数的函数。静态函数调用
不需要传递实例即可直接调用的函数。
由于我们可以从 .
分隔符的左边知道 get_activeSelf
来自 UnityEngine.GameObject
,因此我们认定此函数为类函数调用
,你也可以在Unity找到这个类/方法的手册来确定,所以第一个 PUSH
是将该函数的实例推入栈中。
综上,我编写了一条规则:
- 调用
类函数
的时候,UdonSharp汇编中与EXTERN
相关的第一个PUSH
指令,总是为这个类的实例
,其余每个PUSH
都为该函数的参数。 - 调用
静态函数
的时候,有关该EXTERN
的所有PUSH
从第一个开始依次为该函数的参数。 - 调用
带有返回值的函数
的时候,有关该EXTERN
的最后一个PUSH
(即离该EXTERN
最近的PUSH
)总是为接收返回值的变量。(尽管这条没有详细举例说明,但是我还是列出来了。另外返回值总是存在的,但是这里提到的带有返回值
指的是不为SystemVoid
的返回值)
根据上述规则,我们可以解读代码片段:
- 根据
0
,我们知道PUSH 0x0000000000000001(__instance_0[UnityEngine.GameObject])
为推入实例到栈中。 - 根据
2
,我们知道PUSH 0x0000000000000000(__Boolean_0[System.Boolean])
为推入返回值到栈中。
所以我们可以手写出原代码,bool __Boolean_0 = __instance_0.get_activeSelf();
让我们看看之前哪个例子再试试 SystemInt32.__op_GreaterThan__SystemInt32_SystemInt32__SystemBoolean
现有以下代码:
0x0000000000003E74 PUSH 0x0000000000000191(__39__intnlparam[System.Int32])
0x0000000000003E7C PUSH 0x00000000000000AA(__const_SystemInt32_1[System.Int32])
0x0000000000003E84 PUSH 0x00000000000003CE(__intnl_SystemBoolean_43[System.Boolean])
0x0000000000003E8C EXTERN "SystemInt32.__op_GreaterThan__SystemInt32_SystemInt32__SystemBoolean"
- 根据类名
SystemInt32
我们知道,这属于基本类型,而不属于一个类,因此可以适用为1
。 - 根据
1
,我们知道,前面的两个PUSH
为推入参数。 - 根据
2
,我们知道__intnl_SystemBoolean_43[System.Boolean]
承载了返回值。
现在我们已经成功解读了这个调用,我们试试手工将其转换为原代码:
bool __intnl_SystemBoolean_43 = __39__intnlparam > __const_SystemInt32_1;
todo...
因为UdonSharp的设计比较简单,所以我认为这些东西不难理解。
我在这里再抛出几个问题留给你思考:
- 在不知道有一个
EXTERN
指令的情况下,你假设接下来的指令可能是EXTERN
,是否能够只通过PUSH
指令来猜测到函数的调用参数和返回值? - 请自行解读下方几个指令:
0x0000000000003F98 PUSH 0x00000000000003D1(__intnl_SystemString_30[System.String])
0x0000000000003FA0 PUSH 0x0000000000000014(_meshNumber[System.Int32])
0x0000000000003FA8 PUSH 0x00000000000003D2(__intnl_SystemString_31[System.String])
0x0000000000003FB0 EXTERN "SystemString.__Concat__SystemObject_SystemObject__SystemString"
- 解读一下
UnityEngineVector4Array.__ctor__SystemInt32__UnityEngineVector4Array
。