[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》等少数游戏。

希望此文能帮助各位有兴趣探索别的游戏制作方法的朋友。

posted @ 2010-07-23 13:48  炎峰森林影  阅读(1018)  评论(0编辑  收藏  举报