Опасная функция ExitProcess.

July 15th, 2007

Как вы думаете, какая их двух функций опаснее: ExitProcess или TerminateProcess? Ответ, конечно, зависит от определения того, что считать более безопасным. Однако если задать этот вопрос нескольким людям, большинство автоматически укажет на TerminateProcess. Почему? Потому что TerminateProcess в отличие от ExitProcess не делает попыток освободить занятые ресурсы. К примеру, программа не сможет записать несохранённые данные на диск, тем самым нарушив их целостность.

В реальности функция ExitProcess ни чем не безопаснее, чем TerminateProcess, а может и ещё хуже, так как внушает чувство ложной безопасности. Рассмотрим алгоритм работы ExitProcess. Помимо закрытия всех открытых описателей (handles) объектов и установки состояния объектов завершаемого процесса и принадлежащих ему потоков в состояние «signaled», ExitProcess делает две важные вещи:

  • Завершает все потоки процесса, кроме потока, вызвавшего ExitProcess;
  • Оповещает все загруженные DLL, вызывая DllMain c кодом DLL_PROCESS_DETACH.

В случае если один из потоков выполнял код, защищённый критической секцией, в момент своего завершения, то критическая секция может остаться захваченной навсегда. Если один из обработчиков DLL_PROCESS_DETACH попытается захватить эту секцию, то он будет заблокирован на ней навсегда. (Это пример из статьи в MSDN, посвященной ExitProcess).

Подобная ситуация возникает чаще, чем может показаться. Фактически, любые глобальные данные (всяческие списки, таблицы и т.п.), используемые из нескольких потоков, защищаются критическими секциями или чем-то подобным. Даже если вы уверены, что ваше приложение не использует критические секции во время завершения программы, остаются системные функции, которые также могут быть источником этой проблемы.

И наконец в общем случае вы не можете быть уверены какие DLL загружены в ваш процесс, так как:

  1. Набор библиотек может отличаться на разных версиях операционной системы;
  2. Набор библиотек может отличаться в зависимости он версий сторонних библиотек, которые использует ваше приложение;
  3. Другие приложения могут внедрять свой код в ваш процесс, например с помощью SetWindowsHookEx или CreateRemoteThread. В этом случае, кстати, вероятность того, что внедряемая DLL синхронизирует доступ к глобальным данным с помощью критических секций стремится к 100%.

Т.е. это означает, что вызывая ExitProcess, вы выполняете заранее неизвестный код, обладающий неизвестным поведением.

Единственный способ сделать вызов ExitProcess безопасным – подготовиться к вызову этой функции должным образом: корректно завершить все потоки и вызвать ExitProcess из единственного оставшегося потока. Если в процессе подготовки что-то пошло не так, то имеет смысл вызвать TerminateProcess вместо ExitProcess, так как хуже все равно не будет – состояние программы уже не определено. Следует также помнить, что CRT функции exit, _exit и прочие, а также выход из функции main в конце концов приводят в вызову ExitProcess. Т.е. они подвержены тем же самым проблемам, что и ExitProcess.

,

  1. zhmur
    July 16th, 2007 at 00:46 | #1

    Не догнал что имеется ввиду под критической секцией. Если EnterCriticalSection, то это user mode синхропримитив и должен корректно отваливаться при завершении процесса. Или имеется ввиду CreateMutex? Тогда это кернел косяк однако, обработать синхропримитивы зависшие на умерших процессах не должно быть так сложно.

  2. July 16th, 2007 at 01:55 | #2

    Я вот только одного не понимаю: а как тогда не рушатся приложения, которые завершили поток функции main, но у которых продолжают работать порожденные потоки? Ситуация, возможно, не очень типичная, но я вроде никаких глюков в таких случаях не встречал….

  3. July 16th, 2007 at 03:33 | #3

    Идеально конечно же через PostThreadMessage нотифицировать потокам о завершении, ждать пока хендлы потоков засигналятся и только тогда завершать процесс.

  4. July 16th, 2007 at 08:16 | #4

     

    Если EnterCriticalSection, то это user mode синхропримитив и должен корректно отваливаться при завершении процесса. Или имеется ввиду CreateMutex?

    Я имею ввиду CriticalSection и все остальные примитивы кроме Mutex.

    void access_global_data()
    {
        // 1. thread_a захватывает секцию cs
        // 4. thread_b пытается захватить секцию cs, которую никогда
        //    не освободит thread_a
        EnterCriticalSection(&cs);
    
        // thread_b вызывает ExitProcess
        ...
        LeaveCriticalSection(&cs);
    }
    
    BOOL WINAPI DllMain(
        HINSTANCE instance,
        DWORD reason,
        LPVOID reserved
        )
    {
        switch (reason)
        {
        case DLL_PROCESS_DETACH:
            // 3. ExitProcess уведомляет о завершении процесса каждую из
            //    загруженных библиотек DllMain(DLL_PROCESS_DETACH)
            access_global_data();
            break;
        }
    
        return TRUE;
    }
    
    void thread_a()
    {
        access_global_data();
    }
    
    void thread_b()
    {
        // 2. thread_b пытается завершить процесс. ExitProcess убивает thread_a.
        ExitProcess(0);
    }
  5. July 16th, 2007 at 08:37 | #5

     

    Я вот только одного не понимаю: а как тогда не рушатся приложения, которые завершили поток функции main, но у которых продолжают работать порожденные потоки? Ситуация, возможно, не очень типичная, но я вроде никаких глюков в таких случаях не встречал….

    Они, видимо, не пользуются стандартной C runtime library. Или завершают основной поток минуя runtime library.

  6. July 16th, 2007 at 08:55 | #6

    >>Они, видимо, не пользуются стандартной C runtime library. Или завершают основной поток минуя runtime library.

  7. July 16th, 2007 at 10:19 | #7

    Может я чего-то не понимаю, но мне кажется в первом предложении ошибка, должно быть “какая из 2-х функций опаснее”…

  8. July 16th, 2007 at 10:31 | #8

     

    Может я чего-то не понимаю, но мне кажется в первом предложении ошибка, должно быть “какая из 2-х функций опаснее”…

    Точно. Поправил.

  9. July 16th, 2007 at 15:40 | #9

     

    Если EnterCriticalSection, то это user mode синхропримитив и должен корректно отваливаться при завершении процесса. Или имеется ввиду CreateMutex?

    Я имею ввиду CriticalSection и все остальные примитивы кроме Mutex.

    Хе-хе. Всё оказалось даже интереснее. Специально для этого случая в EnterCriticalSection добавили проверку. Расскажу об этом подробнее отдельным постом.

  10. Konstantin
    July 17th, 2007 at 09:49 | #10

    http://blogs.msdn.com/oldnewthing/archive/2007/05/03/2383346.aspx

    Тут вроде товарищ Чен написал, что после вызова ExitProcess все вызовы EnterCriticalSection срабатывают.

  1. July 15th, 2007 at 22:59 | #1
  2. July 18th, 2007 at 23:21 | #2