il2cpp动态调试_IL2CPP内部构件–生成代码的调试技巧
il2cpp动态调试
This is the third blog post in the IL2CPP Internals series. In this post, we will explore some tips which make debugging C++ code generated by IL2CPP a little bit easier. We will see how to set breakpoints, view the content of strings and user defined types and determine where exceptions occur.
这是IL2CPP Internals系列中的第三篇博客文章。 在本文中,我们将探索一些技巧,这些技巧使调试由IL2CPP生成的C ++代码更加容易。 我们将看到如何设置断点,查看字符串的内容和用户定义的类型以及确定发生异常的位置。
As we get into this, consider that we are debugging generated C++ code created from .NET IL code. So debugging it will likely not be the most pleasant experience. However, with a few of these tips, it is possible to gain meaningful insight into how the code for a Unity project executes on the actual target device (we’ll talk a little bit about debugging managed code at the end of the post).
进入这一过程时,请考虑我们正在调试从.NET IL代码创建的生成的C ++代码。 因此,调试将可能不是最令人愉快的体验。 但是,使用其中的一些技巧,可以对Unity项目的代码在实际目标设备上的执行方式进行有意义的了解(我们将在文章末尾讨论调试托管代码的内容)。
Also, be prepared for the generated code in your project to differ from this code. With each new version of Unity, we are looking for ways to make the generated code better, faster and smaller.
另外,请准备好使项目中生成的代码与此代码不同。 对于每个新版本的Unity,我们都在寻找使生成的代码更好,更快和更小的方法。
The setup
设置
For this post, I’m using Unity 5.0.1p3 on OSX. I’ll use the same example project as in the post about generated code, but this time I’ll build for the iOS target using the IL2CPP scripting backend. As I did in the previous post, I’ll build with the “Development Player” option selected, so that il2cpp.exe will generate C++ code with type and method names based on the names in the IL code.
对于本文,我在OSX上使用Unity 5.0.1p3。 我将使用与有关生成代码的帖子中的示例项目相同的示例项目,但是这次我将使用IL2CPP脚本编写后端为iOS目标进行构建。 正如我在上一篇文章中所做的那样,我将选择“ Development Player”选项进行构建,以便il2cpp.exe将基于IL代码中的名称生成具有类型和方法名称的C ++代码。
After Unity is finished generating the Xcode project, I can open it in Xcode (I have version 6.3.1, but any recent version should work), choose my target device (an iPad Mini 3, but any iOS device should work) and build the project in Xcode.
Unity完成生成Xcode项目后,我可以在Xcode中打开它(我具有6.3.1版,但是任何最新版本都可以使用),选择我的目标设备(iPad Mini 3,但是任何iOS设备都可以使用)并进行构建Xcode中的项目。
Setting breakpoints
设置断点
Before running the project, I’ll first set a breakpoint at the top of the Start
method in the HelloWorld
class. As we saw in the previous post, the name of this method in the generated C++ code is HelloWorld_Start_m3
. We can use Cmd+Shift+O and start typing the name of this method to find in in Xcode, then set a breakpoint in it.
在运行项目之前,我将首先在HelloWorld
类的Start
方法的顶部设置一个断点。 如上一篇文章所述,在生成的C ++代码中此方法的名称为HelloWorld_Start_m3
。 我们可以使用Cmd + Shift + O并开始键入此方法的名称以在Xcode中查找,然后在其中设置一个断点。
We can also choose Debug > Breakpoints > Create Symbolic Breakpoint in XCode, and set it to break at this method.
我们还可以在XCode中选择“调试”>“断点”>“创建符号断点”,并将其设置为在此方法处断点。
Now when I run the Xcode project, I immediately see it break at the start of the method.
现在,当我运行Xcode项目时,我立即看到它在方法开始时中断。
We can set breakpoints on other methods in the generated code like this if we know the name of the method. We can also set breakpoints in Xcode at a specific line in one of the generated code files. In fact, all of the generated files are part of the Xcode project. You will find them in the Project Navigator in the Classes/Native directory.
如果我们知道方法的名称,则可以在生成的代码中在其他方法上设置断点,如下所示。 我们还可以在生成的代码文件之一中的特定行的Xcode中设置断点。 实际上,所有生成的文件都是Xcode项目的一部分。 您可以在Classes / Native目录的Project Navigator中找到它们。
Viewing strings
查看字符串
There are two ways to view the representation of an IL2CPP string in Xcode. We can view the memory of a string directly, or we can call one of the string utilities in libil2cpp to convert the string to a std::string
, which Xcode can display. Let’s look at the value of the string named _stringLiteral1
(spoiler alert: its contents are “Hello, IL2CPP!”).
有两种方法可以查看Xcode中IL2CPP字符串的表示形式。 我们可以直接查看字符串的内存,也可以调用libil2cpp中的字符串实用程序之一将字符串转换为Xcode可以显示的std::string
。 让我们看一下名为_stringLiteral1
的字符串的值( _stringLiteral1
警报:其内容为“ Hello,IL2CPP!”)。
In the generated code with Ctags built (or using Cmd+Ctrl+J in Xcode), we can jump to the definition of _stringLiteral1
and see that its type is Il2CppString_14
:
在生成的带有内置Ctags的代码中(或在Xcode中使用Cmd + Ctrl + J),我们可以跳转到_stringLiteral1
的定义,并查看其类型为Il2CppString_14
:
1
2
3
4
5
6
|
struct Il2CppString_14
{
Il2CppDataSegmentString header;
int32_t length;
uint16_t chars[15];
};
|
1
2
3
4
5
6
|
struct Il2CppString_14
{
Il2CppDataSegmentString header ;
int32_t length ;
uint16_t chars [ 15 ] ;
} ;
|
In fact, all strings in IL2CPP are represented like this. You can find the definition of Il2CppString
in the object-internals.h header file. These strings include the standard header part of any managed type in IL2CPP, Il2CppObject
(which is accessed via the Il2CppDataSegmentString
typedef), followed by a four byte length, then an array of two bytes characters. Strings defined at compile time, like _stringLiteral1
end up with a fixed-length chars
array, whereas strings created at runtime have an allocated array. The characters in the string are encoded as UTF-16.
实际上,IL2CPP中的所有字符串都是这样表示的。 您可以在object-internals.h头文件中找到Il2CppString
的定义。 这些字符串包括IL2CPP中任何托管类型的标准标头部分, Il2CppObject
(可通过Il2CppDataSegmentString
typedef访问),后跟四个字节的长度,然后是两个字节字符的数组。 在编译时定义的字符串(如_stringLiteral1
最终以固定长度的chars
数组结尾,而在运行时创建的字符串具有分配的数组。 字符串中的字符编码为UTF-16。
If we add _stringLiteral1
to the watch window in Xcode, we can select the View Memory of “_stringLiteral1” option to see the layout of the string in memory.
如果将_stringLiteral1
添加到Xcode的监视窗口中,则可以选择“ _stringLiteral1”的“查看内存”选项以查看字符串在内存中的布局。
Then in the memory viewer, we can see this:
然后在内存查看器中,我们可以看到:
The header member of the string is 16 bytes, so after we skip past that, we can see that the four bytes for the size have a value of 0x000E (14). The next byte after the length is the first character of the string, 0x0048 (‘H’). Since each character is two bytes wide, but in this string all of the characters fit in only one byte, Xcode displays them on the right with dots in between each character. Still, the content of the string is clearly visible. This method of viewing string does work, but it is a bit difficult for more complex strings.
字符串的标头成员为16个字节,因此跳过之后,我们可以看到该大小的四个字节的值为0x000E(14)。 长度后的下一个字节是字符串的第一个字符0x0048('H')。 由于每个字符都是两个字节宽,但是在此字符串中,所有字符都只能容纳一个字节,因此Xcode在右侧显示它们,每个字符之间都有点。 字符串的内容仍然清晰可见。 这种查看字符串的方法确实有效,但是对于更复杂的字符串来说有点困难。
We can also view the content of a string from the lldb prompt in Xcode. The utils/StringUtils.h header gives us the interface for some string utilities in libil2cpp that we can use. Specifically, let’s call the Utf16ToUtf8
method from the lldb prompt. Its interface looks like this:
我们还可以从Xcode的lldb提示符下查看字符串的内容。 utils / StringUtils.h标头为我们提供了我们可以使用的libil2cpp中一些字符串实用程序的接口。 具体来说,让我们从lldb提示符中调用Utf16ToUtf8
方法。 其界面如下所示:
1
|
static std::string Utf16ToUtf8 (const uint16_t* utf16String);
|
1
|
static std :: string Utf16ToUtf8 ( const uint16_t * utf16String ) ;
|
We can pass the chars member of the C++ structure to this method, and it will return a UTF-8 encoded std::string
. Then, at the lldb prompt, if we use the p command, we can print the content of the string.
我们可以将C ++结构的chars成员传递给此方法,它将返回UTF-8编码的std::string
。 然后,在lldb提示符下,如果使用p命令,则可以打印字符串的内容。
1
2
|
(lldb) p il2cpp::utils::StringUtils::Utf16ToUtf8(_stringLiteral1.chars)
(std::__1::string) $1 = "Hello, IL2CPP!"
|
1
2
|
( lldb ) p il2cpp :: utils :: StringUtils :: Utf16ToUtf8 ( _stringLiteral1 . chars )
( std :: __1 :: string ) $ 1 = "Hello, IL2CPP!"
|
Viewing user defined types
查看用户定义的类型
We can also view the contents of a user defined type. In the simple script code in this project, we have created a C# type named Important
with a field named InstanceIdentifier
. If I set a breakpoint just after we create the second instance of the Important
type in the script, I can see that the generated code has set InstanceIdentifier
to a value of 1, as expected.
我们还可以查看用户定义类型的内容。 在该项目中的简单脚本代码中,我们创建了一个名为Important
的C#类型,并带有一个InstanceIdentifier
字段。 如果在脚本中创建了Important
类型的第二个实例后立即设置断点,则可以看到生成的代码已将InstanceIdentifier
设置为值1,正如预期的那样。
So viewing the contents of user defined types in generated code is done that same way as you normally would in C++ code in Xcode.
因此,以与通常在Xcode中的C ++代码中相同的方式来查看生成的代码中用户定义类型的内容。
Breaking on exceptions in generated code
打破生成代码中的异常
Often I find myself debugging generated code to try to track down the cause of a bug. In many cases these bugs are manifested as managed exceptions. As we discussed in the last post, IL2CPP uses C++ exceptions to implement managed exceptions, so we can break when a managed exception occurs in Xcode in a few ways.
我经常发现自己正在调试生成的代码,以试图找出错误的原因。 在许多情况下,这些错误都表现为托管异常。 正如我们在上一篇文章中讨论的那样,IL2CPP使用C ++异常来实现托管异常,因此当Xcode中发生托管异常时,我们可以通过几种方式中断。
The easiest way to break when a managed exception is thrown is to set a breakpoint on the il2cpp_codegen_raise_exception
function, which is used by il2cpp.exe any place where a managed exception is explicitly thrown.
引发托管异常时中断的最简单方法是在il2cpp_codegen_raise_exception
函数上设置一个断点, il2cpp_codegen_raise_exception
在显式引发托管异常的任何地方都使用该断点。
If I then let the project run, Xcode will break when the code in Start
throws an InvalidOperationException
exception. This is a place where viewing string content can be very useful. If I dig into the members of the ex
argument, I can see that it has a ___message_2
member, which is a string representing the message of the exception.
如果我随后让项目运行,则当Start
的代码引发InvalidOperationException
异常时,Xcode将中断。 这是查看字符串内容非常有用的地方。 如果我深入研究ex
参数的成员,可以看到它具有___message_2
成员,该成员是表示异常消息的字符串。
With a little bit of fiddling, we can print the value of this string and see what the problem is:
稍微摆弄一下,我们可以打印此字符串的值,看看问题出在哪里:
1
2
|
(lldb) p il2cpp::utils::StringUtils::Utf16ToUtf8(&ex->___message_2->___start_char_1)
(std::__1::string) $88 = "Don't panic"
|
1
2
|
( lldb ) p il2cpp :: utils :: StringUtils :: Utf16ToUtf8 ( & amp ; ex - & gt ; ___message_2 - & gt ; ___start_char_1 )
( std :: __1 :: string ) $ 88 = "Don't panic"
|
Note that the string here has the same layout as above, but the names of the generated fields are slightly different. The chars
field is named ___start_char_1
and its type is uint16_t
, not uint16_t[]
. It is still the first character of an array though, so we can pass its address to the conversion function, and we find that the message in this exception is rather comforting.
请注意,这里的字符串具有与上述相同的布局,但是生成的字段的名称略有不同。 chars
字段名为___start_char_1
,其类型为uint16_t
,而不是uint16_t[]
。 但是,它仍然是数组的第一个字符,因此我们可以将其地址传递给转换函数,并且发现此异常中的消息相当令人欣慰。
But not all managed exceptions are explicitly thrown by generated code. The libil2cpp runtime code will throw managed exceptions in some cases, and it does not call il2cpp_codegen_raise_exception
to do so. How can we catch these exceptions?
但是并非所有托管异常都由生成的代码显式抛出。 在某些情况下,libil2cpp运行时代码将引发托管异常,并且它不会调用il2cpp_codegen_raise_exception
这样做。 我们如何捕获这些异常?
If we use Debug > Breakpoints > Create Exception Breakpoint in Xcode, then edit the breakpoint, we can choose C++ exceptions and break when an exception of type Il2CppExceptionWrapper
is thrown. Since this C++ type is used to wrap all managed exceptions, it will allow us to catch all managed exceptions.
如果我们在Xcode中使用“调试”>“断点”>“创建异常断点”,然后编辑该断点,则可以选择C ++异常,并在引发Il2CppExceptionWrapper
类型的异常时中断。 由于此C ++类型用于包装所有托管异常,因此它将使我们能够捕获所有托管异常。
Let’s prove this works by adding the following two lines of code to the top of the Start method in our script:
让我们通过将以下两行代码添加到脚本中Start方法的顶部来证明这是可行的:
1
2
|
Important boom = null;
Debug.Log(boom.InstanceIdentifier);
|
1
2
|
Important boom = null ;
Debug . Log ( boom . InstanceIdentifier ) ;
|
The second line here will cause a NullReferenceException
to be thrown. If we run this code in Xcode with the exception breakpoint set, we’ll see that Xcode will indeed break when the exception is thrown. However, the breakpoint is in code in libil2cpp, so all we see is assembly code. If we take a look at the call stack, we can see that we need to move up a few frames to the NullCheck
method, which is injected by il2cpp.exe into the generated code.
第二行将导致引发NullReferenceException
。 如果我们在设置了异常断点的情况下在Xcode中运行此代码,我们将看到抛出异常时Xcode确实会中断。 但是,断点在libil2cpp中的代码中,因此我们所看到的只是汇编代码。 如果看一下调用堆栈,我们可以看到我们需要向上移动几帧到NullCheck
方法,该方法由il2cpp.exe注入到生成的代码中。
From there, we can move back up one more frame, and see that our instance of the Important
type does indeed have a value of NULL
.
从那里,我们可以再返回一帧,然后看到我们的Important
类型实例确实具有NULL
。
Conclusion
结论
After discussing a few tips for debugging generated code, I hope that you have a better understanding about how to track down possible problems using the C++ code generated by IL2CPP. I encourage you to investigate the layout of other types used by IL2CPP to learn more about how to debug the generated code.
在讨论了一些调试生成的代码的技巧之后,希望您对如何使用IL2CPP生成的C ++代码查找可能的问题有更好的了解。 我鼓励您研究IL2CPP使用的其他类型的布局,以了解有关如何调试生成的代码的更多信息。
Where is the IL2CPP managed code debugger though? Shouldn’t we be able to debug managed code running via the IL2CPP scripting backend on a device? In fact, this is possible. We have an internal, alpha-quality managed code debugger for IL2CPP now. It’s not ready for release yet, but it is on our roadmap, so stay tuned.
IL2CPP托管代码调试器在哪里? 我们不应该能够调试通过设备上的IL2CPP脚本后端运行的托管代码吗? 实际上,这是可能的。 现在,我们为IL2CPP提供了一个内部的,具有Alpha质量的托管代码调试器。 它尚未准备好发布,但是已经在我们的路线图上,请继续关注。
The next post in this series will investigate the different ways the IL2CPP scripting backend implements various types of method invocations present in managed code. We will look at the runtime cost of each type of method invocation.
本系列的下一篇文章将研究IL2CPP脚本后端实现托管代码中存在的各种类型的方法调用的不同方式。 我们将研究每种类型的方法调用的运行时成本。
翻译自: https://blogs.unity3d.com/2015/05/20/il2cpp-internals-debugging-tips-for-generated-code/
il2cpp动态调试