Как избежать утечек памяти при использовании ShellExecuteEx? - PullRequest
0 голосов
/ 03 декабря 2018

Пример минимального, полного и проверяемого:

Visual Studio 2017 Pro 15.9.3 Windows 10 "1803" (17134.441) x64 Переменная среды OANOCACHE установлена ​​в 1. Данные / Снимки экрана показаны для 32 битUnicode build.

ОБНОВЛЕНИЕ: точно такое же поведение на другом компьютере с Windows 10 "1803" (17134.407) ОБНОВЛЕНИЕ: Ноль утечки на старом ноутбуке с Windows Seven ОБНОВЛЕНИЕ: точно такое же поведение (утечки) на другом компьютере с 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 для запуска в цикле 64 экземпляров Internet Explorer с URL-адресом about:blank.SEE_MASK_NOCLOSEPROCESS используется для возможности использования API TerminateProcess .

Я замечаю два вида утечек:

  1. Обрабатывает утечки: запуск Process Explorerкогда цикл завершен, но программа все еще работает, я вижу несколько блоков из 64 дескрипторов (дескрипторы процессов и дескрипторы реестров для различных ключей)
  2. Утечки памяти: присоединение к программе отладчика Visual C ++ 2017 доВ цикле я сделал первый снимок кучи и второй после цикла. Я вижу 64 блока по 8192 байта, начиная с windows.storage.dll!CInvokeCreateProcessVerb::_BuildEnvironmentForNewProcess()

Вы можете прочитать некоторую информацию об утечках дескрипторов здесь: ShellExecute обрабатывает утечки

Вот несколько снимков экрана: во-первых, PID запущен и завершен: PID Launched and terminated

Во-вторых: те же пиды, что ив Process Explorer: Process Handles

Process Explorer также отображает 64 * 3 открытых дескриптора реестра для HKCR\.exe, HKCR\exefile и HKCR\exefile\shell\open.

Registry Handles leaks

Один из 64 леaked "Environment" (8192 байта и стек вызовов): Visual Studio 2017 Heap Snapshot

Last: снимок экрана Process Explorer, показывающий "Private Bytes" во время выполнения MCVE, измененного с помощьюсчетчик 1024 петли.Время работы составляет приблизительно 36 минут, PV начинается в 1.1 Mo (до CoInitializeEx) и заканчивается в 19 Mo (после CoUninitialize).Затем значение стабилизируется на уровне 18,9 Process Explorer Private Bytes (1024 ShellExecuteEx

Что я делаю не так?Я вижу утечки там, где их нет?

1 Ответ

0 голосов
/ 11 декабря 2018

это ошибка windows, в версии 1803. минимальный код для воспроизведения:

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.в окнах 1709 CInvokeCreateProcessVerb::Launch() передайте указатель на static_cast<IServiceProvider*>(pObj) вместо аргумента вместо 6 на ShellDDEExec::InitializeByShellInternal, где pObj указывает на экземпляр класса CBindAndInvokeStaticVerb.но в версии 1803 здесь передается указатель на static_cast<IServiceProvider*>(this) - поэтому указатель на self .InitializeByShellInternal хранит этот указатель внутри себя и добавляет ссылку на него.обратите внимание, что ShellDDEExec является подобъектом из CInvokeCreateProcessVerb.поэтому деструктор ShellDDEExec не будет вызван, пока деструктор CInvokeCreateProcessVerb не будет вызван.но деструктор CInvokeCreateProcessVerb не будет вызываться до тех пор, пока счетчик ссылок не достигнет 0. Но этого не произойдет, пока 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

...