Wer.dll скачать и исправить отсутствующую wer.dll ошибкy – Driversol

Приложение скрытно завершается – что делать?

Как должно быть уже понятно, это возможно в следующих случаях:

  • Приложение само явно вызвало TerminateProcess или ExitProcess (прямо или опосредованно – например, через Halt).
  • Приложение явно завершило все свои потоки (вышло из процедур потоков или же явно вызвало TerminateThread или ExitThread). Это не бывает в Delphi, поскольку компилятор Delphi вставляет неявный вызов Halt в конец главного потока (т.е. всегда вызывает ExitProcess в конце работы главного потока), но это может случиться, если внешний процесс уничтожит главный поток в вашей программе.
  • Какой-то внешний процесс закрыл или уничтожил или ваш процесс или все потоки в нём.
  • В вашей программе произошло необработанное (фатальное) исключение, но в Доктор Ватсон / WER отключен диалог об ошибках.
  • В вашей программе произошло необработанное (фатальное) исключение, в системе зарегистрирован сторонний посмертный отладчик с автоматическим запуском, который не показал сообщения об ошибке.
  • В вашей программе произошло необработанное (фатальное) исключение, которое настолько серьёзно, что система даже не смогла показать сообщение, а посмертный отладчик не зарегистрирован.

Что же в таком случае делать?

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

Примечание: в списке ниже ключ реестра Windows Error Reportingчто-то обозначает ключ HKCUSoftwareMicrosoftWindowsWindows Error Reportingчто-то, а при его отсутствии – HKLMSoftwareMicrosoftWindowsWindows Error Reportingчто-то, либо HKLMSoftwareWow6432NodeMicrosoftWindowsWindows Error Reportingчто-то (для 32-битных приложений на 64-битной машине).

Что происходит при сбое

Сбой в приложении как правило представляет собой исключение, которое может быть или программным (вызываемым явно через

raise

, т.е. происходящим синхронно) или аппаратным (прерыванием от процессора, т.е. происходящим асинхронно – например, ошибочная попытка доступа к памяти).

Когда происходит исключение, система ищет обработчики в таком порядке:

  1. Если к программе подключен отладчик, система передаёт управление ему. Отладчик решает: остановить ли программу, продолжить ли её выполнение, или же показать уведомление.
  2. Если отладчик не подключен, система передаёт управление на блок обработки пользовательского кода (try/finally и try/except). Как правило, такие обработчики либо обрабатывают исключения сами, реализуя логику отката, либо показывают сообщение об ошибке пользователю.
    В частности, в Delphi даже если вы явно не написали в своём коде блок try/except – всё равно RTL Delphi содержит такой блок, который ловит все исключения и показывает сообщение об ошибке (см. также раздел “Что Delphi делает не так” ниже):
  3. Если пользовательского блока нет, либо они закончились (обработчики могут быть вложены и выполняются по очереди – это называется SEH, Structured Exception Handling, структурированная обработка исключений), система передаёт управление глобальному обработчику необработанных исключений (unhandled exception handler).
    Тут надо заметить, что в Delphi приложениях это бывает крайне редко (см. раздел “Что Delphi делает не так” ниже).
  4. Если глобального обработчика нет – программа “вылетает”, т.е. система завершает процесс.
  5. Поскольку сообщение показывается из самого процесса, то в особо запущенных случаях процесс может быть завершён без показа сообщения.

(для простоты я опустил случай подключения отладчика ядра)

Что если я хочу использовать отчёты, но не хочу использовать wer?

Да, использовать WER для Delphi приложений довольно сложно:

  1. Достаточно сложно всё настроить и зарегистрироваться
  2. Требуется сертификат цифровой подписи
  3. Отчёты доставляются с задержкой, а сам сайт sysdev работает неторопливо
  4. Нужно явно регистрировать каждую публикуемую сборку приложения
  5. Отчёты с Windows XP (и ранее) практически бесполезны, поскольку не включают в себя дамп и не позволяют приложить пользовательские файлы
  6. Невозможно использовать отладчик Delphi или Visual Studio для анализа дампов, будет работать только WinDbg
  7. Невозможно анализировать дампы 64-разрядных процессов
  8. В случае неконтролируемого вылета (т.е. когда WER вызывает система, а не мы сами), пользовательских данных у отчёта не будет


Поэтому вы можете захотеть использовать свой собственный механизм отправки и сбора отчётов. Для этого вам потребуется две вещи:

  1. Централизованный сервер для сбора отчётов, включая их сортировку/группировку и возможности просмотра
  2. Код отправки вылета приложения на сервер

В качестве сервера я крайне рекомендую

или

– это единственные известные мне системы отслеживания ошибок (среди прочих: Mantis, JIRA, Bugzilla, Redmine, YouTrack), которые были созданы с прицелом на автоматический сбор отчётов. Только FogBugz и Exceptionless поддерживают понятие bug hash / bugID / alias с автоматической группировкой, учётом (count / occurencies) и контролем сбора.

Во всех остальных трекерах отсутствует часть возможностей. Будете делать отправку отчётов через REST API FogBugz – ищите по ключевым словам BugzScout и sScoutDescription. У Exceptionless API ещё проще, но слабо документирован – проще посмотреть их примеры и перехватить трафик.

К счастью, мы уже рассмотрели все модификации кода, которые вам нужно сделать в вашем приложении выше – теперь вам осталось только поместить ваш код отправки отчёта вместо ReportFault / WerSubmitReport в функцию UnhandledExceptionHandler выше.

Что delphi делает не так

Концепция обработки ошибок в Delphi была создана в Delphi 1 и существенно не менялась с тех пор. Напомню, что в те времена не было возможности подключения посмертного отладчика, сбора информации о сбое и автоматической отправки отчёта. Поэтому если приложение вылетало – оно вылетало с концами.

Поэтому Delphi исторически реализует концепцию “ни за что не вылетать”. Делает это она путём расстановки блоков try/except везде, где только возможно. В частности, в оконных приложениях VCL обработка каждого сообщения заключена в явный блок try/except с вызовом Application.HandleException для обработки каждого необработанного (unhandled) исключения:

