[D3D] - 用PerfHUD来调试商业游戏
来源:http://blog.csdn.net/RAINini/archive/2008/10/28/3162112.aspx
PerfHUD是NV一个非常好用的工具,可以用于查看游戏的运行效率,找出瓶颈,也可以用于分析游戏渲染流程,看每个DPC的渲染操作,包括渲染状态,所用的shader等,非常强大。
PerfHUD正常的用途是用于调试自己写的程序,但是有时候看到别的游戏一些好的效果,也想了解是怎么实现的,这时,PerfHUD也可以派上用场。
要使用PerfHUD,就要对自己的D3D程序进行修改,在CreateDevice时,Adapter这个参数不要选用D3DADAPTER_DEFAULT,而是枚举所有的Adapter,选中Description中带有“PerfHUD”字符串的那个Adapter,一般来说,对于单显卡的机器,通常都是第二个Adapter,我没有多显卡,所以不知道多显卡情况下是不是往后顺移。
同时,DeviceType不要选D3DDEVTYPE_HAL,而要选D3DDEVTYPE_REF。
通过这样来创建Device的程序,才能用PerfHUD来挂接并分析,更详细的创建过程,查看PerfHUD的帮助文档吧。
由上可以看出,对于别人的程序,Device都不是自己创建的,似乎不能用PerfHUD来分析了,但经过对要分析的程序做一些手脚后,也是可以的,以下拿《魔兽世界》来举例。
首先,打开VC(我用的2005,2003大同小异,VC6没试过),确保Options里的这个选项是选中的,
只有选中它了,才能用函数名称来作为断点断住DLL里的函数。
然后用VC来把要分析的游戏的exe执行起来,点击VC的File->Open ->Project,在弹出的对话框中选中要分析的exe文件,如下图:
把文件加进来后,在Solution下可以看到该文件,右键选Debug->Step into new instance,启动程序,然后下一个函数断点,点击菜单中的Debug->New Breakpoint->Break at function,在弹出的对话框中写入Direct3DCreate9,如下图:
点OK后,就下好断点了,如果一个程序用的D3D,那么应该会在这个断点断住,要靠它来获取IDirect3D9这个interface的指针。
做完以上步骤后,按F5让程序继续运行,应该会断在刚才那个函数断点,如下图:
这时按Shift+F11,退出这个函数体,如果创建成功,应该就获取到了IDirect3D9,因为这个函数的返回值就是IDirect3D9的指针,只需看eax这个寄存器的值,就能找到这个指针,因为eax是存放函数返回值。
在watch窗口里打入eax,当前eax的值是0x0016CFC0。
打开一个memory窗口,把0x0016CFC0这个值敲进去,然后点在memory窗口下点右键,用4-byte Integer来查看内存,把0x0016CFC0这块内存的前4个字节的值拷贝下来,我机器上显示的值是4b641a98,为什么要拷贝这个值呢,因为一个带虚拟函数的类的指针所指向的地址,最开头的4个字节就是虚函数表的指针,通过它能找到各个虚函数的函数地址。
在memory窗口里把这个值加上h后敲进去,同样用4-byte Integer来查看,得到就是IDirect3D9这个接口的各个函数的入口地址,真正要找的就是CreateDevice这个函数的地址,这时,打开你装的D3D SDK的d3d.h,搜索IDirect3D9这个接口的声明,应该会找到如下代码:
DECLARE_INTERFACE_(IDirect3D9, IUnknown)
{
/*** IUnknown methods ***/
STDMETHOD(QueryInterface)(THIS_ REFIID riid, void** ppvObj) PURE;
STDMETHOD_(ULONG,AddRef)(THIS) PURE;
STDMETHOD_(ULONG,Release)(THIS) PURE;
/*** IDirect3D9 methods ***/
STDMETHOD(RegisterSoftwareDevice)(THIS_ void* pInitializeFunction) PURE;
STDMETHOD_(UINT, GetAdapterCount)(THIS) PURE;
STDMETHOD(GetAdapterIdentifier)(THIS_ UINT Adapter,DWORD Flags,D3DADAPTER_IDENTIFIER9* pIdentifier) PURE;
STDMETHOD_(UINT, GetAdapterModeCount)(THIS_ UINT Adapter,D3DFORMAT Format) PURE;
STDMETHOD(EnumAdapterModes)(THIS_ UINT Adapter,D3DFORMAT Format,UINT Mode,D3DDISPLAYMODE* pMode) PURE;
STDMETHOD(GetAdapterDisplayMode)(THIS_ UINT Adapter,D3DDISPLAYMODE* pMode) PURE;
STDMETHOD(CheckDeviceType)(THIS_ UINT Adapter,D3DDEVTYPE DevType,D3DFORMAT AdapterFormat,D3DFORMAT BackBufferFormat,BOOL bWindowed) PURE;
STDMETHOD(CheckDeviceFormat)(THIS_ UINT Adapter,D3DDEVTYPE DeviceType,D3DFORMAT AdapterFormat,DWORD Usage,D3DRESOURCETYPE RType,D3DFORMAT CheckFormat) PURE;
STDMETHOD(CheckDeviceMultiSampleType)(THIS_ UINT Adapter,D3DDEVTYPE DeviceType,D3DFORMAT SurfaceFormat,BOOL Windowed,D3DMULTISAMPLE_TYPE MultiSampleType,DWORD* pQualityLevels) PURE;
STDMETHOD(CheckDepthStencilMatch)(THIS_ UINT Adapter,D3DDEVTYPE DeviceType,D3DFORMAT AdapterFormat,D3DFORMAT RenderTargetFormat,D3DFORMAT DepthStencilFormat) PURE;
STDMETHOD(CheckDeviceFormatConversion)(THIS_ UINT Adapter,D3DDEVTYPE DeviceType,D3DFORMAT SourceFormat,D3DFORMAT TargetFormat) PURE;
STDMETHOD(GetDeviceCaps)(THIS_ UINT Adapter,D3DDEVTYPE DeviceType,D3DCAPS9* pCaps) PURE;
STDMETHOD_(HMONITOR, GetAdapterMonitor)(THIS_ UINT Adapter) PURE;
STDMETHOD(CreateDevice)(THIS_ UINT Adapter,D3DDEVTYPE DeviceType,HWND hFocusWindow,DWORD BehaviorFlags,D3DPRESENT_PARAMETERS* pPresentationParameters,IDirect3DDevice9** ppReturnedDeviceInterface) PURE;
#ifdef D3D_DEBUG_INFO
LPCWSTR Version;
#endif
};
数了一下,发现CreateDevice是第17个函数,所以应该在虚函数表里的第17个元素,所以在4b641a98h所指向的内存往后数,第17个指针就是CreateDevice的函数入口点,我机器上的值是4b6c1670,应该说,在没做特别处理的情况下,同个D3D的版本,某个函数的函数地址都是一样的。
在Disassembly代码窗口顶部的Address里输入4b6c1670h这个值,然后敲回车,就能定位到CreateDevice的函数体,按F9下个断点,然后按F5让程序继续运行,应该能断到这个函数,如下图:
这时,查看esp的值,找出调用这个函数的地方,当前esp的值是005a4c4b,在刚进入函数体时,esp保存的是这个函数返回后要继续执行的下条指令的地址,所以在这条指令的上方,就是调用CreateDevice的代码了,同样,在Disassembly窗口里输入005a4c4bh,定位到该条指令,如下图:
断点上方的那条call指令就是调用CreateDevice的地方
上图中,从那条call指令往上数,第一个push是push this指针,也就是IDirect3D接口的指针push进去,第二个push就push第一个参数,可以看到,它原来是直接push 0的,也就是用D3DADAPTER_DEFAULT来作为第一个参数传给CreateDevice,再上面的一个push就是放入第二个参数的,可以看到是以下指令完成的,
mov ebx,1
push ebx
也就是说把1做为第二个参数,其实也就是D3DDEVTYPE_HAL,所以,《魔兽世界》调用CreateDevice是中规中矩的。这时要做的就是把修改这段代码,从mov ebx,1这条指令开始替换,把它变成push 2,push 1,也就是把传给CreateDevice的第一个参数改成1,代表第二个Adapter,把第二个参数改成2,也就是D3DDEVTYPE_REF所代表的值。
上图里的这三条指令就是要替换掉的指令:
005A4C3D BB 01 00 00 00 mov ebx,1
005A4C42 53 push ebx
005A4C43 6A 00 push 0
要替换成
Push 2
Push 1
这两条指令,一共是要替换8个字节的机器码。
这时可以通过直接修改exe文件本身来达到目的,先把wow.exe复制一份,我把复制后的exe起名为wow_m.exe,然后用UltraEdit打开该exe文件,查找要修改的指令的机器码,从mov ebx,1这条指令的机器码搜索起,可以看到,该条指令的机器码是BB 01 00 00 00,一般来说,搜索的字节越多,越准确,因为你搜得太少,极有可能有好多个地方调用了同条指令,所以我直接把这条指令后面的机器码也加上,如下图:
记得把“查找 ASCII”这个勾去掉,然后点下一个,就能定位到该指令所在的地方。一定要确保找到的地方只有一处,如果有多处,就用再多一些的机器码来搜索。最终定位到的地方如下图:
从BB这个字节开始替换,把光标定位在第一个B处,然后顺序输入6A 02 6A 01,也就是push 2和push 1的机器码。这时还剩下4个字节,可全部替换成90,替换后的代码如下:
修改工作到此结束,保存该exe,然后就可以用PerfHUD来调试了,下图就是挂接上PerfHUD后的《魔兽世界》运行截图:
总结以上步骤:
1.在程序启动时下断点,断到执行Direct3DCreate9的地方,获取到IDirect3D指针。
2.用IDirect3D指针获取到虚函数表指针,查找到CreateDevice的函数体。
3.在该函数体处下断点,断住后用esp来找到调用该函数的地方。
4.找到给CreateDevice传参数的代码,并修改成PerfHUD所要求的参数。
只要是没加过密的D3D9程序,应该都可以用以上方法来修改,我只试过《Crysis》,《WOW》,《PES2009》等少数游戏。
希望此文能帮助各位有兴趣探索别的游戏制作方法的朋友。