unity c#脚本_爆发! 如何在Unity C#脚本中停止无限循环
unity c#脚本
(or what I did at my first Hack Week with Unity)
(或者我在Unity的第一个Hack Week所做的事情)
这篇文章是关于打破Unity中脚本无限循环的小技巧。 它可以在Windows / 64位编辑器中使用,也可以在64位版本中启用脚本调试。 稍加努力,就可以在32位上运行,甚至可以在禁用脚本调试的情况下进行构建。 (This post is about a little trick for breaking infinite loops in scripts in Unity. It works with Windows / 64 bit in the Editor and in 64 bit builds with script debugging enabled. With a little more effort it can be made to work on 32 bit and even builds with script debugging disabled.)
Infinite loops seems to be something that should be easily avoidable. But from time to time, I’ve encountered them in sneaky variants. Once it was the broken random function returning 1.000001 on impossibly rare cases. Another time a degenerate mesh sent a NaN right into an unsuspecting while(1) { d += 1.0; if(d>10.0) break; /* .. */ }
loop. And then there was the broken data structure traversed by an algorithm that assumed current = current.next;
would surely lead to an end eventually.
无限循环似乎应该很容易避免。 但是我时不时地在各种变体中遇到它们。 一旦发生故障,随机函数将在极少数情况下返回1.000001。 又一次,退化的网格将 NaN 正确 发送给了 一个不知不觉的while(1) { d += 1.0; if(d>10.0) break; /* .. */ }
while(1) { d += 1.0; if(d>10.0) break; /* .. */ }
while(1) { d += 1.0; if(d>10.0) break; /* .. */ }
循环。 然后有一个假定的算法遍历了破碎的数据结构
current = current.next;
肯定会最终结束。
If you have experienced an infinite loop in your script code in Unity, you know it is quite unpleasant. Unity becomes unresponsive and you may have to kill the Editor entirely to get out of the mess. If you were lucky enough to have the debugger attached before running your game, you may be able to break it. But usually only if you can guess the right place in the code to set a breakpoint.
如果您在Unity中的脚本代码中遇到了无限循环,那么您就会知道这很不愉快。 Unity变得React迟钝,您可能必须完全杀死编辑器才能摆脱混乱。 如果您有幸在运行游戏之前连接了调试器,则可以将其破坏。 但是通常只有在您可以猜出代码中正确的位置设置断点的情况下,才可以。
Some years back, before joining Unity, I found a way to break a script that is stuck like this. But it was not until my first Hack Week here, that I got to talk to the right people and realized why the trick works and how it may be used to create a proper way to break scripts in Unity (sneak peek of my Hack Week project). Until we get that feature properly done and release it, you can use the trick below. Or just hang on for the fun of a little tour into disassembled, jit’ed code. What could possibly go wrong?
几年前,在加入Unity之前,我找到了一种方法来破坏像这样卡住的脚本。 但是直到我第一次在这里参加“黑客周”活动时,我才开始与合适的人进行交谈,并意识到 了 该技巧 为何 起作用以及如何使用该技巧来在Unity中创建破坏脚本的正确方法( 偷看我的“黑客周”项目 )。 在我们正确完成该功能并将其发布之前,您可以使用以下技巧。 或者只是继续浏览一下反汇编的jit'ed代码的乐趣。 可能出什么问题了?
不要在工作中尝试这个! (Do not try this at work!)
As a trained professional you know the value of practice, so try this out on a toy project before you attempt a rescue operation at work. Fire up Unity and create an empty project, add a box to an empty scene and create a new C# script “Quicksand” attached to the box. The script should contain this code:
作为训练有素的专业人员,您知道练习的价值,因此,在尝试进行救援操作之前,请在玩具项目中进行尝试。 启动Unity并创建一个空项目,在一个空场景中添加一个框,并在框上创建一个新的C#脚本“ Quicksand”。 该脚本应包含以下代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
using UnityEngine;
public class Quicksand : MonoBehaviour {
void OnMouseDown()
{
while(true)
{
// "Mind you, you'll keep sinking forever!!", -- My mom
}
}
}
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
using UnityEngine ;
public class Quicksand : MonoBehaviour {
void OnMouseDown ( )
{
while ( true )
{
// "Mind you, you'll keep sinking forever!!", -- My mom
}
}
}
|
Now hit play and click the box. Observe Unity freeze up and experience the onsetting rush of panic until you remember that this is just a test. No actual work is going to be harmed!
现在点击播放,然后单击框。 观察Unity冻结并体验一下开始出现的紧急恐慌,直到您记住这只是一个测试。 实际的工作都不会受到伤害!
So now your script is stuck and Unity seems to be hung. Let us start up a new instance of Visual Studio.
因此,现在您的脚本卡住了,Unity似乎已挂起。 让我们启动一个Visual Studio的新实例。
For this to work you probably (I didn’t check to be honest) need to have selected C++ as one of the languages when you installed VS. Go to Debug menu and select Attach to Process (NOTE: this is not the same option you usually choose for attaching to Unity). Locate the Unity process and attach to it.
为此,您可能(坦白地说,我没有检查过)需要在安装VS时选择C ++作为语言之一。 转到调试菜单,然后选择附加到进程(注意:这与您通常选择附加到Unity的选项不同)。 找到Unity进程并将其附加。
Having attached the debugger to the stuck Unity, select “Debug | Break all” and find the disassembly view showing the code currently executing on the main thread. The following gif shows the little dance you have to perform. Perhaps you even need to click on “show disassembly” or something like that depending on your configuration of Visual Studio. (On one machine where I tried it, I had to hit F10, which does a single-step, for the disassembly to show).
将调试器附加到卡住的Unity后,选择“ Debug | 全部破坏”,然后找到反汇编视图,其中显示了当前在主线程上执行的代码。 以下gif显示了您必须执行的小舞蹈。 也许您甚至需要单击“显示反汇编”或类似的内容,具体取决于您对Visual Studio的配置。 (在我尝试过的一台机器上,我必须按F10键,然后一步一步显示拆卸效果)。
As you maybe know, the scripts are — for performance reasons — translated into machine code on the fly before being executed. This is known as jit-compiling (just-in-time compiling). The result is what you see in the disassembly window. Hopefully it looks something like this:
如您所知,出于性能原因,这些脚本在执行之前即刻转换为机器代码。 这被称为即时编译(Jit-compiling)(即时编译)。 结果就是您在反汇编窗口中看到的内容。 希望它看起来像这样:
In this case you can almost see the infinite loop (indicated by the red arrow I have artistically rendered on top of the screen snip). There is one mov, one cmp and a lot of nop’s and then the jmp loops right back to where we started. No escape!
在这种情况下,您几乎可以看到无限循环(由我在屏幕截图上方艺术性地绘制的红色箭头指示)。 有一个 mov ,一个 cmp 和许多 nop ,然后 jmp 循环回到我们开始的地方。 无法逃避!
In a more realistic case, your C# code will be more complicated, and it will be harder to tell what is going on, but you don’t really have to understand it, because here comes the trick: hit F10 (single step) a number of times until you get to one of the “cmp dword ptr [r11], 0″ instructions. They should be sprinkled liberally all over the code because they are part of the debugging infrastructure. After a few steps, you may end up with something like this:
在更现实的情况下,您的C#代码将更复杂,并且将很难分辨正在发生的事情,但是您并不需要真正理解它,因为这是窍门:按下F10(单步)直到获得“ cmp dword ptr [r11],0” 指令之一的次数。 应该将它们自由地散布在整个代码中,因为它们是调试基础结构的一部分。 经过几个步骤,您可能会得到如下结果:
With a bit of luck you already have a window named “Autos” (if not, use Debug | Windows | Auto to find it). It should show you the value of the registers that are in play at this point:
幸运的是,您已经有了一个名为“ Autos”的窗口(如果没有,请使用Debug | Windows | Auto查找它)。 它应该向您显示此时正在使用的寄存器的值:
Now simply change the value of R11 to 0. Like this:
现在,只需将R11的值更改为0。就像这样:
If you were to execute the cmp instruction now, it would try to read from address 0 which is going to generate an exception. And that is actually exactly what we want: So hit F5 (continue program execution) and answer “Continue” to the dialog box that pops up:
如果您现在要执行cmp指令,它将尝试从地址0读取,这将生成异常。 这实际上正是我们想要的:因此,请按F5键(继续执行程序),然后对弹出的对话框回答“ Continue”:
If all went well, you now get a proper (Mono-)exception in Unity, the loop is broken and Unity is back to normal. You can save your work and look at the call stack in the Console to see which part of your script code caused the hang.
如果一切顺利,您现在可以在Unity中获得适当的(Mono-)异常,循环中断并且Unity恢复正常。 您可以保存工作并在控制台中查看调用堆栈,以查看脚本代码的哪一部分导致了挂起。
That’s it. Go forth and loop! A little warning is in place: you have now messed around pretty deep inside Unity and it is prudent to save your work (if needed) and restart the Editor. My experience is that everything seem quite healthy, but just to be on the safe side.
而已。 出去循环! 出现了一些警告:您现在已经在Unity的内部深处陷入混乱,谨慎地保存您的工作(如果需要)并重新启动编辑器。 我的经验是,一切似乎都很健康,但只是为了安全起见。
为什么这种骇客运作? (Why does this hack work?)
The reason this works at all is that Mono has a built in system for debugging scripts. It works by sprinkling (actually, once for each C# line) the jit-code with reads from a specific memory address. That is the “cmp dword ptr [r11], 0” instructions we saw above. When you are in debugging mode and are single stepping through your code, the page that holds this memory address is made read-only and we will hit an exception once for every C# line of code. The Mono framework can catch this outside of the jit-ed code and basically pause the execution after every line.
完全有效的原因是Mono具有用于调试脚本的内置系统。 它通过向jit代码撒入(从每条C#行开始实际上是从特定内存地址读取)来工作。 那就是 我们在上面看到的 “ cmp dword ptr [r11],0” 指令。 当您处于调试模式并单步执行代码时,保存此内存地址的页面将变为只读状态,并且每行C#代码我们都会遇到一次异常。 Mono框架可以在jit-ed代码之外捕获该代码,并基本上在每一行之后暂停执行。
The trick we did above, where we set the register r11 to be 0, will end up generating the same type of exception, because the address 0 is never readable. So the debugging framework sees something that looks like single-stepping, but because we are not really debugging, a NullReferenceException is generated and we get a nice call stack. Very convenient!
上面我们将寄存器r11设置为0的技巧最终将产生相同类型的异常,因为地址0永远无法读取。 因此,调试框架看到的东西看起来像是单步执行,但是由于我们不是真正的调试人员,因此会生成NullReferenceException并得到一个不错的调用堆栈。 很方便!
This technique also works for standalone games. You attach to yourgame.exe, break all, find the jit code, force a memory fault and you should be good. You will have to look up the call stack in the log file, though.
此技术也适用于独立游戏。 您附加到yourgame.exe,破坏所有代码,找到jit代码,强制发生内存错误,您应该会很好。 但是,您将必须在日志文件中查找调用堆栈。
角落案例 (Corner cases)
The example we just looked at was, to put it mildly, conveniently simplified for tutorial purposes! In reality, there are a number of catches you may run into. When you hit “break all” you may not actually break into ‘clean’ jit’ed code. If your C# code makes use of any API calls, the program might be inside some of the core Unity code. It will look like this:
出于温和的考虑,我们刚刚看过的示例出于教程目的而方便地进行了简化! 实际上,您可能会遇到很多问题。 当您点击“全部破坏”时,您实际上可能不会陷入“干净”的jit'ed代码中。 如果您的C#代码使用了任何API调用,则该程序可能在某些Unity核心代码中。 它看起来像这样:
Here we see the program having called into GetPosition. When the top of the Call stack contains real function names and not just hex addresses, it is usually a sign we have left mono / jit’ed code. But just hit Shift-F11 (Step Out) a few times until you are back in jit-land (nop’s galore is also a good indicator of jit-code).
在这里,我们看到该程序已调用GetPosition。 当调用堆栈的顶部包含实际函数名称而不仅仅是十六进制地址时,通常这是我们留下了mono / jit'ed代码的标志。 但是只需几次按Shift-F11(跳出),直到您回到jit-land(nop的性能也很好地表明了jit代码)。
Sometimes you can manage to break Unity at a point where the main thread is not active. It is probably easiest just to continue (F5) and then break all again until the main thread is active.
有时,您可以在主线程不活动的时候设法中断Unity。 继续(F5),然后再次中断所有操作,直到主线程处于活动状态,这可能是最容易的。
There are probably more weird cases, but hey, this is debugging, so improvisation is key!
可能还有更多奇怪的情况,但是,嘿,这是调试,所以即兴演奏是关键!
那32位呢 (What about 32bit)
You can do something similar in 32bit mode. The jit-code looks a little different. Perhaps something like this:
您可以在32位模式下执行类似的操作。 jit代码看起来有些不同。 也许是这样的:
This means we read from 0xB10000 in this case. To provoke a page fault, you need to actually change the code because the address is hardcoded directly in the instruction and not in a register as with 64 bit. So you open a memory view (Debug | Windows | Memory | Memory1) and navigate to the address of the instruction (the yellow arrow), that is, 0x65163DC in our case. Here we find:
这意味着在这种情况下,我们从0xB10000读取。 要引发页面错误,您实际上需要更改代码,因为地址直接在指令中而不是在64位寄存器中进行了硬编码。 因此,您将打开一个内存视图(“调试” |“ Windows” |“内存” |“ Memory1”)并导航到指令的地址(黄色箭头),在本例中为0x65163DC。 在这里我们找到:
You can recognize the address: it is the “b1” found 4 bytes in from the start. Change it to 00 and then continue (F5) as before. This will have the same effect, but with the difference from the 64 bit case, that you will break out every time you hit this location.
您可以识别该地址:它是从头开始4字节的“ b1”。 将其更改为00,然后像以前一样继续(F5)。 这将具有相同的效果,但是与64位的情况有所不同,您每次碰到此位置都会爆发。
那么非调试模式呢? (So what about non debug mode?)
Ok if you are really unlucky, you may have a bug that is only reproducible when you compile your scripts with debugging disabled. In this case you have to improvise a bit. If you can look at the code and find a way to provoke a read fault you should be golden, but it may or may not be super easy. As a last resort you may need to inject, manually, something like cmp eax, dword ptr ds:[0x0] which we know from above happens to be the sequence 3b 05 00 00 00 00. But maybe we are a bit more lucky. Let us try our example script from above. Breaking it yields:
好吧,如果您真的很不幸,那么您可能会遇到一个错误,该错误仅在禁用调试的情况下编译脚本时才可以重现。 在这种情况下,您必须即兴创作。 如果您可以看一下代码并找到引发读取错误的方法,那么您应该会很聪明,但这可能会或可能不会非常容易。 作为最后的选择,您可能需要手动注入 cmp eax,dword ptr ds:[0x0]之类的东西 ,而从上面我们知道这恰好是序列3b 05 00 00 0000。但是也许我们更幸运了。 让我们从上方尝试示例脚本。 打破它会产生:
Oh no! The worst. The compiler optimized it to just one jmp instruction looping on itself. There isn’t even room for adding in our cmp (the jmp is relative and only takes up 2 bytes). However, since we are assuming everyone is desperate (a release build hung, after all) we don’t have to be too careful and can just trash the code with our read. Navigate to 4D34446 in the memory window and fill in 3b 05 00 00 00 00 on top on whatever is there. Hit continue (F5) and hope. In my case the game (I was doing this part of the test with a standalone game) came back to life and I could inspect the output log to find:
不好了! 最差的。 编译器对其进行了优化,使其自身仅循环了一条jmp指令。 甚至没有添加cmp的空间(jmp是相对的,仅占用2个字节)。 但是,由于我们假设每个人都很绝望(毕竟,发布版本已被挂起),所以我们不必太小心,只需将代码与读取的内容一起丢弃即可。 导航到内存窗口中的4D34446,然后在上面的任何内容上填写3b 05 00 00 00 00。 打继续(F5)和希望。 就我而言,游戏(我当时是通过独立游戏进行测试的这一部分)恢复了生命,我可以检查输出日志以找到:
At this point you really should shut down the game as you have effectively ruined a part of the jit generated code and your scripts will likely not work anymore. But at least you know where you were stuck.
此时,您确实应该关闭游戏,因为您已经有效破坏了jit生成的代码的一部分,并且脚本可能不再起作用。 但是至少您知道自己被困在哪里。
Sometimes you can find a read instruction in the vicinity of where you stopped. Then you can right click on it, select “Set next statement” and by setting a register to 0 you may be able to create the right exception that way.
有时,您可以在停下的地方附近找到一条阅读说明。 然后,您可以右键单击它,选择“设置下一条语句”,然后通过将寄存器设置为0,可以以这种方式创建正确的异常。
结论 (Conclusion)
So with a bit of trickery it seems unbreakable loops are in fact breakable. Hurry up and try it out so you can join the ranks of grizzled veterans growling “Ha! In my days we did it in disassembly!”. Soon we’ll ship a better solution and it will be forever too late!
因此,通过一些技巧,似乎牢不可破的循环实际上是易碎的。 快点尝试一下,这样您就可以加入咆哮的“老兄! 在我的日子里,我们是拆装的!”。 不久我们将提供更好的解决方案,这将永远为时已晚!
翻译自: https://blogs.unity3d.com/2016/05/24/breakout-how-to-stop-an-infinite-loop-in-a-unity-c-script/
unity c#脚本