winapi - 如何使用 ShellExecuteEx 避免内存泄漏?
问题描述
最小、完整和可验证的示例:
Visual Studio 2017 Pro 15.9.3 Windows 10 "1803" (17134.441) x64 环境变量OANOCACHE
设置为 1。显示 32 位 Unicode 构建的数据/屏幕截图。
更新:在另一台装有 Windows 10“1803”(17134.407)的机器上的行为完全相同 更新:在装有 Windows 7 的旧笔记本电脑上的零泄漏更新:在另一台装有 W10“1803”(17134.335)的机器上完全相同的行为(泄漏)
#include <windows.h>
#include <cstdio>
int main() {
getchar();
CoInitializeEx( NULL, COINIT_APARTMENTTHREADED | COINIT_DISABLE_OLE1DDE );
printf( "Launching and terminating processes...\n" );
for ( size_t i = 0; i < 64; ++i ) {
SHELLEXECUTEINFO sei;
memset( &sei, 0, sizeof( sei ) );
sei.cbSize = sizeof( sei );
sei.lpFile = L"iexplore.exe";
sei.lpParameters = L"about:blank";
sei.fMask = SEE_MASK_FLAG_NO_UI | SEE_MASK_NOCLOSEPROCESS | SEE_MASK_NOASYNC;
BOOL bSuccess = ShellExecuteEx( &sei );
if ( bSuccess == FALSE ) {
printf( "\nShellExecuteEx failed with Win32 code %d and hInstApp %d. Exiting...\n",
GetLastError(), (int)sei.hInstApp );
CoUninitialize();
return 0;
} // endif
printf( "%d", (int)GetProcessId( sei.hProcess ) );
Sleep( 1000 );
bSuccess = TerminateProcess( sei.hProcess, 0 );
if ( bSuccess == FALSE ) {
printf( "\nTerminateProcess failed with Win32 code %d. Exiting...\n",
GetLastError() );
CloseHandle( sei.hProcess );
CoUninitialize();
return 0;
} // endif
DWORD dwRetCode = WaitForSingleObject( sei.hProcess, 5000 );
if ( dwRetCode != WAIT_OBJECT_0 ) {
printf( "\nWaitForSingleObject failed with code %x. Exiting...\n",
dwRetCode );
CloseHandle( sei.hProcess );
CoUninitialize();
return 0;
} // endif
CloseHandle( sei.hProcess );
printf( "K " );
Sleep( 1000 );
} // end for
printf( "\nDone!" );
CoUninitialize();
getchar();
} // main
该代码使用ShellExecuteEx在循环中使用about:blank
URL 启动 64 个 Internet Explorer 实例。SEE_MASK_NOCLOSEPROCESS
用于随后能够使用TerminateProcess API。
我注意到两种泄漏:
- 句柄泄漏:当循环完成但程序仍在运行时启动 Process Explorer,我看到几个 64 个句柄块(进程句柄和各种键的注册表句柄)
- 内存泄漏:将可视化 C++ 2017 调试器附加到程序中,在循环之前,我拍摄了第一个堆快照,在循环之后拍摄了第二个。我看到 64 个 8192 字节的块,来自
windows.storage.dll!CInvokeCreateProcessVerb::_BuildEnvironmentForNewProcess()
您可以在此处阅读有关句柄泄漏的一些信息:ShellExecute 泄漏句柄
第二:相同的 pid,如 Process Explorer 中所示:
Process Explorer 还显示 64*3 打开的注册表句柄,HKCR\.exe
forHKCR\exefile
和HKCR\exefile\shell\open
.
最后:Process Explorer 的屏幕截图,显示了在 MCVE 执行期间使用 1024 循环计数器修改的“Private Bytes”。运行时间约为 36 分钟,PV 从 1.1 Mo(CoInitializeEx 之前)开始,到 19 Mo(CoUninitialize 之后)结束。然后该值稳定在 18.9
我究竟做错了什么?我看到没有泄漏的地方吗?
解决方案
这是版本 1803 中的 Windows 错误。重现的最少代码:
if (0 <= CoInitialize(0))
{
SHELLEXECUTEINFO sei = {
sizeof(sei), 0, 0, 0, L"notepad.exe", 0, 0, SW_SHOW
};
ShellExecuteEx( &sei );
CoUninitialize();
}
执行这段代码后,可以查看notepad.exe进程和第一个线程的句柄——这个句柄当然不能存在(被关闭),不能关闭键
\REGISTRY\MACHINE\SOFTWARE\Classes\.exe
\REGISTRY\MACHINE\SOFTWARE\Classes\exefile
在此调用之后,进程中也存在私有内存泄漏。
当然这个错误会导致explorer.exe和任何进程中的永久资源泄漏,它使用ShellExecute[Ex]
正是研究这个错误 -这里
这里的根本问题似乎在windows.storage.dll中。特别是,
CInvokeCreateProcessVerb
对象永远不会被销毁,因为关联的引用计数永远不会达到 0。这会泄漏与 关联的所有对象CInvokeCreateProcessVerb
,包括 4 个句柄和一些内存。引用计数从未达到 0 的原因似乎与
ShellDDEExec::InitializeByShellInternal
从 Windows 10 1709 到 1803 的参数更改有关,由CInvokeCreateProcessVerb::Launch()
.
更具体地说,我们有一个对象 ( CInvokeCreateProcessVerb
) 对其自身的循环引用。
CInvokeCreateProcessVerb::Launch()
从自我调用的方法内部更具体的错误
HRESULT ShellDDEExec::InitializeByShellInternal(
IAssociationElement*,
CreateProcessMethod,
PCWSTR,
STARTUPINFOEXW*,
IShellItem2*,
IUnknown*, // !!!
PCWSTR,
PCWSTR,
PCWSTR);
有错误的 6 个参数。CInvokeCreateProcessVerb
包含内部ShellDDEExec
子对象的类。在 Windows 1709中,将指向 6 个参数的CInvokeCreateProcessVerb::Launch()
指针传递到指向类实例的位置。但在 1803 版本中,这里传递了指向- 所以指向self的指针。将this 指针存储在 self 中并添加对它的引用。请注意,这是 的子对象。所以在不调用析构函数之前不会调用析构函数。但是在它的引用计数达到0之前不会调用析构函数。但这不会发生,直到不释放self指针,该指针只会在它的析构函数内部..static_cast<IServiceProvider*>(pObj)
ShellDDEExec::InitializeByShellInternal
pObj
CBindAndInvokeStaticVerb
static_cast<IServiceProvider*>(this)
InitializeByShellInternal
ShellDDEExec
CInvokeCreateProcessVerb
ShellDDEExec
CInvokeCreateProcessVerb
CInvokeCreateProcessVerb
ShellDDEExec
CInvokeCreateProcessVerb
这在伪代码中可能更明显
class ShellDDEExec
{
CComPtr<IUnknown*> _pUnk;
HRESULT InitializeByShellInternal(..IUnknown* pUnk..)
{
_pUnk = pUnk;
}
};
class CInvokeCreateProcessVerb : CExecuteCommandBase, IServiceProvider /**/
{
IServiceProvider* _pVerb;//point to static_cast<IServiceProvider*>(CBindAndInvokeStaticVerb*)
ShellDDEExec _exec;
TRYRESULT CInvokeCreateProcessVerb::Launch()
{
// in 1709
// _exec.InitializeByShellInternal(_pVerb);
// in 1803
_exec.InitializeByShellInternal(..static_cast<IServiceProvider*>(this)..); // !! error !!
}
};
ShellDDEExec::_pUnk
持有指向包含对象的指针,CInvokeCreateProcessVerb
这个指针只会在CComPtr
析构函数内部被释放,从ShellDDEExec
析构函数调用。从CInvokeCreateProcessVerb
析构函数调用,当引用计数变为 0 时调用,但这永远不会发生,因为额外的引用保持ShellDDEExec::_pUnk
所以对象存储引用了指向自身的指针。在此引用计数CInvokeCreateProcessVerb
永远不会达到 0之后
推荐阅读
- r - 在导航栏页面右侧添加操作按钮
- git - 直接克隆远程仓库和先分叉远程仓库的区别
- keycloak - KeyCloak 中多个客户端的唯一登录
- machine-learning - 如何将单独创建的编码器和解码器连接在一起?
- firefox-addon - 如何为 Firefox 插件提供类似 Firefox Devtools 检查器的功能?
- xslt - 如何摆脱 XML 注释中的二进制字符?
- r - 在内部将 rscript 编织为 pdf
- c++ - std::byte 可以替换 std::aligned_storage 吗?
- c# - 如何编写具有相同方法和继承的 2 个类?
- python - 在 ibm-cloud 部署中找不到 xmlsec1