procedure TWinControl.MainWndProc(var Message: TMessage);
begin
  try
    try
      WindowProc(Message);
    finally
      FreeDeviceContexts;
      FreeMemoryContexts;
    end;
  except
    Application.HandleException(Self);
  end;
end;

Application.HandleException

просто показывает сообщение (.Message) исключения (в новых версиях Delphi – вместе с вложенными):


После чего приложение продолжает обычную работу (цикл выборки сообщений).

Если VCL (или аналогичный фреймворк) не используется, либо если исключение происходит вне цикла выборки сообщений, то управление получает глобальный обработчик исключений RTL System._ExceptionHandler, который вызывает пользовательский обработчик из System.ExceptProc:

var
  ExceptProc: procedure(ExceptObject: TObject; ExceptAddr: Pointer); // Unhandled exception handler

В 99% случаев этот обработчик установлен в

SysUtils.ExceptHandler

, который показывает сообщение через

SysUtils.ShowException

, а затем завершает работу приложения с кодом возврата равным 1:

procedure ExceptHandler(ExceptObject: TObject; ExceptAddr: Pointer);
begin
  ShowException(ExceptObject, ExceptAddr);
  Halt(1);
end;

SysUtils.ShowException

показывает упрощённо-техническое сообщение:


Если же модуль

SysUtils

не подключен, либо исключение возникло до инициализации модуля

SysUtils

, то

System.ExceptProc

будет не назначен (равен

nil

), так что

System._ExceptionHandler

попытается обработать сообщение самостоятельно. Поскольку объект исключения определяется в том же модуле

SysUtils

, модуль

System

не может оперировать объектом исключения. Вместо этого он завершит приложение с “кодом ошибки” 217 (необработанное исключение) – через

System._RunError

procedure _RunError(errorCode: Byte);
begin
  ErrorAddr := ReturnAddress;
  Halt(errorCode);
end;


Что приведёт к завершению приложения. При этом

System.Halt

увидит, что установлен код ошибки, поэтому он сообщит об этом:

procedure _Halt0;
begin
  // ...

  if ErrorAddr <> nil then
  begin
    MakeErrorMessage;
    WriteErrorMessage;
    ErrorAddr := nil;
  end;

  // ...
  ExitProcess(ExitCode);
end;

Примечание: код 217, на самом деле, означает закрытие консольного приложения через

CtrlBreak

. По совместительству он же используется для указания необработанного исключения. Также код может быть 230 – настоящий код для необработанных исключений, используется на не-Windows платформах. 204 – код для Invalid Pointer, вызывается менеджером памяти, если передать ему неверный указатель (например, указатель на уже удалённую память).

Если ваш код будет создавать дополнительные фоновые потоки через BeginThread (а равно и через любые обёртки к нему, например, TThread или многопоточный фреймворк), то RTL также оборачивает функцию потока в try/except блок с вызовом System.

_ExceptionHandler. А TThread и вовсе оборачивает .Execute в явный try/except блок с сохранением необработанного исключения в свойство .FatalException, таким образом полностью гася его обработку и оставляя её на ваше усмотрение.

Иными словами (почти) любой код Delphi оказывается обёрнут в обработчик RTL, все исключения обрабатываются либо вашим кодом, либо RTL, поэтому настоящих необработанных исключений в Delphi не бывает.

В те времена (Windows 3.x) это считалось несомненным плюсом – и таковым и преподносилось в рекламе Borland: “посмотрите, как надёжны наши приложения – они не вылетают”. А если вылетают – показывают что-то более удобоваримое, чем просто GPF.

В современном мире повсеместного распространения интернета это оказывается уже не так здорово, как казалось когда то. Если ваше приложение не вылетает – оно не вызывает WER. Не вызывает WER – не отправляет отчёт. Не отправляет отчёт – вы не получаете отчёт. Результат?

Вы или вообще не в курсе, что с вашим приложением что-то не так, либо получаете письмо от пользователя “программа не работает”. Разве не было бы лучше немедленно узнавать о вылетах вашего приложения? Получать чёткие отчёты вида “проблема в строке 42”? Сортировать отчёты по частоте возникновения, чтобы видеть самые “горячие” проблемы?

Поэтому указанное поведение Delphi – зло, от которого нужно избавляться. Мы посмотрим на это чуть позже.

Что я могу извлечь из отчёта wer?

Во-первых, вы можете просмотреть отчёт или его часть локально. Используйте “центр обслуживания” / “журнал стабильности работы” в новых версиях Windows или системный лог – в старых.

(Примечание: “Неправильный путь приложения” – это кривой перевод “Faulting Application Path”: “Путь к сбойнувшему приложению”.)

Альтернативно можно использовать сторонюю утилиту AppCrashView – она показывает существенно больше технических деталей.

Сохраняемая в отчёте информация зависит от версии Windows, настроек WER и способа вызова WER из приложения (вызвало ли WER приложение само, через API, или нет; и если через API – то какие параметры указало).

16-битные windows

В далёкие-далёкие времена не было никаких утилит диагностики. Если программа вылетала – она вылетала совсем. В тяжёлых случаях программа могла утянуть с собой всю систему.

К примеру, General Protection Fault (GPF, общее нарушение защиты, Unrecoverable Application Error, UAE) – это прерывание (fault), возникающее в случае попытки приложения получить доступ к не принадлежащей ему области памяти (сегодня известно как исключение Access Violation большинству разработчиков Delphi).

При получении этого сигнала от процессора операционная система останавливает выполнение приложения, сообщает пользователю и продолжает выполнение других приложений. Но если в процессе обработки GPF (в обработчике GPF) будет возбуждено ещё одно GPF, процессор отправит сигнал “повторный GPF” (double fault), останавливая уже операционную систему.

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

Первая программа диагностики появилась в бета-версии 16-битной Windows 3.0. Она была создана Доном Корбиттом (Don Corbitt), который раньше работал в Borland и был частью TeamB, но потом ушёл в Microsoft, где и написал Доктора Ватсона (Dr. Watson) – первую утилиту сбора информации о вылете приложения в Windows.

