0%

IL2CPP学习

IL2CPP

IL2CPP是什么

首先我们来了解一下它的前缀,IL (Intermediate Language-中间语言)—基于堆栈的伪汇编语言;很多时候还会看到CIL(Common Intermediate Language,特指在.Net平台下的IL标准)。这两者是属于通用语言架构和.NET框架的低阶的人类可读的编程语言。目标位.NET框架的语言被编译成(C)IL,然后汇编为字节码。(C)IL类似于一个面向对象的汇编语言,并且他是完全基于堆栈的,并且是在虚拟机上(.Net Framework, Mono VM)运行的语言。

IL2CPP(把IL中间语言转换成C++源代码,然后利用标准c++平台编译器产生二进制文件。简单的来说就是将IL转化为CPP文件。)

Fundamentally, it differs from the current implementation in that the IL2CPP compiler converts assemblies into C++ source code. It then leverages the standard platform C++ compilers to produce native binaries.(这是下面的文章对IL2CPP的部分描述)

这里有一篇Unity3D官方博客的文章,引出了IL2CPP的概念,以及一篇对IL2CPP的介绍。(值得一看)

The future of scripting in Unity

An introduction to IL2CPP internals

文中指出IL2CPP由AOT预编译器和虚拟机两部分组成。并且明确指出IL2CPP不会重新构建整个.NET框架和Mono工具链;MonoC#编译器和Mono的类、库,以及现在支持的功能和MonoAOT使用的第三方库都将会和IL2CPP继续使用

下图为IL2CPP的工具链概念图

il2cpp-toolchain-smaller

文中还提到了AOT预编译(Ahead of Time),可以做个了解。主要是对程序在运行前进行优化。

IL2CPP是怎样工作的

IL2CPP-3

整体流程可以用如上图表示,例子详见上面的第二篇文章

How IL2CPP works

  1. Unityj scripting API 代码被编译为常规的.NETDLL程序集
  2. 所有的不是脚本的一部分的托管程序集由Unused Bytecode Stripper处理(从动态链接库的DLL中去除一些无用的类和方法),这个过程会显著减小创建游戏的大小。(其上在上面那片文章也有提到)
  3. 所有的托管程序集被翻译为标准的C++代码
  4. 生成的C++代码和IL2CPP运行时生成的部分代码被本地平台的编译器编译
  5. 最后,代码会被连接到可执行文件(取决于你的目标平台)或DLL文件。

至于为什么会产生许多无用代码

我们易一段简单的C++代码和IL汇编代码作为示例

1
2
3
4
5
6
7
#include <stdio.h>

int main(int argc, char **argv) {
int a = 1;
int b = 2;
printf("Hello world: %d\r\n", a + b);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
ldc.i4.1 #将常量1压入栈
stloc.0 #将1从栈中取出,并赋给第一个变量a
ldc.i4.2
stloc.1
ldstr "Hello World: {0}" #将字符串压入栈
ldloc.0 #将变量a压入栈
ldloc.1
add #a+b
box System.Int32 #将类型装箱,其实这一步是在一定程度上优化程序
call System.Void System.Console::WriteLine(System.String,System.Object) #输出函数调用
ret

#这块程序集对应上面大括号里的内容

接下来插入一段对应IL2CPP工作的第一部后出现的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// System.Void HelloWorld.Program::Main(System.String[])
IL2CPP_EXTERN_C IL2CPP_METHOD_ATTR void Program_Main_m7A2CC8035362C204637A882EDBDD0999B3D31776 (StringU5BU5D_t933FB07893230EA91C40FF900D5400665E87B14E* ___args0, const RuntimeMethod* method)
{
static bool s_Il2CppMethodInitialized;
if (!s_Il2CppMethodInitialized)
{
il2cpp_codegen_initialize_method (Program_Main_m7A2CC8035362C204637A882EDBDD0999B3D31776_MetadataUsageId);
s_Il2CppMethodInitialized = true;
}
int32_t V_0 = 0;
int32_t V_1 = 0;
{
V_0 = 2;
int32_t L_0 = V_0;
V_1 = ((int32_t)il2cpp_codegen_add((int32_t)1, (int32_t)L_0));
int32_t L_1 = V_1;
int32_t L_2 = L_1;
RuntimeObject * L_3 = Box(Int32_t585191389E07734F19F3156FF88FB3EF4800D102_il2cpp_TypeInfo_var, &L_2);
IL2CPP_RUNTIME_CLASS_INIT(Console_t5C8E87BA271B0DECA837A3BF9093AC3560DB3D5D_il2cpp_TypeInfo_var);
Console_WriteLine_m22F0C6199F705AB340B551EA46D3DB63EE4C6C56(_stringLiteral331919585E3D6FC59F6389F88AE91D15E4D22DD4, L_3, /*hidden argument*/NULL);
return;
}
}

可以看到代码非常复杂。

如此复杂的原因在于IL2CPP对IL字节码线性扫描,并转换为与C++代码等效的非基于堆栈的代码。所以才会产生如此多的多余的变量和赋值操作。

再接下来,是IL2CPP版本的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
; void __fastcall Program_Main_m2325437134(Il2CppObject *__this, StringU5BU5D_t1642385972 *___args0, MethodInfo *method)
Program_Main_m2325437134 proc near
push rbx
sub rsp, 20h
cmp cs:s_Il2CppMethodInitialized_8016, 0
jnz short loc_14038BFF1
mov ecx, cs:?Program_Main_m2325437134_MetadataUsageId@@3IB
call ?InitializeMethodMetadata@MetadataCache@vm@il2cpp@@SAXI@Z
mov cs:s_Il2CppMethodInitialized_8016, 1
loc_14038BFF1:
mov rcx, cs:?Int32_t2071877448_il2cpp_TypeInfo_var@@3PEAUIl2CppClass@@EA
lea rdx, [rsp+48h]
mov dword ptr [rsp+48h], 3
call ?Box@Object@vm@il2cpp@@SAPEAUIl2CppObject@@PEAUIl2CppClass@@PEAX@Z
mov rcx, cs:?Console_t2311202731_il2cpp_TypeInfo_var@@3PEAUIl2CppClass@@EA
mov rbx, rax
test byte ptr [rcx+10Ah], 1
jz short loc_14038C02B
cmp dword ptr [rcx+0BCh], 0
jnz short loc_14038C02B
call ?ClassInit@Runtime@vm@il2cpp@@SAXPEAUIl2CppClass@@@Z
loc_14038C02B:
mov rdx, cs:?_stringLiteral3443654334@@3PEAUString_t2029220233@@EA
xor r9d, r9d
mov r8, rbx
xor ecx, ecx
call Console_WriteLine_m3776981455
add rsp, 20h
pop rbx
retn
Program_Main_m2325437134 endp

可以看到有很多刚开始没有的符号,这时候就需要用一些工具去除这些多余的符号了,完全去除,就可以开始分析代码了。

写在最后

(本文是在做一个Unitylab的时候当作前置知识学习的一篇简短的笔记,原本想着和Unitylab实验的思路一起发出来,还是提前发了。)