GDI泄露终极解决方案——HOOK API,建立GDI对象引用计数器

来源:互联网 发布:二叉树递归遍历c语言 编辑:程序博客网 时间:2024/06/06 00:51

GDI泄露终极解决方案——HOOK API,建立GDI对象引用计数器


前言:
 和内在泄漏一样,GDI使用不当也会导致对象泄漏。不一样的是GDI对象泄漏更隐蔽,而且目前还没有较好的调试工具可用。等到发现自己的项目因GDI泄漏而shutdown为时已晚。下意识的就会想到回去修正代码。当然最好的办法也是回头修正代码,彻底解决泄漏。但当你回头发现需要修改的代码很多,而且很多其他人写的代码,甚至是UI库本身发生泄漏,又要求短时间内解决,那么回头修正代码就不可能了……当然有别的办法。
接下来要说的就是另一种办法 —— 重建创建GDI对象的API函数,整合属性相同的GDI对象,减少GDI对象的实际数量。


        大概一个月前,突然碰上0x486错误

任务管理器里显示GDI对象数量已经超过了8000……


        GDI对象泄漏导致GDI对象数量达到上限(WIN7下上限默认10000,XP忘了,自己搜吧),谷歌了几遍,国内的论坛关于GDI的讨论基本停留在怎么把图画出来和怎么避免泄露的水平(当然我也是其中之一)。后来在microsoft官网上找到一检测GDI泄漏的工具(GDILeaks http://technet.microsoft.com/zh-cn/library/cc301756.aspx),有源码。不过只说怎么检查GDI泄漏,没说怎么彻底解决(当然是记录到调用堆栈一路跟过去改掉代码最好,不过公司项目里大约有三千行左右操作GDI句柄的代码而且很分散,一但改写必须重新测试,代价太大。)显然,花一周时间去改写这三千行代码略显吃力,再加重新测试,一周不太可能搞定

        至于为什么会出这种问题就不多说了,没基础没规范没制度,这种问题以前有现在有将来还会有。碰上的人不多,一旦碰上很棘手。这篇贴希望对冲杀在一线的小公司序猿们有些帮助。OK,解决思路也很简单,就是HOOK创建GDI对象的函数把创建GDI对象时用的参数和被创建的GDI对象句柄用HASH表保存对应关系为Param -> Handle。当创建GDI对象函数再次被调用时检查该表中是否已经存在与Param对应的Handle,如果是,则增加Handle的引用计数并直接返回Handle,否则,创建GDI对象。销毁GDI对象时减少引用计数,当引用计数小于0时销毁Handle。


        一开始就遇到麻烦。创建GDI对象并没有一个统一的API函数(GDI+可以HOOK GdipAlloc),MFC里的CGdiObject:Attach看起来不错,但不能拦截到非MFC创建的GDI对象,而且Attach被调用的时候GDI对象已经创建好了,要添加引用计数器只能通过对比GDITableEntry表来整合GDI对象(上边提到的GDILeaks就是用的这种方法扫描进程GDI信息的。但这个表在PEB结构中,后面会给出扫描进程中所有GDI对象的方法),另外CGdiObject::Attach不是虚函数,如果要HOOK它要么重建虚函数表要么inline hook,显然,HOOK Attach函数代价太大。吃力不讨好。


        那么,剩下的只有一个妥协的办法了 —— HOOK多个API函数。公司项目里用到的GDI对象最多的是Font、Bitmap(Image)、Icon、Pen(下图)

        其它几种问题不大,所以,要先找到合适的API函数来HOOK。IDA、WinDbg,或者VS下载调试符号后都能分析API函数(创建GDI对象的API函数大多为GDI32.DLL导出,Load开头的函数一般由USER32.DLL导出。调试符号下载这俩就行了)。只要得到调用链并找到最终被调用的创建GDI对象的API函数即可,然后HOOK之。

下面是IDA给出的部分创建GDI对象的函数的交叉参考图(下图)


Font:







Bitmap(Image)、Icon:



LoadBitmapA和LoadBitmapW函数已经被微软弃用了 但公司的项目里仍然在用。LoadBitmapA和LoadBitmapW不存在相互调用关系。


最后,两个销毁GDI对象的函数:

DeleteObject和DestroyIcon,不知道为什么DestroyIcon并没有调到DeleteObject去,所以要HOOK这两个销毁函数。其余几种GDI对象的销毁都会调用DeleteObject函数。


要HOOK的函数(下表):

CreateFontIndirectExWFontLoadBitmapABitmapLoadBitmapWBitmapCreateBitmapBitmapLoadImageWImageLoadIconWImageCreatePenPenDeleteObjectDestroy GDI ObjectDestroyIconDestroy Icon
共九个,分为两类:创建、销毁,处理方法基本相同。


一、建立GDI对象句柄表,保存创建函数创建GDI对象时的参数和被创建的GDI对象句柄。

        这里会遇到用什么表保存GDI对象的问题,链表和数组没有KEY和VALUE的映射关系不能用。如果用HASH表,由于DeleteObject和DestroyIcon的参数是GDI对象句柄,又需要搜索表。那么当然会想到双向映射,但支持双向映射的只有Boost库的bitmap类,MFC的CMap、ATL的CSimpleMap和ATL的map类均不支持双向映射,所以这里又妥协,用CMap(注意:API函数会被多线程调用,所以GDI对象表也应该是线程安全的)。
所以,要分别包装一个线程安全的CMap类,一个处理函数参数的类,和一个包装GDI对象句柄加入了引用计数器的结构。

1)、从CMap派生一个子类CSafeMap,添加一个临界变量CRITICAL_SECTION m_cs;