Доктор Ватсон собирал информацию о системе, сбое и состоянии программы (“симптомы”). Информация записывалась в отладочный лог-файл, который потом мог быть доставлен разработчикам программы для анализа.

:/>  Пропали значки с Рабочего стола Windows 10: как вернуть не отображающиеся ярлыки, 11 шагов

Конечно же, Доктор Ватсон очень понравился разработчикам программ. Мэтт Питрек (автор «Windows Internals» и «Undocumented Windows», тоже, кстати, работал в то время в Borland и тоже входил в TeamB) написал свою собственную версию, изначально называвшуюся “Доктор Франк” (“Dr. Frank”)

– в честь Франка Борленда, основателя Borland. Доктор Франк имел кучу дополнительных возможностей, которые делали его круче Доктора Ватсона. Borland-у понравилась идея утилиты и они включили Доктора Франка в состав Borland C 3.1 – к сожалению, переименовав его в WinSpector.

Компилятор Watcom C также стал поставляться со своим собственным аналогом Доктора Ватсона, называвшегося “Доктор Ватком” (Dr. Watcom).

Пример необработанного исключения в Delphi 1, которое было поймано WinSpector:

См. также:

32-битные windows

Вплоть до Windows 2000 Доктора Ватсона нужно было запускать вручную до запуска программы, в которой происходил вылет. Доктор Ватсон просто работал в фоне и мог собирать информацию о системе, которая потом сбрасывалась в текстовый отчёт.

В Windows 2000 был предусмотрен новый механизм. С помощью ключа реестра HKLMSOFTWAREMicrosoftWindows NTCurrentVersionAeDebug (HKLMSoftwareWow6432NodeMicrosoftWindows NTCurrentVersionAeDebug – для 32-битных программ в 64-битной системе) стало возможным указывать т.н. “посмертный отладчик” (postmortem debugger)

или JIT-отладчик (Just-In-Time – “как раз вовремя”) – для подключения к процессу при его вылете. Этот ключ реестра – документирован в MSDN и TechNet. Вы могли зарегистрировать Доктора Ватсона в качестве такого отладчика, вызвав:

drwtsn32.exe -i


В результате чего Доктор Ватсон регистрировал сам себя:

После этого при вылете приложения система читала ключ реестра, запускала Доктора Ватсона, он подключался к приостановленному процессу, собирал информацию:

При этом, если параметр

Auto

ключа реестра

AeDebug

был равен True (1), то зарегистрированный отладчик запускался сразу, иначе – система выводила обычное сообщение, но с одной дополнительной кнопкой: “Отмена” – для запуска отладчика. Да, вот так коряво была добавлена эта возможность в Windows. Никто не удосужился даже сделать подходящий диалог.

Примечание: строго говоря, ключ реестра AeDebug был ещё в WinNT, а в линейке 9x его функциональность выполнял похожий раздел в Win.ini, тем не менее, ключ -i у Доктора Ватсона впервые появился именно в Windows 2000.

В любом случае, эту информацию можно было затем просмотреть и отправить разработчику программы:

Это был прообраз того, что затем стало службой Windows Error Reporting.

P.S. Разумеется, если у вас был установлен полноценный отладчик, то никто не мешал вам зарегистрировать этот отладчик как посмертный, вместо Доктора Ватсона – что, собственно говоря, и делают Delphi (и Visual Studio). Более того, если в системе зарегистрирован посмертный отладчик, то любое приложение может форсированно его вызвать, сделав вызов системной функции DebugBreak, которая состоит всего из одной ассемблерной команды:

См. также:

Windows vista и позднее

Неудивительно, что в Windows Vista служба отправки отчётов из Windows XP была снова обновлена. Она получила новое название Windows Error Reporting (WER) и представлена

wermgr.exeWER.dll

Для начала, теперь WER вообще не нужно регистрироваться в системе. По умолчанию ключ AeDebug / Debugger вообще не существует, WER вызывается по умолчанию. Кроме того, в системе теперь есть специальная служба WerSvc (“Windows Error Reporting Service” / “Служба регистрации ошибок Windows”), которая по умолчанию стоит на ручном (Manual) запуске и автоматически запускается системой при необходимости.

Бывший Доктор Ватсон теперь поддерживает настройку через групповые политики (“Административные шаблоны” / “Компоненты Windows” / “Windows Error Reporting”), включая возможность указания альтернативных серверов для сбора отчётов (корпоративные настройки) и локальных дампов.

API был расширен и теперь приложения могут вручную сообщать не только о вылетах, но и о почти любой feedback-информации, прикреплять к отчёту дополнительные данные и файлы.

Отчёты теперь включают в себя не только логи, но и минидампы – слепки памяти процесса, которые могут быть загружены в отладчик на машине разработчика для исследования вылета.

Самое главное нововведение – появление локального центра отслеживания проблем. Он называется “Отчёты о проблемах и их решения”, находится в Панели Управления:


В Windows 7 он был сгруппирован с “Обслуживанием” центра решений Windows (“Центр безопасности и обслуживания”):

В Windows Vista можно было изменить все те же опции, что и в Windows XP:

Но уже в Windows 7 набор опций был уменьшен:


А в дальнейшем – и вовсе исчез (к примеру, в Windows 10 вообще нет настроек).

Сами настройки (из реестра) никуда не делись, их всё ещё можно поменять. Просто был убран UI – хотя всегда есть вариант использовать групповые политики.

В реестре эти настройки находятся здесь:

и официально документированы

Windows xp


В Windows XP Доктор Ватсон был существенно расширен и вылизан. Кроме того, он сменил имя на “Problem Reports and Solutions” и представлен

dwwin.exe

(Microsoft Application Error Reporting) и

FaultRep.dll

Во-первых, в Windows XP Доктор Ватсон зарегистрирован по умолчанию в качестве посмертного отладчика, его не нужно регистрировать вручную (несмотря на то, что Доктор Ватсон зарегистрирован в AeDebug / Debugger как drwtsn32.exe, фактически drwtsn32.exe является переходником к dwwin.exe, который и выполняет всю работу).

Во-вторых, он может быть вызван из программы вручную – через функцию ReportFault.

В-третьих, он добавляет события о вылетах в системный журнал.

