在C/C++中应避免句柄泄露

起因

在获取最新.Net Runtime源码,看到"Always call CloseHandle in AsmMan::EmitManifest (#50372)",处理文件句柄泄露的代码.通过这个问题,说明即使在在C/C++开发的老司机也会在使用资源的,造成资源泄露的情况.

修复文件句柄泄露

在SourceTree看到,代码只是将CloseHandle放大更大的作用域.修复文件句柄泄露.
避免文件句柄泄露
看代码(部分代码):
m_dwMResSizeTotal = 0;
m_dwMResNum = 0;
for (i = 0; (pManRes = m_ManResLst.PEEK(i)); i++)
{
	BOOL fOK = TRUE;
	mdToken     tkImplementation = mdFileNil;

	if (!pManRes->m_fNew) continue;
	pManRes->m_fNew = FALSE;

	WszMultiByteToWideChar(g_uCodePage, 0, pManRes->szAlias, -1, wzUniBuf, dwUniBuf);
	if (pManRes->szAsmRefName)
	{
		tkImplementation = GetAsmRefTokByName(pManRes->szAsmRefName);
		if (RidFromToken(tkImplementation) == 0)
		{
			report->error("Undefined AssemblyRef '%s' in MResource '%s'\n", pManRes->szAsmRefName, pManRes->szName);
			fOK = FALSE;
		}
	}
	else if (pManRes->szFileName)
	{
		tkImplementation = GetFileTokByName(pManRes->szFileName);
		if (RidFromToken(tkImplementation) == 0)
		{
			report->error("Undefined File '%s' in MResource '%s'\n", pManRes->szFileName, pManRes->szName);
			fOK = FALSE;
		}
	}
	else // embedded mgd.resource, go after the file
	{
		HANDLE hFile = INVALID_HANDLE_VALUE;
		int j;
		WCHAR   wzFileName[2048];
		WCHAR* pwz;

		pManRes->ulOffset = m_dwMResSizeTotal;
		for (j = 0; (hFile == INVALID_HANDLE_VALUE) && (pwzInputFiles[j] != NULL); j++)
		{
			wcscpy_s(wzFileName, 2048, pwzInputFiles[j]);
			pwz = wcsrchr(wzFileName, DIRECTORY_SEPARATOR_CHAR_A);
#ifdef TARGET_WINDOWS
			if (pwz == NULL) pwz = wcsrchr(wzFileName, ':');
#endif
			if (pwz == NULL) pwz = &wzFileName[0];
			else pwz++;
			wcscpy_s(pwz, 2048 - (pwz - wzFileName), wzUniBuf);
			hFile = WszCreateFile(wzFileName, GENERIC_READ, FILE_SHARE_READ,
				0, OPEN_EXISTING, 0, 0);  //打开文件
		}
		if (hFile == INVALID_HANDLE_VALUE)
		{
			report->error("Failed to open managed resource file '%s'\n", pManRes->szAlias);
			fOK = FALSE;
		}
		else
		{
			if (m_dwMResNum >= MAX_MANIFEST_RESOURCES)   //MAX_MANIFEST_RESOURCES 为1024
			{
				report->error("Too many resources (implementation limit: %d); skipping file '%s'\n", MAX_MANIFEST_RESOURCES, pManRes->szAlias);
				fOK = FALSE;
			}
			else
			{
				m_dwMResSize[m_dwMResNum] = SafeGetFileSize(hFile, NULL);
				if (m_dwMResSize[m_dwMResNum] == 0xFFFFFFFF)
				{
					report->error("Failed to get size of managed resource file '%s'\n", pManRes->szAlias);
					fOK = FALSE;
				}
				else
				{
					m_dwMResSizeTotal += m_dwMResSize[m_dwMResNum] + sizeof(DWORD);
					m_wzMResName[m_dwMResNum] = new WCHAR[wcslen(wzFileName) + 1];
					wcscpy_s(m_wzMResName[m_dwMResNum], wcslen(wzFileName) + 1, wzFileName);
					m_fMResNew[m_dwMResNum] = TRUE;
					m_dwMResNum++;
				}

				//原先在这里使用CloseHandle 关闭打开的文件句柄,在读取的文件数量小于1024时,是不会出现文件句柄泄露的情况
				//在打开文件数量大于的时,代码不会在else作用域执行,才会出现文件句柄泄露
				//所以将CloseHandle的作用域放大,就能避免文件句柄泄露
			}

			CloseHandle(hFile);
		}
	}
	if (fOK || ((Assembler*)m_pAssembler)->OnErrGo)
	{
		WszMultiByteToWideChar(g_uCodePage, 0, pManRes->szName, -1, wzUniBuf, dwUniBuf);
		hr = m_pAsmEmitter->DefineManifestResource(         // S_OK or error.
			(LPCWSTR)wzUniBuf,                          // [IN] Name of the resource.
			tkImplementation,                           // [IN] mdFile or mdAssemblyRef that provides the resource.
			pManRes->ulOffset,                          // [IN] Offset to the beginning of the resource within the file.
			pManRes->dwAttr,                            // [IN] Flags.
			(mdManifestResource*)&(pManRes->tkTok));    // [OUT] Returned ManifestResource token.
		if (FAILED(hr))
			report->error("Failed to define manifest resource '%s': 0x%08X\n", pManRes->szName, hr);
	}
}

具体代码在Runtime/src/coreclr/ilasm/asmman.cpp文件EmitManifest方法中.

注意事项

在Windows平台,调用函数返回HANDLE/HMODULE等,在不需要的时候,一定要使用CloseHandle进行关闭,避免资源泄露.

在Linux平台,返回文件描述符的时候,在不使用的时候,记得使用close关闭.

在C语言,使用标准库,如fopen时,要用fclose进行关闭.

不管句柄还是文件描述符的,一定要成对使用.如open/close.

秋风 2021-05-19