Skip to content

Latest commit

 

History

History
193 lines (138 loc) · 10.4 KB

01.Understand.UdonAsm.md

File metadata and controls

193 lines (138 loc) · 10.4 KB

1.读懂 UdonSharp 指令集

我相信只要有 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

PUSH 和与 PUSH 有关的指令

以下所有出现的汇编都由本组织下的UdonSharpDisassmebler生成。
并且需要说明的是,所有指令都会同时介绍,这是因为这些指令之间非常依赖栈且UdonSharp不具有寄存器

PUSH 是UdonSharp中出现最频繁的一条指令,大部分其它指令的运转都离不开它。

例子1

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

例子2

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

注:JUMP_IF_FALSE 会根据 if 语句是否带有 else 产生变化

如果在原代码中的 if 带有 else,在 JUMP_IF_FALSE 的目标地址的上一条指令必定是 JMP [1],用于从 iftrue 分支出口,例如:

0x0000000000000254  JMP 0x000000000000025C
0x000000000000025C  JMP 0x00000000FFFFFFFC

在上方的示例中, JUMP_IF_FALSE 的目标地址为 0x000000000000025C ,因此上一条指令必定是 JMP ,但是由于在该 ifelse 处不存在任何代码,因此直接生成了一个 JMP 0x000000000000025C

反之,如果原代码中的 if 没有 else ,该 JUMP_IF_FLASE 的目标地址有可能会在非常远的地方。

注:如果其中的一个分支是空的,很可能是因为没有反向检测指令导致的,例如如果存在 JUMP_IF_TRUE 则这种情况可能会改善。

[1] 这个说法也许不正确,它取决于UdonSharp编译器的版本。

例子3

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. 是一个分隔符,在分隔符的左边是命名空间+类名的组合,在右边是函数名+参数个数+返回值类型

由于左边没有任何多余的可解读信息,我们从右边开始,一个完整的调用信息有以下规则:

  1. 每个分段总是以 __ 起头。
  2. 第一个分段总是为被调用的函数名,例如 __get_activeSelf
  3. 第二个分段总是为被调用函数的所有参数类型,例如 __SystemInt32
  4. 第三个分段总是为被调用函数的返回值类型,例如 __SystemBoolean
  5. 一个调用信息至少有两个分段,一个分段为被调用的函数名,另外一个分段为被调用函数的返回值类型
  6. 被调用的函数的所有参数类型 可以被省略。
  7. 被调用的函数的所有参数类型 的参数类型依次以 _ 分割,如果只有一个参数则不存在分隔符。
  8. 如果 被调用的函数名 中以 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 是将该函数的实例推入栈中。

综上,我编写了一条规则:

  1. 调用 类函数 的时候,UdonSharp汇编中与 EXTERN 相关的第一个 PUSH 指令,总是为 这个类的实例,其余每个 PUSH 都为该函数的参数。
  2. 调用 静态函数 的时候,有关该 EXTERN 的所有 PUSH 从第一个开始依次为该函数的参数。
  3. 调用 带有返回值的函数 的时候,有关该 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;

JMP 指令

todo...

结语

因为UdonSharp的设计比较简单,所以我认为这些东西不难理解。

我在这里再抛出几个问题留给你思考:

  1. 在不知道有一个 EXTERN 指令的情况下,你假设接下来的指令可能是 EXTERN ,是否能够只通过 PUSH 指令来猜测到函数的调用参数和返回值?
  2. 请自行解读下方几个指令:
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"
  1. 解读一下 UnityEngineVector4Array.__ctor__SystemInt32__UnityEngineVector4Array