Наконец, в-четвёртых, он может быть сконфигурирован из апплета Система Панели Управления:

Как вы можете видеть, тут есть куча опций – выключить, включить частично, полностью, указать исключения или белый список. Достаточно широкий набор опций.

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

Примечание: однако, если вместо этого вы вручную удалите параметр Debugger ключа AeDebug (в котором и зарегистрирован Доктор Ватсон), то настройки, конечно, будут игнорироваться. Система покажет обычное окно о фатальном сбое в приложении:

Если же вы выключите отчёты, но включите опцию “уведомлять о критических ошибках”, то Доктор Ватсон будет показывать отчёты о вылетах приложений:

но не будет показывать отчёты, сгенерированные вручную (через

ReportFault

). События о вылетах также будут добавлены в системный лог:


Включение же отчётов покажет в диалоге новую опцию: “Отправить отчёт”.

при нажатии на которую собранный отчёт отправляется на серверы Microsoft:

а также добавит отдельное событие в системный лог:

Но почему отчёт отправляется Microsoft, а не разработчику программы? Дело в том, что вылет в модуле (exe или DLL) может не быть виной этого модуля. Быть может просто другой модуль неверно нас вызвал. Например, вылет проводника может быть из-за кривого расширения оболочки.

Вызов unhandledexceptionfilter

Далее, поведение программы зависит и от версии Delphi. Указанная выше (в предыдущем разделе) логика с безусловным вызовом

System.ExceptProcSystem._ExceptionHandler

справедлива лишь для старых версий Delphi. Относительно новые версии Delphi ведут себя так только при запуске под отладчиком. Если же программа запущена вне отладчика, то

System._ExceptionHandler

сначала вызовет системный

UnhandledExceptionFilter

– и вызовет

System.ExceptProc

только лишь если он вернул

EXCEPTION_EXECUTE_HANDLER

. Если же

UnhandledExceptionFilter

вернул

EXCEPTION_CONTINUE_SEARCH

, то

System._ExceptionHandler

не будет обрабатывать исключение и передаст его выше (т.е. исключение будет необработанным и его перехватит ОС, где в дело вступит WER). Если никто специально

UnhandledExceptionFilter

не назначал (Delphi его не назначает), то за его поведение отвечает WER, т.е. поведение зависит от ОС и настроек WER. К примеру, обработчик может ничего не делать и вернуть

EXCEPTION_CONTINUE_SEARCH

– и тогда исключение будет поднято выше, и вы увидите только диалог WER, но не run-time error. Часто обработчик сам обработает исключение (покажет диалог WER) и вернёт

EXCEPTION_EXECUTE_HANDLER

. И тогда вы увидите и диалог WER, и диалог run-time error. Воистину странное сочетание для пользователя!

Замечу, что это поведение (консультация с UnhandledExceptionFilter из System._ExceptionHandler) есть только под Windows, только при запуске вне отладчика, только при обработке исключения модулем System (т.е. не влияет на TThread и VCL), и почти, но не во всех версиях Delphi (правда, появилось оно очень давно).

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

Двойное переполнение стека

Но даже если мы сведём обработчик к тривиальному:

procedure P;
begin
  P;
end;

procedure TForm1.Button1Click(Sender: TObject);
begin
  try
    P;
  except // <- тривиальный (пустой) обработчик
  end;
end;

который, очевидно, всегда будет выполняться успешно, т.к. не занимает места на стеке. Даже если мы возьмём наименьшую версию Delphi и Windows – всё равно приложение может вылететь.

Да, первое нажатие на кнопку возбудит исключение stack overflow, которое успешно будет обработано (пустым) обработчиком исключений. Но вспомните, что исключение stack overflow возбуждается только когда стек дорастает до защитной страницы, после доступа к которой защита снимается и возбуждается первое исключение (stack overflow).

Если затем стек растёт и далее – то никакой защитной страницы уже нет, запись в стек наткнётся на зарезервированную страницу без доступа. Иными словами, если нажать кнопку второй раз – исключения stack overflow уже не будет. Будет фатальное Access Violation, будет вызван WER.

Зачем это делать

Итак, как уже

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

Кроме того, вызов системного обработчика ошибок (будь это WER или что-то ещё) – необходимое условие в некоторых отдельных случаях. Например, при сертификации своей программы для Windows, для реакции системных сервисов перезапуска (Restart & Recovery, в т.ч. в службах).

Поэтому с концепцией Delphi “ни за что не вылетать” нужно срочно что-то делать.

Лирическое отступление для тех, кому вообще не нравится концепция отправки отчётов

Если по каким-либо причинам вы не хотите отправлять отчёты и хотите всегда показывать своё сообщение пользователю:

Примечание: а если вы принципиально не против идеи отчётов, но не хотите использовать WER, то – см. ниже.

Для всех же нормальных разработчиков – давайте посмотрим, что мы можем сделать.

Во-первых, в первом приближении очень хорошо сработала бы такая комбинация:

function WERPassThrough(const Exception: TExceptionPointers): Integer; stdcall;

  function IsUnhandledExceptionCode(const AExceptionCode: DWORD): Boolean;
  begin
    Result := (AExceptionCode = EXCEPTION_ACCESS_VIOLATION) or
              (AExceptionCode = EXCEPTION_PRIV_INSTRUCTION) or
              (AExceptionCode = EXCEPTION_ILLEGAL_INSTRUCTION) or
              (AExceptionCode = EXCEPTION_NONCONTINUABLE_EXCEPTION) or
              (AExceptionCode = EXCEPTION_DATATYPE_MISALIGNMENT) or
              (AExceptionCode = EXCEPTION_BREAKPOINT) or
              (AExceptionCode = EXCEPTION_SINGLE_STEP) or
              (AExceptionCode = EXCEPTION_INVALID_HANDLE);
  end;

const
  EXCEPTION_CONTINUE_SEARCH = 0;
  EXCEPTION_EXECUTE_HANDLER = 1;
begin
  if IsUnhandledExceptionCode(Exception.ExceptionRecord.ExceptionCode) then
    Result := EXCEPTION_CONTINUE_SEARCH
  else
    Result := EXCEPTION_EXECUTE_HANDLER;
