Crush The Crash--汇编级看函数调用

Crush The Crash--汇编级看函数调用_第1张图片

游戏在后期polish以及上线之后,一个不可避免的部分就是要处理各种bug,包括crash。

汇编?似乎只是学校里学习了一下,在现在都倾向于使用高层语言的时代,还有用么?答案是肯定的。

有大量的crash以及bug都是只发生在retail版中,现场都是优化过的汇编代码,大部分是minidump,里面包含的信息非常有限,你拿到的就是一个优化过的汇编代码,加上少量的stack上的内存信息,这种情况下要处理掉crash,能从这些汇编代码中解析minidump并最终击杀问题是唯一的选择。

本文涉及的知识在学校的时候是n本书,也有一些工具什么的需要在实际工作中积累,这里罗列一个在实际处理问题中需要了解的最小集吧。


function & thread & stack size

 函数总是运行在某个线程上的,这就牵涉到一个stack size。

stack overflow

在创建线程的时候,比如CreateThread里面都允许指定stack size,如果写程序的时候,栈溢出了,那么就是知名技术网站的来历了:StackOverFlow,这时候一般会有一个异常抛出,看dump的时候一般都会有显示的说stackoverflow。

但是实际中,也遇到过直接crash,但是没有抛出这个异常,最后看下来,当前执行的地方(ESP)和栈的base pointer(EBP)的距离已经超过stacksize,这种情况下,依旧是stack overflow。


查看thread stack size

目前比较好的工具来看是vmmap:

Crush The Crash--汇编级看函数调用_第2张图片

Crush The Crash--汇编级看函数调用_第3张图片


修改thread stack size

自己创建的Thread当然可以随心去指定stack size了,但是如果想修改其他模块创建的thread的信息的话,就需要去hookCreateThread函数,使用微软的detours库可以做到这一点。这个库不是免费的了,自己玩玩就随便下一个,商业化的时候需要注意购买哦。

修改thread stack size对于大部分游戏来说并无必要,实践中,两种情况可以考虑去修改:

内存吃紧

但是如果内存非常的吃紧,游戏里集成模块很多,导致很多线程,一般线程默认是1mb的stacksize,这时候如果大部分允许256k就好的话,那么就有可能节省几十MB,这就是很有意义的一件事。

其他模块的锅

实际中也遇到过nvidia的某个版本的驱动,会出现stack overflow,查看下来是这些thread创建了64kb stacksize的thread,这时候的hook再次出马搞定。


函数调用过程

调用一个函数(比如void foo())涉及到几个因素:传的参数,返回地址和运行过程中使用的空间。

stack frame

一个函数调用,需要的stack上的空间我们称为stack frame,这部分地址:

  • 起始部分存在寄存器EBP(base pointer)
  • 结束地址放在寄存器ESP(stack pointer)

看下一个函数调用汇编代码就很清晰了:

00B7D4B0  push        ebp  
00B7D4B1  mov         ebp,esp  
00B7D4B3  and          esp,0FFFFFFF0h  
00B7D4B6  sub           esp,354h  
可以看到,进入一个函数调用之后,ebp被push一次,然后上一级的stack pointer作为这一级function的base pointer,一个mov操作,然后esp再做减法,这之间的空间就是stack frame了。

再看下函数返回:

00B7D4FA  mov         esp,ebp  
00B7D4FC  pop          ebp  
00B7D4FD  ret            18h  
ebp回赋给esp,然后pop出ebp,回复到调用函数之前的样子。

esp使用

了解了这些之后,遇到一个minidump,一看,都是优化的汇编代码,但是你需要看stack上的信息,就可以使用esp寄存器来查看:

Crush The Crash--汇编级看函数调用_第4张图片


ebp使用

ebp有一些更多的信息:

16(%ebp) - third function parameter
12(%ebp) - second function parameter
8(%ebp) - first function parameter
4(%ebp) - old %EIP (the function's "return address")
0(%ebp) - old %EBP (previous function's base pointer)
-4(%ebp) - first local variable
-8(%ebp) - second local variable
-12(%ebp) - third local variable

从这里我们可以看到,参数的传递也是通过ebp可以得到:


当然上面这个case是无意之举,visual studio可以正确的显示出参数,其他一些情况,visual studio不能用的时候,通过ebp来这样看参数就有意义了。


esi

上面主要说的是stack frame的ebp和esp,还有一个寄存器ESI也比较重要,它是用来存放当前代码执行到那里的地址(代码也是一块内存么)。

之前有一次出现crash,dump里看不出是那个模块crash了,那么通过ESI可以确定crash的内存地址,然后再查询哥哥module的地址区域,最后就可以确定出是那个module crash了。

这个也是非常有帮助的。


reference:

https://en.wikipedia.org/wiki/Call_stack
http://unixwiz.net/techtips/win32-callconv.html
http://unixwiz.net/techtips/win32-callconv-asm.html
http://blog.csdn.net/talking12391239/article/details/8678295

你可能感兴趣的:(General)