/**线程安全的CMap类。借助CAutoLock自动进入、退出临界。**.fuhao*.2014年9月25日15时55分。*/template< typename KEY, typename ARG_KEY, typename VALUE, typename ARG_VALUE>class CSafeMap : public CMap< KEY, ARG_KEY, VALUE, ARG_VALUE>{public:CSafeMap(){InitializeCriticalSection( &m_cs );}~CSafeMap(){DeleteCriticalSection( &m_cs );}void EnterCriticalSection(){::EnterCriticalSection( &m_cs );}void LeaveCriticalSection(){::LeaveCriticalSection( &m_cs );}CRITICAL_SECTION *GetCriticalSection(){return &m_cs;}ARG_KEY FindValue( ARG_VALUE value );private:CRITICAL_SECTION m_cs;};
如果想要更方便,可以重载CSafeMap类向CRITICAL_SECTION类型转换符,返回m_cs即可。

2)、CAutoLock类。自动进入、退出临界。
class CAutoLock{public:CAutoLock( CRITICAL_SECTION *pcs ){m_pcs = pcs;EnterCriticalSection( m_pcs );}virtual ~CAutoLock(){LeaveCriticalSection( m_pcs );}protected:CRITICAL_SECTION *m_pcs;private:CAutoLock();};

3)、处理创建GDI对象时参数的结构。_tagCreateTimeParam
/**用来装载创建GDI对象时参数的结构。作为GDI对象表的KEY。**.fuhao*.2014年9月25日15时53分。*/typedef struct _tagCreateTimeParam{__readonly unsigned char szCreateTimeParam[MAX_PATH+128];// 创建该GDI对象的参数(至少要能装下一个路径字串)__readonly size_t cbCreateTimeParamSize;// szCreateTimeParam的长度(一般// 也表示创建GDI对象的函数的参数长度)size_t PushCreateTimeParam( __in LPVOID pFirstParamAddress,__in size_t cbParamLenght ){int nCpySize = cbParamLenght > sizeof( szCreateTimeParam ) ?sizeof( szCreateTimeParam ) : cbParamLenght;memcpy( szCreateTimeParam, pFirstParamAddress, nCpySize );return cbCreateTimeParamSize = nCpySize;}size_t AppendCreateTimeParam( __in LPVOID lpParam,__in size_t cbParamSize ){int nCopySize = cbCreateTimeParamSize + cbParamSize > sizeof( szCreateTimeParam ) ?sizeof( szCreateTimeParam ) - cbCreateTimeParamSize :cbParamSize;memcpy( szCreateTimeParam + cbCreateTimeParamSize, lpParam, nCopySize );return (cbCreateTimeParamSize+=nCopySize);}size_t AppendString( __in LPCTSTR lpcszString ){return AppendCreateTimeParam( (LPVOID)const_cast< char* > ( lpcszString ),lstrlen( lpcszString ) );}size_t AppendString( __in LPCWSTR lpwszString ){return AppendCreateTimeParam( (LPVOID)const_cast< wchar_t *>( lpwszString ),wcslen( lpwszString ) * sizeof( wchar_t ) );}bool operator==( _tagCreateTimeParam &t ){return cbCreateTimeParamSize == t.cbCreateTimeParamSize ?!memcmp( szCreateTimeParam, t.szCreateTimeParam, cbCreateTimeParamSize ) :false;}}CREATE_TIME_PARAM, *PCREATE_TIME_PARAM;

4)、和一个用来操作CREATE_TIME_PARAM的类。
/* *类似于智能指针类。用来操作CREATE_TIME_PARAM * *.fuhao *.2014年9月28日13时28分 */class CAutoCreateTimeParam : public CAutoLock{public:CAutoCreateTimeParam( __in CRITICAL_SECTION *pcs, __in_opt PVOID lpFirstParamAddress = NULL, __in_opt size_t cbParamSize = 0 ):CAutoLock( pcs ){m_pCreateTimeParam = new CREATE_TIME_PARAM;memset( m_pCreateTimeParam, 0, sizeof( CREATE_TIME_PARAM ) );m_pCreateTimeParam->PushCreateTimeParam( lpFirstParamAddress, cbParamSize );}virtual ~CAutoCreateTimeParam(){delete m_pCreateTimeParam;}CREATE_TIME_PARAM * operator ->(){return m_pCreateTimeParam;}operator PCREATE_TIME_PARAM(){// 一旦向PCAMERA_TIME_PARAM转换后将移交m_pCreateTimeParam// 指针控制权,析构时不再自动销毁构造时分配的CREATE_TIME_PARAM对象。PCREATE_TIME_PARAM retval = m_pCreateTimeParam;m_pCreateTimeParam = NULL;return retval;}PCREATE_TIME_PARAM GetCreateTimeParam(){return m_pCreateTimeParam;}private:CAutoCreateTimeParam();PCREATE_TIME_PARAM m_pCreateTimeParam;};


5)、包装GDI对象句柄的引用计数器类。
//// 包装GDI对象句柄,作为GDI对象表的VALUE//// .fuhao// .2014年9月26日11时35分。//typedef struct _tagGDIOBJECT{__readonly HANDLE hObject;// GDI对象句柄。int nQuoteCount;// 引用计算器,当该值为0时,hObject指向的// GDI对象将被销毁,并从GDI对象表从移除映射。_tagGDIOBJECT(){ memset( this, 0, sizeof( _tagGDIOBJECT ) ); }}GDIOBJECT, *PGDIOBJECT;

6)、建表
/* *映射创建GDI对象时所用的参数与GDI对象句柄。 * *值得注意的是该表的KEY是PCREATE_TIME_PARAM而非CREATE_TIME_PARAM对象 * *.fuhao *.2014年9月25日16时40分 */typedef CSafeMap< PCREATE_TIME_PARAM, PCREATE_TIME_PARAM, PGDIOBJECT, PGDIOBJECT >CMapParamToObject;extern CMapParamToObject g_mapParamToObject;