end;

initialization
  JITEnable := 1;
  SetUnhandledExceptionFilter(@WERPassThrough);
end.

В данном случае мы вызываем WER для всех перечисленных кодов аппаратных исключений и RTL обработчики – для всего остального. Список, конечно, приведён как пример, и его нужно изменить под ваше приложение. Например, если вы делаете определение запуска виртуальной машины через выполнение секретной инструкции, то код

:/>  Файл CMD - Как открыть файл .cmd? [Шаг-за-шагом] |

EXCEPTION_PRIV_INSTRUCTION

(и, возможно,

EXCEPTION_ILLEGAL_INSTRUCTION

) нужно убрать. Если вы проверяете аргументы выражений, то коды числовых исключений (деление на ноль, переполнение и т.п.) хорошо бы добавить. В тривиальном случае можно также считать что любое аппаратное исключение нужно передать WER. В сложном случае – фильтровать не только аппаратные, но и программные исключения (установив

JITEnable

в 2).

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

К сожалению, в Delphi не предусмотрено никакой возможности отменить только “глобальные” блоки try/except, нет никакого аналога опции JITEnable. Вместо этого нам придётся назначить свой обработчик и вызывать WER вручную – через API.

Итак, для начала нам нужна функция, которую мы будем вызывать для необработанных исключений:

Как мне получать отчёты wer?

Для этого необходимо зарегистрировать в Microsoft свою программу (а точнее – каждую версию каждого исполняемого файла).

В первую очередь для этого вам потребуется сертификат для цифровой подписи – только так вы можете подтвердить, что вы именно тот человек, который собрал программу. Ранее для этого требовался только особый сертификат от одобренного Microsoft поставщика: VeriSign – стоимостью $500 в год.

К счастью, сегодня это не так. Вам подойдёт любой сертификат для цифровой подписи. Ключевые слова для поиска: code signing certificate или Microsoft Authenticode. Стоимость – что-то от $65 в год (при покупке на несколько лет) до $200 – в зависимости от поставщика.

Остались и сертификаты за $500, но они теперь уже EV: “Extended Validation”. Для них производится более тщательная проверка перед выдачей сертификата вас как компании. Такие подписи получают бонус от Smart-фильтров браузеров, а также требуются для подписи драйверов.

Подписывать свои программы – есть хорошо в любом случае, так что, если вы профессионально разрабатываете софт, то сертификат для подписи у вас и так уже есть.

Как только у вас появится сертификат, вам, возможно, придётся переконвертировать в подходящий формат. Это бывает редко, но иногда необходимо. Поскольку я понятия не имею, в каком формате у вас сертификат – идите в Google. Вам нужно сконвертировать в формат PFX.

Далее, вам потребуется Windows SDK. Не хочу давать прямую ссылку, они меняются. Поищите в Google по ключевым: download Windows SDK. Устанавливаете SDK. Вас там интересует только утилита SignTool.

Когда SDK поставили – можно подписывать файлы. Делать это нужно после каждой компиляции для релиза. Можете использовать для этого Post-Build Event в современных IDE, либо что-то вроде FinalBuilder.

Класс исключения

Хорошо, с аппаратными исключениями понятно – они отличаются кодом. Но как отличить одно исключение Delphi от другого? Ведь любое исключение, представленное объектом Delphi, в результате будет иметь один и тот же код $0EEDFADE? Ну, в ситуации если WER вызван системой или через API уровня WinXP (т.е. функцию

ReportFault

, как мы сделали это в примере выше) – никак. Вам придётся исследовать слепок (дамп) процесса. Если же вы вызываете WER вручную (аналогично нашему примеру выше) и используете API уровня Vista (

WerCreateReportWerSubmitReport

), то вы можете скопировать имя класса и/или сообщение в один из десяти произвольных строковых параметров отчёта (модифицированный код из примера выше):

Метод 3: устанавливаем/переустанавливаем пакет microsoft visual c redistributable package

Ошибку Wer.dll часто можно обнаружить, когда неправильно работает Microsoft Visual C Redistribtable Package. Следует проверить все обновления или переустановить ПО. Сперва воспользуйтесь поиском Windows Updates для поиска Microsoft Visual C Redistributable Package, чтобы обновить/удалить более раннюю версию на новую.

  • Нажимаем клавишу с лого Windows для выбора Панель управления. Здесь смотрим на категории и нажимаем Uninstall.
  • Проверяем версию Microsoft Visual C Redistributable и удаляем самую раннюю из них.
  • Повторяем процедуру удаления с остальными частями Microsoft Visual C Redistributable.
  • Также можно установить 3-ю версию редистрибутива 2022 года Visual C Redistribtable, воспользовавшись загрузочной ссылкой на официальном сайте Microsoft.
  • Как только загрузка установочного файла завершится, запускаем и устанавливаем его на ПК.
  • Перезагружаем ПК.

Данный метод не смог помочь? Тогда переходите к следующему.

Метод 5: сканируйте систему на вредоносные по и вирусы

System File Checker (SFC) является утилитой в операционной системе Windows, которая позволяет проводить сканирование системных файлов Windows и выявлять повреждения, а также с целью восстановить файлы системы. Данное руководство предоставляет информацию о том, как верно запускать System File Checker (SFC.exe) для сканирования системных файлов и восстановления недостающих/поврежденных системных файлов, к примеру, .DLL.

Когда файл Windows Resource Protection (WRP) имеет повреждения или попросту отсутствует, система Windows начинает вести себя неправильно и с проблемами. Часто определенные функции Windows перестают функционировать и компьютер выходит из строя. Опцию “sfc scannow” используют как один из специальных переключателей, которая доступна благодаря команды sfc, команды командной строки, которая используется на запуск System File Checker.

Для ее запуска сперва необходимо открыть командную строку, после чего ввести “командную строку” в поле “Поиск”. Теперь нажимаем правой кнопкой мыши на “Командная строка” и выбираем “Запуск от имени администратора”. Необходимо обязательно запускать командную строку, чтобы сделать сканирование SFC.

  • Запуск полного сканирования системы благодаря антивирусной программы. Не следует надеяться лишь на Windows Defender, а выбираем дополнительно проверенную антивирусную программу.
  • Как только обнаружится угроза, нужно переустановить программу, которая показывает уведомление о заражении. Лучше сразу переустановить программу.
  • Пробуем провести восстановление при запуске системы, но только тогда, когда вышеперечисленные методы не сработали.
  • Если ничего не помогает, тогда переустанавливаем ОС Windows.

