IL2CPP 내부: 생성된 코드에 대한 디버깅 팁

JOSH PETERSON / UNITY TECHNOLOGIESSenior Software Engineer
May 20, 2015|8 분
IL2CPP 내부: 생성된 코드에 대한 디버깅 팁
이 웹페이지는 이해를 돕기 위해 기계 번역으로 제공됩니다. 기계 번역으로 제공되는 콘텐츠에 대한 정확도나 신뢰도는 보장되지 않습니다. 번역된 콘텐츠의 정확도에 관해 의문이 있는 경우 웹페이지의 공식 영어 원문을 참고해 주시기 바랍니다.

이 글은 IL2CPP 내부 시리즈의 세 번째 블로그 게시물입니다. 이 글에서는 IL2CPP로 생성된 C++ 코드를 조금 더 쉽게 디버깅할 수 있는 몇 가지 팁을 살펴보겠습니다. 중단점을 설정하고, 문자열 및 사용자 정의 유형의 내용을 보고, 예외가 발생하는 위치를 확인하는 방법을 살펴봅니다.

이 내용을 살펴보면서 .NET IL 코드에서 생성된 C++ 코드를 디버깅하고 있다고 생각해 보세요. 따라서 디버깅하는 것이 그다지 즐거운 경험은 아닐 것입니다. 하지만 이러한 몇 가지 팁을 통해 Unity 프로젝트의 코드가 실제 대상 기기에서 어떻게 실행되는지에 대한 의미 있는 인사이트를 얻을 수 있습니다(관리 코드 디버깅에 대해서는 포스트의 마지막 부분에서 자세히 설명하겠습니다).

또한 프로젝트에서 생성된 코드가 이 코드와 다를 수 있으므로 이에 대비하세요. 유니티는 새로운 버전이 출시될 때마다 생성되는 코드를 더 빠르고, 더 작고, 더 효율적으로 만들 수 있는 방법을 모색하고 있습니다.

설정

이 포스팅에서는 OSX에서 Unity 5.0.1p3를 사용하고 있습니다. 생성된 코드에 대한 포스트에서와 동일한 예제 프로젝트를 사용하겠지만 이번에는 IL2CPP 스크립팅 백엔드를 사용하여 iOS 타겟에 맞게 빌드하겠습니다. 이전 게시물에서 했던 것처럼 '개발 플레이어' 옵션을 선택한 상태로 빌드하여 il2cpp.exe가 IL 코드의 이름을 기반으로 유형 및 메서드 이름을 가진 C++ 코드를 생성하도록 하겠습니다.

Unity가 Xcode 프로젝트 생성을 완료하면 Xcode에서 프로젝트를 열고(저는 6.3.1 버전이지만 최신 버전이면 모두 작동합니다), 대상 디바이스(iPad Mini 3이지만 모든 iOS 디바이스가 작동합니다)를 선택한 다음 Xcode에서 프로젝트를 빌드할 수 있습니다.

중단점 설정

프로젝트를 실행하기 전에 먼저 HelloWorld 클래스의 Start 메서드 상단에 중단점을 설정하겠습니다. 이전 게시물에서 살펴본 것처럼 생성된 C++ 코드에서 이 메서드의 이름은 HelloWorld_Start_m3입니다. Cmd+Shift+O를 사용하여 Xcode에서 찾을 메서드의 이름을 입력한 다음 중단점을 설정할 수 있습니다.

image05

또한 XCode에서 디버그 > 중단점 > 심볼릭 중단점 생성을 선택하고 이 메서드에서 중단되도록 설정할 수도 있습니다.

image02

이제 Xcode 프로젝트를 실행하면 메서드가 시작될 때 즉시 중단되는 것을 볼 수 있습니다.

메서드 이름을 알고 있다면 이렇게 생성된 코드에서 다른 메서드에 중단점을 설정할 수 있습니다. 생성된 코드 파일 중 하나의 특정 줄에 Xcode에서 중단점을 설정할 수도 있습니다. 실제로 생성된 모든 파일은 Xcode 프로젝트의 일부입니다. 프로젝트 탐색기의 클래스/네이티브 디렉터리에서 찾을 수 있습니다.

image03

문자열 보기

Xcode에서 IL2CPP 문자열의 표현을 보는 방법은 두 가지가 있습니다. 문자열의 메모리를 직접 보거나 libil2cpp의 문자열 유틸리티 중 하나를 호출하여 문자열을 Xcode에서 표시할 수 있는 std::string으로 변환할 수 있습니다. 문자열 _stringLiteral1의 값을 살펴보겠습니다(스포일러 경고: "Hello, IL2CPP!"라는 내용입니다).

