游戏在后期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:
修改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,这部分地址:
看下一个函数调用汇编代码就很清晰了:
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,回复到调用函数之前的样子。
了解了这些之后,遇到一个minidump,一看,都是优化的汇编代码,但是你需要看stack上的信息,就可以使用esp寄存器来查看:
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: