|
2 | 2 |
|
3 | 3 | 我相信只要有 x86_64 汇编基础的人都能够很轻易的理解指令集。
|
4 | 4 |
|
5 |
| -WIP |
| 5 | +以下是一些在他们编译器(或汇编器?)中找到的指令集: |
| 6 | + |
| 7 | +- `NOP` 什么都不做的指令。 *[2]* |
| 8 | +- `PUSH` 将一个地址推入栈。 |
| 9 | +- `POP` 将栈中一条数据弹出。 *[1]* |
| 10 | +- `JUMP_IF_FALSE` 如果结果为 `FALSE` 则跳转,并弹出1次栈。 |
| 11 | +- `JMP` 无条件跳转,同时承载了调用函数的作用。 |
| 12 | +- `EXTERN` 调用外部函数,并弹出`参数个数`的栈。 |
| 13 | +- `ANNOTATION` 调用外部函数,并弹出`参数个数`的栈。 *[2]* |
| 14 | +- `JUMP_INDIRECT` 无条件跳转至取消引用的地址,相当于x86_64下的`jmp [eax]`。 *[1]* |
| 15 | +- `COPY` 将堆栈中的`栈顶中第二个值`赋值给`堆栈顶部的值`,并弹出2次栈。 |
| 16 | + |
| 17 | +`[1]` 未证实的内容,仅通过猜测得出的结论。 |
| 18 | + |
| 19 | +`[2]` 这条指令一般不会出现在正常编译后的 UdonSharp 汇编中。 |
| 20 | + |
| 21 | +# 举例来理解它们 |
| 22 | +``` |
| 23 | +以下所有出现的汇编都由本组织下的UdonSharpDisassmebler生成。 |
| 24 | +并且需要说明的是,所有指令都会同时介绍,这是因为这些指令之间非常依赖栈且UdonSharp不具有寄存器 |
| 25 | +``` |
| 26 | +`PUSH` 是UdonSharp中出现最频繁的一条指令,大部分其它指令的运转都离不开它。 |
| 27 | + |
| 28 | +## 例子1 |
| 29 | + |
| 30 | +`PUSH` 指令如何和 `COPY` 指令配合: |
| 31 | +``` |
| 32 | +0x0000000000000000 PUSH 0x0000000000000016(N1[UnityEngine.GameObject]) |
| 33 | +0x0000000000000008 PUSH 0x0000000000000001(__instance_0[UnityEngine.GameObject]) |
| 34 | +0x0000000000000010 COPY |
| 35 | +``` |
| 36 | +首先 `PUSH` 指令将 `N1[UnityEngine.GameObject]` 推入栈中,此时栈的情况如下: |
| 37 | + |
| 38 | +``` |
| 39 | +[0] -> N1[UnityEngine.GameObject] |
| 40 | +``` |
| 41 | +然后 `PUSH` 指令将 `__instance_0[UnityEngine.GameObject]` 推入栈中,此时栈的情况如下: |
| 42 | + |
| 43 | +``` |
| 44 | +[0] -> __instance_0[UnityEngine.GameObject] |
| 45 | +[1] -> N1[UnityEngine.GameObject] |
| 46 | +``` |
| 47 | +接着调用 `COPY` 指令,将堆栈中的`栈顶中第二个值`赋值给`堆栈顶部的值`,并弹出2次栈。 |
| 48 | +此时堆栈为空,然后执行了以下操作 `__instance_0 = N1`。 |
| 49 | + |
| 50 | +## 例子2 |
| 51 | + |
| 52 | +`PUSH` 指令如何 `JUMP_IF_FALSE` 配合: |
| 53 | +``` |
| 54 | +0x000000000000002C PUSH 0x0000000000000000(__Boolean_0[System.Boolean]) |
| 55 | +0x0000000000000034 JNE 0x000000000000025C |
| 56 | +``` |
| 57 | +首先 `PUSH` 指令将 `__Boolean_0[System.Boolean]` 推入栈中,此时堆栈情况如下: |
| 58 | + |
| 59 | +``` |
| 60 | +[0] -> __Boolean_0[System.Boolean] |
| 61 | +``` |
| 62 | +然后调用 `JUMP_IF_FALSE` 指令,`JUMP_IF_FALSE` 将检测栈顶中的 `bool` 变量是否为 `FALSE`,并弹出一次栈,如果是,则跳转至 `0x000000000000025C`,否则继续执行。 |
| 63 | + |
| 64 | +### JUMP_IF_FALSE |
| 65 | +注:`JUMP_IF_FALSE` 会根据 `if` 语句是否带有 `else` 产生变化 |
| 66 | + |
| 67 | +如果在原代码中的 `if` 带有 `else`,在 `JUMP_IF_FALSE` 的目标地址的上一条指令必定是 `JMP` *[1]*,用于从 `if` 的 `true` 分支出口,例如: |
| 68 | +``` |
| 69 | +0x0000000000000254 JMP 0x000000000000025C |
| 70 | +0x000000000000025C JMP 0x00000000FFFFFFFC |
| 71 | +``` |
| 72 | +在上方的示例中, `JUMP_IF_FALSE` 的目标地址为 `0x000000000000025C` ,因此上一条指令必定是 `JMP` ,但是由于在该 `if` 的 `else` 处不存在任何代码,因此直接生成了一个 `JMP 0x000000000000025C`。 |
| 73 | + |
| 74 | +反之,如果原代码中的 `if` 没有 `else` ,该 `JUMP_IF_FLASE` 的目标地址有可能会在非常远的地方。 |
| 75 | + |
| 76 | +`[1]` 这个说法也许不正确,它取决于UdonSharp编译器的版本。 |
| 77 | + |
| 78 | +## 例子3 |
| 79 | + |
| 80 | +`PUSH` 指令如何和 `EXTERN` 指令配合: |
| 81 | +``` |
| 82 | +0x0000000000000014 PUSH 0x0000000000000001(__instance_0[UnityEngine.GameObject]) |
| 83 | +0x000000000000001C PUSH 0x0000000000000000(__Boolean_0[System.Boolean]) |
| 84 | +0x0000000000000024 EXTERN "UnityEngineGameObject.__get_activeSelf__SystemBoolean" |
| 85 | +``` |
| 86 | +由于在前面的小节中我已经介绍了两次 `PUSH` 产生的作用和效果,因此在本例子中,将不再逐条解析。 |
| 87 | + |
| 88 | +`EXTERN` 指令非常`多变`,它对栈中的个数要求非常随便,有可能是2,有可能是5,有可能是3。 |
| 89 | + |
| 90 | +当然,我只是在瞎说,`EXTERN` 指令对栈的要求主要是基于被调用函数的属性决定的,在本例子中的代码片段,`PUSH` 指令出现了两次。 |
| 91 | + |
| 92 | +在开始解读之前,我需要介绍一下如何读懂 EXTERN 调用的函数名中包含什么信息。 |
| 93 | + |
| 94 | +### 函数名包含的信息 |
| 95 | + |
| 96 | +我们现在有这样一串文本 `UnityEngineGameObject.__get_activeSelf__SystemBoolean` ,`.` 是一个分隔符,在分隔符的左边是`命名空间+类名的组合`,在右边是`函数名+参数个数+返回值类型`。 |
| 97 | + |
| 98 | +由于左边没有任何多余的可解读信息,我们从右边开始,一个完整的调用信息有以下规则: |
| 99 | + |
| 100 | +0. 每个分段总是以 `__` 起头。 |
| 101 | +1. 第一个分段总是为`被调用的函数名`,例如 `__get_activeSelf` 。 |
| 102 | +2. 第二个分段总是为`被调用函数的所有参数类型`,例如 `__SystemInt32`。 |
| 103 | +3. 第三个分段总是为`被调用函数的返回值类型`,例如 `__SystemBoolean`。 |
| 104 | +4. 一个调用信息至少有两个分段,一个分段为`被调用的函数名`,另外一个分段为`被调用函数的返回值类型`。 |
| 105 | +5. `被调用的函数的所有参数类型` 可以被省略。 |
| 106 | +6. `被调用的函数的所有参数类型` 的参数类型依次以 `_` 分割,如果只有一个参数则不存在分隔符。 |
| 107 | +7. 如果 `被调用的函数名` 中以 `op_` 起头,则意味着这个函数属于`运算符函数`,可以缩写为对应操作符。 |
| 108 | + |
| 109 | +根据以上规则,我们来解读一下 `__get_activeSelf__SystemBoolean`。 |
| 110 | + |
| 111 | +- 根据 `1`,我们知道函数名是 `get_activeSelf`。 |
| 112 | +- 根据 `5`,我们知道这个函数实际上没有参数。 |
| 113 | +- 根据 `3`,我们知道返回值类型为`SystemBoolean`。 |
| 114 | + |
| 115 | +现在另外有一个函数名 `SystemInt32.__op_GreaterThan__SystemInt32_SystemInt32__SystemBoolean`,根据规则再解读一次 |
| 116 | + |
| 117 | +- 根据 `1`,我们知道函数名是 `get_activeSelf`。 |
| 118 | +- 根据 `2`,我们知道这个函数带有参数。 |
| 119 | +- 根据 `6`,我们知道这个函数的`参数一`的类型为 `SystemInt32`,`参数二`的类型为 `SystemInt32`。 |
| 120 | +- 根据 `3`,我们知道返回值类型为`SystemBoolean`。 |
| 121 | +- 根据 `7`,我们可以将这个函数缩写为 `>` ,例如 `a = 1 > 2` 。 |
| 122 | + |
| 123 | +恭喜!你已经能够解读UdonSharp的函数调用了! |
| 124 | + |
| 125 | +让我们回到之前那个案例,我们还需要继续解读本示例的代码片段 |
| 126 | + |
| 127 | +在UdonSharp中,调用栈的顺序和`x86_64`是完全相反的,在`x86_64`中,第一个调用参数总是在最后才被推入栈,然而,UdonSharp完全相反,第一个调用参数总是第一个被推入栈。 |
| 128 | + |
| 129 | +在上述的代码片段中,由于 U# 是基于 C# 的,而 C# 是一个`面向对象`语言,因此在调用此类函数的时候需要传递一个`实例(instance)`,下面是两种调用类型 |
| 130 | + |
| 131 | +- `类函数调用` 需要传递一个实例作为第一个参数的函数。 |
| 132 | +- `静态函数调用` 不需要传递实例即可直接调用的函数。 |
| 133 | + |
| 134 | +由于我们可以从 `.` 分隔符的左边知道 `get_activeSelf` 来自 `UnityEngine.GameObject`,因此我们认定此函数为`类函数调用`,你也可以在Unity找到这个类/方法的手册来确定,所以第一个 `PUSH` 是将该函数的实例推入栈中。 |
| 135 | + |
| 136 | +综上,我编写了一条规则: |
| 137 | + |
| 138 | +0. 调用 `类函数` 的时候,UdonSharp汇编中与 `EXTERN` 相关的第一个 `PUSH` 指令,总是为 `这个类的实例`,其余每个 `PUSH` 都为该函数的参数。 |
| 139 | +1. 调用 `静态函数` 的时候,有关该 `EXTERN` 的所有 `PUSH` 从第一个开始依次为该函数的参数。 |
| 140 | +2. 调用 `带有返回值的函数` 的时候,有关该 `EXTERN` 的最后一个 `PUSH` (即离该 `EXTERN` 最近的 `PUSH` )总是为接收返回值的变量。(尽管这条没有详细举例说明,但是我还是列出来了。另外返回值总是存在的,但是这里提到的 `带有返回值` 指的是不为 `SystemVoid` 的返回值) |
| 141 | + |
| 142 | +根据上述规则,我们可以解读代码片段: |
| 143 | + |
| 144 | +- 根据 `0`,我们知道 `PUSH 0x0000000000000001(__instance_0[UnityEngine.GameObject])` 为推入实例到栈中。 |
| 145 | +- 根据 `2`,我们知道 `PUSH 0x0000000000000000(__Boolean_0[System.Boolean])` 为推入返回值到栈中。 |
| 146 | + |
| 147 | +所以我们可以手写出原代码,`bool __Boolean_0 = __instance_0.get_activeSelf();` |
| 148 | + |
| 149 | +让我们看看之前哪个例子再试试 `SystemInt32.__op_GreaterThan__SystemInt32_SystemInt32__SystemBoolean` |
| 150 | + |
| 151 | +现有以下代码: |
| 152 | +``` |
| 153 | +0x0000000000003E74 PUSH 0x0000000000000191(__39__intnlparam[System.Int32]) |
| 154 | +0x0000000000003E7C PUSH 0x00000000000000AA(__const_SystemInt32_1[System.Int32]) |
| 155 | +0x0000000000003E84 PUSH 0x00000000000003CE(__intnl_SystemBoolean_43[System.Boolean]) |
| 156 | +0x0000000000003E8C EXTERN "SystemInt32.__op_GreaterThan__SystemInt32_SystemInt32__SystemBoolean" |
| 157 | +``` |
| 158 | + |
| 159 | +- 根据类名 `SystemInt32` 我们知道,这属于基本类型,而不属于一个类,因此可以适用为 `1`。 |
| 160 | +- 根据 `1`,我们知道,前面的两个 `PUSH` 为推入参数。 |
| 161 | +- 根据 `2`,我们知道 `__intnl_SystemBoolean_43[System.Boolean]` 承载了返回值。 |
| 162 | + |
| 163 | +现在我们已经成功解读了这个调用,我们试试手工将其转换为原代码: |
| 164 | +```cs |
| 165 | +bool __intnl_SystemBoolean_43 = __39__intnlparam > __const_SystemInt32_1; |
| 166 | +``` |
| 167 | + |
| 168 | +# 结语 |
| 169 | +我认为这些东西不难理解,重点在于你是否具有一定编程基础,因为UdonSharp的设计过于简单,我不会说解读这样的反汇编很难,因为它只是一串一串对于人类来说非常可读的指令。 |
| 170 | + |
| 171 | +我在这里再抛出几个问题留给你思考: |
| 172 | + |
| 173 | +1. 在不知道有一个 `EXTERN` 指令的情况下,你假设接下来的指令可能是 `EXTERN` ,是否能够只通过 `PUSH` 指令来猜测到函数的调用参数和返回值? |
| 174 | +2. 请自行解读下方几个指令: |
| 175 | +``` |
| 176 | +0x0000000000003F98 PUSH 0x00000000000003D1(__intnl_SystemString_30[System.String]) |
| 177 | +0x0000000000003FA0 PUSH 0x0000000000000014(_meshNumber[System.Int32]) |
| 178 | +0x0000000000003FA8 PUSH 0x00000000000003D2(__intnl_SystemString_31[System.String]) |
| 179 | +0x0000000000003FB0 EXTERN "SystemString.__Concat__SystemObject_SystemObject__SystemString" |
| 180 | +``` |
| 181 | +3. 解读一下 `UnityEngineVector4Array.__ctor__SystemInt32__UnityEngineVector4Array`。 |
| 182 | + |
0 commit comments