时间:2016-9-29来源:本站原创作者:佚名

本帖是关于在Unity脚本中如何跳出死循环的一点小技巧。在Windows64位编辑器下工作,脚本也是64位的Dbug版本。它可以运行在32位下,不是Dbug版本也可以。   死循环看上去应该是比较容易避免的。但时不是,我还是会以其他的方式遇到它们。有一次,打断的随机函数,返回了不可思议的1.。还有一次,没有初始化的值传到了whil(1){d+=1.0;if(d10.0)brak;/*..*/}这个循环中。还有,用算法产生的打断,假设currnt=currnt.nxt,一定要确定它会有一个终点。   在Unity中,如果如果有你死循环的代码经验,你知道,那是相当的烦人。Unity没有响应,你需要杀掉编辑器进程来摆脱困境。如果你运气好,以Dbug方式运行的游戏,你可以中断它,但是前提是你能猜到打断点的正确地方。   几年前,还没有加入Unity的时候,我想找到一种像这样的方法打断程序,但都不成功,直到我写了我的第一个HackWk程序,我找到一些对的人交流,认识到了为什么这个技巧可以工作,以及如何在Unity中以正确的方式中断脚本。你可以用下面这个技巧,直到功能能正确的运行,并发布,或者为了JIT代码的乐趣,这有什么问题呢?   

不要在工作中这样做!

专业的你知道实践很重要,在你接到一个救援工作以前,在测试工程中测试一下。运行Unity,创建一个空项目,在空白场景中加一个box,创建一个新C#脚本“Quicksand”,挂到box上去,脚本的代码如下: 

usingUnityEngin;

classQuicksand:publicMonobhaviour

{

publicOnMousDown()

{

whil(tru)

{

//Mindyou,youllkpsinkingforvr!!,--Mymom

}

}

}

现在运行,并点击box。观察Unity被冻结,经历恐慌,直到你意识到这只是一个测试,实际的工作没有受到任何伤害。   现在你的脚本卡住了,Untiy看上去被挂起了。让我们重新打开VS。   做这项工作,你可能需要在安装VS的时候,选择C++(说实话,我没有检查)。在Dbug菜单,选择“AttachtoProcss”找到Unity进程,选中它。把卡住的Unity绑定到调试器上,选择“Dbug

Brakall”,Disassmbly界面会显示当前主线程执行的代码。下面的gif展示了你需要进行的操作。也许你还需要点开“showdisassmbly”或是其他的VS设置。(我用的这台电脑,我点击F10,进行单步跟踪显示)

你也许知道,因为效率原因,脚本在执行前被转换成机器语言。这就是即时编译,这就是你在disassmbly窗口上看到。像这样:

本例中,你能看到这个死循环(我用红色箭头标注)这里有一个mov,有一个cmp,有许多nop,然后跳转到开始的地方去了。   现实情况下,你的C#脚本可能更为复杂,很难讲会发生什么事,但是你不需要真的懂解他们。技巧在于:不断按下F10(单步跟踪),直到找到这一句“cmpdwordptr[r11],0″。它可能大量充斥在代码中,因为这也是调式的一部分。许多步之后,你可能像下面这样结束:

幸亏还有“Autos”这个界面(如果没有,Dbug

Windows

Auto菜单找到它)。这里可以显示运行的寄存器值。

现在把R11的值改为0,像这样:

如果你现在执行这个cmp语句,它就可能去读一个0x0地址,这会产生一个异常。这正是我们想要的,按下F5(继续执行程序),在弹出的对话框中选择“Continu”

如果一切顺利,在Unity中,你将会得到正确的异常。这个循环被打断,Unity回到正常装态。保存你的工程,看一看调用Consol界面中的堆栈中,是哪一部分代码造成了挂起。

就这样。一个小警告:你已经把Unity的环境弄坏了,最好是存储一下(如果你需要的话),然后重启编辑器。我的经验是,虽然一切看上去都是好的,但是为了安全起见还是重启一下吧。

  

为什么这个方法能奏效?

  