Ctag가 빌드된 상태에서 생성된 코드(또는 Xcode에서 Cmd+Ctrl+J 사용)에서 _stringLiteral1의 정의로 이동하여 해당 유형이 Il2CppString_14임을 확인할 수 있습니다:

알 수 없는 블록 유형 "codeBlock"인 경우 `serializers.types` 프롭에서 해당 블록에 대한 직렬화기를 지정하세요.

실제로 IL2CPP의 모든 문자열은 이와 같이 표현됩니다. Il2CppString의 정의는 object-internals.h 헤더 파일에서 찾을 수 있습니다. 이러한 문자열에는 IL2CPP에서 관리되는 모든 유형의 표준 헤더 부분인 Il2CppObject(Il2CppDataSegmentString typedef를 통해 액세스)와 4바이트 길이, 그리고 2바이트 문자로 구성된 배열이 포함됩니다. 컴파일 타임에 정의된 문자열(예: _stringLiteral1)은 고정 길이 문자 배열로 끝나는 반면 런타임에 생성된 문자열은 할당된 배열을 갖습니다. 문자열의 문자는 UTF-16으로 인코딩됩니다.

Xcode의 감시 창에 _stringLiteral1을 추가하면 메모리에서 문자열의 레이아웃을 볼 수 있는 "_stringLiteral1의 메모리 보기" 옵션을 선택할 수 있습니다.

image06

그러면 메모리 뷰어에서 이것을 볼 수 있습니다:

image00

문자열의 헤더 멤버는 16바이트이므로 이를 건너뛰고 나면 크기에 대한 4바이트의 값이 0x000E(14)임을 알 수 있습니다. 길이 뒤의 다음 바이트는 문자열의 첫 번째 문자 0x0048('H')입니다. 각 문자의 너비는 2바이트이지만 이 문자열에서는 모든 문자가 1바이트에만 들어가므로 Xcode는 각 문자 사이에 점을 찍어 오른쪽에 표시합니다. 그래도 문자열의 내용은 명확하게 볼 수 있습니다. 이 문자열 보기 방법은 작동하지만 더 복잡한 문자열의 경우 약간 어렵습니다.

Xcode의 lldb 프롬프트에서 문자열의 내용을 볼 수도 있습니다. utils/StringUtils.h 헤더는 libil2cpp에서 사용할 수 있는 일부 문자열 유틸리티에 대한 인터페이스를 제공합니다. 구체적으로 lldb 프롬프트에서 Utf16ToUtf8 메서드를 호출해 보겠습니다. 인터페이스는 다음과 같습니다:

알 수 없는 블록 유형 "codeBlock"인 경우 `serializers.types` 프롭에서 해당 블록에 대한 직렬화기를 지정하세요.

이 메서드에 C++ 구조체의 문자 멤버를 전달하면 UTF-8로 인코딩된 std::문자열을 반환합니다. 그런 다음 lldb 프롬프트에서 p 명령을 사용하면 문자열의 내용을 인쇄할 수 있습니다.

알 수 없는 블록 유형 "codeBlock"인 경우 `serializers.types` 프롭에서 해당 블록에 대한 직렬화기를 지정하세요.


사용자 정의 유형 보기

사용자 정의 유형의 콘텐츠도 볼 수 있습니다. 이 프로젝트의 간단한 스크립트 코드에서는 중요라는 이름의 C# 유형과 인스턴스 식별자라는 필드를 만들었습니다. 스크립트에서 중요 유형의 두 번째 인스턴스를 생성한 직후에 중단점을 설정하면 생성된 코드가 예상대로 InstanceIdentifier를 1 값으로 설정한 것을 볼 수 있습니다.

image09

따라서 생성된 코드에서 사용자 정의 유형의 내용을 보는 것은 Xcode의 C++ 코드에서 일반적으로 하는 것과 동일한 방식으로 수행됩니다.

생성된 코드에서 예외 발생 시 중단

버그의 원인을 추적하기 위해 생성된 코드를 디버깅하는 경우가 종종 있습니다. 대부분의 경우 이러한 버그는 관리되는 예외로 나타납니다. 지난 포스트에서 설명한 것처럼 IL2CPP는 C++ 예외를 사용하여 관리되는 예외를 구현하므로 몇 가지 방법으로 Xcode에서 관리되는 예외가 발생하면 중단할 수 있습니다.