В окне командной строки нужно ввести команду “sfc /scannow” и нажать Enter. System File Checker начнет свою работу, которая продлится не более 15 минут. Ждем, пока сканирование завершится, после чего перезагружаем ПК. Теперь ошибка “Программа не может запуститься из-за ошибки Wer.dll отсутствует на вашем компьютере не должна появляться.

Настройка посмертного отладчика

Как я уже описывал, у вас есть возможность зарегистрировать любую программу на ваш выбор в качестве посмертного (postmortem) или JIT (Just-In-Time) отладчика. Когда приложение вылетает, система добавит кнопку “Отладка” (“Debug”) в диалог фатального вылета.

Нажатие на эту кнопку запустит указанный вами отладчик, передав ему идентификатор процесса, в котором произошёл вылет. Отладчик сможет подключиться к процессу и исследовать его. Отладчик может быть классическим интерактивным – вроде отладчика Delphi. Или же это может быть автоматизированная утилита, которая просто соберёт информацию – вроде Доктора Ватсона.

Посмертный отладчик регистрируется в ключе реестра HKLMSOFTWAREMicrosoftWindows NTCurrentVersionAeDebug (или HKLMSOFTWAREWow6432NodeMicrosoftWindows NTCurrentVersionAeDebug – для 32-битных приложений в 64-битной системе).

Для регистрации отладчика вам нужно создать или изменить значение Debugger в ключе AeDebug (строкового типа). Строка должна содержать корректную командную строку. Иными словами, если имя/путь отладчика включает в себя пробелы, его нужно заключать в кавычки. Путь обязательно должен быть полным и абсолютным.

В командной строке нужно указать как минимум один параметр %ld – это шаблон, который будет заменён на PID процесса.

Опционально можно добавить ещё два параметра: второй %ld будет заменён на описатель (handle) события, который отладчик может взвести, чтобы возобновить выполнения процесса. При этом считается, что отладчик исправил проблему, и процесс может продолжить работу.

Если его не указывать, либо отладчик его не взведёт, то система будет ждать завершения процесса отладчика, после чего возобновит обычную работу WER, т.е. считается, что проблема не исправлена. В большинстве случаев это не очень полезная возможность, которой на практике обычно не пользуются.

Наконец, можно добавить третий параметр %p, который будет заменён на адрес записи JIT_DEBUG_INFO в целевом процессе. Отладчик (или вы, вручную) может прочитать оттуда дополнительную информацию.

type 
  _JIT_DEBUG_INFO = record
    dwSize: DWORD;
    dwProcessorArchitecture: DWORD;
    dwThreadID: DWORD;
    dwReserved0: DWORD;
    lpExceptionAddress: UInt64;
    lpExceptionRecord: UInt64;
    lpContextRecord: UInt64;
  end;
  JIT_DEBUG_INFO = _JIT_DEBUG_INFO;
  TJITDebugInfo = JIT_DEBUG_INFO;
  PJITDebugInfo = ^JIT_DEBUG_INFO;

Кроме того, вы можете создать строковый параметр

Auto

и установить его в ‘0’ или ‘1’. Несложно сообразить, что при Auto = 1 диалоговое окно не показывается, посмертный отладчик запускается сразу. При Auto = 0 (умолчание), соответственно, появляется обычное окно с дополнительной кнопкой “Отладка” (“Debug”).

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

Delphi при установке регистрирует себя в качестве посмертного отладчика. Но если потом устанавливается другая среда разработки, то она может перезаписать регистрацию посмертного отладчика. К сожалению, в Delphi нет никакого способа автоматически перерегистрировать посмертный отладчик, вам придётся сделать это вручную.

К примеру, для Delphi 5-7 установите Debugger в:

"C:путь-к-DelphiBinbordbg70.exe" -aeargs %ld %ld

только замените 70 (Delphi 7) на 50 (Delphi 5) или 60 (Delphi 6).

Для более новых версий Delphi используйте:

"C:путь-к-DelphiBinBDS.exe" /attach:%ld;%ld

В настоящее время параметр %p не принимает ни одна версия Delphi.

Указанный выше отладчик Delphi 5-7 (bordbg70.exe) является, по-сути, удалённым отладчиком (remote debugger) и поэтому зависит от соответствующей библиотеки bordbk (bordbk50.dll, bordbk60.dll, bordbk61.dll, bordbk70.dll), которую можно найти в папке C:Program FilesCommon FilesBorland SharedDebugger (да, даже на 64-битной системе используется C:Program Files, а не C:Program Files (x86)).

Если при запуске отладчика Delphi 5-7 вы получаете сообщение о невозможности загрузки библиотеки bordbk, то соответствующую библиотеку нужно зарегистрировать вызовом tregsvr (лежит в папке bin Delphi) или regsvr32 (два экземпляра лежат в папке C:WindowsSystem32 – под каждую разрядность; вызывать нужно, разумеется, 32-битный), например:

C:WindowsSysWOW64regsvr32.exe "C:Program FilesCommon FilesBorland SharedDebuggerbordbk70.dll"

Помимо Delphi посмертным отладчиком регистрируются и отладчики от Microsoft. Только в отличие от Delphi, многие из них имеют возможность автоматической перерегистрации. Например, отладчики WinDbg, CDB и NTSD могут быть зарегистрированы следующими командами:

windbg -I
cdb.exe -iae
ntsd.exe -iae

К сожалению, отладчик WinDbg при этом не регистрирует параметр для

JIT_DEBUG_INFO

, поэтому лучше всего отредактировать его регистрацию вручную:

"C:путь-к-windbgwindbg.exe" -p %ld -e %ld -c ".jdinfo 0x%p"


Отладчик Visual Studio не имеет возможности перерегистрации, его нужно регистрировать заново вручную. Для этого используется такая командная строка:

