본문 바로가기

Unity

C# 컴파일 그리고 IL2CPP

출처 - http://blogs.unity3d.com/kr/2015/09/22/kr-csharp-compile-il2cpp/

이글은 il2cpp를 이해하기전에, C#의 컴파일 과정을 설명하는 글입니다.

사실 il2cpp가 나온지 꽤 되서, 저보다 더 자세히 아시는 분들도 많을꺼라 생각됩니다.

이 블로그는 아직 il2cpp를 잘 모르는, 그리고 영어라 하면 머리에 쥐가 나는 분들을 위한 페이지라고 생각해 주시고 읽어 주시면 감사하겠습니다. 꾸벅.

(영어도 잘하시고, C# 컴파일에 난 빠삭하게 아신다고 하시는 분들은 요기 (AN INTRODUCTION TO IL2CPP INTERNALS)를 읽어 주세요 ^^;)

자 그럼 설명 들어갑니다.

il2cpp는 무엇일까요?

간단하게 정의하면, IL코드를 C++형태로 변환하는 프로그램입니다.

윙?

이게 끝?

에이 설마 ㅋㅋ

사실 IL2CPP를 설명하기 위해서는 간단하게 C# 코드의 컴파일 과정을 살짝 아실 필요가 있습니다.

C#코드는 msc.exe라는 프로그램에 의해 IL코드로 변환됩니다.

(이것은 Mono framework에 해당하는 이야기입니다. 아마도 MS에서는 프로그램명이 다를꺼라 생각됩니다.)

자 그럼 한번 살펴 볼까요?

이게 제가 예제로 만들어본 C# 코드입니다.

[code language=”csharp”]
using UnityEngine;
using System.Collections;

public class HelloWorld : MonoBehaviour {
void Start() {
Debug.Log ("Hello, World!");
}
}
[/code]

이걸 유니티에서 Build를 하면, Assembly-CSharp.dll이라는 결과 파일이 나옵니다.

그걸 Mono에 가져와서 한번 살펴 보면,

[code language=”csharp”]
.class public auto ansi beforefieldinit HelloWorld
extends [UnityEngine]UnityEngine.MonoBehaviour
{
// Methods
.method public hidebysig specialname rtspecialname
instance void .ctor () cil managed
{
// Method begins at RVA 0x2050
// Code size 7 (0x7)
.maxstack 8

IL_0000: ldarg.0
IL_0001: call instance void [UnityEngine]UnityEngine.MonoBehaviour::.ctor()
IL_0006: ret
} // end of method HelloWorld::.ctor

.method private hidebysig
instance void Start () cil managed
{
// Method begins at RVA 0x2058
// Code size 11 (0xb)
.maxstack 8

IL_0000: ldstr "Hello, World!"
IL_0005: call void [UnityEngine]UnityEngine.Debug::Log(object)
IL_000a: ret
} // end of method HelloWorld::Start

} // end of class HelloWorld
[/code]

이런식으로 변환 되었습니다.

이런 형태를 가진것을 우리는 IL(Intermediate Language)라고 부릅니다.

그리고 이러한 IL이 실제 기기로 들어가져서 실행되는거죠.

자, 여기서 한가지 집고 가야할께, IL코드는 Assembly 코드와 형태가 비슷하지만, 이게 기계에 들어가는 것만으로는 실행되지 않습니다.

우리가 IL코드를 실행시키고 싶으면, IL을 이해하고 IL을 Assembly 코드(즉, Binary)로 변환할 수 있는 프로그램이 기계에 깔려 있어야 하죠.

그러한 일을 하는게 바로 mono라는 녀석입니다.

(주로 libmono.so의 형태로 되어 있습니다.)

이 mono라는 녀석이 유저의 IL코드를 한줄씩 읽고 그걸 해당 기기에 맞는 Assembly어(즉 Binary)로 변환하여 실제 게임이 실행됩니다.

이러한 일련의 과정을 우리는 JIT컴파일링이라고 합니다.

(IL을 해석하고 기계어로 변환하는 과정만을 JIT 컴파일링이라 합니다.)

그렇다면, 과연 이 일련의 과정이 IL2CPP에서는 어떻게 변경 될까요?

먼저, IL2CPP도 기존 msc.exe를 이용해서 IL코드를 생성합니다. 하지만 IL을 그대로 이용하는것은 아니고, il2cpp.exe라는 프로그램을 이용해 IL을 C++형태로 변환합니다.

위의 예제 IL코드가 어떻게 C++로 변환되었는지를 확인해 보면,

[code language=”cpp”]
// System.Void HelloWorld::.ctor()

extern "C" void HelloWorld__ctor_m0 (HelloWorld_t1 * __this, const MethodInfo* method)
{
{
MonoBehaviour__ctor_m2(__this, /*hidden argument*/NULL);
return;
}
}

// System.Void HelloWorld::Start()

extern Il2CppCodeGenString* _stringLiteral0;

extern "C" void HelloWorld_Start_m1 (HelloWorld_t1 * __this, const MethodInfo* method)
{
static bool s_Il2CppMethodIntialized;

if (!s_Il2CppMethodIntialized)
{
_stringLiteral0 = il2cpp_codegen_string_literal_from_index(0);
s_Il2CppMethodIntialized = true;
}

{
Debug_Log_m3(NULL /*static, unused*/, _stringLiteral0, /*hidden argument*/NULL);
return;
}
}
[/code]

요렇게 변했습니다.  이 코드를 찬찬히 살펴보시면, 위의 IL의 코드와 거의 유사한 형태인것을 알 수 있습니다.

IL코드도 생성자함수(.ctor())와 Start함수(Start ()) 요렇게 두개만 있고,

위의 C++로 변환된 코드도 생성자함수(HelloWorld__ctor_m0)와 Start함수(HelloWorld_Start_m1)요렇게 두개만 있습니다.

그리고 Start함수 내부를 살펴봐도,

IL코드가 하는 일은
1. “Hello, World!”를 메모리에서 읽어서, stack에 저장하고

[code language=”csharp”] IL_0000: ldstr "Hello, World!" [/code]

2. UnityEngine.dll파일의 UnityEngine.Debug::Log를 호출

[code language=”csharp”] IL_0005: call void [UnityEngine]UnityEngine.Debug::Log(object) [/code]

3. 그리고 return

[code language=”csharp”] IL_000a: ret [/code]

이고, C++코드의 Start함수가 하는 일을 보면,

1. 메모리에서 “Hello World!”를 읽고,

[code language=”cpp”] _stringLiteral0 = il2cpp_codegen_string_literal_from_index(0); [/code]

2. 1번에서 읽은 값을 매개변수로, Debug_Log_m3 함수를 호출

[code language=”cpp”] Debug_Log_m3(NULL /*static, unused*/, _stringLiteral0, /*hidden argument*/NULL); [/code]

3. 그리고 return

[code language=”cpp”] return; [/code]

입니다. 이렇게 보니깐, IL이랑 생성된 CPP랑 거의 유사한 형태라는 것을 아시겠죠?

그리고 이왕 C++까지 변환되거, 바로 기계어까지 생성합니다.

(현재 사용되는 거의 모든 컴파일러들은 C++를 이용하여 기계어를 생성할 수 있습니다.)

이렇게 되면, JIT컴파일링과 다르게 모든게 이미 기계어로 변환되어 있기 때문에, 게임 실행중에 IL을 이해하고 그것을 기계어로 변환하는 과정이 없습니다.

그렇기때문에 게임의 성능이 훨씬 좋아지게 되는거죠.

(사실 이러식으로 실행도중 컴파일 과정이 없는 형태를 AOT컴파일링이라고 합니다. mono framework에서도 이 기능을 제공하죠. 다만,

mono framework에서는 c++로 변환하는 과정없이 IL을 바로 기계어로 변환합니다.)

자 이제 IL2CPP에 대해서 간략하게 머리에 그려지셨나요?

마지막으로 이글을 정리한 것을 그림으로 표현하면 이렇게 됩니다.

csharp2binary

그럼 이 내용을 바탕으로 더욱 자세한 il2cpp의 세상으로 고고~ 

AN INTRODUCTION TO IL2CPP INTERNALS