DLL должна освобождать кучу памяти, только если DLL выгружается динамически? - PullRequest
0 голосов
/ 17 декабря 2018

Вопрос Цель: проверка реальности на MS документах DllMain.

Это "общепринятое" знание, что вы не должны делать слишком много в DllMain, есть определенные вещивы никогда не должны делать, некоторые лучшие практики .

Теперь я наткнулся на новый драгоценный камень в документах, что для меня мало смысла: (emph. mine)

При обработке DLL_PROCESS_DETACH DLL должна освобождать ресурсы, такие как куча памяти, только если DLL выгружается динамически (параметр lpReserved равен NULL).Если процесс завершается (параметр lpvReserved не равен NULL), все потоки в процессе , за исключением текущего потока, либо уже вышли, либо были явно прерваны путем вызоваExitProcess функция, которая может оставить некоторые ресурсы процесса, такие как в виде кучи , в несогласованном состоянии .В этом случае для DLL небезопасно очищать ресурсы.Вместо этого DLL должна позволять операционной системе освобождать память.

Поскольку глобальные объекты C ++ очищаются во время DllMain / DETACH, это подразумевает, что глобальные объекты C ++ не должны освобождать динамическую память, посколькукуча может быть в несовместимом состоянии./ Когда DLL "статически" связана с исполняемым файлом./ Конечно, не то, что я вижу там - глобальные объекты C ++ (если они есть) из различных (наших и сторонних) библиотек прекрасно распределяют и освобождают в своих деструкторах.(За исключением других ошибок в заказе, oc)

Итак, на какую конкретную техническую проблему направлено это предупреждение?

Поскольку в параграфе упоминается завершение потока, может быть кучапроблема коррупции, когда некоторые потоки не очищены правильно?

Ответы [ 2 ]

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

В качестве дополнения к отличному ответу RbMm, я добавлю цитату из ExitProcess, которая делает намного лучшую работу - чем документы DllMain - при объяснении, почему операция кучи (или любая операция, действительно) может быть скомпрометирована:

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

Итак, все сводится к следующему: если в вашем приложении есть «убегающие» потоки, которые могут содержать Любая блокировка , ярким примером которой является блокировка кучи (CRT), во время выключения возникает большая проблема, когда вам нужно получить доступ к тем же структурам (например, куче), которые используют ваши «убегающие» потоки.

Это говорит о том, что вы должны все контролировать ваши потоки контролируемым образом.

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

API ExitProcess в целом выполняет следующие действия:

  • Вход в критическую секцию блокировки загрузчика
  • блокировка куча основного процесса (возвращается GetProcessHeap()) через HeapLock(GetProcessHeap()) (хорошо, конечно, через RtlLockHeap) (это очень важный шаг для избежания тупика)
  • затем завершить все потоки в процессе, кроме текущего (по вызову NtTerminateProcess(0, 0))
  • , затем вызвать LdrShutdownProcess - внутри этого загрузчика API пройти по списку загруженных модулей и отправить DLL_PROCESS_DETACH с lpvReserved ненулевым.
  • наконец, вызовите NtTerminateProcess(NtCurrentProcess(), ExitCode ), который завершает процесс.

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

Обратите внимание, что это не влияет на кучу основного процесса, потому что мы вызываем HeapLock для него до того, как завершит все потоки (кроме текущего).Цель этого: мы ждем в этом вызове, пока все другие потоки не выйдут из критической секции кучи процессов, и после того, как мы получим критическую секцию, никакие другие потоки не смогут войти в нее - потому что куча основного процесса заблокирована.

Итак, когда мы завершаем потоки после блокировки основной кучи - мы можем быть уверены, что никакие другие уничтоженные потоки не находятся внутри критической секции главной кучи или структуры кучи в несогласованном состоянии.Благодаря RtlLockHeap звоните.Но это связано только с основной кучей процессов.Любые другие кучи в процессе не заблокированы.Таким образом, эти могут находиться в несогласованном состоянии в течение DLL_PROCESS_DETACH или могут быть получены исключительно уже завершенным потоком.

Итак, использование HeapFree для GetProcessHeap или высказывание LocalFree здесь безопасно (однако не задокументировано).

Использование HeapFree для любых других куч является небезопасным , если во время завершения процесса вызывается DllMain.

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

Так что это примечание является предупреждениемчто когда параметр lpvReserved равен не-NULL (что означает DllMain вызывается во время завершения процесса), вы должны быть особенно осторожны при очистке ресурсов.В любом случае, после завершения процесса все выделения внутренней памяти будут свободны операционной системой.

...