原因是因为Mono有一个内建的系统调试脚本。它的原理是即时编译的代码向一个特定的内存读取数据,就是我们看到那句“cmpdwordptr[r11],0”。调试模式下,单步跟踪贯穿了你的代码,系统将持有内存地址设为只读的,使得C#代码的每一句都会产生一个异常。Mono框架捕获到即时代码之外的异常,暂停了执行。(译注:这里解释了调试模式的原理,为什么调试时,每一句都可以停下来,是因为每一句之间都插入了cmp指令,每一步都产生了异常。)   我们上面做的这个技巧,设备了寄存器R11的值为0,因为地址0不可读,产生了一个异常。调试框架以为是一个单步跟踪,但是由于我们没有真正在调试,所以一个NullRfrncExcption异常生成,我们得到一个调用堆栈。非常适用!   这个方法也可以用于单机游戏。用你的游戏的EXE文件,执行BrakALL。找到即时编译的代码,强制让一个内存失败,你可以在日志文件查看堆栈。   

极端情况

  

我们刚才看这个例子,说得客气一点,是为了教学。实际情况,你可能会遇到大量的异常。当你按下“BrakAll”,你可以正好停在一些被“清理”过的即时代码中。如果你的脚本调用过任何API,程序可能插入一些Unity的核心代码进去,它看上去是这样的:

  这里我们看到的程序调用了GtPosition。堆栈顶部有真实的函数名字,但是没有十六进制的地址。这只是一个代码标记,我们离开了mono或是jit代码。按下Shift-F11(跳出)直到你回到即时代码中(nop的出现可以认为是了一个比较好的指标)   有时候,在Unity的主线程还没有被激活时,你打一个点断,只需要按下F5,主线程激活之后,它就会被中断。   这可能是些比较奇怪的例子,但是,这只是Dbug,可以随性发挥。   

32位情况如何?

  

你可以在32位做相同的事情,JIT代码看上去有一点点不同,它可能像这样:

 

在这个例子中,我们需要从0xB地址读取数据。让它失败,你需要修改代码,因为地址被写死在指令中了,不像在64位上,它在寄存器中。所以,你可以打开Mmory视图(Dbug

Windows

Mmory

Mmory1),指向指定中的地址(黄色箭头处),在这个例子中是0xDC。我们找到了:

  你可以重组这个   

非调试模式会如何呢?

  

如果你真的运气不好,遇到一个bug,只在你编脚本的时候,没有打开Dbug模式才会重现。在这种情况下,你只有凑合一下了。如果你能看到代码,并找到一个可以引起读错误的方法,你运气真不错,但是那是相当难的。最后一个办法,你需要手动注入,类似cmpax,dwordptrds:[0x0],像上面我们说的3b那样。让我们试一上面的方法,打断它:

Ohno!最糟糕的情况出现在了。编译器优化了它,产生了一个跳转到自己的循环。这里没有足够的空间,加一个我们想要的CMP指令(jmp只需要两个字节),然后,每个人都很绝望(毕竟发布版本挂掉了),我们就不需要再过于小心,直接破坏读取的代码.在Mmory界面,指向4D不论怎么样填入3b。按F5执行,我测试时,这游戏(我做这一部分测度的时候,用的独立游戏)又话过来了,我检查输出日志时发现:

  到这里,你可以关掉游戏了,你破不了JIT的部分代码,你的脚本也不会再正常工作了。但是,至少,你还是知道你在什么地方卡住了。 有时,在你停止的附近能找到一个读的指令,你可以右键,选择,“Stnxtstatmnt”,把寄存器的值设为0,这样也能创建一个正确的异常。

结论

在使用了一些技巧之后,看似不可能的死循环被中断了。赶快试一下,你就可以加加入老兵的阵线“哈,我们在汇编代码中做过这些!”很快,我们会推出一种更好的解决方案,这将永远不会晚。原文链接:







































西宁好的白癜风医院
点滴状白癜风

转载请注明原文网址:http://www.helimiaopu.com/cxkf/973.html

------分隔线----------------------------