"C:WINDOWSsystem32vsjitdebugger.exe" -p %ld -e %ld

Утилита ProcDump от SysInternals также может быть зарегистрирована посмертным отладчиком с помощью такой команды:

procdump.exe -i

Утилита ProcDump создаёт дамп процесса. По умолчанию создаётся мини-дамп: списки процессов, потоков, модулей, описателей. Дамп может быть расширен указанием опций

:/>  Как удалить (восстановить) корзину на рабочем стола в Windows 10

-ma

или

-mp

. Подробнее – см. справку по параметрам ProcDump.

Если вы пользуетесь в основном Delphi, то вы регистрируете отладчик Delphi в качестве посмертного – и на этом всё. Если же вы используете несколько сред разработки, то, возможно, вы бы хотели переключаться между отладчиками. В этом случае вы можете написать свою утилиту-переходник и зарегистрировать её в качестве посмертного отладчика. Например:

"C:ProjectsProject1.exe" %ld %ld %p

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

CreateProcess

, передав ему отформатированные параметры командной строки (хотя бы так: “Args := StringReplace(StringReplace(StringReplace(Args, ‘%ld’, ParamStr(1), []), ‘%ld’, ParamStr(2), []), ‘%p’, ParamStr(3), []);”). Не забудьте наследовать описатель события (второй параметр

%ld

) в целевой процесс. Дождитесь завершения процесса отладчика и выходите.

Например, отладчик Visual Studio позволяет выбрать отладчик так:

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

Необработанное исключение в не-rtl потоке

Для начала, самый простой случай – код в потоке, создаваемом системной функцией

CreateThread

, по очевидным причинам не будет иметь обработчика исключений RTL (в отличие от RTL-функции

BeginThread

function CrashMe(Arg: Pointer): Integer; stdcall;
begin
  raise Exception.Create('Apocalypse');
  Result := 0;
end;

...

TH := CreateThread(nil, 0, @CrashMe, nil, 0, TID);

или

function CrashMe(Arg: Pointer): Integer; stdcall;
var
  P: Pointer;
begin
  P := nil;
  P^ := 0; 
  Result := 0;
end;

...

TH := CreateThread(nil, 0, @CrashMe, nil, 0, TID);


Оба эти примера кода приведут к вылету приложения: будет вызван WER, создан/отправлен отчёт о вылете, приложение будет закрыто.

P.S. Кстати, этот метод – простейший способ заставить работать Restart & Recovery для приложений-служб. Просто реализуйте логику службы в потоках, создаваемых через CreateThread в OnStart. Не используйте OnExecute.

Перенаправление на jit-отладчик

Далее, начиная с Delphi 5 в модуле

System

появляется новая глобальная переменная:

var
  JITEnable: Byte = 0;        { 1 to call UnhandledExceptionFilter if the exception is not a Pascal exception.
                                >1 to call UnhandledExceptionFilter for all exceptions }

по-умолчанию она равна 0 и означает, что все обработчики исключений работают как обычно. Если эта переменная отлична от нуля, то блоки

except

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

System.JITEnable

= 1) или произвольное (

System.JITEnable

= 2). Переменная

System.JITEnable

не изменяется кодом RTL/VCL и предназначена для изменения вами.

Иными словами, эта настройка ничего не делает, если программа запущена под отладчиком. В этом случае программа будет работать как обычно, все блоки except будут выполняться, все обработчики будут запускаться. Но если программа запущена без отладчика, то эта опция позволяет выбрать как/чем обрабатывать исключения – встроенными обработчиками или отдавать исключения наружу. 1, соответственно, отдаёт наружу только аппаратные исключения (типа Access Violation), 2 – любые.

Ну, а когда вышла Windows XP (а затем – и Vista), ровно эта же переменная (System.JITEnable) позволила вызывать WER и инициировать, таким образом, отправку отчётов.

Т.е.

procedure TForm1.Button1Click(Sender: TObject);
var
  P: PInteger;
begin
  JITEnable := 1;
  P := nil;
  P^ := 0;    // <- вызовет WER (вне отладчика)
end;
procedure TForm1.Button1Click(Sender: TObject);
begin
  JITEnable := 1;
  raise Exception.Create('Error Message'); // <- запустит обработчик исключений VCL
                                           // Application.HandleException, что
                                           // покажет сообщений "Error Message"
end;
procedure TForm1.Button1Click(Sender: TObject);
begin
  JITEnable := 2;
  raise Exception.Create('Error Message'); // <- вызовет WER (вне отладчика)
end;
procedure TForm1.Button1Click(Sender: TObject);
var
  P: PInteger;
begin
  JITEnable := 1;
  try
    P := nil;
    P^ := 0;    // <- вызовет WER (вне отладчика)
  except
    MessageBox(0, 'Error!', 'Caption', MB_OK); // <- не будет выполнен (вне отладчика)
  end;
end;
procedure TForm1.Button1Click(Sender: TObject);
var
  P: PInteger;
begin
  JITEnable := 1;
  try
    P := AllocMem(1024);
    try
      P := nil;
      P^ := 0;    // <- вызовет WER (вне отладчика)
    finally
      FreeMem(P); // <- не будет выполнен (вне отладчика)
    end;
  except
    MessageBox(0, 'Error!', 'Caption', MB_OK); // <- не будет выполнен (вне отладчика)
  end;
end;

Очевидно, что переменная

System.JITEnable

не предназначена для использования в production-версии вашего кода. По крайней мере, при значении 2 – точно. Ведь в вашем коде написаны какие-то обработчики исключений, стоят блоки

tryexcept

, ваш код предполагает, что обработчики будут выполняться, ведь они выполняют какую-то работу по откату. Но если вы включаете опцию

System.JITEnable

, то ни один из ваших обработчиков не будет вызван.

System.JITEnable предназначена для отладки ваших приложений. Если в вашем приложении происходит исключение/вылет, которое вы не можете поймать под отладчиком (происходит только при запуске вне отладчика), то вы можете включить System.

