如何使用 ShellExecuteEx 避免内存泄漏?

man*_*ell 6 winapi memory-leaks visual-c++

最小、完整和可验证的示例:

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
Run Code Online (Sandbox Code Playgroud)

该代码使用ShellExecuteEx循环启动具有about:blankURL 的64 个 Internet Explorer 实例。该SEE_MASK_NOCLOSEPROCESS用于能够再使用了TerminateProcess API。

我注意到两种泄漏:

  1. 处理泄漏:当循环完成但程序仍在运行时启动进程资源管理器,我看到几个 64 句柄块(进程句柄和各种键的注册表句柄)
  2. 内存泄漏:将 Visual C++ 2017 调试器附加到程序中,在循环之前,我拍摄了第一个堆快照,然后在循环之后拍摄了第二个。我看到 64 个 8192 字节的块,来自 windows.storage.dll!CInvokeCreateProcessVerb::_BuildEnvironmentForNewProcess()

您可以在此处阅读有关句柄泄漏的一些信息:ShellExecute 泄漏句柄

下面是一些截图: 首先,PID 启动和终止: PID 启动和终止

第二:相同的 pid,如 Process Explorer 中所示: 进程句柄

Process Explorer 还显示 64*3 打开的注册表句柄,用于HKCR\.exeHKCR\exefileHKCR\exefile\shell\open

注册表处理泄漏

64 个泄露的“环境”之一(8192 字节和调用堆栈): Visual Studio 2017 堆快照

最后:Process Explorer 的屏幕截图,显示了在执行 MCVE 期间使用 1024 循环计数器修改的“私有字节”。运行时间约为 36 分钟,PV 从 1.1 Mo(CoInitializeEx 之前)开始,并在 19 Mo(CoUninitialize 之后)结束。该值随后稳定在 18.9 进程资源管理器私有字节 (1024 ShellExecuteEx

我究竟做错了什么?我是否看到没有泄漏的地方?

RbM*_*bMm 3

这是版本 1803 中的 Windows 错误。重现的最少代码:

if (0 <= CoInitialize(0))
{
    SHELLEXECUTEINFO sei = {
        sizeof(sei), 0, 0, 0, L"notepad.exe", 0, 0, SW_SHOW
    };

    ShellExecuteEx( &sei );

    CoUninitialize();
}
Run Code Online (Sandbox Code Playgroud)

执行此代码后,可以查看 notepad.exe 进程和第一个线程的句柄 - 该句柄当然不能存在(被关闭),而不是关闭的键

\REGISTRY\MACHINE\SOFTWARE\Classes\.exe
\REGISTRY\MACHINE\SOFTWARE\Classes\exefile
Run Code Online (Sandbox Code Playgroud)

此调用后进程中还存在私有内存泄漏。

当然,这个错误会导致explorer.exe和任何使用它的进程永久资源泄漏ShellExecute[Ex]

这个错误的确切研究 -这里

这里的根本问题似乎是在windows.storage.dll中。特别是,该CInvokeCreateProcessVerb对象永远不会被销毁,因为关联的引用计数永远不会达到 0。这会泄漏与 关联的所有对象 CInvokeCreateProcessVerb,包括 4 个句柄和一些内存。

ShellDDEExec::InitializeByShellInternal 引用计数从未达到 0 的原因似乎与由 执行的从 Windows 10 1709 到 1803 的 参数更改有关CInvokeCreateProcessVerb::Launch()

更具体地说,我们有一个对象(CInvokeCreateProcessVerb)对其自身的循环引用。

CInvokeCreateProcessVerb::Launch()从自身调用的方法内部出现更具体的错误

HRESULT ShellDDEExec::InitializeByShellInternal(
    IAssociationElement*, 
    CreateProcessMethod,
    PCWSTR,
    STARTUPINFOEXW*, 
    IShellItem2*, 
    IUnknown*, // !!!
    PCWSTR, 
    PCWSTR, 
    PCWSTR);
Run Code Online (Sandbox Code Playgroud)

6 个参数错误。CInvokeCreateProcessVerb包含内部ShellDDEExec子对象的类。在 Windows 1709 中,CInvokeCreateProcessVerb::Launch()将指针传递到static_cast<IServiceProvider*>(pObj)第 6 个参数,其中ShellDDEExec::InitializeByShellInternal指向pObj类的实例CBindAndInvokeStaticVerb。但在 1803 版本中,这里将指针传递给static_cast<IServiceProvider*>(this)-so 指向self的指针。将InitializeByShellInternal这个指针存储在 self 中并添加对其的引用。请注意,这ShellDDEExec是的子对象CInvokeCreateProcessVerb。所以析构函数ShellDDEExec不会被调用,直到析构函数CInvokeCreateProcessVerb不被调用。但CInvokeCreateProcessVerb在引用计数达到 0 之前,不会调用 of 的析构函数。但是,直到ShellDDEExec不释放 self 指针时,才会发生这种情况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 !!
    }
};
Run Code Online (Sandbox Code Playgroud)

ShellDDEExec::_pUnk持有指向包含对象的指针,CInvokeCreateProcessVerb该指针仅在析构函数内释放CComPtr,从ShellDDEExec析构函数调用。打电话自CInvokeCreateProcessVerb析构函数调用,当引用计数变为 0 时调用,但这永远不会发生,因为额外的引用保持ShellDDEExec::_pUnk

所以对象存储引用指向 self 的指针。在此之后引用计数CInvokeCreateProcessVerb永远不会达到 0