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

时间:2018-12-03 09:10:18

标签: winapi visual-c++ memory-leaks

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

Visual Studio 2017专业版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的Internet Explorer的64个实例。 SEE_MASK_NOCLOSEPROCESS用于随后可以使用TerminateProcess API。

我注意到两种泄漏:

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

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

以下是一些屏幕截图: 首先,PID启动并终止: PID Launched and terminated

第二个:相同的pid,如Process Explorer中所示: Process Handles

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

Registry Handles leaks

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

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

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

1 个答案:

答案 0 :(得分:2)

这是Windows错误,版本1803。用于复制的最少代码:

declare @StartYear int  = '2014';
declare @EndYear int = '2018';

select count(i.InvoiceKey) as [Number of Payments made on Invoice]
       -- this will never by null because of the WHERE clause
      ,year(p.TransPostDate) as PaymentYear
       -- I assume multiple payments to one invoice, so don't want to sum here.
       -- All values are the same, just take the MAX. MIN would work too.
      ,max(i.Amt) as [Invoice Amount]
      ,sum(p.Amt) as [Total Payment Made]
       -- count of partial payments
      ,count(case when p.Amt < i.Amt then i.InvoiceKey end) [Number of partial payments made]
       -- sum of partial payments
      ,sum(case when p.Amt < i.Amt then p.Amt end) [Sum of partial payments made]
from.[Invoices] i
inner join [Payments] p
on i.InvoiceKey = p.InvoiceKey
where i.ClientKey = '518'
      and p.InvClientKey = '518'
      and i.CloseDate is null
      -- if this is not true, then by definition p.TransPostDate is not null so
      -- you don't need to specify "p.TransPostDate IS NOT NULL".
      -- There is no need to multiply by 100
      and year(p.TransPostDate) between @StartYear and @EndYear
group by i.InvoiceKey
        ,year(p.TransPostDate)
order by year(p.TransPostDate) asc;

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

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

    ShellExecuteEx( &sei );

    CoUninitialize();
}

此调用之后,进程中也存在私有内存泄漏。

当然,此错误会导致 explorer.exe 和使用\REGISTRY\MACHINE\SOFTWARE\Classes\.exe \REGISTRY\MACHINE\SOFTWARE\Classes\exefile

的任何进程永久资源泄漏

对此错误进行了精确的研究-here

  

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

     

引用计数从未达到0的原因似乎与之相关   CInvokeCreateProcessVerb的参数更改   从Windows 10 1709到1803,由   ShellDDEExec::InitializeByShellInternal

在这里更具体,我们有一个对象(CInvokeCreateProcessVerb::Launch())对其自身的循环引用。

方法CInvokeCreateProcessVerb内部有更具体的错误,该错误是从自身调用

CInvokeCreateProcessVerb::Launch()

参数错误6。包含内部HRESULT ShellDDEExec::InitializeByShellInternal( IAssociationElement*, CreateProcessMethod, PCWSTR, STARTUPINFOEXW*, IShellItem2*, IUnknown*, // !!! PCWSTR, PCWSTR, PCWSTR); 子对象的CInvokeCreateProcessVerb类。在Windows 1709中,ShellDDEExec将指向CInvokeCreateProcessVerb::Launch()的指针传递给static_cast<IServiceProvider*>(pObj)的第6个参数,其中ShellDDEExec::InitializeByShellInternal指向pObj类的实例。但在1803版中,此处传递了指向CBindAndInvokeStaticVerb的指针-因此指向 self 的指针。 static_cast<IServiceProvider*>(this)将此指针存储在self内并添加对其的引用。请注意,InitializeByShellInternalShellDDEExec子对象。因此在不调用CInvokeCreateProcessVerb的析构函数之前,不会调用ShellDDEExec的析构函数。但是CInvokeCreateProcessVerb的析构函数将在其引用计数达到0之前不被调用。但是直到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析构函数调用,在引用计数变为0时调用,但这从未发生,因为额外的引用保留CInvokeCreateProcessVerb

因此对象存储引用了指向self的指针。在ShellDDEExec::_pUnk的引用计数之后永远不会达到0