首页 > 解决方案 > 如何使用 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:blankURL 启动 64 个 Internet Explorer 实例。SEE_MASK_NOCLOSEPROCESS用于随后能够使用TerminateProcess API

我注意到两种泄漏:

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

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

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

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

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

注册表处理泄漏

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

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

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

标签: winapivisual-c++memory-leaks

解决方案


这是版本 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::InitializeByShellInternalpObjCBindAndInvokeStaticVerbstatic_cast<IServiceProvider*>(this)InitializeByShellInternalShellDDEExecCInvokeCreateProcessVerbShellDDEExecCInvokeCreateProcessVerbCInvokeCreateProcessVerbShellDDEExecCInvokeCreateProcessVerb

这在伪代码中可能更明显

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之后


推荐阅读