注意:
        因为CMap类的某些函数会直接复制KEY(如GetNextAssoc),所以CMapParamToObject的KEY和ARG_KEY类型均为CREATE_TIME_PARAM结构的指针,而非结构体类型本身。所以,要给CREATE_TIME_PARAM*类型分别特例化一个计算HASH值和一个对比元素函数。

/* *特化两个HashKey和CompareElements函数,给CMapParamToObject用。 * *.fuhao *.2014年9月25日18时28分 */template<>AFX_INLINE UINT HashKey<__SMART_GDI__::PCREATE_TIME_PARAM >( __SMART_GDI__::PCREATE_TIME_PARAM lpCreateTimeParam ){if( lpCreateTimeParam == NULL ){AfxThrowInvalidArgException();}// 从HaskKey< LPCTSTR >( LPCTSTR )抄来的UINT uHashVal = 2166136261U;UINT uFirst = 0;UINT uLast = lpCreateTimeParam->cbCreateTimeParamSize;UINT uStride = 1 + uLast / 10;for(; uFirst < uLast; uFirst += uStride){uHashVal = 16777619U * uHashVal ^ (UINT)lpCreateTimeParam->szCreateTimeParam[uFirst];}return(uHashVal);}template<>static BOOL AFXAPI CompareElements(const __SMART_GDI__::PCREATE_TIME_PARAM *pElement1,    const __SMART_GDI__::PCREATE_TIME_PARAM *pElement2){return *(*pElement2) == *(*pElement1);// 参见CREATE_TIME_PARAM operator == 运算符重载}



二、安装HOOK。

        直接用microsft的detours库来完成 inline hook(下载完自行编译,地址:http://research.microsoft.com/en-us/downloads/d36340fb-4d3c-4ddd-bf5b-1db25d03713d/default.aspx )。


1)、以下5个DECLARE_REFLECT_API宏用来映射API函数到自定义函数中去,分别用来处理不同参数个数的函数(限于篇幅只贴出一个)。g_mapPointerToDetour为保存即将被HOOK的函数和对应的Rebuilding函数表。
/**用来装载即将被HOOK的函数表。SGDI_Initialize函数被调*用后会HOOK该表中所有函数。*CAPIReflect类的对象构造时会自动向该表中添加映射。**.fuhao*.2014年9月24日17时40分。*//* __declspec(selectany) extern,CAPIReflect的构造函数被调用的时候会向函数映射表中 *//*添加HOOK映射,但此时的映射表可能还未被创建。g_mapPointerToDetour函数用来确 *//*映射表被访问时已经创建了 */static CSafeMap< DWORD*, DWORD*, DWORD, DWORD >& g_mapPointerToDetour(){static CSafeMap< DWORD*, DWORD*, DWORD, DWORD > mapPointerToDetour;return mapPointerToDetour;}template< typename Type > struct CAPIReflect {CAPIReflect( DWORD *pfn1, DWORD pfn2, LPCTSTR lpcszAPIName, LPCTSTR lpcszCustomName ) {CAutoLock AutoLock( g_mapPointerToDetour().GetCriticalSection() );// 这里好像不用模板就行……g_mapPointerToDetour().SetAt( pfn1, pfn2 );TRACE( "APIName = %s, CustomName = %s\n", lpcszAPIName, lpcszCustomName );TRACE( "APIAddress = 0x%x, CustomAddress = 0x%x\n", pfn1, pfn2 );}};/**DECLARE_REFLECT_API,用来映射API函数到自定义函数中去:**参数:*APIFunction,API函数名。*CustomFucntion,自定义函数名(必须与被映射的API函数定义完全一致)*ResultType,APIFunction指向的函数的返回值类型。*vType(1,2,3...n),APIFunction指向的函数的参数类型,vType1表示第一个参数*vType2表示第2个,以此类推。**示例:*DECLARE_REFLECT_API1( SetWindowTextA,*MySetWindowTextA,*BOOL,*HWND,*LPCTSTR );*经过映射后,本进程中所有调用的SetWindowTextA函数都会跳转到MySetWindowTextA*函数中。如果要调用映射前的SetWindowTextA,要使用Real_SetWindowTextA函数名调用。**注:*这个宏编译时可能会有警告,暂时未屏蔽警告。(VC6不能使用该宏)**.fuhao*.2014年9月24日18时30分。*/#define DECLARE_REFLECT_API1( APIFunction, CustomFunction, ResultType, vType1 )\static ResultType( WINAPI *Real_##APIFunction)( vType1 ) =\(ResultType( __stdcall*)(vType1))__global_api::APIFunction;\__declspec( selectany ) extern __SMART_GDI__::CAPIReflect< ResultType (WINAPI *  )( vType1 ) >\APIFunction##Reflect( (DWORD*)&Real_##APIFunction,\(DWORD)CustomFunction, #APIFunction, #CustomFunction );\__pragma( message( "\nDECLARE_REFLECT_API1\nMap: '"#APIFunction "' -> '" #CustomFunction "'" ) ); \__pragma( message( "reality '" #APIFunction "' function call address: 'Real_"  #APIFunction "'\n~~~" ) )

注意:
        CAPIReflect前的__declspec( selectany ) extern 是为了让变量最先初始化和只定义一次。如果不加此声名会引起链接错误或多次定义。另外,映射后如果要调回原函数去只要在函数名前加“Real_“即可。


OK,万事俱备只欠东风。下边给出核心代码。
1)、定义重建函数并使用DECLARE_REFLECT_API把API函数映射到重建函数中;(以下只给出部分代码)
class CSharedGDIHandle{private:CSharedGDIHandle();~CSharedGDIHandle();public:// fontstatic  HFONTWINAPICreateFontIndirectExW(__in CONST ENUMLOGFONTEXDVW *elf );// destroy font, bitmapstatic BOOL WINAPI DeleteObject( __in HGDIOBJ ho);// 释放GDI对象。static BOOL FreeGDIObject( HANDLE ho,void *pfnRealFreeProc );};

使用DECLARE_REFLECT_API,映射API函数到自定义的重建函数中去。
// fontDECLARE_REFLECT_API1( CreateFontIndirectExW,__SMART_GDI__::CSharedGDIHandle::CreateFontIndirectExW,HFONT, ENUMLOGFONTEXDVW* );// destroy bitmap ,fontDECLARE_REFLECT_API1( DeleteObject,__SMART_GDI__::CSharedGDIHandle::DeleteObject,BOOL,HGDIOBJ );

2)、实现重建函数,给GDI对象添加引用计数器。(以下只给出部分代码)


HFONT __SMART_GDI__::CSharedGDIHandle::CreateFontIndirectExW( __in CONST ENUMLOGFONTEXDVW *elf ){// CAutoCreateTimeParam类用来保存创建时参数和自动进入、退出临界区并自动分配一个// CREATE_TIME_PARAM对象。// 保存参数需要注意参数类型,如果是指针变量,一般来说应该保留指针指向的内容。// 也有例外,比如LoadBitmapA和LoadBitmapW使用lpBitmapName直接表示位图资源// ID。这也是为什么LoadBitmapA不会调用LoadBitmapW的原因,因为它的参数定义虽// 是字符指针,实际表示位图资源ID(数值)。// 参数栈是连续的,所以可以直接通过第一个参数地址向后偏移找到所有参数。CAutoCreateTimeParam CreateTimeParam( g_mapParamToObject.GetCriticalSection(),(LPVOID)elf,(size_t)&(((LOGFONTW *)0)->lfFaceName) );CreateTimeParam->AppendString( elf->elfEnumLogfontEx.elfLogFont.lfFaceName );// 在MAP表中查找是否已经使用该参数创建过GDI对象,如果是,增加引用计数并返回// 已创建的GDI对象句柄。否则,创建GDI对象并把创建时参数和GDI对象句柄保存到// 该表中。PGDIOBJECT pObject = NULL;if( g_mapParamToObject.Lookup( CreateTimeParam.GetCreateTimeParam(), pObject ) ){// 已经被创建了。++pObject->nQuoteCount;return (HFONT)pObject->hObject;}ASSERT( pObject == NULL );// 创建GDI对象并保存pObject = new GDIOBJECT();pObject->hObject = (HANDLE)Real_CreateFontIndirectExW( const_cast<ENUMLOGFONTEXDVW*>( elf ) );if( pObject->hObject ){g_mapParamToObject.SetAt( CreateTimeParam, pObject );return (HFONT)pObject->hObject;}delete pObject;return NULL;}

以下是删除GDI对象:
BOOL  __SMART_GDI__::CSharedGDIHandle::DeleteObject( __in HGDIOBJ ho ){return FreeGDIObject( ho, Real_DeleteObject );}BOOL __SMART_GDI__::CSharedGDIHandle::FreeGDIObject( HANDLE ho, void *pfnRealFreeProc ){CAutoLock AutoLock( g_mapParamToObject.GetCriticalSection() );// 在GDI对象表中搜索ho,如果没在对象表中,则直接调回原来的// 删除GDI对象函数。// 这里搜索ho会降低效率,之前提到过,MFC里并没有提供支持双向// 映射的HASH表,所以,又是妥协之策。POSITION pos = g_mapParamToObject.GetStartPosition();PGDIOBJECT pObject = NULL;PCREATE_TIME_PARAM pCreateTimeParam = NULL;for( ; pos != NULL; pObject = NULL, pCreateTimeParam = NULL ){g_mapParamToObject.GetNextAssoc( pos, pCreateTimeParam, pObject );if( pObject->hObject == (HANDLE)ho ){break;}}// 没找着,这个GDI对象没在对象表里。不进行处理。if( pObject == NULL ){return ((BOOL( __stdcall *)(HANDLE))pfnRealFreeProc)( ho );}// 减少引用计数。--pObject->nQuoteCount;BOOL retval = TRUE;// 如果引用计数减到了负数,删除GDI对象。if( pObject->nQuoteCount < 0 ){g_mapParamToObject.RemoveKey( pCreateTimeParam );delete pCreateTimeParam;retval = ((BOOL( __stdcall *)(HANDLE))pfnRealFreeProc)( pObject ->hObject );delete pObject;}return retval;}

最后的安装HOOK函数,用Detour库很容易实现。
BOOL __SMART_GDI__::SGDI_Initiaized(){DetourRestoreAfterWith();DetourTransactionBegin();DWORD dwCurrentThreadID = GetCurrentThreadId(),dwCurrentProcessID = GetCurrentProcessId();HANDLE hThreadSnapshot = CreateToolhelp32Snapshot( TH32CS_SNAPTHREAD,dwCurrentProcessID ),hThread = NULL;if( hThreadSnapshot == INVALID_HANDLE_VALUE ){DetourTransactionCommit();return FALSE;}// DetourAttach修改目标函数时可能有线程正在调用它。所以调用DetourUpdateThread// 会对线程调用SuspendThread挂起线程后再修改目标函数。// // 这里遍历当前进程中所有线程,对除当前线程之外的所有线程调用一次DetrouUpdateThread。// 但用快照遍历线程是不可靠的。因为调用CreateToolhelp32Snapshot后可能又有新线程被启// 动去访问目标函数。没研究DetourAttach内部到底是怎么实现的,也许原子操作不会有这种// 问题。但只是猜测,这里还是尽可能保证不出错。THREADENTRY32 te;te.dwSize = sizeof( te );if( !Thread32First( hThreadSnapshot, &te ) ){CloseHandle( hThreadSnapshot );DetourTransactionCommit();return FALSE;}do{if( (te.th32OwnerProcessID == dwCurrentProcessID) &&(te.th32ThreadID != dwCurrentThreadID) &&(te.dwSize >= FIELD_OFFSET( THREADENTRY32, th32OwnerProcessID ) + sizeof( te.th32OwnerProcessID ))){// 避免挂起当前线程。DetourUpdateThread内部会调用GetCurrentThread与参数hThread对比,如果不// 是当前线程,则调用SuspendThread挂起hThread。// GetCurrentThread返回的是伪句柄(0xfffffffe),OpenThread会返回真实句柄,所以,以下代码会导// 致当前线程被挂起:// // DetourUpdateThread( OpenThread( ,, GetCurrentThreadId() ) );if( hThread = OpenThread( THREAD_SUSPEND_RESUME, FALSE, te.th32ThreadID ) ){DetourUpdateThread( hThread );CloseHandle( hThread );}}te.dwSize = sizeof( te );}while( Thread32Next( hThreadSnapshot, &te ) );CloseHandle( hThreadSnapshot );CAutoLock AutoLock( g_mapPointerToDetour().GetCriticalSection() );DWORD *pfn1 = NULL,pfn2 = 0;for( POSITION pos = g_mapPointerToDetour().GetStartPosition(); pos != NULL; ){g_mapPointerToDetour().GetNextAssoc( pos, pfn1, pfn2 );DetourAttach( &(PVOID&)*(DWORD*)pfn1,  (PVOID)pfn2 );}// 注意这里,必须为g_mapParamToObject调用InitHashTable// 来初始化HASH表长度。(建议初始长度为奇数);// CMap的HASH表长度默认为17,如果不对该长度进行初始化// 那么调用 Lookup接口时将会频繁调用CompareElements对比// 其中元素导致效率降低。g_mapParamToObject.InitHashTable( 10241 );DetourTransactionCommit();return TRUE;}



OK,到此为止,8000降到800,绘图一切正常:



绘制图像:




文字、图标:




0 0