Замечу, что даже если System.JITEnable отлична от нуля, то это просто передаст исключение “наверх”, т.е. UnhandledExceptionFilter будет вызываться. И если вы (или кто-то ещё) назначит обработчик UnhandledExceptionFilter (через SetUnhandledExceptionFilter) и он будет возвращать EXCEPTION_EXECUTE_HANDLER (для всех или только избранных исключений), то соответствующие блоки except всё же будут выполняться.

Переполнение стека

Тем не менее, можно привести и аналогичные примеры. Например:

procedure P;
begin
  P;
end;

procedure TForm1.Button1Click(Sender: TObject);
begin
  P;
end;


Этот код вызывает просто переполнение стека (stack overflow). Часто – это обычное исключение, которое будет поймано ближайшим обработчиком и обработано. В данном случае – будет показано сообщение через

Application.HandleException

. Тем не менее, для обработки исключения тоже нужно свободное место на стеке. И если обработчик займёт слишком много места на стеке – получится исключение внутри обработчика исключения. Новое исключение будет передано выше, пока не дойдёт до самого верхнего уровня, где и будет обработано WER.

К примеру, сообщение о переполнении стека с высокой долей вероятности будет успешно показано из приложения Delphi 7 на Windows XP, поскольку обработчик представляет собой простой MessageBox(E.Message). Но в комбинации Delphi 10.1 Berlin на Windows 10 – приложение, вероятнее всего, вылетит в WER, поскольку там и обработчик немного сложнее и MessageBox устроен сложнее.

Повреждение стека/обработчика


Случай посложнее. Иногда система просто не может вызвать обработчики исключений: и тогда единственное, что она может сделать – вызвать глобальный обработчик (WER).

Например:

procedure TForm1.Button1Click(Sender: TObject);
var
  S: array [0..1] of Integer;
  I: Integer;
begin
  I := -6;          // пусть мы ошиблись в вычислении индекса I
  try
    S[I]     := 1;  // вместо записи в массив мы стираем обработчик исключений, установленный try
    S[I   1] := 2;
    S[I   2] := 3;
    Abort;          // <- полный вылет программы, т.к. система не смогла найти обработчик
  except
    ShowMessage('Aborted');
  end;
end;

Ошибки переполнения буфера могут привести к перезаписи важных адресов на стеке потока. В результате блоки

try

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

except

. Обработчик вызвать не удастся, так что будет вызван только WER.

Пример с перезаписью стека справедлив только для x86-32, т.к. на x86-64 стек не используется для блоков try (вместо этого используются табличные исключения, где все блоки try зарегистрированы в отдельной таблице, не хранящейся в стеке).

Смещение/адрес исключения

Как только вы опознали что именно произошло, осталось понять где это произошло. Для этого посмотрите на параметры имя модуля (не приложения!) и смещение (offset) для этого модуля.

Имя модуля покажет модуль, в котором было возбуждено исключение. Смещение – это смещение указателя (на код) от начала этого модуля. К примеру, если исключение возникло по адресу $004AF70A в модуле

CrashMe5.exe

, который загружен по (стандартному для .exe) адресу $00400000, то смещение будет равно: $004AF70A – $00400000 = $AF70A. И наоборот, если вы знаете смещение, то, узнав адрес загруженного модуля, можете узнать и где произошло исключение: $000AF70A $00400000 = $004AF70A.

Почему вообще используется какое-то смещение? Почему бы в отчёте просто не указать адрес?

Потому что этот адрес не будет иметь для вас смысла. К примеру, на машине где произошёл вылет адрес оказался равен $77A0098E, а модуль был загружен по адресу $77990000. Ну а на вашей машине этот модуль оказался загруженным по адресу $5AD20000. И что теперь? Как вы найдёте место вылета? Адрес $77A0098E на вашей машине вообще не указывает внутрь модуля!

Но если вы знаете смещение ($77A0098E – $77990000 = $7098E), то легко определите и адрес вылета: $5AD20000 $7098E = $5AD9098E – именно в этом месте произошло исключение.

Окей, но как же узнать, что за код выполнялся по этому адресу?

Стек, переменные и другая информация

Окей, с базовой информацией мы разобрались. Но что делать с самой вкусной частью пирога – стеком вызова и, возможно, значениями переменных? К сожалению, в Delphi вы не сделаете ничего. Но вы можете использовать отладчик Microsoft. Для этого вам потребуется следующее:

  1. Vista и выше. На Windows XP дампы не создаются.
  2. Отчёт вылета должен содержать в себе дамп процесса (дамп процесса также называется мини-дампом, противопоставляя себя полному дампу режима ядра). Дамп процесса – это “слепок” памяти и, возможно, объектов процесса. Если вы вызываете WER вручную, то параметры создания дампа вы указываете самостоятельно – в вызове WER. См. код-пример выше, где мы вызывали WerReportAddDump для добавления мини-дампа в отчёт. Если вылет происходит под управлением системы, то мини-дамп создаётся (или нет) согласно настройке WER. В любом случае, файл мини-дампа будет упомянут в отчёте (файл .mdmp или, реже, .dmp), а ссылки на локальные хранилища я приводил выше (в описании WER).
  3. Отладочная информация в формате, которую может понять отладчик Microsoft. Для этого вам нужно создавать отладочную информацию в формате MAP и/или TDS/TD32, а затем использовать конвертер в DBG или PDB. Для этого подойдёт какой-либо из вариантов утилиты map2dbg:

    или утилиты tds2pdb:

  4. Сама Visual Studio или WinDbg.

Я решил вынести этот вопрос за рамки статьи, потому что я не уверен, что эта информация нужна по следующим причинам:

  • Это достаточно трудоёмкий процесс.
  • Интеграция не идеальна. К примеру, Visual Studio мне так и не удалось заставить читать DBG/PDB, созданные указанными утилитами. Это работает для WinDbg, но для Delphi-ста изучать WinDbg (отладчик, управляемый в основном текстовыми командами) – весьма нетривиальная задача. Также есть проблемы с 64-разрядными файлами.
  • Намного проще добавить к отчёту произвольный файл(ы) (через WerReportAddFile) – куда вы можете записать стеки, дампы кучи, переменные, логи, скриншоты, рабочие файлы и вообще всё, что душа пожелает.

Оставьте комментарий

Adblock
detector