Skip to content

Commit 300136c

Browse files
authored
Update 00.Understand.UdonAsm.md
1 parent 4db3bd0 commit 300136c

File tree

1 file changed

+178
-1
lines changed

1 file changed

+178
-1
lines changed

00.Understand.UdonAsm.md

Lines changed: 178 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,181 @@
22

33
我相信只要有 x86_64 汇编基础的人都能够很轻易的理解指令集。
44

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

Comments
 (0)