¿Qué estás buscando?
Engine & platform

IL2CPP interno: Consejos para depurar el código generado

JOSH PETERSON / UNITY TECHNOLOGIESSenior Software Engineer
May 20, 2015|8 minutos
IL2CPP interno: Consejos para depurar el código generado
Para tu comodidad, tradujimos esta página mediante traducción automática. No podemos garantizar la precisión ni la confiabilidad del contenido traducido. Si tienes alguna duda sobre la precisión del contenido traducido, consulta la versión oficial en inglés de la página web.

Esta es la tercera entrada del blog de la serie IL2CPP Internals. En este post, exploraremos algunos consejos que hacen que depurar código C++ generado por IL2CPP sea un poco más fácil. Veremos cómo establecer puntos de interrupción, ver el contenido de cadenas y tipos definidos por el usuario y determinar dónde se producen excepciones.

Mientras nos adentramos en esto, considera que estamos depurando código C++ generado creado a partir de código .NET IL. Así que depurarlo probablemente no será la experiencia más agradable. Sin embargo, con algunos de estos consejos, es posible obtener una visión significativa de cómo el código de un proyecto Unity se ejecuta en el dispositivo de destino real (hablaremos un poco sobre la depuración de código gestionado al final del post).

Además, prepárese para que el código generado en su proyecto difiera de este código. Con cada nueva versión de Unity, buscamos formas de hacer que el código generado sea mejor, más rápido y más pequeño.

La configuración

Para este post, estoy usando Unity 5.0.1p3 en OSX. Voy a utilizar el mismo proyecto de ejemplo que en el post sobre el código generado, pero esta vez voy a construir para el objetivo iOS utilizando el backend de scripting IL2CPP. Como hice en el post anterior, construiré con la opción "Development Player" seleccionada, para que il2cpp.exe genere código C++ con nombres de tipos y métodos basados en los nombres del código IL.

Después de que Unity termine de generar el proyecto en Xcode, puedo abrirlo en Xcode (tengo la versión 6.3.1, pero cualquier versión reciente debería funcionar), elegir mi dispositivo de destino (un iPad Mini 3, pero cualquier dispositivo iOS debería funcionar) y construir el proyecto en Xcode.

Establecer puntos de interrupción

Antes de ejecutar el proyecto, primero estableceré un punto de interrupción en la parte superior del método Start de la clase HelloWorld. Como vimos en el post anterior, el nombre de este método en el código C++ generado es HelloWorld_Start_m3. Podemos utilizar Cmd + Shift + O y empezar a escribir el nombre de este método para encontrar en Xcode, a continuación, establecer un punto de interrupción en el mismo.

image05

También podemos elegir Debug > Breakpoints > Create Symbolic Breakpoint en XCode, y configurarlo para que se rompa en este método.

image02

Ahora, cuando ejecuto el proyecto de Xcode, veo inmediatamente que se rompe al principio del método.

Podemos establecer puntos de interrupción en otros métodos en el código generado así si sabemos el nombre del método. También podemos establecer puntos de interrupción en Xcode en una línea específica de uno de los archivos de código generados. De hecho, todos los archivos generados forman parte del proyecto Xcode. Los encontrará en el navegador de proyectos, en el directorio Classes/Native.

image03

Ver cadenas

Hay dos formas de ver la representación de una cadena IL2CPP en Xcode. Podemos ver la memoria de una cadena directamente, o podemos llamar a una de las utilidades de cadena en libil2cpp para convertir la cadena a un std::string, que Xcode puede mostrar. Veamos el valor de la cadena llamada _stringLiteral1 (alerta de spoiler: su contenido es "¡Hola, IL2CPP!").

En el código generado con Ctags construido (o usando Cmd+Ctrl+J en Xcode), podemos saltar a la definición de _stringLiteral1 y ver que su tipo es Il2CppString_14:

Tipo de bloque desconocido "codeBlock", especifique un serializador para él en la propiedad `serializers.types`.

De hecho, todas las cadenas en IL2CPP se representan así. Puede encontrar la definición de Il2CppString en el archivo de cabecera object-internals.h. Estas cadenas incluyen la parte de la cabecera estándar de cualquier tipo gestionado en IL2CPP, Il2CppObject (a la que se accede a través del typedef Il2CppDataSegmentString), seguida de una longitud de cuatro bytes y, a continuación, una matriz de caracteres de dos bytes. Las cadenas definidas en tiempo de compilación, como _stringLiteral1 terminan con una matriz de caracteres de longitud fija, mientras que las cadenas creadas en tiempo de ejecución tienen una matriz asignada. Los caracteres de la cadena se codifican como UTF-16.

Si añadimos _stringLiteral1 a la ventana de observación en Xcode, podemos seleccionar la opción Ver Memoria de "_stringLiteral1" para ver la disposición de la cadena en memoria.

image06

Luego, en el visor de memoria, podemos ver esto:

image00

El miembro de la cabecera de la cadena es de 16 bytes, por lo que después de saltar más allá de eso, podemos ver que los cuatro bytes para el tamaño tienen un valor de 0x000E (14). El siguiente byte después de la longitud es el primer carácter de la cadena, 0x0048 ('H'). Como cada carácter tiene dos bytes de ancho, pero en esta cadena todos los caracteres caben en un solo byte, Xcode los muestra a la derecha con puntos entre cada carácter. Aun así, el contenido de la cadena es claramente visible. Este método de visualización de cadenas funciona, pero es un poco difícil para cadenas más complejas.

