之前在做内存泄漏分析模块功能开发时,发现在windows下的输出结果并不是很准确,很多内存泄漏都显示是在windows的api或crt函数中,比如CoInitializeEx,stderror,IsOS等。
以CoInitializeEx为例,调用CoUninitialize后,它申请的内存并没有相应的free掉;stderr中,申请的内存也没释放;
下面分别对这些API深入分析下,看看它们的内存分配和释放到底是如何进行的;
stderror
内存分析模块捕捉到的堆栈信息为:
4 allocate: addr = 0000017BD340F610, size = 186
00007FF67B08F81B _calloc_dbg() at f:\dd\vctools\crt\crtw32\misc\dbgheap.c:652(0x7b08f7d0)
00007FF67B087A56 strerror() at f:\dd\vctools\crt\crtw32\misc\strerror.c:75(0x7b087a00)
00007FF67A718E8B luaL_fileresult() at e:\lib_aux.c:39(0x7a718de0)
00007FF67A8A3D54 lj_cf_io_open() at e:\lib_io.c:420(0x7a8a3c90)
第3个栈帧lib_aux.c:39调用的就是stderror函数;
我们直接看stderr的源码(CRT VC12):
wchar_t * cdecl _wcserror(
#else /* _UNICODE */
char * __cdecl strerror (
#endif /* _UNICODE */
int errnum
)
{
_TCHAR *errmsg;
_ptiddata ptd = _getptd_noexit();
if (!ptd)
return _T("Visual C++ CRT: Not enough memory to complete call to strerror.");
if ( (ptd->_terrmsg == NULL) && ((ptd->_terrmsg =
_calloc_crt(_ERRMSGLEN_, sizeof(_TCHAR)))
== NULL) )
return _T("Visual C++ CRT: Not enough memory to complete call to strerror.");
else
errmsg = ptd->_terrmsg;
#ifdef _UNICODE
_ERRCHECK(mbstowcs_s(NULL, errmsg, _ERRMSGLEN_, _get_sys_err_msg(errnum), _ERRMSGLEN_ - 1));
#else /* _UNICODE */
_ERRCHECK(strcpy_s(errmsg, _ERRMSGLEN_, _get_sys_err_msg(errnum)));
#endif /* _UNICODE */
return(errmsg);
}
_getptd_noexit()函数实现:
_ptiddata __cdecl _getptd_noexit (
void
)
{
_ptiddata ptd;
DWORD TL_LastError;
TL_LastError = GetLastError();
if ( (ptd = __crtFlsGetValue(__flsindex)) == NULL ) {
/*
* no per-thread data structure for this thread. try to create
* one.
*/
#ifdef _DEBUG
extern void * __cdecl _calloc_dbg_impl(size_t, size_t, int, const char *, int, int *);
if ((ptd = _calloc_dbg_impl(1, sizeof(struct _tiddata), _CRT_BLOCK, __FILE__, __LINE__, NULL)) != NULL) {
#else /* _DEBUG */
if ((ptd = _calloc_crt(1, sizeof(struct _tiddata))) != NULL) {
#endif /* _DEBUG */
if (__crtFlsSetValue(__flsindex, (LPVOID)ptd) ) {
/*
* Initialize of per-thread data
*/
_initptd(ptd,NULL);
ptd->_tid = GetCurrentThreadId();
ptd->_thandle = (uintptr_t)(-1);
}
else {
/*
* Return NULL to indicate failure
*/
_free_crt(ptd);
ptd = NULL;
}
}
}
SetLastError(TL_LastError);
return(ptd);
}
它首先通过TLS查找线程相关数据,如果没有找到,就分配一块内存,存放_tiddata结构,并将这块内存与__flsindex相关联。
TLS是Win32中常用的存取线程相关数据的一种技术,由操作系统的Tls*系列函数提供支持。
例如,可以在程序开始的地方调用TlsAlloc()函数,获得一个TLS index,这个index在进程范围内有效,然后可以创建n个线程,在每个线程中使用TlsSetValue(index,data)将线程相关数据和index关联起来,使用TlsGetValue(index)来获取当前线程和index相关联的的线程相关数据。
(顺便提一句,经常说的要使用_beginthread而不是CreateThread,也是因为当线程函数调用errno或localtime或其他需要TLS支持的函数时,这些函数会调用_getptd_noexit()函数初始化一个VC运行时库的TLS数据,当线程函数退出时,这块内存不会自动释放,因此产生了泄漏)
所以,stderror主要有两处地方申请内存(虽然有包含关系,但还是分开说):
1,_ptiddata 线程相关数据
2,ptd→_terrmsg
从上面的分析也不难推断出:ptd→_terrmsg的释放是在当前线程退出时,释放_ptiddata时一起进行的。
代码为:
_CRTIMP void
WINAPI
_freefls (
void *data
)
{
_ptiddata ptd;
pthreadlocinfo ptloci;
pthreadmbcinfo ptmbci;
/*
* Free up the _tiddata structure & its malloc-ed buffers.
*/
ptd = data;
if (ptd != NULL) {
if(ptd->_errmsg)
_free_crt((void *)ptd->_errmsg);
if(ptd->_namebuf0)
_free_crt((void *)ptd->_namebuf0);
if(ptd->_namebuf1)
_free_crt((void *)ptd->_namebuf1);
if(ptd->_asctimebuf)
_free_crt((void *)ptd->_asctimebuf);
if(ptd->_wasctimebuf)
_free_crt((void *)ptd->_wasctimebuf);
if(ptd->_gmtimebuf)
_free_crt((void *)ptd->_gmtimebuf);
if(ptd->_cvtbuf)
_free_crt((void *)ptd->_cvtbuf);
if (ptd->_pxcptacttab != _XcptActTab)
_free_crt((void *)ptd->_pxcptacttab);
_mlock(_MB_CP_LOCK);
__try {
if ( ((ptmbci = ptd->ptmbcinfo) != NULL) &&
(InterlockedDecrement(&(ptmbci->refcount)) == 0) &&
(ptmbci != &__initialmbcinfo) )
_free_crt(ptmbci);
}
__finally {
_munlock(_MB_CP_LOCK);
}
_mlock(_SETLOCALE_LOCK);
__try {
if ( (ptloci = ptd->ptlocinfo) != NULL )
{
__removelocaleref(ptloci);
if ( (ptloci != __ptlocinfo) &&
(ptloci != &__initiallocinfo) &&
(ptloci->refcount == 0) )
__freetlocinfo(ptloci);
}
}
__finally {
_munlock(_SETLOCALE_LOCK);
}
_free_crt((void *)ptd);
}
return;
}
调用流程为:
_endThread → _freeptd ->_freefls 。
因此,stderor申请的内存,只有在当前线程结束时,才会释放。(errno也一样)
CoInitializeEx
CoInitializeEx接口就没有源码分析了,只能通过microsoft的文档来理解:
CoInitializeEx接口是用来初始化COM库的,且每个线程至少一次调用(允许多次);
既然是初始化,且线程间相互独立,可以大胆推测内部也有线程相关的数据空间的分配,它的释放也要等到线程结束或进程结束。
CoInitializeSecurity
内存分析模块捕捉到的堆栈信息为:
80 allocate: addr = 000001A722464900, size = 256
00007FFAEE9DCF76 error: 487, bRet = 1
00007FFAEF8F1AD0 CFastBH::CreateFromBindingString() at onecore\com\combase\common\core\fastbh.cxx:176(0xef8f1a68)
00007FFAEF8B1370 <lambda_24b83035e99092592b8833e72f33f0cc>::operator()() at onecore\com\combase\dcomrem\resolver.cxx:609(0xef8b10d0)
00007FFAEF882559 CRpcResolver::GetConnection() at onecore\com\combase\dcomrem\resolver.cxx:875(0xef8824cc)
00007FFAEF8B2465 CoInitializeSecurity() at onecore\com\combase\dcomrem\security.cxx:3216(0xef8b23c0)
00007FF716EE8D14 get_bios_uuid_from_wmi() at e:\src\lib\dmidecode\windows\smbios.cpp:607(0x16ee8bb0)
在smbios.cpp:607处调用了CoInitializeSecurity接口;倒数第二帧又调用了CFastBH::CreateFromBindingString()接口,找不到任何的接口信息,只能根据名称推测,内部创建了String相关的结构。但是没有找到释放的逻辑。
结论:
从上述分析看,wind32 api或crt内部申请的内存,不一定有相对应的接口直接进行释放,而是会在合适的时机才释放:
1,部分线程相关结构/数据,在线程结束时,进行了清理;–比如stderror,CoInitializeEx
2,部分进程相关结构/数据,在进程结束时,进行了清理;–CoInitializeEx的部分内存可能在进程退出时释放。
3,部分全局结构/变量,在进程结束时,进行了清理; --比如_localtime64_s
win32 api内部实现资料实在太少,没有代码,上述结论都是基于microsoft doc以及调试得出的,不一定100%准确。