Введение
Просматривая ленту, наткнулся на статью Неправильное использование атомов и трудноуловимая бага в VCL. После прочтения возникла мысль описать еще одну проблему в той же самой области, о которой не рассказано в этой статье. Наша команда натолкнулась на нее самостоятельно, потом оказалось, что это уже известный баг VCL. Разработка ведется на Delphi 7, и я не уверен, существует ли ошибка в более новых версиях. Судя по приведенным чуть ниже ссылкам, есть, как есть и исправление. На исправление версии 7 надеяться, по понятным причинам, не приходится.
В статье MrShoor описывается переполнение т.н. таблицы атомов в случае, если приложение на Delphi завершено некорректно, и некоторые атомы не удаляются. Оказывается, для переполнения таблицы атомов вовсе необязательно «убивать насильно» ваше приложение. Вполне достаточно его запустить и корректно закрыть, но много-много раз подряд.
Давайте посмотрим, как это происходит:
Описание механизма переполнения
После получения очередной жалобы а-ля «out of memory», мы обнаружили, что таблица атомов забита элементами вида ControlOfs<шестнадцатеричный ID>. Эти элементы появляются при запуске каждого приложения (а наш сервер приложений запускает экземпляры по одному для каждого содинения), и остаются в таблице навсегда.
Рассмотрим еще раз участок кода из InitControls в Controls.pas — тот же самый, что и в упомянутой выше статье:
WindowAtomString := Format('Delphi%.8X',[GetCurrentProcessID]);
WindowAtom := GlobalAddAtom(PChar(WindowAtomStrinjg));
ControlAtomString := Format('ControlOfs%.8X%.8X', [HInstance, GetCurrentThreadID]);
ControlAtom := GlobalAddAtom(PChar(ControlAtomString));
RM_GetObjectInstance := RegisterWindowMessage(PChar(ControlAtomString));
Самая последня строка, как казалось, тоже создает атом. При регистрации нового типа сообщения также в таблицу добавляется новый атом, который каждый раз имеет новое имя. Проблема в том, что RegisterWindowsMessage см. MSDN в принципе не предполагает обратного unregister действия, т.к. данная функция позволяет нескольким программам использовать некоторый Message ID совместно:
The RegisterWindowMessage function is typically used to register messages for communicating between two cooperating applications.
If two different applications register the same message string, the applications return the same message value. The message remains registered until the session ends.
Это уже серьезно, т.к. у нас нет никаких рычагов влияния на ситуацию. Закрыть такой атом сторонним приложением невозможно — программа, предложенная в статье, побудившей меня к написанию данного поста, здесь бессильна. Так как таблица атомов имеет корни в 16 битной эпохе, что налагает ограничение на размер таблицы, то этот досадный баг довольно быстро выводил серверную часть из строя, т.к. не было возможности запустить ни одно Delphi приложение без перезагрузки системы.
Вот пример того, как выглядит таблица атомов после 20 итераций «нормальный запуск-корректное завершение» для простой программы из 1 окна:
Красным выделены те атомы, что остались как раз после 20 запусков.
Эта ошибка была описана в нескольких местах, например:
Детальное описание ошибки
Более краткий bug report
Ну, и конечно же, Stack Overflow
Метод решения
Поскольку удалить атом пост-фактум нельзя, требуется не допустить его создания. Наша команда пошла по пути IAT хука, который перехватывает вызов к RegisterWindowMessageA и в случае, если регистрируется сообщение с именем вида ControlOfs<что-то там>, вместо него регистрируется любой другой идентификатор, который одинаков для всех приложений. Как оказалось, ему вовсе не обязательно было быть уникальным, на что также указывается в баг репорте, на который я уже ссылался.
Код хука тривиален, как и механизм установки его. Более того, в интернете есть готовые библиотеки для Delphi для хуков на IAT. Сам хук всего навсего проверяет максимально быстрым способом, не соответствует ли регистрируемое сообщение префиксу ControlOfs, и, в случае, если соответствует, происходит подмена на RM_GetObjectInstance — подобный идентификатор, одинаковый для всех приложений, с вызовом оригинальной RegisterWindowMessage.
Надеюсь, кому-то это поможет избежать долгой и трудной отладки.