También podemos ver el contenido de una cadena desde el prompt lldb en Xcode. La cabecera utils/StringUtils.h nos da la interfaz para algunas utilidades de cadena en libil2cpp que podemos utilizar. En concreto, vamos a llamar al método Utf16ToUtf8 desde el prompt lldb. Su interfaz tiene este aspecto:

Tipo de bloque desconocido "codeBlock", especifique un serializador para él en la propiedad `serializers.types`.

Podemos pasar el miembro chars de la estructura C++ a este método, y devolverá una cadena std::string codificada en UTF-8. A continuación, en el indicador lldb, si utilizamos el comando p, podemos imprimir el contenido de la cadena.

Tipo de bloque desconocido "codeBlock", especifique un serializador para él en la propiedad `serializers.types`.


Visualización de los tipos definidos por el usuario

También podemos ver el contenido de un tipo definido por el usuario. En el código script simple de este proyecto, hemos creado un tipo C# llamado Importante con un campo llamado InstanceIdentifier. Si establezco un punto de interrupción justo después de crear la segunda instancia del tipo Importante en el script, puedo ver que el código generado ha establecido InstanceIdentifier a un valor de 1, como era de esperar.

image09

Así que ver el contenido de los tipos definidos por el usuario en el código generado se hace de la misma manera que lo haría normalmente en el código C ++ en Xcode.

Interrupción de excepciones en el código generado

A menudo me encuentro depurando código generado para intentar localizar la causa de un error. En muchos casos, estos fallos se manifiestan como excepciones gestionadas. Como comentamos en el último post, IL2CPP utiliza excepciones C++ para implementar excepciones gestionadas, por lo que podemos romper cuando se produce una excepción gestionada en Xcode de varias maneras.

La forma más fácil de romper cuando se lanza una excepción gestionada es establecer un punto de interrupción en la función il2cpp_codegen_raise_exception, que es utilizada por il2cpp.exe en cualquier lugar donde se lance explícitamente una excepción gestionada.

image08

Si luego dejo que el proyecto se ejecute, Xcode se romperá cuando el código en Start lance una excepción InvalidOperationException. Aquí puede ser muy útil ver el contenido de las cadenas. Si indago en los miembros del argumento ex, puedo ver que tiene un miembro ___message_2, que es una cadena que representa el mensaje de la excepción.

image07

Con un poco de maña, podemos imprimir el valor de esta cadena y ver cuál es el problema:

Tipo de bloque desconocido "codeBlock", especifique un serializador para él en la propiedad `serializers.types`.


Observe que aquí la cadena tiene el mismo diseño que antes, pero los nombres de los campos generados son ligeramente diferentes. El campo chars se llama ___start_char_1 y su tipo es uint16_t, no uint16_t[]. Sin embargo, sigue siendo el primer carácter de una matriz, por lo que podemos pasar su dirección a la función de conversión, y nos encontramos con que el mensaje de esta excepción es bastante reconfortante.

Pero no todas las excepciones gestionadas son lanzadas explícitamente por el código generado. El código en tiempo de ejecución de libil2cpp lanzará excepciones gestionadas en algunos casos, y no llama a il2cpp_codegen_raise_exception para hacerlo. ¿Cómo podemos detectar estas excepciones?

Si usamos Debug > Breakpoints > Create Exception Breakpoint en Xcode, luego editamos el breakpoint, podemos elegir C++ exceptions y romper cuando una excepción de tipo Il2CppExceptionWrapper es lanzada. Dado que este tipo C++ se utiliza para envolver todas las excepciones gestionadas, nos permitirá atrapar todas las excepciones gestionadas.

image10

Probemos que esto funciona añadiendo las siguientes dos líneas de código al principio del método Start de nuestro script:

Tipo de bloque desconocido "codeBlock", especifique un serializador para él en la propiedad `serializers.types`.

La segunda línea provocará una NullReferenceException. Si ejecutamos este código en Xcode con el punto de interrupción de excepción establecido, veremos que Xcode se interrumpirá cuando se lance la excepción. Sin embargo, el punto de interrupción está en código en libil2cpp, por lo que todo lo que vemos es código ensamblador. Si echamos un vistazo a la pila de llamadas, podemos ver que necesitamos subir unos cuantos frames hasta el método NullCheck, que es inyectado por il2cpp.exe en el código generado.

image01

A partir de ahí, podemos retroceder un fotograma más, y ver que nuestra instancia del tipo Importante tiene efectivamente el valor NULL.

image04

Conclusión

Después de discutir algunos consejos para depurar el código generado, espero que tengas una mejor comprensión sobre cómo rastrear posibles problemas utilizando el código C++ generado por IL2CPP. Te animo a investigar la disposición de otros tipos utilizados por IL2CPP para aprender más sobre cómo depurar el código generado.

Pero, ¿dónde está el depurador de código gestionado IL2CPP? ¿No deberíamos poder depurar el código gestionado que se ejecuta a través del backend de scripting IL2CPP en un dispositivo? De hecho, esto es posible. Ya disponemos de un depurador interno de código gestionado de calidad alfa para IL2CPP. Aún no está listo para su lanzamiento, pero está en nuestra hoja de ruta, así que permanezca atento.

El próximo post de esta serie investigará las diferentes formas en que el backend de scripting IL2CPP implementa varios tipos de invocaciones de métodos presentes en el código administrado. Examinaremos el coste en tiempo de ejecución de cada tipo de invocación de métodos.