관리되는 예외가 발생할 때 중단하는 가장 쉬운 방법은 관리되는 예외가 명시적으로 발생하는 모든 곳에서 il2cpp.exe가 사용하는 il2cpp_codegen_raise_exception 함수에 중단점을 설정하는 것입니다.

image08

그런 다음 프로젝트를 실행하면 시작의 코드에서 InvalidOperationException 예외가 발생하면 Xcode가 중단됩니다. 문자열 콘텐츠 보기가 매우 유용할 수 있는 곳입니다. ex 인수의 멤버를 자세히 살펴보면 예외의 메시지를 나타내는 문자열인 ___message_2 멤버가 있음을 알 수 있습니다.

image07

약간의 조작을 통해 이 문자열의 값을 인쇄하고 문제가 무엇인지 확인할 수 있습니다:

알 수 없는 블록 유형 "codeBlock"인 경우 `serializers.types` 프롭에서 해당 블록에 대한 직렬화기를 지정하세요.


여기의 문자열은 위와 레이아웃이 동일하지만 생성된 필드의 이름이 약간 다릅니다. 문자 필드의 이름은 ___start_char_1이고 유형은 uint16_t[]가 아닌 uint16_t입니다. 그래도 여전히 배열의 첫 번째 문자이므로 변환 함수에 주소를 전달할 수 있으며, 이 예외의 메시지가 오히려 위안이 됩니다.

그러나 모든 관리되는 예외가 생성된 코드에서 명시적으로 던져지는 것은 아닙니다. libil2cpp 런타임 코드는 경우에 따라 관리되는 예외를 던지며, 이를 위해 il2cpp_codegen_raise_exception을 호출하지 않습니다. 이러한 예외를 어떻게 포착할 수 있나요?

Xcode에서 디버그 > 중단점 > 예외 중단점 생성을 사용한 다음 중단점을 편집하면 C++ 예외를 선택하고 Il2CppExceptionWrapper 유형의 예외가 발생하면 중단할 수 있습니다. 이 C++ 유형은 모든 관리되는 예외를 래핑하는 데 사용되므로 모든 관리되는 예외를 포착할 수 있습니다.

image10

스크립트의 Start 메서드 상단에 다음 두 줄의 코드를 추가하여 이것이 작동한다는 것을 증명해 보겠습니다:

알 수 없는 블록 유형 "codeBlock"인 경우 `serializers.types` 프롭에서 해당 블록에 대한 직렬화기를 지정하세요.

여기서 두 번째 줄은 NullReferenceException을 발생시킵니다. 예외 중단점을 설정한 상태에서 이 코드를 Xcode에서 실행하면 예외가 발생했을 때 Xcode가 실제로 중단되는 것을 볼 수 있습니다. 그러나 중단점은 libil2cpp의 코드에 있으므로 어셈블리 코드만 표시됩니다. 호출 스택을 살펴보면, 생성된 코드에 il2cpp.exe가 주입하는 NullCheck 메서드까지 몇 프레임을 이동해야 한다는 것을 알 수 있습니다.

image01

거기에서 한 프레임 더 위로 이동하여 중요 유형의 인스턴스가 실제로 NULL 값을 갖는 것을 확인할 수 있습니다.

image04

맺는말

생성된 코드 디버깅을 위한 몇 가지 팁을 살펴본 후, IL2CPP에서 생성된 C++ 코드를 사용하여 가능한 문제를 추적하는 방법에 대해 더 잘 이해하셨기를 바랍니다. 생성된 코드를 디버깅하는 방법에 대해 자세히 알아보려면 IL2CPP에서 사용하는 다른 유형의 레이아웃을 살펴보는 것이 좋습니다.

IL2CPP 관리형 코드 디버거는 어디에 있나요? 기기에서 IL2CPP 스크립팅 백엔드를 통해 실행되는 관리 코드를 디버깅할 수 있어야 하지 않을까요? 실제로 가능합니다. 현재 IL2CPP를 위한 알파급 관리형 코드 디버거를 내부적으로 보유하고 있습니다. 아직 출시할 준비가 되지 않았지만 로드맵에 포함되어 있으니 계속 지켜봐 주세요.

이 시리즈의 다음 글에서는 IL2CPP 스크립팅 백엔드가 관리 코드에 존재하는 다양한 유형의 메서드 호출을 구현하는 다양한 방법을 살펴볼 것입니다. 각 메서드 호출 유형의 런타임 비용을 살펴보겠습니다.