Skip to content

Commit 9dcbe36

Browse files
committed
inital commit
0 parents  commit 9dcbe36

File tree

6 files changed

+271
-0
lines changed

6 files changed

+271
-0
lines changed

.github/workflows/ci.yml

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
name: Create Tag
2+
3+
on:
4+
push:
5+
branches:
6+
- main
7+
8+
jobs:
9+
build:
10+
runs-on: ubuntu-latest
11+
steps:
12+
- uses: actions/checkout@v2
13+
- uses: Klemensas/action-autotag@stable
14+
with:
15+
GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}"

Documentation/unity-dlls.png

90.9 KB
Loading
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
{
2+
"name": "com.bbbirder.unity-injection.Editor",
3+
"rootNamespace": "",
4+
"references": [
5+
"com.bbbirder.unity-injection"
6+
],
7+
"includePlatforms": [
8+
"Editor"
9+
],
10+
"excludePlatforms": [],
11+
"allowUnsafeCode": false,
12+
"overrideReferences": false,
13+
"precompiledReferences": [],
14+
"autoReferenced": true,
15+
"defineConstraints": [],
16+
"versionDefines": [],
17+
"noEngineReferences": false
18+
}

README.md

Lines changed: 214 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,214 @@
1+
# Unity Injection V2 (持续更新中)
2+
3+
计划声明:此库文档先行,暂时不上传实现,用于分享背后原理和征集意见。
4+
5+
转载声明:此文档包含博客性质内容,创作不易,转请注。
6+
7+
叠甲声明:本人尚处于学习阶段,内容设计面较广,如有纰漏还请大神读者指出。
8+
9+
10+
## 版本计划
11+
12+
此仓库将逐渐取代原Unity Injection,原本Unity Injection将在适时Archive
13+
14+
## 0. 系统设计考量
15+
16+
原本V1存在的问题:
17+
* 使用调试工具调试Unity.exe,可以看到占用了一部分dll,对于其中的.net dll,在编辑器模式下是无法织入的。
18+
![unity占用的dll](Documentation/unity-dlls.png)
19+
* 编辑器模式织入效率低
20+
* ~~程序集查找路径不可靠~~(v1可解决)
21+
22+
期望中的系统应满足以下要求:
23+
24+
* Unity全平台、全backend支持
25+
* Editor下快速无感注入
26+
* 接口简洁,方便功能开发
27+
* 稳定可靠
28+
29+
基本工作过程:系统在代码中扫描InjectionInfo,并逐一Patch
30+
31+
## 1. 工作原理分析
32+
33+
### 1.1 Unity构建工作流
34+
### 1.2 不同阶段的执行逻辑
35+
36+
初步方案是player使用织入,editor使用动态程序集+jit hook
37+
38+
下文的`clr`应包括包括广义上的mono clr、core clr、libil2cpp
39+
40+
## 2. Jit Hook类的解决方案
41+
42+
这里分析两位大神的仓库实现:
43+
44+
* https://github.com/bigbaldy1128/DotNetDetour
45+
* https://github.com/Misaka-Mikoto-Tech/MonoHook
46+
47+
如果不了解原理,先结合 https://bbs.csdn.net/topics/391958344 或者 https://zhuanlan.zhihu.com/p/547772502 ,看DotNetDetour,再看MonoHook。主要区别是Misaka-Mikoto-Tech大神丰富了abi,增强了稳定性,适配了Unity环境。
48+
49+
两者都是典型的hook方案,原理不再赘述。
50+
51+
请确保能完全理解DetourHook,并熟读il2cpp源码后再继续阅读以下子章节。
52+
53+
### 2.0 引入的需求
54+
55+
此仓库希望Jit Hook库:
56+
* 不需要用户定义一个相同的代理函数。如果编译时确定,可以借助Roslyn的SourceGenerate;否则,可以通过动态程序集
57+
* 无参方式调用原函数。见[无参调用](#210-无参调用和tail-call)
58+
* replace method传入Delegate而不是MethodBase。见[delegate-和-cx寄存器](#29-delegate-和-cx寄存器)
59+
60+
### 2.1 AX寄存器
61+
62+
DotNetDetour方案先压栈保留了原ax的值,而MonoHook直接修改ax。两位作者都没有进行更深入的解析,我只能按照自己的理解分析。
63+
64+
我们知道ax用于函数返回,如果原函数的“5字节”修改了ax,那么是需要保留ax的。对于Debug模式下生成的Jit指令,这显然是成立的,因为每个函数都有前缀的trap codes,远大于跳转指令长度;但是Release模式下,如果jit code过早的mov ax,或者call xxx,那么ax是处于修改状态的,需要保留。
65+
66+
### 2.2 多线程访问
67+
68+
DotNetDetour的作者bigbaldy1128大神在博客中提到过:
69+
>到了这里,我们就知道了修改从push ebp开始的5个字节为jmp跳转指令,跳入我们自己的函数就可以达到hook的目的,但执行到我们的函数后,如果我们并不是要拦截执行流程,那么我们最终是需要再调用原函数的,但原函数已经被修改了,这会想到的办法就是恢复那修改的5字节指令,但这又会引发另一个问题,就是当我们恢复时,正好另一个线程调用到这个函数,那么程序将会崩溃,或者说漏掉一次函数调用,修改时暂停其他线程并等待正跑在其中的CPU执行完这5字节再去恢复指令也许是个不错的办法,但感觉并不容易实现,而且影响性能,所以我放弃了这种办法
70+
71+
事实上,如果不是每次都修改,只是“Install”的时候修改jit依然会有多线程race condition的问题。当我们在内存上memcpy时,如果恰好另一线程访问了此处代码,就会发生不可预料的结果。
72+
73+
想要彻底解决race condition:
74+
75+
* 获取当前进程下所有线程,将函数id转为Windows下的Handle,再调用Kernel32的SuspendThread方法;循环挂起,直到没有新的线程;“install”;恢复挂起的线程
76+
* 修改内存Flag为CopyOnWrite,在memcpy结束后修改函数指针入口
77+
78+
### 2.3 代理函数注入方法
79+
80+
上述两个仓库的实现在这点上有所不同,DotNetDetour是修改函数入口,而MonoHook直接修改函数jit code。
81+
82+
值得注意的是,DotNetDetour的入口修改方法在Unity下是无效的:
83+
```csharp
84+
*((ulong*)((uint*)method.MethodHandle.Value.ToPointer() + 2)) = (ulong)ptr;
85+
```
86+
原库是在.Net Framework环境下工作的,但对于mono和core,需要其他的方法修改入口:
87+
阅读[dotnet/runtime](https://github.com/dotnet/runtime)的源码可以发现,clr会在一个lut中保留MonoJitInfo,修改JitInfo中的字段方可改变函数入口。实现详见:https://github.com/bigbaldy1128/DotNetDetour/issues/21#issuecomment-2059344745
88+
89+
但是这种做法并不是完美的,见[2.4](#24-修改引用的写后屏障)[2.5](#25-修改方法的导出问题)
90+
91+
92+
### 2.4 修改引用的写后屏障(入口修改副作用)
93+
94+
为了使gc正常工作,我们在修改了内存中的引用后,需要调用一次gc_wbarrier,将白色垃圾置灰,此方法在gc库中实现。然而,考虑到运行时的gc库可能各不相同,如:低版本mono和il2cpp的BoehmGC、高版本clr的SGenGC、一些项目替换的其他gc实现等。
95+
96+
因此,确定一个可靠的写屏障方法变得困难。
97+
98+
### 2.5 修改方法的导出问题(入口修改副作用)
99+
100+
比如,修改方法用到了mono_jit_info_table_find,这个方法因为是export function,所以才可以调用;除此之外,还有icall指令可以用类似的方法访问。
101+
102+
Unity可以使用libMono中的export function,得益于UnityEditor只是将libMono作为动态库加载。但是对于纯c#环境,不能直接访问到此方法。
103+
104+
理论上讲,只要我们能够获得方法的地址,我们就可以调用对应clr内部方法。我们可以通过符号表、地址偏移等方法得到一个特定环境下的入口,但是此类方法难以普及,并且可能非常不稳定。
105+
106+
可见,修改函数入口有诸多问题,但是如果修改jit code buffer呢?
107+
> 通过阅读[dotnet/runtime](https://github.com/dotnet/runtime)的源码可以发现,jit实现中,会将所有的jit放入块表中,每个块表大小相当,但不完全相同。free jit code时,先找到对应的块表,再通过code start和code size判断正确的内存区块,如果method已被置为tombstone,则可以安全释放。这就是jit code在内存中的基本组织形式。
108+
109+
### 2.6 icache一致性(函数体修改副作用)
110+
111+
对于cpu中的icache,在修改了jit code后,将面临缓冲失效的问题。如果此时系统没有立即替换缓冲区,则会导致程序依然执行旧的逻辑。
112+
113+
如你可能Install后,直接执行原方法,会发现并没有生效:
114+
```csharp
115+
HookTool.Install();
116+
TargetMethod(); // not hooked
117+
```
118+
119+
此问题在不同的cpu上会有不同的表现。在Install时可以使用extern调用`mfence`的方式保证icache有效性。
120+
121+
122+
### 2.7 Jit code共享(函数体修改副作用)
123+
124+
对于相同的jit方法,共享一份jit code是可行的。
125+
126+
然而事实上,通过阅读源码发现`dotnet/runtime`没有对相同的jit code进行共享,因此修改特定方法的jit code不会有副作用。
127+
128+
但是不同的clr种类、未来的版本中,很难保证不会引入jit共享的特性。
129+
130+
### 2.8 Inline Method
131+
对于如下代码:
132+
```csharp
133+
// [MethodImpl(MethodImplOptions.NoInlining)]
134+
void Foo(){
135+
Console.WriteLine("Foo");
136+
}
137+
138+
void Bar(){
139+
Foo(); // not hooked
140+
}
141+
```
142+
如果我们hook了`Foo`,那么执行`Bar`之后,你可能并不会看到hook后的效果,这是因为,Foo的jit代码已经内嵌到了Bar的jit中,我们再修改Foo的jit已经于事无补。
143+
144+
我们增加`MethodImplOptions.NoInlining`后,再次执行`Bar`,可以看到hook后的结果。
145+
146+
```csharp
147+
[MethodImpl(MethodImplOptions.NoInlining)]
148+
void Foo(){
149+
Console.WriteLine("Foo");
150+
}
151+
152+
void Bar(){
153+
Foo(); // it works
154+
}
155+
```
156+
157+
然而对于一个目标方法,在运行时无法给定`MethodImplOptions.NoInlining`,时机往往太迟了。
158+
159+
### 2.9 Delegate 和 CX寄存器
160+
161+
为使用灵活性考虑,本仓库希望传入的replace method不是MethodBase,而是Delegate类型。有什么区别呢?
162+
163+
Delegate比MethodBase包含了更多的信息,如:this caller。此Delegate可能来自lambada。
164+
165+
以x86和x64为例,含this的代理和成员方法会先压入一个this指针或修改CX为this指针,其他写入dx或继续压入SP。
166+
167+
因此使用原本的函数签名调用一个含this的代理时,压入的参数缺了一个(对于lambda则是编译器生成的`Display_Class`上下文类)。
168+
169+
想要修复这个问题,我们首先要明确目标函数的调用规约,如传参顺序、位置、清栈者等。
170+
171+
如,对于x86 fastcall,需要大致如下的汇编指令:
172+
```asm
173+
push [rsp+param_offset]
174+
push edx
175+
mov [rsp+param_offset],edx
176+
mov edx,ecx
177+
mov ecx,inst_addr
178+
add rsp,4
179+
180+
jump code...
181+
182+
sub rsp,4
183+
pop edx
184+
pop [rsp+param_offset]
185+
```
186+
187+
同时兼顾不同的调用规约、指令集,是一件比较麻烦的事情。
188+
189+
### 2.10 无参调用和tail-call
190+
191+
为使用灵活性考虑,本仓库希望同时产出一个无参回调代理函数。
192+
193+
比较容易想到的做法是修改ret n指令为ret,即不对原堆栈进行修改。但是此做法的前提是callee清栈,对于caller清栈则无能为力。而且对于拷贝后的相对地址jmp指令也会失效。
194+
195+
因此,可以使用拷贝堆栈的方式,类似2.9中的方法,并且把ret指令替换为ret n指令。但这样的实现会增加一些调用负载。
196+
197+
## 3. 各类CLR在“方法注入”相关部分的源码解读及分析
198+
199+
### 3.0 CLR Jit内部实现,对比JsCore、Mono、LLVM
200+
### 3.1 Mono 和 IL2CPP Delegate内部的实现
201+
### 3.2 Mono 和 IL2CPP Expression内部的实现
202+
### 3.3 Mono 泛型共享
203+
### 3.4 IL2CPP 泛型共享和完全泛型共享
204+
### 3.5 Mono方法调用的内部实现
205+
### 3.6 IL2CPP方法调用的内部实现
206+
### 3.7 Mono动态程序集
207+
### 3.8 UnityEditor下“动态逆向”的实现方法
208+
209+
210+
## 4. 最终草案
211+
212+
editor下优先织入,织入失败再尝试hook
213+
214+
player下只织入
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
{
2+
"name": "com.bbbirder.unity-injection",
3+
"rootNamespace": "",
4+
"references": [],
5+
"includePlatforms": [],
6+
"excludePlatforms": [],
7+
"allowUnsafeCode": false,
8+
"overrideReferences": false,
9+
"precompiledReferences": [],
10+
"autoReferenced": true,
11+
"defineConstraints": [],
12+
"versionDefines": [],
13+
"noEngineReferences": false
14+
}

package.json

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{
2+
"name": "com.bbbirder.unity-injection",
3+
"displayName": "unity-injection",
4+
"version": "0.0.1",
5+
"hideInEditor": false,
6+
"scripts": {
7+
8+
},
9+
"author": "bbbirder <502100554@qq.com>"
10+
}

0 commit comments

Comments
 (0)