RunError — Процедура Delphi


Содержание
(для простоты я опустил случай подключения отладчика ядра) История Windows Error Reporting 16-битные Windows В далёкие-далёкие времена не было никаких утилит диагностики. Если программа вылетала — она вылетала совсем. В тяжёлых случаях программа могла утянуть с собой всю систему. К примеру, General Protection Fault (GPF, общее нарушение защиты, Unrecoverable Application Error, UAE) — это прерывание (fault), возникающее в случае попытки приложения получить доступ к не принадлежащей ему области памяти (сегодня известно как исключение Access Violation большинству разработчиков Delphi). При получении этого сигнала от процессора операционная система останавливает выполнение приложения, сообщает пользователю и продолжает выполнение других приложений. Но если в процессе обработки GPF (в обработчике GPF) будет возбуждено ещё одно GPF, процессор отправит сигнал «повторный GPF» (double fault), останавливая уже операционную систему. Если при этом снова произойдёт GPF (triple fault), процессор прекратит работу и его нельзя будет перезапустить (нужен будет перезапуск всего компьютера). В те времена основным способом исправить ошибку в программе было воспроизведение проблемы под отладчиком. Первая программа диагностики появилась в бета-версии 16-битной Windows 3.0. Она была создана Доном Корбиттом (Don Corbitt), который раньше работал в Borland и был частью TeamB, но потом ушёл в Microsoft, где и написал Доктора Ватсона (Dr. Watson) — первую утилиту сбора информации о вылете приложения в Windows. Как вы, вероятно, уже предположили, имя «Доктор Ватсон» взято у Доктора Ватсона — персонажа историй Артура Конана Дойля про Шерлока Холмса. Доктор Ватсон собирал информацию о системе, сбое и состоянии программы («симптомы»). Информация записывалась в отладочный лог-файл, который потом мог быть доставлен разработчикам программы для анализа. Конечно же, Доктор Ватсон очень понравился разработчикам программ. Мэтт Питрек (автор «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 был предусмотрен новый механизм. С помощью ключа реестра HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\AeDebug ( HKLM\Software\Wow6432Node\Microsoft\Windows NT\CurrentVersion\AeDebug — для 32-битных программ в 64-битной системе) стало возможным указывать т.н. «посмертный отладчик» (postmortem debugger) или JIT-отладчик (Just-In-Time — «как раз вовремя») — для подключения к процессу при его вылете. Этот ключ реестра — документирован в MSDN и TechNet. Вы могли зарегистрировать Доктора Ватсона в качестве такого отладчика, вызвав: В результате чего Доктор Ватсон регистрировал сам себя: После этого при вылете приложения система читала ключ реестра, запускала Доктора Ватсона, он подключался к приостановленному процессу, собирал информацию: При этом, если параметр Auto ключа реестра AeDebug был равен True (1), то зарегистрированный отладчик запускался сразу, иначе — система выводила обычное сообщение, но с одной дополнительной кнопкой: «Отмена» — для запуска отладчика. Да, вот так коряво была добавлена эта возможность в Windows. Никто не удосужился даже сделать подходящий диалог. Примечание: строго говоря, ключ реестра AeDebug был ещё в WinNT, а в линейке 9x его функциональность выполнял похожий раздел в Win.ini , тем не менее, ключ -i у Доктора Ватсона впервые появился именно в Windows 2000. В любом случае, эту информацию можно было затем просмотреть и отправить разработчику программы: Это был прообраз того, что затем стало службой Windows Error Reporting. P.S. Разумеется, если у вас был установлен полноценный отладчик, то никто не мешал вам зарегистрировать этот отладчик как посмертный, вместо Доктора Ватсона — что, собственно говоря, и делают Delphi (и Visual Studio). Более того, если в системе зарегистрирован посмертный отладчик, то любое приложение может форсированно его вызвать, сделав вызов системной функции DebugBreak , которая состоит всего из одной ассемблерной команды: $CC — программная (пользовательская) точка останова. Разумеется, если посмертный отладчик не зарегистрирован и программа не отлаживается, то такой код приведёт к обычному вылету приложения. 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) может не быть виной этого модуля. Быть может просто другой модуль неверно нас вызвал. Например, вылет проводника может быть из-за кривого расширения оболочки. Вылет игры может быть обусловлен глюком в видеодрайвере и т.д. Вот почему отчёты отправляются в централизованное хранилище. Там они сортируются и к отчётам допускаются все разработчики, чьи модули (exe или DLL) были упомянуты в отчёте. Microsoft не использует данные отчётов для каких-либо маркетинговых анализов, анализов частоты ошибок в различных программах т.п. Все данные пользователей защищаются от постороннего доступа и используются только для поиска причины ошибки. Отчёты отправляются по защищённому SSL соединению и хранятся на защищённых серверах, которые не используются ни для каких других целей. Для доступа к отчётам нужно предоставить логин и пароль (задаваемые при регистрации в WinQual). Любой разработчик может видеть только отчёты для своих программ. Все компании, использующие WER, обязуются следовать аналогичным политикам. Разумеется, нет гарантий, что небольшая компания из трёх человек, занимающаяся разработкой shareware-софта, заинтересована в соблюдении вашей конфиденциальности столько, сколько сама Microsoft (хотя она и согласилась следовать соответствующим политикам). Кроме того, по-умолчанию данные отчёта не содержат никаких данных пользователя, кроме тех, которые случайно попадут в дамп. Т.е. персональные данные специально не собираются. В отчёт они могут попасть только случайно. Тем не менее, вы можете посмотреть данные отчёта перед отправкой в случае, если вы работали с важными данными перед вылетом программы. А зачем вообще нужно отправлять отчёты о вылетах приложения? Дело в том, что в противном случае разработчик увидит только небольшую часть проблем в своём приложении, про большинство проблем он не будет знать (ведь про них не сообщают). Если разработчик не знает про проблему — он её и не исправит. Кроме того, автоматическая система сбора отчётов может сообщить вам, какая ошибка встречается в вашей программе чаще всего. Иными словами, вы можете сразу же исправить ту ошибку, которая действительно важная, которая мешает жить наибольшему количеству ваших пользователей. Т.е. вы можете исправить одну ошибку вместо сотни других — но эффект будет тот же самый. В частности, после того как Microsoft реализовала механизм отправки отчётов в Windows XP, она позднее провела широкомасштабный анализ присланных данных, который показал, что 80% пользовательских проблем могут быть решены исправлением 20% наиболее «популярных» ошибок. Даже исправление 1% самых частых ошибок устранит 50% пользовательских проблем! Windows Vista и позднее Неудивительно, что в Windows Vista служба отправки отчётов из Windows XP была снова обновлена. Она получила новое название Windows Error Reporting (WER) и представлена wermgr.exe и WER.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 — хотя всегда есть вариант использовать групповые политики. В реестре эти настройки находятся здесь: HKCU\Software\Microsoft\Windows\Windows Error Reporting HKLM\Software\Microsoft\Windows\Windows Error Reporting HKLM\Software\WOW6432Node\Microsoft\Windows\Windows Error Reporting и официально документированы тут. Как я уже сказал, самое значительное изменение — появление локального центра сбора отчётов. Отчёты/дампы теперь хранятся локально в %ALLUSERSPROFILE%\Microsoft\Windows\WER\ (например: C:\ProgramData\Microsoft\Windows\WER\ ; а также они могут быть в %LOCALAPPDATA%\CrashDumps\ ) и могут быть отправлены в любой момент времени. Их можно просмотреть в так называемом «журнале стабильности работы»: Жирным отмечены отчёты, которые не были отправлены. Вы можете просмотреть технические сведения по любому отчёту: отправить любой отчёт или все сразу, а также удалить все отчёты. Опция «Проверить решения» заново отправит сигнатуры всех отчётов (что может быть очень долго!) и сообщит вам, если разработчики программ выпустили какие-либо исправления к этим отчётам. Замечу, что «журнал стабильности работы» — это отдельный, независимый журнал. Он никак не связан с системным логом «Приложения» (Event Log). События о вылетах добавляются и туда, и туда, но очищать журналы можно индивидуально. Поскольку WER теперь нельзя отключить удалением ключа AeDebug / Debugger , то записи в системном логе и журнале стабильности работы будут добавляться всегда — даже если служба WerSvc будет отключена (Disabled). Вариант «Никогда не проверять решения» ( Windows Error Reporting / Disabled = 1), как и ожидается, покажет простое сообщение об ошибке — с единственной опцией: закрыть программу. Следующий вариант «Всегда спрашивать» ( Windows Error Reporting / Disabled = 0; Windows Error Reporting / Concent / DefaultConsent = 1) предложит или закрыть программу или отправить отчёт: Нажатие на кнопку отправки запустит отправку отчёта: Тут надо заметить, что, на самом деле, в этот момент идёт не отправка непосредственно отчёта, а только проверка, нужно ли его отправлять. Т.е. на сервер отправляются идентификационные данные — т.н. первый уровень (Level One), состоящий из основных параметров, идентифицирующих вылет (имя приложения, модуля, их версии, адрес/смещение, тип вылета, время и т.п.). Если сервер сообщит, что эту проблему ещё никто не встречал, либо если разработчик захотел собрать дополнительные отчёты, то вам будет предложено отправить сам отчёт (это называется «отправка дополнительной информации») — т.н. второй уровень (Level Two), состоящий из детализированного отчёта, включая слепки памяти (минидампы процесса): Заметьте, что в отличие от Windows XP, в Windows Vista помимо собственно лога прикладывается ещё и дамп процесса (.mdmp), который можно потом загрузить в отладчик Visual Studio или WinDbg, чтобы исследовать проблему. В противном случае (о сбое уже сообщили, уровень два не отправляется) программа просто завершается (возможно, с финальным диалогом — см. ниже). Соответственно, при выборе любой из двух «автоматических» опций ( DefaultConsent = 2 для «авто-проверять» / «Parameters only»; и = 3 для «и отправлять» / «Parameters and safe data»), начальный вопрос «отправлять» / «закрывать» пропускается, а сразу идёт отправка. Разница между 2 и 3 состоит в том, что при 2 Windows автоматически отправляет только данные «уровня один» — параметры для идентификации отчёта. Все данные «уровня два» (т.е. сам отчёт) при этом отправляются только после явного разрешения пользователя. Вариант 3 же автоматически отправляет и «уровень один» и «уровень два», без запроса разрешения пользователя — но только данные, которые система смогла опознать как, вероятно, не содержащие личную информацию. Для отправки любой другой дополнительной информации Windows всё же спросит разрешения пользователя. На практике эти два положения обычно не отличаются. Замечу, что DefaultConsent также может принимать значение 4 — автоматически отправлять вообще все данные без запроса пользователя. Это значение нельзя установить через UI в Windows Vista/7, хотя система его прекрасно понимает (т.е. диалог «отправка дополнительной информации» никогда не показывается). Но это значение стоит по умолчанию в последних версиях Windows (например, Windows 10). Отключение службы WerSvc может повлиять на поведение отчётов. Например, Windows не сможет определить, был ли уже отправлен отчёт и каждый отчёт будет считать новым. Отправка отчётов внешне будет успешна, но, похоже, отчёты просто встают в очередь на отправку. В некоторых случаях при вылете приложения Windows также может предложить поменять опции на «авто»: Важно: если вы разрабатываете невизуальное приложение (или если ваше приложение вылетает до показа первого окна), то в случае, если вы установите опцию WER в любую из двух (трёх) «авто» позиций, диалог не будет показан, если в системе уже есть точно такой же вылет и отчёт по нему был отправлен! Иными словами, если (невидимое) приложение вылетело в первый раз — система сделает локальный отчёт и отправит его (автоматически). Диалог будет показан. Если же то же самое приложение вылетает ровно на этом же месте с той же проблемой второй раз — система просто втихую закроет (невидимое) приложение без показа сообщения! Очистка старых отчётов приведёт к тому, что вылет снова будет считаться «новым» и приведёт к показу сообщения об ошибке. Визуальные (оконные) приложения показывают диалог всегда. Вы также можете форсировать диалог для невизуальных приложений сделав вызов WerSetFlags(WER_FAULT_REPORTING_ALWAYS_SHOW_UI); при старте своего приложения. (И наоборот, если вы сделаете вызов SetErrorMode(SEM_NOGPFAULTERRORBOX); или (недокументированный) WerSetFlags(WER_FAULT_REPORTING_NO_UI); , то ваше приложение не будет показывать диалог WER, даже если оно визуальное.) В любом случае, в конечном итоге — если финальный диалог о вылете должен быть показан, то он выглядит так: Если при этом вы зарегистрировали посмертный отладчик через AeDebug / Debugger , то будет показана дополнительная кнопка отладки: Это последний диалог, после которого приложение будет закрыто (за исключением случаев, когда приложение самостоятельно инициировало обращение к WER — в этом случае дальнейшие действия определяет приложение). Приложение скрытно завершается — что делать? Как должно быть уже понятно, это возможно в следующих случаях: Приложение само явно вызвало TerminateProcess или ExitProcess (прямо или опосредованно — например, через Halt ). Приложение явно завершило все свои потоки (вышло из процедур потоков или же явно вызвало TerminateThread или ExitThread ). Это не бывает в Delphi, поскольку компилятор Delphi вставляет неявный вызов Halt в конец главного потока (т.е. всегда вызывает ExitProcess в конце работы главного потока), но это может случиться, если внешний процесс уничтожит главный поток в вашей программе. Какой-то внешний процесс закрыл или уничтожил или ваш процесс или все потоки в нём. В вашей программе произошло необработанное (фатальное) исключение, но в Доктор Ватсон / WER отключен диалог об ошибках. В вашей программе произошло необработанное (фатальное) исключение, в системе зарегистрирован сторонний посмертный отладчик с автоматическим запуском, который не показал сообщения об ошибке. В вашей программе произошло необработанное (фатальное) исключение, которое настолько серьёзно, что система даже не смогла показать сообщение, а посмертный отладчик не зарегистрирован. Что же в таком случае делать? В целом нужно попытаться определить, завершилось ли приложение штатно (вызвало Exit/Terminate, либо это сделал кто-то за него), либо же вылетело с необработанным исключением. Подробный список — ниже. Примечание: в списке ниже ключ реестра Windows Error Reporting\что-то обозначает ключ HKCU\Software\Microsoft\Windows\Windows Error Reporting\что-то , а при его отсутствии — HKLM\Software\Microsoft\Windows\Windows Error Reporting\что-то , либо HKLM\Software\Wow6432Node\Microsoft\Windows\Windows Error Reporting\что-то (для 32-битных приложений на 64-битной машине). Попробуйте запустить приложение под отладчиком. Убедитесь, что в отладчике не отключены уведомления об исключениях. Если приложение под отладчиком не вылетает или подключить отладчик нет возможности — см. шаги ниже. Первым делом удалите регистрацию посмертного отладчика в ключе реестра AeDebug , либо хотя бы сбросьте параметр Auto в 0. [Vista+] Убедитесь, что «Служба регистрации ошибок Windows» («Windows Error Reporting Service», WerSvc ) не отключена (не находится в состоянии Disabled; по-умолчанию у неё тип запуска — Manual, но для надёжности вы можете её запустить вручную). Запустите Доктор Ватсон в Windows 2000, настройки отчётов в Windows XP, настройки WER в Windows Vista и позднее — и включите визуальные оповещения (Windows 2000), отчёты об ошибках (Windows XP), запрос согласия, т.е. не включайте автоматическую отправку (Windows Vista и выше). [Vista+] Проверьте настройки групповых политик WER. Убедитесь, что UI не отключен, логгинг не отключен, согласие (consent) не установлено в автоматическую отправку без запросов ( DefaultConcent = 1). Не забудьте проверить как политики машины, так и пользователя. [Vista+] Убедитесь, что ключа реестра Windows Error Reporting\DebugApplications\* нет или он установлен в 1. [Vista+] Убедитесь, что ключа реестра Windows Error Reporting\DontShowUI нет или он установлен в 0. [Vista+] Убедитесь, что ключа реестра Windows Error Reporting\LoggingDisabled нет или он установлен в 0. [Vista+] Очистите все отчёты в журнале стабильности системы. Убедитесь, что вы не вызываете SetErrorMode с одним из следующих флагов: SEM_FAILCRITICALERRORS , SEM_NOGPFAULTERRORBOX , SEM_NOOPENFILEERRORBOX . Для надёжности сделайте вызов SetErrorMode(0); первым действием при запуске своего приложения. [Win7+] Убедитесь, что вы не вызываете SetThreadErrorMode с одним из следующих флагов: SEM_FAILCRITICALERRORS , SEM_NOGPFAULTERRORBOX , SEM_NOOPENFILEERRORBOX для ваших потоков. Для надёжности сделайте вызов SetThreadErrorMode(0); первым действием ваших потоков. [Vista+] Убедитесь, что ваш код не делает вызов WerSetFlags(WER_FAULT_REPORTING_NO_UI); . [Vista+] Сделайте вызов WerSetFlags(WER_FAULT_REPORTING_ALWAYS_SHOW_UI); первым действием при запуске своего приложения. Убедитесь, что глобальная переменная System.JITEnable равна 0. Для надёжности присвойте её 0 первым действием при старте приложения. Запустите ваше приложение и дайте ему вылететь. Если никакого диалога так и не появилось, то выполните нижеследующие шаги. Проверьте, есть ли записи о вылете приложения в системном логе «Приложения». Проверьте, нет ли свежих записей в «журнале стабильности системы» или логов в %APPDATA%\Microsoft\Windows\WER\ReportArchive\ / %APPDATA%\CrashDumps\ . Попробуйте назначить свой глобальный обработчик необработанных исключений через системную функцию SetUnhandledExceptionFilter . Установите точки останова или хуки (в рамках вашего процесса) на TerminateProcess , ExitProcess , а если это не помогло — то и на TerminateThread и ExitThread . Установите точки останова или хуки на системную функцию kernel32.KiUserExceptionDispatcher — если эта функция будет вызвана непосредственно перед вылетом, то 99% за то, что у вас произошло крайне серьёзное необработанное исключение, при котором система даже не смогла показать сообщение. Наконец, попробуйте установить глобальный хук (все процессы) на TerminateProcess , TerminateThread , чтобы узнать, не завершает ли ваш процесс кто-то ещё. Также попробуйте пересобрать приложение под x86-64 или использовать другую версию Delphi (как более новую, так и более старую). Что Delphi делает не так Концепция обработки ошибок в Delphi была создана в Delphi 1 и существенно не менялась с тех пор. Напомню, что в те времена не было возможности подключения посмертного отладчика, сбора информации о сбое и автоматической отправки отчёта. Поэтому если приложение вылетало — оно вылетало с концами. Поэтому Delphi исторически реализует концепцию «ни за что не вылетать». Делает это она путём расстановки блоков try / except везде, где только возможно. В частности, в оконных приложениях VCL обработка каждого сообщения заключена в явный блок try / except с вызовом Application.HandleException для обработки каждого необработанного (unhandled) исключения: Application.HandleException просто показывает сообщение (.Message) исключения (в новых версиях Delphi — вместе с вложенными): После чего приложение продолжает обычную работу (цикл выборки сообщений). Если VCL (или аналогичный фреймворк) не используется, либо если исключение происходит вне цикла выборки сообщений, то управление получает глобальный обработчик исключений RTL System._ExceptionHandler , который вызывает пользовательский обработчик из System.ExceptProc : В 99% случаев этот обработчик установлен в SysUtils.ExceptHandler , который показывает сообщение через SysUtils.ShowException , а затем завершает работу приложения с кодом возврата равным 1: SysUtils.ShowException показывает упрощённо-техническое сообщение: Если же модуль SysUtils не подключен, либо исключение возникло до инициализации модуля SysUtils , то System.ExceptProc будет не назначен (равен nil ), так что System._ExceptionHandler попытается обработать сообщение самостоятельно. Поскольку объект исключения определяется в том же модуле SysUtils , модуль System не может оперировать объектом исключения. Вместо этого он завершит приложение с «кодом ошибки» 217 (необработанное исключение) — через System._RunError : Что приведёт к завершению приложения. При этом System.Halt увидит, что установлен код ошибки, поэтому он сообщит об этом: Примечание: код 217, на самом деле, означает закрытие консольного приложения через Ctrl + Break . По совместительству он же используется для указания необработанного исключения. Также код может быть 230 — настоящий код для необработанных исключений, используется на не-Windows платформах. 204 — код для Invalid Pointer, вызывается менеджером памяти, если передать ему неверный указатель (например, указатель на уже удалённую память). А также частый код 216 — если необработанное исключения является аппаратным Access Violation. Для других аппаратных исключений также есть свои собственные коды, но на практике в 99% случаев вы увидите только 216, 217 или 204. Если ваш код будет создавать дополнительные фоновые потоки через 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 — зло, от которого нужно избавляться. Мы посмотрим на это чуть позже. Когда приложение Delphi может привести к вызову WER? А пока — посмотрим на то, как приложение Delphi может вызывать WER, даже хотя оно почти полностью завёрнуто в обработчики исключений, и необработанным исключениям возникнуть, вроде бы, неоткуда. Необработанное исключение в не-RTL потоке Для начала, самый простой случай — код в потоке, создаваемом системной функцией CreateThread , по очевидным причинам не будет иметь обработчика исключений RTL (в отличие от RTL-функции BeginThread ). или Оба эти примера кода приведут к вылету приложения: будет вызван WER, создан/отправлен отчёт о вылете, приложение будет закрыто. P.S. Кстати, этот метод — простейший способ заставить работать Restart & Recovery для приложений-служб. Просто реализуйте логику службы в потоках, создаваемых через CreateThread в OnStart . Не используйте OnExecute . Повреждение стека/обработчика Случай посложнее. Иногда система просто не может вызвать обработчики исключений: и тогда единственное, что она может сделать — вызвать глобальный обработчик (WER). Например: Ошибки переполнения буфера могут привести к перезаписи важных адресов на стеке потока. В результате блоки try будут испорчены. И если в этот момент произойдёт исключение, система увидит на стеке мусор вместо указателя на блок except . Обработчик вызвать не удастся, так что будет вызван только WER. Пример с перезаписью стека справедлив только для x86-32, т.к. на x86-64 стек не используется для блоков try (вместо этого используются табличные исключения, где все блоки try зарегистрированы в отдельной таблице, не хранящейся в стеке). Переполнение стека Тем не менее, можно привести и аналогичные примеры. Например: Этот код вызывает просто переполнение стека (stack overflow). Часто — это обычное исключение, которое будет поймано ближайшим обработчиком и обработано. В данном случае — будет показано сообщение через Application.HandleException . Тем не менее, для обработки исключения тоже нужно свободное место на стеке. И если обработчик займёт слишком много места на стеке — получится исключение внутри обработчика исключения. Новое исключение будет передано выше, пока не дойдёт до самого верхнего уровня, где и будет обработано WER. К примеру, сообщение о переполнении стека с высокой долей вероятности будет успешно показано из приложения Delphi 7 на Windows XP, поскольку обработчик представляет собой простой MessageBox(E.Message) . Но в комбинации Delphi 10.1 Berlin на Windows 10 — приложение, вероятнее всего, вылетит в WER, поскольку там и обработчик немного сложнее и MessageBox устроен сложнее. Двойное переполнение стека Но даже если мы сведём обработчик к тривиальному: который, очевидно, всегда будет выполняться успешно, т.к. не занимает места на стеке. Даже если мы возьмём наименьшую версию Delphi и Windows — всё равно приложение может вылететь. Да, первое нажатие на кнопку возбудит исключение stack overflow, которое успешно будет обработано (пустым) обработчиком исключений. Но вспомните, что исключение stack overflow возбуждается только когда стек дорастает до защитной страницы, после доступа к которой защита снимается и возбуждается первое исключение (stack overflow). Если затем стек растёт и далее — то никакой защитной страницы уже нет, запись в стек наткнётся на зарезервированную страницу без доступа. Иными словами, если нажать кнопку второй раз — исключения stack overflow уже не будет. Будет фатальное Access Violation, будет вызван WER. Вызов UnhandledExceptionFilter Далее, поведение программы зависит и от версии Delphi. Указанная выше (в предыдущем разделе) логика с безусловным вызовом System.ExceptProc из System._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 ). Именно поэтому эта опция ничего не делает, если вы уже отлаживаете свою программу: в конце концов, задача опции — подключить отладчик, а если вы уже отлаживаетесь, то подключать ничего не надо. Перенаправление на JIT-отладчик Далее, начиная с Delphi 5 в модуле System появляется новая глобальная переменная: по-умолчанию она равна 0 и означает, что все обработчики исключений работают как обычно. Если эта переменная отлична от нуля, то блоки except не будут вызываться если, во-первых, программа не отлаживается, во-вторых, исключение — аппаратное (для System.JITEnable = 1) или произвольное ( System.JITEnable = 2). Переменная System.JITEnable не изменяется кодом RTL/VCL и предназначена для изменения вами. Иными словами, эта настройка ничего не делает, если программа запущена под отладчиком. В этом случае программа будет работать как обычно, все блоки except будут выполняться, все обработчики будут запускаться. Но если программа запущена без отладчика, то эта опция позволяет выбрать как/чем обрабатывать исключения — встроенными обработчиками или отдавать исключения наружу. 1, соответственно, отдаёт наружу только аппаратные исключения (типа Access Violation), 2 — любые. Ну, а когда вышла Windows XP (а затем — и Vista), ровно эта же переменная ( System.JITEnable ) позволила вызывать WER и инициировать, таким образом, отправку отчётов. Очевидно, что переменная System.JITEnable не предназначена для использования в production-версии вашего кода. По крайней мере, при значении 2 — точно. Ведь в вашем коде написаны какие-то обработчики исключений, стоят блоки try / except , ваш код предполагает, что обработчики будут выполняться, ведь они выполняют какую-то работу по откату. Но если вы включаете опцию System.JITEnable , то ни один из ваших обработчиков не будет вызван. System.JITEnable предназначена для отладки ваших приложений. Если в вашем приложении происходит исключение/вылет, которое вы не можете поймать под отладчиком (происходит только при запуске вне отладчика), то вы можете включить System.JITEnable и запустить вашу программу вне отладчика. Пусть она вылетит, вы подключите к программе посмертный отладчик (отладчик Delphi, конечно же) и исследуете проблему на месте. Замечу, что даже если System.JITEnable отлична от нуля, то это просто передаст исключение «наверх», т.е. UnhandledExceptionFilter будет вызываться. И если вы (или кто-то ещё) назначит обработчик UnhandledExceptionFilter (через SetUnhandledExceptionFilter ) и он будет возвращать EXCEPTION_EXECUTE_HANDLER (для всех или только избранных исключений), то соответствующие блоки except всё же будут выполняться. Таким образом, вы всегда можете сделать тонкую настройку, выполнять обработку только избранных исключений. И в таком виде System.JITEnable вполне имеет право на жизнь и в production-коде. Ручной вызов WER В конце концов, ваш код может просто вызвать WER вручную — через его API. Примеры вызова мы посмотрим ниже. Настройка Delphi-приложений для отправки отчётов Зачем это делать Итак, как уже должно быть понятно, отправка отчётов при вылете приложения — есть хорошо, т.к. позволяет разработчикам программ узнавать о вылетах и проблемах в приложениях без участия пользователя, даёт точные технические описания и позволяет делать сортировку/статистику. Кроме того, вызов системного обработчика ошибок (будь это WER или что-то ещё) — необходимое условие в некоторых отдельных случаях. Например, при сертификации своей программы для Windows, для реакции системных сервисов перезапуска (Restart & Recovery, в т.ч. в службах). Поэтому с концепцией Delphi «ни за что не вылетать» нужно срочно что-то делать. Лирическое отступление для тех, кому вообще не нравится концепция отправки отчётов Если по каким-либо причинам вы не хотите отправлять отчёты и хотите всегда показывать своё сообщение пользователю: Не используйте CreateThread . Всегда используйте BeginThread . Не изменяйте System.JITEnable . Установите: и/или: Заключайте каждую функцию потока, созданного CreateThread / BeginThread / QueueUserWorkItem (и аналогичными функциями), в явный блок try / except . Заключайте в блоки try / except каждую секцию initialization , каждую секцию finalization и блок begin / end .dpr проекта. В блоке except вы можете вызывать SysUtils.ShowException или свой собственный код. Вызовите SetErrorMode(SEM_NOGPFAULTERRORBOX); при старте процесса (опционально можно добавить и другие флаги). См. также. Обработайте терминальное исключение главного потока: или: Заключайте в блоки try / except каждую секцию initialization , каждую секцию finalization и блок begin / end .dpr проекта. В блоке except вы можете вызывать SysUtils.ShowException или свой собственный код. Опционально: замените все обработчики исключений на свои. Убедитесь, что вы дополнительно показываете в сообщении/диалоге: имя модуля, его версию и смещение исключения внутри этого модуля (см. ниже). Примечание: а если вы принципиально не против идеи отчётов, но не хотите использовать WER, то — см. ниже. Для всех же нормальных разработчиков — давайте посмотрим, что мы можем сделать. Во-первых, в первом приближении очень хорошо сработала бы такая комбинация: В данном случае мы вызываем WER для всех перечисленных кодов аппаратных исключений и RTL обработчики — для всего остального. Список, конечно, приведён как пример, и его нужно изменить под ваше приложение. Например, если вы делаете определение запуска виртуальной машины через выполнение секретной инструкции, то код EXCEPTION_PRIV_INSTRUCTION (и, возможно, EXCEPTION_ILLEGAL_INSTRUCTION ) нужно убрать. Если вы проверяете аргументы выражений, то коды числовых исключений (деление на ноль, переполнение и т.п.) хорошо бы добавить. В тривиальном случае можно также считать что любое аппаратное исключение нужно передать WER. В сложном случае — фильтровать не только аппаратные, но и программные исключения (установив JITEnable в 2). Но это решение — половинчатое. Ведь нам хотелось бы вызывать WER для всех необработанных исключений, вне зависимости от их типа, и не вызывать для всех остальных (опять же, вне зависимости от типа). Поэтому решение с JITEnable стоит отложить. К сожалению, в Delphi не предусмотрено никакой возможности отменить только «глобальные» блоки try / except , нет никакого аналога опции JITEnable . Вместо этого нам придётся назначить свой обработчик и вызывать WER вручную — через API. Сама Microsoft такой подход считает допустимым, но не рекомендованным. Рекомендованный — конечно же, просто не ловить исключения, которые вы не знаете как обрабатывать. Итак, для начала нам нужна функция, которую мы будем вызывать для необработанных исключений: Как видите, тут ужасно много всего. Центральным местом тут является всего одна функция — ReportFault , которую и нужно нам вызвать. Проблема в том, что на вход функция просит пару ExceptionRecord и Context — это низкоуровневые понятия, которые недоступны из обычного блока except в Delphi. Поэтому большая часть кода посвящена преобразованию между этими двумя понятиями. К сожалению, весьма важный кусок информации — контекст процессора, не доступен. Чтобы его получить, нам придётся прибегнуть к нетривиальным ловушкам, рассмотрение которых выходит за рамки этой статьи. Очень важно, что этот код должен работать без выделения памяти (в куче). Во-первых, нам не известно состояние менеджера памяти в момент вызова нашего обработчика. Может быть, обработчик был вызван как раз потому, что произошло исключение в менеджере памяти. И что тогда? Снова выделять память? Во-вторых, менеджер памяти может быть уже финализирован к моменту вызова вашего обработчика. Например, если исключение возникло в секции initialization модуля, то сначала сработает блок except процедуры InitUnits модуля System , который завершит модули (и менеджер памяти — в том числе), а затем уже вызовет UnhandledExceptionFilter (и, следовательно — наш обработчик). Выделение памяти у уже отработавшего менеджера памяти, опять же, ничем хорошим не закончится. Именно по этой причине вызов GetProcAddress вынесен из кода UnhandledExceptionHandler — он выделяет память (см. реализацию GetProcAddress в модуле Windows ). Подсказка: если вы всё же хотите выделать память в своём обработчике — попробуйте установить альтернативный менеджер памяти. Для простоты это может быть переходник к HeapAlloc или VirtualAlloc . (Это я ещё не про все подводные камни рассказал. Да, как видите, глобальная обработка исключений — не самое простое дело.) Если вызов WER был неудачен (например, WER отключен), либо WER вообще отсутствует (Windows 2000 и ранее), то мы пытаемся показать сообщение как оно показывалось бы ранее — методом ShowException модуля SysUtils . Проблема опять же в том, что делать, если на руках нет объекта исключения Delphi, который можно было бы показать. Напомню, что в этом случае модуль System показывает run-time ошибку (обычно — 217, 216 или 204, как мы обсуждали это выше) — что, на мой взгляд, достаточно бесполезно. Поэтому вместо этого я предлагаю показывать код исключения — что и делает функция ShowExceptionRecord , которая является слегка видоизменённой ShowException . Данный пример использует API уровня Windows XP (хотя будет работать в любой ОС), но вы также можете расширить этот пример на уровень Vista, чтобы полностью настроить поведение WER. В Vista вы можете изменять части диалога, добавлять в отчёт информацию и файлы и многое другое. Итак, имея на руках функцию UnhandledExceptionHandler , мы можем назначить её в качестве глобального обработчика исключений: В данном примере UnhandledExceptionHandler устанавливается обработчиком для оконных приложений VCL, а для остальных мест — мы подразумеваем, что будет использован стандартный обработчик модуля System , который должен вызвать (в не самых древних Delphi) стандартный системный UnhandledExceptionFilter . Соответственно, мы назначаем свой обработчик, чтобы перехватить этот вызов и показать WER. Конечно, в последнем случае гораздо проще просто ничего не делать, т.к., как мы помним из написанного выше, в не самых древних версиях Delphi при прогоне без отладчика исключение останется необработанным и будет поднято до WER. Именно поэтому этот код помечен «опциональным». Тем не менее, если вы захотите поменять WER на, скажем, свой собственный механизм обработки/логгирования исключений, то вам этот код будет нужен, так что я заодно его и показал. Для совсем старых версий Delphi вы можете заменить (или дополнить) вызов SetUnhandledExceptionFilter на назначение обработчика ExceptProc . Вызов же SetWERVisible нужен чтобы окно WER появлялось бы всегда, даже если ошибка произошла в невизуальной части нашего приложения (см. обсуждение этого поведения выше). И если вы хотите получать отчёты, но не хотите использовать WER, то вы можете использовать полностью аналогичный код: В этом случае вызов SetUnhandledExceptionFilter становится обязательным, а не опциональным. Примеры выше были приведены для оконного VCL-приложения, где есть глобальный объект Application , предоставляющий событие OnException . Если вы пишите приложение с использованием другого фреймворка — вы должны адаптировать этот код, изменив назначение глобального обработчика. Например, FireMonkey тоже предоставляет свой собственный глобальный объект Application , у которого также есть событие OnException . Некоторые другие фреймворки предоставляют аналогичное событие. Например, IntraWeb предоставляет событие gServerController.OnLogException . Некоторые фреймворки в качестве такого события используют глобальную переменную Classes.ApplicationHandleException (например, OTL — Omni Thread Library) — так что вы можете назначить свой обработчик туда. Некоторые фреймворки предоставляют процедуру регистрации обработчика вроде RegisterExceptionFilter . Например — OTL и IntraWeb ( TIWServerControllerBase.RegisterExceptionCallback ). Некоторые фреймворки (в основном — многопоточные) не выпускают исключение наружу, а сохраняют его в свойстве типа .FatalException — например, TThread и OTL. Короче говоря, вам нужно изучить документацию фреймворка, найти там как фреймворк обрабатывает исключения. И если фреймворк ловит все необработанные исключения, то узнать как можно назначить свой глобальный обработчик, назначить его — и вызывать из него UnhandledExceptionHandler из примера выше. К сожалению, не всегда можно назначить свой обработчик. К примеру, службы (сервисы Win32) в Delphi не предоставляют ни события, ни какого-либо иного способа зарегистрировать обработчик. В большинстве случаев для обработки исключений вызывается не виртуальный метод LogMessage . В TServiceApplication есть динамический метод DoHandleException, но заменить его нет никакой возможности, т.к. класс TServiceApplication создаётся безусловно, он не извлекается из какой-либо глобальной переменной, в которую мы могли бы записать свой класс. Есть и другие фреймворки, где не предусмотрена возможность указания своего обработчика исключений. Во всех таких случаях нет чистого решения. Вы можете только установить ловушку на нужный код методом сплайсинга. И раз уж вы собираетесь вылетать, то сильно неплохо было бы настроить своё приложение на автоматический перезапуск. Делается это либо вручную (на Windows XP и младше), либо вы можете использовать Restart and Recovery API на Windows Vista и выше. Не забудьте только передавать флаг WER_SUBMIT_HONOR_RESTART . Рассмотрение Restart and Recovery API выходит за рамки этой статьи. Настройка посмертного отладчика Как я уже описывал, у вас есть возможность зарегистрировать любую программу на ваш выбор в качестве посмертного (postmortem) или JIT (Just-In-Time) отладчика. Когда приложение вылетает, система добавит кнопку «Отладка» («Debug») в диалог фатального вылета. Нажатие на эту кнопку запустит указанный вами отладчик, передав ему идентификатор процесса, в котором произошёл вылет. Отладчик сможет подключиться к процессу и исследовать его. Отладчик может быть классическим интерактивным — вроде отладчика Delphi. Или же это может быть автоматизированная утилита, которая просто соберёт информацию — вроде Доктора Ватсона. Посмертный отладчик регистрируется в ключе реестра HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\AeDebug (или HKLM\SOFTWARE\Wow6432Node\Microsoft\Windows NT\CurrentVersion\AeDebug — для 32-битных приложений в 64-битной системе). Для регистрации отладчика вам нужно создать или изменить значение Debugger в ключе AeDebug (строкового типа). Строка должна содержать корректную командную строку. Иными словами, если имя/путь отладчика включает в себя пробелы, его нужно заключать в кавычки. Путь обязательно должен быть полным и абсолютным. В командной строке нужно указать как минимум один параметр %ld — это шаблон, который будет заменён на PID процесса. Опционально можно добавить ещё два параметра: второй %ld будет заменён на описатель (handle) события, который отладчик может взвести, чтобы возобновить выполнения процесса. При этом считается, что отладчик исправил проблему, и процесс может продолжить работу. Если его не указывать, либо отладчик его не взведёт, то система будет ждать завершения процесса отладчика, после чего возобновит обычную работу WER, т.е. считается, что проблема не исправлена. В большинстве случаев это не очень полезная возможность, которой на практике обычно не пользуются. Наконец, можно добавить третий параметр %p , который будет заменён на адрес записи JIT_DEBUG_INFO в целевом процессе. Отладчик (или вы, вручную) может прочитать оттуда дополнительную информацию. Кроме того, вы можете создать строковый параметр Auto и установить его в ‘0’ или ‘1’. Несложно сообразить, что при Auto = 1 диалоговое окно не показывается, посмертный отладчик запускается сразу. При Auto = 0 (умолчание), соответственно, появляется обычное окно с дополнительной кнопкой «Отладка» («Debug»). Примечание: поскольку настройки посмертного отладчика хранятся в HKEY_LOCAL_MACHINE — вам потребуются права администратора. Поэтому все действия ниже нужно выполнять из программ, запущенных под администратором (и с элевацией прав). Delphi при установке регистрирует себя в качестве посмертного отладчика. Но если потом устанавливается другая среда разработки, то она может перезаписать регистрацию посмертного отладчика. К сожалению, в Delphi нет никакого способа автоматически перерегистрировать посмертный отладчик, вам придётся сделать это вручную. К примеру, для Delphi 5-7 установите Debugger в: только замените 70 (Delphi 7) на 50 (Delphi 5) или 60 (Delphi 6). Для более новых версий Delphi используйте: В настоящее время параметр %p не принимает ни одна версия Delphi. Указанный выше отладчик Delphi 5-7 ( bordbg70.exe ) является, по-сути, удалённым отладчиком (remote debugger) и поэтому зависит от соответствующей библиотеки bordbk ( bordbk50.dll , bordbk60.dll , bordbk61.dll , bordbk70.dll ), которую можно найти в папке C:\Program Files\Common Files\Borland Shared\Debugger\ (да, даже на 64-битной системе используется C:\Program Files\ , а не C:\Program Files (x86)\ ). Если при запуске отладчика Delphi 5-7 вы получаете сообщение о невозможности загрузки библиотеки bordbk, то соответствующую библиотеку нужно зарегистрировать вызовом tregsvr (лежит в папке \bin\ Delphi) или regsvr32 (два экземпляра лежат в папке C:\Windows\System32\ — под каждую разрядность; вызывать нужно, разумеется, 32-битный), например: Помимо Delphi посмертным отладчиком регистрируются и отладчики от Microsoft. Только в отличие от Delphi, многие из них имеют возможность автоматической перерегистрации. Например, отладчики WinDbg, CDB и NTSD могут быть зарегистрированы следующими командами: К сожалению, отладчик WinDbg при этом не регистрирует параметр для JIT_DEBUG_INFO , поэтому лучше всего отредактировать его регистрацию вручную: Отладчик Visual Studio не имеет возможности перерегистрации, его нужно регистрировать заново вручную. Для этого используется такая командная строка: Утилита ProcDump от SysInternals также может быть зарегистрирована посмертным отладчиком с помощью такой команды: Утилита ProcDump создаёт дамп процесса. По умолчанию создаётся мини-дамп: списки процессов, потоков, модулей, описателей. Дамп может быть расширен указанием опций -ma или -mp . Подробнее — см. справку по параметрам ProcDump. Если вы пользуетесь в основном Delphi, то вы регистрируете отладчик Delphi в качестве посмертного — и на этом всё. Если же вы используете несколько сред разработки, то, возможно, вы бы хотели переключаться между отладчиками. В этом случае вы можете написать свою утилиту-переходник и зарегистрировать её в качестве посмертного отладчика. Например: При запуске вы можете показать диалоговое окно со списком отладчиков, которые вы используете в работе. При выборе отладчика — запустите его через CreateProcess , передав ему отформатированные параметры командной строки (хотя бы так: «Args := StringReplace(StringReplace(StringReplace(Args, ‘%ld’, ParamStr(1), []), ‘%ld’, ParamStr(2), []), ‘%p’, ParamStr(3), []);»). Не забудьте наследовать описатель события (второй параметр %ld ) в целевой процесс. Дождитесь завершения процесса отладчика и выходите. Например, отладчик Visual Studio позволяет выбрать отладчик так: Этот список позволяет выбрать native-отладчик, управляемый (.NET) или Java-отладчик. Delphi там, само собой, нет. Это я просто пример привёл, как это визуально может выглядеть, если вы захотите сделать такую утилиту самостоятельно. Использование Threads Snapshot в качестве посмертного отладчика В качестве посмертного отладчика вы также можете использовать утилиту Threads Snapshot. Она входит в состав трейсера исключений EurekaLog. Если у вас нет EurekaLog, то вы можете скачать бесплатный Tools Pack. Для установки Threads Snapshot в качестве посмертного отладчика достаточно запустить её с параметром » /install «: Само собой, запускать нужно под администратором (и с элевацией прав). Когда утилита Threads Snapshot зарегистрирована в качестве посмертного отладчика, и вылетает любое приложение — вы можете нажать кнопку Debug для снятия снимка процесса: Нажатие на кнопку Debug запустит снятие снимка процесса: В конце утилита Threads Snapshot подготовит отчёт и спросит у вас, куда его сохранять. Отчёт будет сохранён в обычный .el формат (отчёт EurekaLog), который можно просмотреть в любом текстовом редакторе или в бесплатной утилите EurekaLog Viewer: Восстановить регистрацию предыдущего посмертного отладчика можно запуском Threads Snapshot с параметром » /uninstall «. Что я могу извлечь из отчёта WER? Во-первых, вы можете просмотреть отчёт или его часть локально. Используйте «центр обслуживания» / «журнал стабильности работы» в новых версиях Windows или системный лог — в старых. (Примечание: «Неправильный путь приложения» — это кривой перевод «Faulting Application Path»: «Путь к сбойнувшему приложению».) Альтернативно можно использовать сторонюю утилиту AppCrashView — она показывает существенно больше технических деталей. Сохраняемая в отчёте информация зависит от версии Windows, настроек WER и способа вызова WER из приложения (вызвало ли WER приложение само, через API, или нет; и если через API — то какие параметры указало). Тип вылета Код исключения Далее, смотрим код ошибки. Он же — код исключения. Самые частые коды: $C0000005 — это Access Violation,

EEDFADE — исключение-объект Delphi. Могут быть и другие коды. Если вы видите неопознанный код ошибки — попробуйте использовать утилиту Error Lookup. Она входит в состав трейсера исключений EurekaLog. Если у вас нет EurekaLog, то вы можете скачать бесплатный Tools Pack. Класс исключения Хорошо, с аппаратными исключениями понятно — они отличаются кодом. Но как отличить одно исключение Delphi от другого? Ведь любое исключение, представленное объектом Delphi, в результате будет иметь один и тот же код

EEDFADE? Ну, в ситуации если WER вызван системой или через API уровня WinXP (т.е. функцию ReportFault , как мы сделали это в примере выше) — никак. Вам придётся исследовать слепок (дамп) процесса. Если же вы вызываете WER вручную (аналогично нашему примеру выше) и используете API уровня Vista+ ( WerCreateReport / WerSubmitReport ), то вы можете скопировать имя класса и/или сообщение в один из десяти произвольных строковых параметров отчёта (модифицированный код из примера выше): Да, ужасно много работы ради замены всего двух параметров. Получился не самый тривиальный код, который, к тому же работает без выделения памяти в куче стандартного менеджера памяти. Увы, вызвать WER — это вам не класс с перезаписью виртуального метода унаследовать. Вы можете модифицировать этот код под ваши нужды. Например, выводить имя класса и сообщение через (не используемые WER) параметры WER_P8 и WER_P9, или заменить один из стандартных параметров WER_P0-WER_P7. И насчёт сообщения — будьте аккуратны. Параметры отчёта используются WER для группировки и классификации отчётов, а сообщение об ошибке может быть локализовано, что приведёт к ненужному дублированию отчётов. Экспериментируйте. В любом случае, в результате имеем: Для исключений Delphi теперь показывается имя класса и сообщение исключения. Для аппаратных исключений всё осталось по прежнему, дополнительной информации мы не добавляем. Напомню, эта кастомизация возможна только на Windows Vista и выше. На Windows XP отчёт не изменится. Смещение/адрес исключения Как только вы опознали что именно произошло, осталось понять где это произошло. Для этого посмотрите на параметры имя модуля (не приложения!) и смещение (offset) для этого модуля. Имя модуля покажет модуль, в котором было возбуждено исключение. Смещение — это смещение указателя (на код) от начала этого модуля. К примеру, если исключение возникло по адресу

4AF70A в модуле CrashMe5.exe , который загружен по (стандартному для .exe) адресу

400000, то смещение будет равно:

4AF70A —

400000 = $AF70A. И наоборот, если вы знаете смещение, то, узнав адрес загруженного модуля, можете узнать и где произошло исключение:

0AF70A +

400000 =

4AF70A. Почему вообще используется какое-то смещение? Почему бы в отчёте просто не указать адрес? Потому что этот адрес не будет иметь для вас смысла. К примеру, на машине где произошёл вылет адрес оказался равен A0098E, а модуль был загружен по адресу 990000. Ну а на вашей машине этот модуль оказался загруженным по адресу AD20000. И что теперь? Как вы найдёте место вылета? Адрес A0098E на вашей машине вообще не указывает внутрь модуля! Но если вы знаете смещение (A0098E — 990000 = 98E), то легко определите и адрес вылета: AD20000 + 98E = AD9098E — именно в этом месте произошло исключение. Окей, но как же узнать, что за код выполнялся по этому адресу? Для этого вы можете использовать утилиту Address Lookup. Она входит в состав трейсера исключений EurekaLog. Если у вас нет EurekaLog, то вы можете скачать бесплатный Tools Pack. Запустите утилиту, укажите на модуль вылета и укажите смещение: Утилита покажет вам в каких модуле, классе, методе/функции и строке произошёл вылет. Чтобы это стало возможным утилите необходима отладочная информация. Она понимает множество форматов: map-файлы, TDS/TD32, JCL, DBG и другие. Но это также означает, что вам нужно заранее позаботится об этом. Включите генерацию map-файла (или используйте любой другой из поддерживаемых форматов). Разумеется, эти map-файлы (а также .tds, .dbg, .jdbg и т.п.) нужно будет хранить у себя. И не перепутать два разных файла от двух разных версий одной программы. Чтобы не перепутать — используйте поле версии в версионной информации модуля. Некоторые форматы поддерживают внедрение (собственный формат EurekaLog, TDS/TD32, JCL и т.п.) — вы можете использовать их. Тогда дополнительных файлов хранить не нужно (только сам модуль) и нельзя перепутать версии файлов, что тоже есть хорошо. Но увеличивается размер модуля, что не есть хорошо. Выбор за вами. Альтернативно, вы можете использовать и саму Delphi. Для этого запустите программу (ровно той версии, что упомянута в отчёте) и поставьте её на паузу (Run / Pause), затем откройте окно со списком модулей: View / Debug / Modules и выясните по какому адресу оказался загружен модуль на вашей машине. У вас есть базовый адрес, у вас есть смещение — вычислите полный адрес, сложив два значения. Теперь используйте Search / Go to address и введите полученный абсолютный адрес: Если вы всё сделали правильно, то IDE откроет нужный модуль и ткнёт вас в строчку с ошибкой. Однако у этого способа куча минусов. Во-первых, не только версия программы должна полностью совпадать с модулем, в котором произошёл вылет, вам также необходимо иметь копию всех исходников для этой версии, а также быть 100% уверенными, что при перекомпиляции вы получите точно ту же самую программу. В противном случае адреса будут отличаться и этот метод ткнёт вас в неверное место. Во-вторых, IDE попросту не поддерживает сторонние отладочные форматы. Поэтому если вылет произошёл во внешнем модуле (в частном случае — в модуле ОС), то IDE откроет CPU-отладчик и ткнёт вас в конкретную ассемблерную инструкцию. Никакой дополнительной информации она показать не сможет. На практике хранить все копии исходников, восстанавливать их для исследования каждого вылета, пересобирать программу, гарантировать/проверять её совпадение — крайне трудоёмко и чревато ошибками, поэтому этот способ (с IDE) стоит приберечь только на самый последний случай (когда заранее к нему не подготовились), а сильно предпочтительнее просто генерировать отладочную информацию в одном из форматов и хранить её (либо ещё лучше — внедрять), а затем использовать Address Lookup. P.S. Аналогичную технику можно использовать и для анализа любых других адресов в сообщениях об ошибках. Например, «Runtime error 217 at 004060CD» или «Access Violation at address 005D2500. «. К сожалению, Delphi сделана в этом отношении не совсем грамотно: она показывает абсолютный адрес, а не смещение. В итоге получается, что если ошибка произошла в .exe — вам (возможно) повезло: .exe (почти) всегда грузится по фиксированному адресу

400000. Если же исключение произошло в DLL — вы пролетели, если только вам сильно не повезёт (каким-то образом вы узнаете базовый адрес DLL, или же DLL на вашей машине окажется загруженной по тому же адресу). Стек, переменные и другая информация Окей, с базовой информацией мы разобрались. Но что делать с самой вкусной частью пирога — стеком вызова и, возможно, значениями переменных? К сожалению, в Delphi вы не сделаете ничего. Но вы можете использовать отладчик Microsoft. Для этого вам потребуется следующее: Vista и выше. На Windows XP дампы не создаются. Отчёт вылета должен содержать в себе дамп процесса (дамп процесса также называется мини-дампом, противопоставляя себя полному дампу режима ядра). Дамп процесса — это «слепок» памяти и, возможно, объектов процесса. Если вы вызываете WER вручную, то параметры создания дампа вы указываете самостоятельно — в вызове WER. См. код-пример выше, где мы вызывали WerReportAddDump для добавления мини-дампа в отчёт. Если вылет происходит под управлением системы, то мини-дамп создаётся (или нет) согласно настройке WER. В любом случае, файл мини-дампа будет упомянут в отчёте (файл .mdmp или, реже, .dmp), а ссылки на локальные хранилища я приводил выше (в описании WER). Отладочная информация в формате, которую может понять отладчик Microsoft. Для этого вам нужно создавать отладочную информацию в формате MAP и/или TDS/TD32, а затем использовать конвертер в DBG или PDB. Для этого подойдёт какой-либо из вариантов утилиты map2dbg: https://code.google.com/p/map2dbg/ https://github.com/andremussche/map2dbg https://github.com/garethm/map2dbg https://bitbucket.org/wpostma/map2dbg или утилиты tds2pdb: https://github.com/andremussche/map2dbg/tree/master/tds2pdb https://sourceforge.net/projects/tds2pdb/ Сама Visual Studio или WinDbg. Я решил вынести этот вопрос за рамки статьи, потому что я не уверен, что эта информация нужна по следующим причинам: Это достаточно трудоёмкий процесс. Интеграция не идеальна. К примеру, Visual Studio мне так и не удалось заставить читать DBG/PDB, созданные указанными утилитами. Это работает для WinDbg, но для Delphi-ста изучать WinDbg (отладчик, управляемый в основном текстовыми командами) — весьма нетривиальная задача. Также есть проблемы с 64-разрядными файлами. Намного проще добавить к отчёту произвольный файл(ы) (через WerReportAddFile) — куда вы можете записать стеки, дампы кучи, переменные, логи, скриншоты, рабочие файлы и вообще всё, что душа пожелает. Как мне получать отчёты WER? Для этого необходимо зарегистрировать в Microsoft свою программу (а точнее — каждую версию каждого исполняемого файла). В первую очередь для этого вам потребуется сертификат для цифровой подписи — только так вы можете подтвердить, что вы именно тот человек, который собрал программу. Ранее для этого требовался только особый сертификат от одобренного Microsoft поставщика: VeriSign — стоимостью 0 в год. К счастью, сегодня это не так. Вам подойдёт любой сертификат для цифровой подписи. Ключевые слова для поиска: code signing certificate или Microsoft Authenticode. Стоимость — что-то от в год (при покупке на несколько лет) до 0 — в зависимости от поставщика. Остались и сертификаты за 0, но они теперь уже EV: «Extended Validation». Для них производится более тщательная проверка перед выдачей сертификата вас как компании. Такие подписи получают бонус от Smart-фильтров браузеров, а также требуются для подписи драйверов. Для обычного же разработчика вполне достаточно сертификатов за в год. Только убедитесь, что вы покупаете сертификат именно для подписи исполняемых файлов Windows. Подписывать свои программы — есть хорошо в любом случае, так что, если вы профессионально разрабатываете софт, то сертификат для подписи у вас и так уже есть. Как только у вас появится сертификат, вам, возможно, придётся переконвертировать в подходящий формат. Это бывает редко, но иногда необходимо. Поскольку я понятия не имею, в каком формате у вас сертификат — идите в Google. Вам нужно сконвертировать в формат PFX. Далее, вам потребуется Windows SDK. Не хочу давать прямую ссылку, они меняются. Поищите в Google по ключевым: download Windows SDK. Устанавливаете SDK. Вас там интересует только утилита SignTool. Когда SDK поставили — можно подписывать файлы. Делать это нужно после каждой компиляции для релиза. Можете использовать для этого Post-Build Event в современных IDE, либо что-то вроде FinalBuilder. Если вы не используете FinalBuilder, то вызывать SignTool вам придётся вручную. Как-то так: «C:\Program Files (x86)\Windows Kits.0\bin\x86\signtool.exe» sign /f «C:\путь-к-сертификату\сертификат.pfx» /p «пароль» /tr timestamp-сервер «C:\путь-к-программе\модуль.exe» /d «Краткое описание программы» /du «http://сайт-программы/» где » timestamp-сервер » может быть: http://timestamp.comodoca.com , http://sha256timestamp.ws.symantec.com/sha256/timestamp , http://tsa.starfieldtech.com/ или любым другим timestamp-сервером, поддерживающим RFC 3161. Примечание: все современные сертификаты используют SHA-256 и не распознаются Windows XP и ниже (которая поддерживает только SHA-1). Как только вы подписали файл — можно регистрировать его в Microsoft. Веб-сайт Windows Error Reporting (бывший WinQual) сейчас переехал на sysdev.microsoft.com (который, в свою очередь, в настоящее время находится в процессе переезда на https://developer.microsoft.com/dashboard/hardware — который пока только для разработчиков драйверов). Само собой, потребуется учётная запись Microsoft (Live), а также необходимо будет использовать только Internet Explorer. Регистрация происходит путём подписи файла. Вам предложат скачать .exe файл, вы должны подписать его своим сертификатом и закачать на сайт. Профиль компании будет создан автоматически по сертификату, вы станете администратором. Если же доступа к сертификату у вас нет, то вы можете выбрать одну из уже зарегистрированных компаний и сможете присоединиться к ней после одобрения заявки администратором компании. Выглядит это примерно так (на скриншотах показан процесс для драйверов на developer.microsoft.com, поэтому и упоминается EV; я не могу сделать скриншоты для обычного сертификата, поскольку этот процесс я уже прошёл; напомню, что в настоящее время вам нужно заходить на sysdev.microsoft.com, где произойдёт аналогичный процесс): Как только вы успешно зарегистрировались и вошли — вам будет доступна консоль (dashboard). Большинство опций в ней предназначены для разработчиков драйверов и сертификации приложений — и потому нам не интересны. Фактически, нас интересуют только отчёты (Reports). Но прежде нам нужно сделать ещё две вещи. Первое, что вам придётся в ней сделать — подписать соглашение. Для этого откройте Administration / Legal agreements: Как и ранее — тут куча всего для драйверов и сертификации, что нам не интересно. Выберите в фильтре тип «Signable» (т.е. что можно подписать, но ещё не подписано) и найдите в списке WER. Я не могу показать процесс подписи именно для этого соглашения (потому что у меня оно уже подписано), но вот скриншот для другого соглашения: Важно: вы должны использовать только Internet Explorer и у вас должен стоять Adobe Acrobat. Фактически, тут вам нужно продублировать ваше имя и дату ( Ctrl + C / Ctrl + V ) и нажать «Submit». Наконец, последнее, что нам осталось сделать — зарегистрировать свои программы. Делается это не тривиально. Откройте раздел Reports (где будет написано, что у вас отчётов нет) и проскрольте вниз, ищите ссылку на «Product mapping Tool»: Тут вас поджидает сюрприз, ибо установщик запросит установленный Windows Live Essentials, который. не доступен для скачивания. Придётся искать оффлайн установщик по неофициальным каналам. Короче, гуглите. В любом случае, текущая версия Product Mapping Tool называется аж Microsoft Ecosystem Metadata Exchange (или MEME). Она попросит вас войти в вашу учётку и синхронизируется с сервером Microsoft. Ну вот с помощью её мы и добавляем каждый выпуск (release) нашей программы. Само собой, добавляем Product, а не Driver. Ну и если у вас прям сложная программа, то можете создать ещё Product Group, но это не обязательно. Не забываем добавить и сами файлы (которые уже должны быть подписаны вашим сертификатом). Ещё раз: файл обязан быть подписан! И быть подписан именно вашим сертификатом, на аккаунт которого вы вошли в MEME. Если файл не будет подписан или будет подписан любым другим сертификатом (включая двойную подпись) — MEME покажет ошибку. Ещё один сюрприз тут — утилиту нужно вручную запускать с правами администратора (запускать из: C:\Program Files (x86)\Microsoft Ecosystem Metadata Exchange\MetadataExchange\ ). По крайней мере, на Windows 10. Иначе попытка добавления файла закончится ошибкой недостаточных привилегий (утилита выдаст общее сообщение «ошибка — см. лог», а подробности можно найти в системном логе «Приложения»). Я понятия не имею, что будет, если версионная информация в исполняемом модуле не будет совпадать с мета-информацией, которую вы вводите в MEME. Лучше бы, наверное, чтобы она совпадала как можно лучше — особенно номера версий. Не забудьте опубликовать внесённые изменения — ссылка «Publish Changes» в заголовке: Публикуйтесь — и на этом всё! Конечно, процесс ассоциации нужно будет проводить заново для каждого релиза/версии каждой вашей программы. Но по крайней мере, вы уже зарегистрированы в системе. После этого можно деплоить и крашиться! После регистрации вид раздела Reports изменится на рабочий: Учтите только, что отчётам нужно время, чтобы пройти отправку и сортировку, появляются они не сразу. Задержка будет от нескольких дней до месяца. Вот как будет выглядеть раздел отчётов при поступлении багов: Это общий вид «Product group summary», где создано две версии одного продукта, одна из которых раскрыта. Как вы можете видеть, отчёты сортируются по количеству возникновения. Чем больше произошло вылетов — тем выше в списке запись. Есть также «Company hotlist» — куда попадают самые «популярные» быги из всех ваших продуктов. Подразумевается, что «Company hotlist» будет служить своеобразным списком TO-DO для исправления багов. Вы можете раскрыть любую запись: Будут показана статистика и возможность скачать отчёты. У записей также может не быть никаких данных, кроме статистики. Это потому, что они не собраны. Вы должны щёлкнуть «Request more data»: В любом случае, когда вы скачаете отчёт (.cab или .zip-файл — в зависимости от ОС), в нём будет: Вот два отчёта с разных систем. В обоих случаях присутствует дамп процесса ( minidump.mdmp ), общая информация ( WERInternalMetadata.xml ) и пользовательский файл, присоединённый через WerReportAddFile ( dump.txt ) — куда вы можете сохранить информацию об исключении, стеки вызовов и т.п. WERInternalMetadata.xml выглядит примерно так (полный вариант из отчёта с 6-ю файлами; вариант из отчёта с тремя файлами существенно короче): sysinfo.txt содержит статистику работы системы (что-то вроде performance counters). WERDataCollectionStatus.txt — служебный. А memory.csv содержит список процессов (со статистикой). Что если я хочу использовать отчёты, но не хочу использовать WER? Да, использовать WER для Delphi приложений довольно сложно: Достаточно сложно всё настроить и зарегистрироваться Требуется сертификат цифровой подписи Отчёты доставляются с задержкой, а сам сайт sysdev работает неторопливо Нужно явно регистрировать каждую публикуемую сборку приложения Отчёты с Windows XP (и ранее) практически бесполезны, поскольку не включают в себя дамп и не позволяют приложить пользовательские файлы Невозможно использовать отладчик Delphi или Visual Studio для анализа дампов, будет работать только WinDbg Невозможно анализировать дампы 64-разрядных процессов В случае неконтролируемого вылета (т.е. когда WER вызывает система, а не мы сами), пользовательских данных у отчёта не будет Поэтому вы можете захотеть использовать свой собственный механизм отправки и сбора отчётов. Для этого вам потребуется две вещи: Централизованный сервер для сбора отчётов, включая их сортировку/группировку и возможности просмотра Код отправки вылета приложения на сервер В качестве сервера я крайне рекомендую FogBugz — это единственная известная мне система отслеживания ошибок (среди прочих: Mantis, JIRA, Bugzilla, Redmine), которая была создана с прицелом на автоматический сбор отчётов. Только она поддерживает понятие bug hash / bugID / alias с автоматической группировкой, учётом (count / occurencies) и контролем сбора. Во всех остальных трекерах отсутствует часть возможностей. Будете делать отправку отчётов через REST API FogBugz — ищите по ключевым словам BugzScout и sScoutDescription. К счастью, мы уже рассмотрели все модификации кода, которые вам нужно сделать в вашем приложении выше — теперь вам осталось только поместить ваш код отправки отчёта вместо ReportFault / WerSubmitReport в функцию UnhandledExceptionHandler выше. Что если я не хочу изобретать при этом велосипед? Тогда вам нужно использовать готовое решение. Для Delphi есть два трейсера исключений с поддержкой отправки отчётов в FogBugz: EurekaLog и madExcept. Конечно, они поддерживают не только FogBugz, но и другие трекеры. Более того, если вы хотите использовать WER, то EurekaLog поддерживает и его — присоединяя свой отчёт как дополнительный файл. (Я не уверен, но возможно, что madExcept в режиме Windows Logo Compliant Mode делает что-то аналогичное.) Ну и само собой, трейсеры исключений дадут вам и контекст процессора и стек вызовов. Обработка абстрактных методов в Delphi Автор: Антон Злыгостев НФК «Novosoft Inc» Источник: RSDN Magazine #2 Опубликовано: 18.02.2003 Исправлено: 13.03.2005 Версия текста: 1.0 Введение В языке Object Pascal, используемом средой разработки Delphi, существует механизм виртуальных конструкторов. Поклонникам C++ это кажется ужасной ересью, но виртуальные конструкторы очень удобны для создания экземпляров классов, которые еще не определены на этапе компиляции создающего кода. Такая технология позволяет разрабатывать компонентный код без необходимости реализации фабрик классов. Оборотной стороной этой гибкости является возможность случайно создать экземпляр абстрактного класса, что впоследствии почти неизбежно приведет к вызову одного из абстрактных методов. Если в C++ вызов чисто виртуальной функции требует изрядной ловкости, и практически невозможно произвести его нечаянно, то в Delphi это достигается одним неосторожным движением. К сожалению, встроенные в Delphi механизмы обнаружения и обработки абстрактных методов предоставляют лишь минимум информации об источнике ошибки. Эта статья посвящена реализации улучшенных механизмов обнаружения абстрактных методов и обработки их вызовов. Виртуальные конструкторы Концепция виртуальных конструкторов в Delphi тесно связана с существованием специального типа «ссылка на класс». Совместное их использование позволяет создавать объекты классов, которые еще не существуют в момент компиляции кода, создающего экземпляр класса. Например, в стандартном классе TComponent объявлен виртуальный конструктор Create: ПРИМЕЧАНИЕ Все примеры в этой статье, если не указано иное, компилировались и тестировались на Borland Delphi 5. В сочетании со ссылкой на класс компонента: это позволяет создавать любые компоненты, имея ссылку на их класс, даже если он разработан после компиляции следующего кода: Такая возможность является ключевой для работы механизма чтения форм из DFM-файлов и ресурсов. Кроме того, она может быть полезной и для пользовательского кода, который не связан напрямую с VCL. Наиболее популярные области применения подобной функциональности – это сериализация объектов и регистрация plug-in. Кроме этого, на основе этого механизма и RTTI в Delphi 6 реализованы веб-сервисы. Абстрактные методы Рассмотрим теперь следующий код: На первый взгляд, все в порядке. Есть абстрактный класс, который декларирует некую функциональность, и есть его потомок, реализующий эту функциональность. Мы предполагаем использовать это примерно таким образом: (Реальный код, конечно, будет несколько сложнее. Скорее всего, объекты будут создаваться в одном месте, а использоваться в другом, но суть дела это не меняет.) В чем же проблема? А в том, что нерадивый прикладной программист запросто может передать в нашу процедуру в качестве параметра ссылку на абстрактный класс: Такой код вполне удачно скомпилируется. Что же произойдет во время работы приложения? Простейший эксперимент покажет, что результатом будет выдача исключения EAbstractError в момент вызова метода DoSomeJob. Казалось бы, все в порядке: нарушитель пойман, справедливость восстановлена. Ан нет. EAbstractError – на редкость неинформативный класс. Он не предоставляет никакой информации о контексте и причине ошибки. Если вы – разработчик, и приложение выдало соответствующее сообщение, то у вас есть шанс потратить некоторое время на общение с отладчиком и пошаговое выполнение, чтобы отловить класс-нарушитель. Но если вы скомпилировали свою библиотеку без отладочной информации и исходных текстов, то прикладной программист сможет только гадать, что же он сделал не так. Есть, конечно, весьма простой способ «обойти» проблему – никогда не объявлять абстрактных методов. VCL использует «пустые» определения методов сплошь и рядом. Однако это не путь для настоящих программистов. Хотя бы по той причине, что «пустая» реализация процедуры еще имеет какой-то смысл, но любая функция должна возвращать какое-то значение. Более естественным способом является запрет на создание экземпляров абстрактных классов, как это сделано, например, в C++. Увы, компилятор Delphi ограничится предупреждением: “constructing instance of class … containing abstract methods” . Вывод этого предупреждения можно подавить соответствующими опциями компилятора. Как правило, аккуратные программисты внимательно следят за предупреждениями, выдаваемыми компилятором. Но в ситуации, которая описана выше, «гром не грянет» и причин креститься у программиста не будет. Тестовое приложение Проиллюстрируем технику использования особенностей объектной модели Object Pascal на примере несложного приложения. Наша программа будет предельно простой. Она позволит пользователю ввести два целых числа и сделать с ними набор простых арифметических операций. В реализацию программы будет входить только сложение и умножение, но мы позаботимся о том, чтобы программисты могли помогать пользователю программы идти в ногу со временем, разрабатывая дополнения к программе. Для этого мы будем использовать механизм пакетов времени выполнения (runtime packages). Разработчик дополнительных операторов должен будет реализовать свой класс-наследник и включить его в пакет. Наше приложение будет сканировать текущую папку в поисках файлов с расширением .bpl и динамически загружать их в свое адресное пространство. Для проверки концепции мы создадим пакет расширения, в котором реализуем два класса сложных целочисленных операторов: TPowerOp – оператор возведения в степень и TCnkOp – оператор количества сочетаний. В классе TCnkOp мы «забудем» перекрыть один из абстрактных методов, объявленных в базовом классе. Мы убедимся, что стандартная обработка таких ошибок не дает никакой информации о причинах возникновения ошибки, и построим свою обработку так, чтобы можно было сразу определить, в каком классе и какой метод был оставлен абстрактным. Получение дополнительной информации Чтобы узнать больше о том, что привело к абстрактному вызову, необходимо разобраться с тем, как Delphi реализует обработку абстрактных методов. Стандартный обработчик Если протрассировать вызов абстрактного метода TAbstractObject.DoSomeJob, то выяснится интересная подробность: управление передается в системную процедуру _AbstractError: Эта процедура объявлена в секции implementation модуля System, то есть является недокументированной подробностью реализации Object Pascal и VCL. В ней проверяется, присвоено ли значение указателю AbstractErrorProc, и, если это так, то управление передается по этому адресу. Иначе приложение аварийно завершается с ошибкой 210. Если в проект включен модуль SysUtils (как правило, это так), то этому указателю будет присвоен адрес процедуры SysUtils.AbstractErrorHandler. Эта процедура и выбрасывает исключение EAbstractError, которое так мало говорит об источнике проблем. Усовершенствованный обработчик Из предыдущего раздела можно сделать два вывода: Существует документированный способ зарегистрировать свой обработчик абстрактных вызовов. Несмотря на то, что среда не передает в этот обработчик никаких параметров, функции, которые вызывают наш обработчик, никак не воздействуют на контекст вызова. Последствия, вытекающие из второго вывода, значительно менее «безопасны». Однако из него следует, что можно получить некоторую информацию о контексте, в котором произошла ошибка. Проще говоря, вывод 2 заявляет, что значение псевдопеременной self не изменилось и все еще доступно. Благодаря этому, мы можем произвести «подмену класса». То есть, для того, чтобы отвлечься от способа, которым Delphi передает в методы указатель на объект, мы просто зарегистрируем в качестве обработчика адрес метода объекта: Обратите внимание на код процедуры TAbstractHandler.HandleAbstract – он генерирует исключение с именем класса в качестве текста сообщения. На первый взгляд кажется, что он всегда будет возвращать строку “TAbstractHandler”, но это не так. Дело в том, что мы вызвали метод TAbstractHandler.HandleAbstract на объекте совсем другого класса! Фактически выполняющийся код очень похож на вот такой: В таком примере текст исключения будет содержать “TAbstractObject”. Обычно подобные вызовы приводят к ошибкам, но при соблюдении некоторых правил они вполне безопасны. «Пессимистическая» версия этих правил такова: вызывать «чужой» метод можно только в том случае, если он пользуется только полями и методами общего предка «своего» и «чужого» класса. На практике свободы больше, но для нашего случая ее уже вполне достаточно. Метод HandleAbstract пользуется только методом ClassName, доступным в TObject, который гарантированно является предком всех классов Delphi. ПРЕДУПРЕЖДЕНИЕ Эта методика не работает при вызове абстрактного метода класса. В методах класса self указывает на класс, а не на объект, и используемая подмена некорректна. К сожалению, надежного способа борьбы с этим я не вижу – довольно-таки сложно отличить указатель на VMT от указателя на указатель на VMT. Когда такое исключение возникает во время работы программы, можно посмотреть на объявление указанного класса и найти там метод, который остался абстрактным. Либо, если метод был оставлен абстрактным намеренно, в предположении, что он будет реализован в потомках, можно поискать то место в программе, которое приводит к созданию экземпляра абстрактного класса вместо класса-потомка. Подробнее об этом в следующем разделе. Раннее упреждение Чтобы предотвратить создание экземпляров абстрактных классов, надо, прежде всего, ответить на вопрос: «является ли данный класс абстрактным?». Ответ на этот вопрос прост: «класс является абстрактным, если он содержит абстрактные методы». Сама Delphi не содержит встроенных средств для проверки методов на абстрактность, поэтому такие средства придется изобрести самостоятельно. Чтобы узнать, абстрактен ли метод класса, придется немного покопаться в темных глубинах модуля System при помощи пошаговой отладки. Как мы уже знаем из предыдущего раздела, попытка вызвать абстрактный метод приводит нас в процедуру _AbstractError. Теперь нам необходимо проследить путь, ведущий в эту процедуру. Исследования структуры таблицы виртуальных методов (VMT), создаваемой компилятором, и RTTI вообще, являются интереснейшим процессом, который может доставить любознательному разработчику массу удовольствия. Для тех же, кто не хочет терять время на препарирование системного кода Delphi, я привожу необходимую информацию в готовом к употреблению виде. Структура классов Delphi Устройство данных, размещенных по адресу, задаваемому TClass, является деталью реализации, скрытой от программиста. Относительно безопасно можно делать следующее: Сравнивать ссылки на класс для проверки типа объекта. Вызывать методы класса. Вызывать конструкторы. К сожалению, этой функциональности недостаточно для поиска абстрактных методов. Для такого поиска нам придется заглянуть «под капот» класса, а именно – посмотреть, как работает метод TObject.ClassType. Реализация, конечно, может меняться от версии к версии. В Delphi 5 код предельно лаконичен: Delphi 6 не вносит ничего нового, хотя тот же код на Паскале читается легче, чем на ассемблере: Итак, этот метод возвращает адрес, на который указывают самые первые четыре байта в теле объекта. Нам это вряд ли помогло бы, если бы не знание о совместимости Delphi с COM. Как известно, структура COM-объектов строго стандартизована. В начале объекта должен быть расположен указатель на VMT. Дополнительным подтверждением этому служат константы с именами, начинающимися на vmt*, определенные в модуле System: Как интересно! Часть из них меньше нуля. Судя по именам констант, вплоть до vmtAfterConstruction (смещение -28) расположены указатели на различные интересные данные. Затем идут указатели на виртуальные методы, декларированные в самом TObject: AfterConstruction, BeforeDestruction, Dispatch, DefaultHandler, NewInstance, FreeInstance, Destroy. Затем идут методы с неотрицательными смещениями. Таким образом, указатель, расположенный в начале объекта, ссылается куда-то «в середину» VMT. И эта середина – ровно то место, с которого будут располагаться виртуальные методы, объявленные в классах-потомках. Из названий констант vmtQueryInterface, vmtAddRef и vmtRelease ясно, зачем так сделано – иначе в потомках TObject было бы невозможно реализовать интерфейс IUnknown. Итак, 4 байта, полученных при вызове TObject.ClassType, указывают в начало таблицы виртуальных методов, декларированных в потомках TObject. Этот вывод можно считать «безопасным» до тех пор, пока Delphi поддерживает совместимость с COM. Абстрактные методы Как нам уже известно, выполнение абстрактного вызова приводит нас в магическую процедуру System._AbstractError. Осталось понять, как это происходит. Внимательная трассировка покажет нам со всей неизбежностью, что адрес этой процедуры записывается в те позиции VMT, которые соответствуют абстрактным методам. Таким образом, для любой заданной позиции в VMT можно узнать, реализован ли соответствующий ей метод, сравнив ее значение с адресом процедуры _AbstractError. К сожалению, авторы Delphi позаботились поместить эту процедуру в секцию implementation модуля System, запретив, таким образом, явное получение ее адреса. Конечно, такая мелочь не может остановить настоящих программистов. Получить адрес этой процедуры можно при помощи любого абстрактного метода. Чтобы не зависеть ни от кого, достаточно объявить свой класс с абстрактным методом, и взять адрес метода из VMT. Чтобы не умножать сущностей, разместим весь требуемый код в одном классе: Этот код требует некоторых пояснений. Во-первых, наша процедура AbstractProc объявлена методом класса – это сделано для того, чтобы получить ее адрес без создания экземпляра класса TAbstractHandler. Это не влияет на структуру VMT – методы класса устроены точно так же, только у них self указывает на класс, а не на объект. Во-вторых, для получения адреса используется временная переменная типа procedure of object – указатель на метод. Это самый простой способ вынудить Delphi реально прочитать адрес метода из VMT – попытки взять адрес метода при помощи оператора @ не приведут к желаемому результату. Вместо адреса _AbstractProc будет получен адрес специально сгенерированного псевдометода, который состоит только из инструкции JMP на все тот же адрес _AbstractProc. Судя по всему, этот псевдометод нужен для того, чтобы компилятор мог встроить его вызов в случаях, когда он точно знает класс объекта. В таких ситуациях Delphi не делает косвенного вызова, а подставляет сразу абсолютный адрес метода. Получив указатель на метод класса в переменной TAP, мы выделяем из него указатель на код при помощи документированного приведения к типу SysUtils.TMethod. Однако эти эксперименты мы проводили над классом, который скомпилирован, как часть нашего приложения. В нашем же примере часть классов расположена в отдельном пакете, который компилируется в отдельный файл-библиотеку. Будет ли происходить вызов той же _AbstractProc из таких классов? И если будет, то как? Для получения ответа на эти вопросы необходимо знать о том, как Delphi реализует динамически подключаемые пакеты компонентов. Подробное рассмотрение этой темы выходит за пределы данной статьи. Поэтому я сразу предоставлю здесь результат, пропустив описание своих исследований .bpl-файлов. Да, Delphi строго следит за тем, чтобы в приложение нельзя было загрузить две версии одного и того же модуля в разных пакетах. То есть мы можем быть уверены, что любой абстрактный вызов приведет нас в единственную _AbstractProc. Для этого он пользуется механизмом таблиц импорта, предоставленным форматом PE-файлов Windows. На практике это означает, что соответствующая позиция в VMT будет указывать на фрагмент кода (thunk) следующего вида: Здесь addr – это адрес слота в таблице импорта. По этому адресу лежит настоящий адрес метода. Данная информация позволяет написать код, который сможет отличать указатели на «настоящие» методы от указателей на импортированные методы. Вот этот код: ПРИМЕЧАНИЕ Есть, конечно, определенный риск встретить «настоящий» метод, который будет начинаться с точно такой же инструкции косвенного перехода. Но вероятность этого весьма мала потому, что стандартный пролог метода (то, во что компилируется ключевое слово begin) выглядит по-другому. Для того, чтобы его изменить, от разработчика класса требуются специальные усилия. А реализация _AbstractProc начинается с инструкции CMP и тоже нас устраивает в смысле определения реального адреса. Соответственно этому, мы все указатели на код, который потенциально может быть импортирован, будем прогонять через этот метод. В частности, придется слегка модифицировать TAbstractHandler.AbsProcAddress: Итак, у нас есть образец позиции в VMT, которая соответствует абстрактным методам. Теперь можно оборудовать наш класс методом проверки на абстрактность: Абстрактные классы Теперь мы легко можем проверить любой указатель на предмет совпадения с адресом абстрактного метода. Однако эта возможность мало поможет в ловле ошибок, т.к. нам придется явно проверять все подозрительные методы. Возникает естественное желание реализовать способ проверки всего класса на абстрактность. Идея такой проверки кажется очевидной: мы уже умеем получать адрес первой позиции в VMT, и достаточно пройти по всей таблице в поисках магического адреса. Чтобы это сделать, нужно как-то определить адрес конца VMT. Никаких стандартных способов это сделать не существует. Я потратил довольно много времени на анализ окрестностей VMT, но обнаружил только то, что в Delphi 5 различные RTTI-данные, относящиеся к классу, расположены в непосредственной близости от VMT. В частности, таблица имен полей, таблица имен методов, таблица динамических методов, имя класса, и информация о типе идут после VMT именно в порядке перечисления. А таблица интерфейсов, реализуемых классом, обычно расположена до начала VMT. Это не слишком-то надежные предположения, так что для определения конца VMT мы будем использовать наименьший из указателей, хранящихся в документированных полях VMT: При этом мы будем проверять, что значения этого указателя больше адреса VMT: После определения адресов начала и конца VMT проверка всех методов класса на абстрактность является тривиальной задачей. Осталось только добавить в наш код различные украшения типа определения класса-предка, в котором был декларирован абстрактный метод, форматирования текстов исключений по вкусу и так далее. Заключение Итак, теперь у нас есть все, чтобы закончить реализацию усовершенствованного обработчика абстрактных вызовов. Во-первых, мы получаем интерфейс для явной проверки методов и классов на абстрактность. Я рекомендую встраивать вызов проверки класса на абстрактность в конструкторы пользовательских классов, которые предполагается создавать динамически (например, компонентов). Во-вторых, наш класс зарегистрирует обработчик, который будет не только определять имя класса, на объекте которого был произведен абстрактный вызов, но и выполнять поиск абстрактных методов в этом классе. Полный исходный код модуля приведен в файле Достаточно добавить его в любой проект, и сообщения об абстрактных вызовах станут значительно более информативными. Единственным улучшением, которое я бы внес в код обработчика абстрактных вызовов, является корректировка обработки абстрактных методов класса. Как я уже говорил, данная методика предполагает, что в переменной self хранится указатель на объект, и скорее всего приведет к AV, если на самом деле там хранится указатель на класс. Есть идея реализовать пару функций: основываясь на предположении о том, что в корректной VMT по смещению vmtSelfPtr должен лежать адрес ее начала: Проверку этой гипотезы и усовершенствование кода я оставляю читателям. Проверка боем Проверим работоспособность созданного обработчика на нашем примере. Архив AbstractCalc.zip содержит две версии приложения: SuperCalc.dpr – это первоначальный вариант. SmartCalc.dpr получен из него путем добавления AbstractHandler.pas. ПРИМЕЧАНИЕ Вы можете скомпилировать примеры, следуя инструкциям в файле ReadMe.txt. При запуске первой версии калькулятора попытка выбрать из списка оператор количества сочетаний приводит к появлению следующего сообщения: Рисунок 1: Краткость – сестра таланта Улучшенная версия сможет рассказать об ошибке более подробно: Рисунок 2: Действие ‘сыворотки правды’ на приложение-пример RunError — Процедура Delphi С целью поддержки структурной обработки исключительных ситуаций (exception) в Delphi введены новые расширения языка Pascal. В данной статье будет дано описание того, что из себя представляет такая обработка, почему она полезна, будут приведены соответствующий синтаксис Object Pascal и примеры использования исключительных ситуаций в Delphi. Структурная обработка исключительных ситуаций Структурная обработка исключительных ситуаций — это система, позволяющая программисту при возникновении ошибки (исключительной ситуации) связаться с кодом программы, подготовленным для обработки такой ошибки. Это выполняется с помощью языковых конструкций, которые как бы «охраняют» фрагмент кода программы и определяют обработчики ошибок, которые будут вызываться, если что-то пойдет не так в «охраняемом» участке кода. В данном случае понятие исключительной ситуации относится к языку и не нужно его путать с системными исключительными ситуациями (hardware exceptions), такими как General Protection Fault. Эти исключительные ситуации обычно используют прерывания и особые состояния «железа» для обработки критичной системной ошибки; исключительные ситуации в Delphi же независимы от «железа», не используют прерываний и используются для обработки ошибочных состояний, с которыми подпрограмма не готова иметь дело. Системные исключительные ситуации, конечно, могут быть перехвачены и преобразованы в языковые исключительные ситуации, но это только одно из применений языковых исключительных ситуаций. При традиционной обработке ошибок, ошибки, обнаруженные в процедуре обычно передаются наружу (в вызывавшую процедуру) в виде возвращаемого значения функции, параметров или глобальных переменных (флажков). Каждая вызывающая процедура должна проверять результат вызова на наличие ошибки и выполнять соответствующие действия. Часто, это просто выход еще выше, в более верхнюю вызывающую процедуру и т.д. : функция A вызывает B, B вызывает C, C обнаруживает ошибку и возвращает код ошибки в B, B проверяет возвращаемый код, видит, что возникла ошибка и возвращает код ошибки в A, A проверяет возвращаемый код и выдает сообщение об ошибке либо решает сделать что-нибудь еще, раз первая попытка не удалась. Такая «пожарная бригада» для обработки ошибок трудоемка, требует написания большого количества кода в котором можно легко ошибиться и который трудно отлаживать. Одна ошибка в коде программы или переприсвоение в цепочке возвращаемых значений может привести к тому, что нельзя будет связать ошибочное состояние с положением дел во внешнем мире. Результатом будет ненормальное поведение программы, потеря данных или ресурсов, или крах системы. Структурная обработка исключительной ситуации замещает ручную обработку ошибок автоматической, сгенерированной компилятором системой уведомления. В приведенном выше примере, процедура A установила бы «охрану» со связанным обработчиком ошибки на фрагмент кода, в котором вызывается B. B просто вызывает C. Когда C обнаруживает ошибку, то создает (raise) исключительную ситуацию. Специальный код, сгенерированный компилятором и встроенный в Run-Time Library (RTL) начинает поиск обработчика данной исключительной ситуации. При поиске «защищенного» участка кода используется информация, сохраненная в стеке. В процедурах C и B нет такого участка, а в A — есть. Если один из обработчиков ошибок, которые используются в A, подходит по типу для возникшей в C исключительной ситуации, то программа переходит на его выполнение. При этом, область стека, используемая в B и C, очищается; выполнение этих процедур прекращается. Если в A нет подходящего обработчика, то поиск продолжается в более верхнем уровне, и так может идти, пока поиск не достигнет подходящего обработчика ошибок среди используемых по умолчанию обработчиков в RTL. Обработчики ошибок из RTL только показывают сообщение об ошибке и форсированно прекращают выполнение программы. Любая исключительная ситуация, которая осталась необработанной, приведет к прекращению выполнения приложения. Без проверки возвращаемого кода после каждого вызова подпрограммы, код программы должен быть более простым, а скомпилированный код — более быстрым. При наличии исключительных ситуаций подпрограмма B не должна содержать дополнительный код для проверки возвращаемого результата и передачи его в A. B ничего не должна делать для передачи исключительной ситуации, возникшей в C, в процедуру A — встроенная система обработки исключительных ситуаций делает всю работу. Данная система называется структурной, поскольку обработка ошибок определяется областью «защищенного» кода; такие области могут быть вложенными. Выполнение программы не может перейти на произвольный участок кода; выполнение программы может перейти только на обработчик исключительной ситуации активной программы. Модель исключительных ситуаций в Delphi Модель исключительных ситуаций в Object Pascal является невозобновляемой(non-resumable). При возникновении исключительной ситуации Вы уже не сможете вернуться в точку, где она возникла, для продолжения выполнения программы (это позволяет сделать возобновляемая(resumable) модель). Невозобновляемые исключительные ситуации разрушают стек, поскольку они сканируют его в поисках обработчика; в возобновляемой модели необходимо сохранять стек, состояние регистров процессора в точке возникновения ошибки и выполнять поиск обработчика и его выполнение в отдельном стеке. Возобновляемую систему обработки исключительных ситуаций гораздо труднее создать и применять, нежели невозобновляемую. Синтаксис обработки исключительных ситуаций Теперь, когда мы рассмотрели, что такое исключительные ситуации, давайте дадим ясную картину, как они применяются. Новое ключевое слово, добавленное в язык Object Pascal — try. Оно используется для обозначения первой части защищенного участка кода. Существует два типа защищенных участков: try..except try..finally Первый тип используется для обработки исключительных ситуаций. Его синтаксис: Для уверенности в том, что ресурсы, занятые вашим приложением, освободятся в любом случае, Вы можете использовать конструкцию второго типа. Код, расположенный в части finally, выполняется в любом случае, даже если возникает исключительная ситуация. Соответствующий синтаксис: Примеры обработки исключительных ситуаций Ниже приведены процедуры A,B и C, обсуждавшиеся ранее, воплощенные в новом синтаксисе Object Pascal: При ErrorCondition = True программа выдаст: Возможно вас удивила декларация типа ‘ESampleError = . Описание новой объектной модели дается в других уроках. Здесь же достаточно сказать, что исключительные ситуации (exceptions) являются классами, частью новой объектной модели. Процедура C проверяет наличие ошибки (в нашем случае это значение глобальной переменной) и, если она есть (а это так), C вызывает(raise) исключительную ситуацию класса ESampleError. Процедура A помещает часть кода в блок try..except. Первая часть этого блока содержит часть кода, аналогично конструкции begin..end. Эта часть кода завершается ключевым словом except, далее следует один или более обработчиков исключительных ситуаций on xxxx do yyyy, далее может быть включен необязательный блок else, вся конструкция заканчивается end;. В конструкции, назначающей определенную обработку для конкретной исключительной ситуации (on xxxx do yyyy), после резервного слова on указывается класс исключительной ситуации, а после do следует собственно код обработки данной ошибки. Если возникшая исключительная ситуация подходит по типу к указанному после on, то выполнение программы переходит сюда (на код после do). Исключительная ситуация подходит в том случае, если она того же класса, что указан в on, либо является его потомком. Например, в случае on EFileNotFound обрабатываться будет ситуация, когда файл не найден. А в случае on EFileIO — все ошибки при работе с файлами, в том числе и предыдущая ситуация. В блоке else обрабатываются все ошибки, не обработанные до этого. Приведенные в примере процедуры содержат код (строка с writeln), который отображает путь выполнения программы. Когда C вызывает exception, программа сразу переходит на обработчик ошибок в процедуре A, игнорируя оставшуюся часть кода в процедурах B и C. После того, как найден подходящий обработчик ошибки, поиск оканчивается. После выполнения кода обработчика, программа продолжает выполняться с оператора, стоящего после слова end блока try..except (в примере — writeln(‘Exit A’)). Конструкция try..except подходит, если известно, какой тип ошибок нужно обрабатывать в конкретной ситуации. Но что делать, если требуется выполнить некоторые действия в любом случае, произошла ошибка или нет? Это тот случай, когда понадобится конструкция try..finally. Рассмотрим модифицированную процедуру B: Если C вызывает исключительную ситуацию, то программа уже не возвращается в процедуру B. А что же с теми 1000 байтами памяти, захваченными в B? Строка FreeMem(P,1000) не выполнится и Вы потеряете кусок памяти. Как это исправить? Нужно ненавязчиво включить процедуру B в процесс, например: Если в A поместить вызов NewB вместо B, то программа выведет сообщения следующим образом: Код в блоке finally выполнится при любой ошибке, возникшей в соответствующем блоке try. Он же выполнится и в том случае, если ошибки не возникло. В любом случае память будет освобождена. Если возникла ошибка, то сначала выполняется блок finally, затем начинается поиск подходящего обработчика. В штатной ситуации, после блока finally программа переходит на следующее предложение после блока. Почему вызов GetMem не помещен внутрь блока try? Этот вызов может окончиться неудачно и вызвать exception EOutOfMemory. Если это произошло, то FreeMem попытается освободить память, которая не была распределена. Когда мы размещаем GetMem вне защищаемого участка, то предполагаем, что B сможет получить нужное количество памяти, а если нет, то более верхняя процедура получит уведомление EOutOfMemory. А что, если требуется в B распределить 4 области памяти по схеме все-или-ничего? Если первые две попытки удались, а третья провалилась, то как освободить захваченную область память? Можно так: Установив сперва указатели в NIL, далее можно определить, успешно ли прошел вызов GetMem. Оба типа конструкции try можно использовать в любом месте, допускается вложенность любой глубины. Исключительную ситуацию можно вызывать внутри обработчика ошибки, конструкцию try можно использовать внутри обработчика исключительной ситуации. Вызов исключительной ситуации В процедуре C из примера мы уже могли видеть, как должна поступать программа при обнаружении состояния ошибки — она вызывает исключительную ситуацию: После ключевого слова raise следует код, аналогичный тому, что используется для создания нового экземпляра класса. Действительно, в момент вызова исключительной ситуации создается экземпляр указанного класса; данный экземпляр существует до момента окончания обработки исключительной ситуации и затем автоматически уничтожается. Вся информация, которую нужно сообщить в обработчик ошибки передается в объект через его конструктор в момент создания. Почти все существующие классы исключительных ситуаций являются наследниками базового класса Exception и не содержат новых свойств или методов. Класс Exception имеет несколько конструкторов, какой из них конкретно использовать — зависит от задачи. Описание класса Exception можно найти в on-line Help. Доступ к экземпляру объекта exception До сих пор мы рассматривали механизмы защиты кода и ресурсов, логику работы программы в исключительной ситуации. Теперь нужно немного разобраться с тем, как же обрабатывать возникшую ошибку. А точнее, как получить дополнительную информацию о коде ошибки, текст сообщения и т.п. Как уже говорилось, при вызове исключительной ситуации (raise) автоматически создается экземпляр соответствующего класса, который и содержит информацию об ошибке. Весь вопрос в том, как в обработчике данной ситуации получить доступ к этому объекту. Рассмотрим модифицированную процедуру A в нашем примере: Здесь все изменения внесены в строку Пример демонстрирует еще одно новшество в языке Object Pascal — создание локальной переменной. В нашем примере локальной переменной является ESE — это тот самый экземпляр класса ESampleError, который был создан в процедуре C в момент вызова исключительного состояния. Переменная ESE доступна только внутри блока do. Свойство Message объекта ESE содержит сообщение, которое было передано в конструктор Create в процедуре C. Есть еще один способ доступа к экземпляру exception — использовать функцию ExceptionObject: Предопределенные обработчики исключительных ситуаций Ниже Вы найдете справочную информацию по предопределенным исключениям, необходимую для профессионального программирования в Delphi. Exception — базовый класс-предок всех обработчиков исключительных ситуаций. EAbort — «скрытое» исключение. Используйте его тогда, когда хотите прервать тот или иной процесс с условием, что пользователь программы не должен видеть сообщения об ошибке. Для повышения удобства использования в модуле SysUtils предусмотрена процедура Abort, определенная, как: EComponentError — вызывается в двух ситуациях: при попытке регистрации компоненты за пределами процедуры Register; когда имя компоненты не уникально или не допустимо. EConvertError — происходит в случае возникновения ошибки при выполнении функций StrToInt и StrToFloat, когда конвертация строки в соответствующий числовой тип невозможна. EInOutError — происходит при ошибках ввода/вывода при включенной директиве <$I+>. EIntError — предок исключений, случающихся при выполнении целочисленных операций. EDivByZero — вызывается в случае деления на ноль, как результат RunTime Error 200. EIntOverflow — вызывается при попытке выполнения операций, приводящих к переполнению целых переменных, как результат RunTime Error 215 при включенной директиве <$Q+>. ERangeError — вызывается при попытке обращения к элементам массива по индексу, выходящему за пределы массива, как результат RunTime Error 201 при включенной директиве <$R+>. EInval — General Protection Fault. Соответствует RunTime Error 216. EInval >Delphi, обладая прекрасными средствами доступа к данным, основывающимися на интерфейсе >Особенно важны два свойства класса EDBEngineError : Errors — список всех ошибок, находящихся в стеке ошибок BDE. Индекс первой ошибки 0; ErrorCount — количество ошибок в стеке. Объекты, содержащиеся в Errors, имеют тип TDBError. Доступные свойства класса TDBError: ErrorCode — код ошибки, возвращаемый Borland Database Engine; Category — категория ошибки, описанной в ErrorCode; SubCode — ‘субкод’ ошибки из ErrorCode; NativeError — ошибка, возвращаемая сервером БД. Если NativeError 0, то ошибка в ErrorCode не от сервера; Message — сообщение, переданное сервером, если NativeError не равно 0; сообщение BDE — в противном случае. EDBEditError — наследник Exception ; вызывается, когда данные не совместимы с маской ввода, наложенной на поле. Объявлено в модуле Mask. Заключение Данный урок должен был дать вам достаточно информации для того, чтобы начать исследование того, как Вы можете использовать систему обработки исключительных ситуаций в вашей программе. Вы, конечно, можете обрабатывать ошибки и без привлечения этой системы; но с ней Вы получите лучшие результаты с меньшими усилиями. Процедуры Delphi В процессе программирования на языке Delphi, как и на любом другом языке программирования, основная задача программиста состоит в том, чтобы разработать процедуры (подпрограммы) для обработки определенных событий. Как только это событие возникло, проводится автозапуск процедуры обработки события (если возникло соответствующее событие, Delphi осуществляет вызов процедуры обработки возникшего события). Именно эту процедуру разработчик программы и должен описать. Главное отличие процедуры от функции состоит в том, что есть связь между именем функции и значением. Таким образом, возможно использование имени функции в разнообразных выражениях. Организация процедуры Delphi по сути ничем не отличается от организации функции. Как и для функции, в начале каждой процедуры указывают ее заголовок. Затем называют эту процедуру каким-нибудь именем. После имени в скобках следует перечень параметров, заканчивающийся символом «;» и потом уже описывают разделы констант, типов, переменных и инструкций. Общий вид процедуры Delphi: Общий вид процедуры Delphi выглядит следующим образом: В приведенном выше примере, как мы видим, в состав заголовка процедуры сначала входит служебное словоprocedure. Далее за этим ключевым словом следом располагается имя процедуры, использующееся при вызове процедуры, обеспечивая активизацию ее выполнения. Если процедура обладает какими-либо параметрами, то программист их указывает в скобках, сразу после имени процедуры. В конце заголовка процедуры ставится символ «;». В случае, когда в процедуре имеются именованные константы, программист объявляет их в разделеconst. Далее за этим разделом идет раздел type, использующийся для объявления типов. После располагается раздел var, содержащий все переменные, которые впоследствии программист использует в своей программе. Затем следует раздел инструкций , которые включает в себя исполняемые инструкции данной процедуры. Примеры процедуры Delphi: Приведем пример процедуры Delphi, вычисляющей стоимость некоторой покупки:
  • Блог GunSmoker-а
  • 8 мая 2020 г.
  • Windows Error Reporting и Delphi
  • Содержание
  • Что происходит при сбое
  • История Windows Error Reporting
  • 16-битные Windows
  • 32-битные Windows
  • Windows XP
  • Windows Vista и позднее
  • Приложение скрытно завершается — что делать?
  • Что Delphi делает не так
  • Когда приложение Delphi может привести к вызову WER?
  • Необработанное исключение в не-RTL потоке
  • Повреждение стека/обработчика
  • Переполнение стека
  • Двойное переполнение стека
  • Вызов UnhandledExceptionFilter
  • Перенаправление на JIT-отладчик
  • Ручной вызов WER
  • Настройка Delphi-приложений для отправки отчётов
  • Зачем это делать
  • Лирическое отступление для тех, кому вообще не нравится концепция отправки отчётов
  • Настройка посмертного отладчика
  • Использование Threads Snapshot в качестве посмертного отладчика
  • Что я могу извлечь из отчёта WER?
  • Тип вылета
  • Код исключения
  • Класс исключения
  • Смещение/адрес исключения
  • Стек, переменные и другая информация
  • Как мне получать отчёты WER?
  • Что если я хочу использовать отчёты, но не хочу использовать WER?
  • Что если я не хочу изобретать при этом велосипед?
  • Обработка абстрактных методов в Delphi
  • Введение
  • Виртуальные конструкторы
  • Абстрактные методы
  • Тестовое приложение
  • Получение дополнительной информации
  • Стандартный обработчик
  • Усовершенствованный обработчик
  • Раннее упреждение
  • Структура классов Delphi
  • Абстрактные методы
  • Абстрактные классы
  • Заключение
  • Проверка боем
  • RunError — Процедура Delphi
  • Структурная обработка исключительных ситуаций
  • Модель исключительных ситуаций в Delphi
  • Синтаксис обработки исключительных ситуаций
  • Примеры обработки исключительных ситуаций
  • Вызов исключительной ситуации
  • Доступ к экземпляру объекта exception
  • Предопределенные обработчики исключительных ситуаций
  • Заключение
  • Процедуры Delphi
  • Общий вид процедуры Delphi:
  • Примеры процедуры Delphi:
  • RunError — Процедура Delphi

    С целью поддержки структурной обработки исключительных ситуаций (exception) в Delphi введены новые расширения языка Pascal. В данной статье будет дано описание того, что из себя представляет такая обработка, почему она полезна, будут приведены соответствующий синтаксис Object Pascal и примеры использования исключительных ситуаций в Delphi.

    Структурная обработка исключительных ситуаций

    Структурная обработка исключительных ситуаций — это система, позволяющая программисту при возникновении ошибки (исключительной ситуации) связаться с кодом программы, подготовленным для обработки такой ошибки. Это выполняется с помощью языковых конструкций, которые как бы «охраняют» фрагмент кода программы и определяют обработчики ошибок, которые будут вызываться, если что-то пойдет не так в «охраняемом» участке кода. В данном случае понятие исключительной ситуации относится к языку и не нужно его путать с системными исключительными ситуациями (hardware exceptions), такими как General Protection Fault. Эти исключительные ситуации обычно используют прерывания и особые состояния «железа» для обработки критичной системной ошибки; исключительные ситуации в Delphi же независимы от «железа», не используют прерываний и используются для обработки ошибочных состояний, с которыми подпрограмма не готова иметь дело. Системные исключительные ситуации, конечно, могут быть перехвачены и преобразованы в языковые исключительные ситуации, но это только одно из применений языковых исключительных ситуаций.

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

    Такая «пожарная бригада» для обработки ошибок трудоемка, требует написания большого количества кода в котором можно легко ошибиться и который трудно отлаживать.

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

    Структурная обработка исключительной ситуации замещает ручную обработку ошибок автоматической, сгенерированной компилятором системой уведомления. В приведенном выше примере, процедура A установила бы «охрану» со связанным обработчиком ошибки на фрагмент кода, в котором вызывается B. B просто вызывает C. Когда C обнаруживает ошибку, то создает (raise) исключительную ситуацию. Специальный код, сгенерированный компилятором и встроенный в Run-Time Library (RTL) начинает поиск обработчика данной исключительной ситуации. При поиске «защищенного» участка кода используется информация, сохраненная в стеке. В процедурах C и B нет такого участка, а в A — есть. Если один из обработчиков ошибок, которые используются в A, подходит по типу для возникшей в C исключительной ситуации, то программа переходит на его выполнение. При этом, область стека, используемая в B и C, очищается; выполнение этих процедур прекращается.

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

    Без проверки возвращаемого кода после каждого вызова подпрограммы, код программы должен быть более простым, а скомпилированный код — более быстрым. При наличии исключительных ситуаций подпрограмма B не должна содержать дополнительный код для проверки возвращаемого результата и передачи его в A. B ничего не должна делать для передачи исключительной ситуации, возникшей в C, в процедуру A — встроенная система обработки исключительных ситуаций делает всю работу.

    Данная система называется структурной, поскольку обработка ошибок определяется областью «защищенного» кода; такие области могут быть вложенными. Выполнение программы не может перейти на произвольный участок кода; выполнение программы может перейти только на обработчик исключительной ситуации активной программы.

    Модель исключительных ситуаций в Delphi

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

    Синтаксис обработки исключительных ситуаций

    Теперь, когда мы рассмотрели, что такое исключительные ситуации, давайте дадим ясную картину, как они применяются. Новое ключевое слово, добавленное в язык Object Pascal — try. Оно используется для обозначения первой части защищенного участка кода. Существует два типа защищенных участков:

    • try..except
    • try..finally

    Первый тип используется для обработки исключительных ситуаций. Его синтаксис:

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

    Примеры обработки исключительных ситуаций

    Ниже приведены процедуры A,B и C, обсуждавшиеся ранее, воплощенные в новом синтаксисе Object Pascal:

    При ErrorCondition = True программа выдаст:

    Возможно вас удивила декларация типа ‘ESampleError = . Описание новой объектной модели дается в других уроках. Здесь же достаточно сказать, что исключительные ситуации (exceptions) являются классами, частью новой объектной модели.

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

    Процедура A помещает часть кода в блок try..except. Первая часть этого блока содержит часть кода, аналогично конструкции begin..end. Эта часть кода завершается ключевым словом except, далее следует один или более обработчиков исключительных ситуаций on xxxx do yyyy, далее может быть включен необязательный блок else, вся конструкция заканчивается end;. В конструкции, назначающей определенную обработку для конкретной исключительной ситуации (on xxxx do yyyy), после резервного слова on указывается класс исключительной ситуации, а после do следует собственно код обработки данной ошибки. Если возникшая исключительная ситуация подходит по типу к указанному после on, то выполнение программы переходит сюда (на код после do). Исключительная ситуация подходит в том случае, если она того же класса, что указан в on, либо является его потомком. Например, в случае on EFileNotFound обрабатываться будет ситуация, когда файл не найден. А в случае on EFileIO — все ошибки при работе с файлами, в том числе и предыдущая ситуация. В блоке else обрабатываются все ошибки, не обработанные до этого.

    Приведенные в примере процедуры содержат код (строка с writeln), который отображает путь выполнения программы. Когда C вызывает exception, программа сразу переходит на обработчик ошибок в процедуре A, игнорируя оставшуюся часть кода в процедурах B и C.

    После того, как найден подходящий обработчик ошибки, поиск оканчивается. После выполнения кода обработчика, программа продолжает выполняться с оператора, стоящего после слова end блока try..except (в примере — writeln(‘Exit A’)).

    Конструкция try..except подходит, если известно, какой тип ошибок нужно обрабатывать в конкретной ситуации. Но что делать, если требуется выполнить некоторые действия в любом случае, произошла ошибка или нет? Это тот случай, когда понадобится конструкция try..finally.

    Рассмотрим модифицированную процедуру B:

    Если C вызывает исключительную ситуацию, то программа уже не возвращается в процедуру B. А что же с теми 1000 байтами памяти, захваченными в B? Строка FreeMem(P,1000) не выполнится и Вы потеряете кусок памяти. Как это исправить? Нужно ненавязчиво включить процедуру B в процесс, например:

    Если в A поместить вызов NewB вместо B, то программа выведет сообщения следующим образом:

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

    Почему вызов GetMem не помещен внутрь блока try? Этот вызов может окончиться неудачно и вызвать exception EOutOfMemory. Если это произошло, то FreeMem попытается освободить память, которая не была распределена. Когда мы размещаем GetMem вне защищаемого участка, то предполагаем, что B сможет получить нужное количество памяти, а если нет, то более верхняя процедура получит уведомление EOutOfMemory.

    А что, если требуется в B распределить 4 области памяти по схеме все-или-ничего? Если первые две попытки удались, а третья провалилась, то как освободить захваченную область память? Можно так:

    Установив сперва указатели в NIL, далее можно определить, успешно ли прошел вызов GetMem.

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

    Вызов исключительной ситуации

    В процедуре C из примера мы уже могли видеть, как должна поступать программа при обнаружении состояния ошибки — она вызывает исключительную ситуацию:

    После ключевого слова raise следует код, аналогичный тому, что используется для создания нового экземпляра класса. Действительно, в момент вызова исключительной ситуации создается экземпляр указанного класса; данный экземпляр существует до момента окончания обработки исключительной ситуации и затем автоматически уничтожается. Вся информация, которую нужно сообщить в обработчик ошибки передается в объект через его конструктор в момент создания.

    Почти все существующие классы исключительных ситуаций являются наследниками базового класса Exception и не содержат новых свойств или методов. Класс Exception имеет несколько конструкторов, какой из них конкретно использовать — зависит от задачи. Описание класса Exception можно найти в on-line Help.

    Доступ к экземпляру объекта exception

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

    Как уже говорилось, при вызове исключительной ситуации (raise) автоматически создается экземпляр соответствующего класса, который и содержит информацию об ошибке. Весь вопрос в том, как в обработчике данной ситуации получить доступ к этому объекту.

    Рассмотрим модифицированную процедуру A в нашем примере:

    Здесь все изменения внесены в строку

    Пример демонстрирует еще одно новшество в языке Object Pascal — создание локальной переменной. В нашем примере локальной переменной является ESE — это тот самый экземпляр класса ESampleError, который был создан в процедуре C в момент вызова исключительного состояния. Переменная ESE доступна только внутри блока do. Свойство Message объекта ESE содержит сообщение, которое было передано в конструктор Create в процедуре C.

    Есть еще один способ доступа к экземпляру exception — использовать функцию ExceptionObject:

    Предопределенные обработчики исключительных ситуаций

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

    • Exception — базовый класс-предок всех обработчиков исключительных ситуаций.
    • EAbort — «скрытое» исключение. Используйте его тогда, когда хотите прервать тот или иной процесс с условием, что пользователь программы не должен видеть сообщения об ошибке. Для повышения удобства использования в модуле SysUtils предусмотрена процедура Abort, определенная, как:
    • EComponentError — вызывается в двух ситуациях:
      1. при попытке регистрации компоненты за пределами процедуры Register;
      2. когда имя компоненты не уникально или не допустимо.
    • EConvertError — происходит в случае возникновения ошибки при выполнении функций StrToInt и StrToFloat, когда конвертация строки в соответствующий числовой тип невозможна.
    • EInOutError — происходит при ошибках ввода/вывода при включенной директиве <$I+>.
    • EIntError — предок исключений, случающихся при выполнении целочисленных операций.
      • EDivByZero — вызывается в случае деления на ноль, как результат RunTime Error 200.
      • EIntOverflow — вызывается при попытке выполнения операций, приводящих к переполнению целых переменных, как результат RunTime Error 215 при включенной директиве <$Q+>.
      • ERangeError — вызывается при попытке обращения к элементам массива по индексу, выходящему за пределы массива, как результат RunTime Error 201 при включенной директиве <$R+>.
    • EInval — General Protection Fault. Соответствует RunTime Error 216.
    • EInval >Delphi, обладая прекрасными средствами доступа к данным, основывающимися на интерфейсе >Особенно важны два свойства класса EDBEngineError : Errors — список всех ошибок, находящихся в стеке ошибок BDE. Индекс первой ошибки 0;
      ErrorCount — количество ошибок в стеке.
      Объекты, содержащиеся в Errors, имеют тип TDBError. Доступные свойства класса TDBError:
      ErrorCode — код ошибки, возвращаемый Borland Database Engine;
      Category — категория ошибки, описанной в ErrorCode;
      SubCode — ‘субкод’ ошибки из ErrorCode;
      NativeError — ошибка, возвращаемая сервером БД. Если NativeError 0, то ошибка в ErrorCode не от сервера;
      Message — сообщение, переданное сервером, если NativeError не равно 0; сообщение BDE — в противном случае.
  • EDBEditError — наследник Exception ; вызывается, когда данные не совместимы с маской ввода, наложенной на поле. Объявлено в модуле Mask.
  • Заключение

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

    RunError — Процедура Delphi

    Making sure you have your e111 for your
    holiday is absolutely essential

    A new book on thinking . now on Amazon

    Delphi Basics
    Delphi Basics supplies reference up to release XE1
    This web site provides help and reference for the fundamentals of the Delphi© language. It gives an introduction to the Delphi Object Oriented Language for newcomers, and provides a ready reference for experienced programmers.

    It limits itself to a broad base of the language in order to keep things simple and manageable. The approach taken is to present the most useable basics of Delphi Programming in as accessible a manner as possible.

    Feel free to contact the author about any aspects of the site. Every effort will be made to respond to all emails received.

    New : An independent review of «Learn to program in Pascal», an online course for those starting out on Delphi programming.

    Currently at a 60% discount

    Learn Javascript course

    Currently at a 70% discount

    Delphi Basics as a downloadable Windows program Download the Windows program : now only Ј5

    A complete version of the web site has been converted into a Windows executable.

    It looks and behaves like the site, but with the added benefits of :

    • No adverts
    • Search facility for finding Run Time Library entries and .Net Methods.
    • Fast access to 1,000+ pages of tutorial/reference pages — the full site and more
    • System.Drawing.Graphics .Net >Download the Windows program : now only Ј5

    Learn Delphi TV
    Learn Delphi with easy tutorial videos.

    Ежедневник, ошибка RunError(100)

    Здравствуйте, у меня куча ошибок и очень невнятный код(не плохое начало?).
    когда записываю инф. в файл и не закрывая проект нажимаю «запланированные(события)» вылетает ошибочка RunError(100). При повторном запуске программы записи выводятся но по 1(хотелось бы чтобы сразу все махом). Еще хотел спросить как открыть этот файл для правки,допустим удалить или изменить запись.

    Вложения

    Run Time Library Reference
    By first letter
    A B C D E F G H I J K L M N O P Q R S T U V W X Y Z $
    By Functionality
    Data types
    Integers, ordinals
    Floating point types
    String, char types
    Pointer types
    Other types

    Dates and Times
    Calculations
    Conversions from
    Conversions to
    Values
    Displaying
    By Unit
    System
    SysUtils
    StrUtils
    DateUtils
    FileCtrl
    ConvUtils
    StdConvs
    Math
    Classes
    Dialogs
    Types
    Variants
    By Category
    Compiler directives
    Directives
    Keywords
    Types
    Variables Constants
    Functions
    Procedures
    1.rar (2.66 Мб, 27 просмотров)
    17.06.2012, 19:23

    Ошибка RunError(100)
    Переменные Процедура с ошибкою, в строке 26 procedure.

    «Project raised exception class runerror», не могу найти ошибку
    procedure TForm1.Button1Click(Sender: TObject); var f:textfile; temp,temp1, x, y: integer; .

    IIS- asp ошибка: HTTP 500.100 — Внутренняя ошибка сервера — ошибка ASP Internet Information Services
    Привет! Конфигурация win2000pro sp2, стандартный IIS, IE 5. При попытке обратиться к.

    Указатели. Найти количество элементов массива, которые больше 100, меньше 100, равны 100
    Разработать функцию,которая находит количество элементов массива,которые больше 100,меньше 100.

    Квадрат размером 100*100 уменьшается до 10*10. На его месте появляется круг и увеличивается, достигая 100 в диаметре
    Квадрат размером 100*100 уменьшается до 10*10. На его месте появляется окружность и увеличивается.

    Delphi in a Nutshell by Ray Lischner

    Stay ahead with the world’s most comprehensive technology and business learning platform.

    With Safari, you learn the way you learn best. Get unlimited access to v > Start Free Trial

    No credit card required

    Syntax

    Description

    The RunError procedure halts the program with an error status. RunError is not a real procedure.

    Tips and Tricks

    When a runtime error occurs, if ErrorProc is nil , Delphi calls RunError to print an error message and stop the program. In a GUI application, Delphi shows the error message in a dialog box. In a console application, Delphi prints the error message to the console.

    If you write your own ErrorProc procedure, you can call RunError to halt the program and display a brief error message, but most likely you will want your ErrorProc procedure to do something different, such as raise an exception or print a different error message.

    The ExitProc procedure and units’ finalization sections get to run before the program terminates.

    Like Halt , RunError is a quick way to terminate a program, but not usually the right way for a GUI application. Instead, you should close the main form to terminate the application.

    RunError — Процедура Delphi

    Как правильно создавать и удалять объекты в run time?
    А то создаю так:

    То в Win всё ок, а на симуляторе iOS объекты не разрушаются, а просто остаются.

    Тогда всё на Win опять всё ок, а на симуляторе разрушается, но при попытке снова создать — приложение крашится.

    Поделиться сообщением

    Ссылка на сообщение
    Поделиться на другие сайты

    2 ответа на этот вопрос

    Рекомендуемые сообщения

    Похожий контент

    Добрый день.
    Знаю, что на форуме есть несколько тем о том, как удалять объекты под Android и счетчике ссылок, однако использование рекомендаций оттуда мне пока не помогло.
    Суть вопроса: создаю свой класс

    type TRext = > Вместо Release и nil использовал также DisposeOf и Nil, а также FreeAndNil — результат аналогичный.
    Как правильно уничтожать составные объекты?

    Пустое приложение. У меня есть формы: А и B.
    А по кнопке создаёт и показывает B и сразу самоуничтожается:

    B := TB.Create(nil); B.Show; A.Free; B в свою очередь делает обратное: A := TA.Create(nil); A.Show; B.Free; На Win32 платформе в диспетчере задач память по мере открытия форм не растёт. В iOS 6.1.1 iPhone 3 проследить память я не смог, но после 5-10 открытий форм приложение падает. Вероятнее всего растёт хип (heap). Почему тогда не работает FREE?? Как правильно освободить память и уничтожить форму в iOS. Я нашёл ответ. Если на форме есть картинки, то их нужно: Image1.Bitmap.Assign(nil);

    Последние посетители 0 пользователей онлайн

    Ни одного зарегистрированного пользователя не просматривает данную страницу

    Использование процедур и функций в Delphi

    Скобки

    Добавление скобок при вызове процедур и функций без параметров уже давно не является новинкой в Delphi, тем не менее, эта возможность мало известна. Эту возможность оценят по достоинству те программисты, которым приходится работать на двух языках (C++ и Delphi), так как им не нужно будет постоянно помнить о разнице в синтаксисе при вызове процедур и функций в разных языках. В Delphi оба варианта, приведенные ниже, считаются корректными.

    Возможность перегрузки

    Впервые концепция перегрузки процедур и функций была реализована в Delphi 4. Она позволяет иметь несколько различных процедур и функций с одинаковыми именами, но с разными списками параметров. Такие процедуры и функции должны быть описаны с применением директивы overload.

    procedure Test (I: integer); overload;
    procedure Test (S: string); overload;
    procedure Test (D: double); overload;

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

    Передача параметров

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

    Передача параметров по значению

    Этот режим передачи параметров применяется по умолчанию. Если параметр передается по значению, создается локальная копия данной переменной, которая и предоставляется для обработки в процедуру или функцию. Посмотрите на следующий пример:

    procedure Test(s: string);

    При вызове указанной процедуры будет создана копия передаваемой ей в качестве параметра строки s, с которой и будет работать процедура Test. При этом все внесенные в строку изменения никак не отразятся на исходной переменной s.

    Однако это не относится к объектам. Например, если в функцию передается переменная (а точнее экземпляр объекта) TStringList, то в данном случае произойдет передача по ссылке (даже если это не указано явно). Этот способ передачи является у большинства самым излюбленным, но в тоже время является и самым не практичным, т.к. для выполнения метода выделяется дополнительная память для создания точной копией передаваемой переменой. Для решения этой проблемы следует использовать один из способов описанных ниже.

    Передача параметров по ссылке

    Pascal позволяет также передавать параметры в функции или процедуры по ссылке — такие параметры называются параметрами-переменными. Передача параметра по ссылке означает, что функция или процедура сможет изменить полученные значения параметров. Для передачи параметров по ссылке используется ключевое слово var, помещаемое в список параметров вызываемой процедуры или функции.

    procedure ChangeMe(var x: longint);
    begin
    x := 2; // Параметр х изменен вызванной процедурой
    end;

    Вместо создания копии переменной x, ключевое слово var требует передачи адреса самой переменной x, что позволяет процедуре непосредственно изменять ее значение.

    Передача параметров констант

    Если нет необходимости изменять передаваемые функции или процедуре данные, можно описать параметр как константу. Ключевое слово const не только защищает параметр от изменения, но и позволяет компилятору сгенерировать более оптимальный код передачи строк и записей. Вот пример объявления параметра-константы:

    procedure Test(const s: string );

    Передача открытых массивов

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

    function AddEmUp(A: array of integer): integer;

    В открытом массиве можно передавать переменные, константы или выражения из констант.

    Для получения информации о фактически передаваемом массиве параметров в функции или процедуре могут использоваться функции High, Low и SizeOf.

    Object Pascal также поддерживает тип array of const, который позволяет передавать в одном массиве данные различных типов. Синтаксис объявления функций или процедур, использующих такой массив для получения параметров, следующий:

    procedure WhatHaveIGot( A: array of const );

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

    procedure WhatHaveIGot( [‘Text’, 10, 5.5, @WhatHaveIGot, 3.14, true, ‘c’] );

    При передаче функции или процедуре массива констант все передаваемые параметры компилятор неявно конвертирует в тип TVarRec. Тип данных TVarRec объявлен в модуле System следующим образом:

    PVarRec = ^TVarRec;
    TVarRec = record
    case Byte of
    vtInteger: (VInteger: Integer; VType: Byte);
    vtBoolean: (VBoolean: Boolean);
    vtChar: (VChar: Char);
    vtExtended: (VExtended: PExtended);
    vtString: (VString: PShortString);
    vtPointer: (VPointer: Pointer);
    vtPChar: (VPChar: PChar);
    vtObject: (VObject: TObject);
    vtClass: (VClass: TClass);
    vtWideChar: (VWideChar: WideChar);
    vtPWideChar: (VPWideChar: PWideChar);
    vtAnsiString: (VAnsiString: Pointer);
    vtCurrency: (VCurrency: PCurrency);
    vtVariant: (VVariant: PVariant);
    vtInterface: (VInterface: Pointer);
    vtWideString: (VWideString: Pointer);
    vtInt64: (VInt64: PInt64);
    end;

    Поле VType определяет тип содержащихся в данном экземпляре записи TVarRec данных и может принимать одно приведенных значений.

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

    procedure WhatHaveIGot( A: array of const );
    var
    i: integer;
    TypeStr: string;
    begin
    for i := Low(A) to High(A) do
    begin
    case A[i].VType of
    vtInteger : TypeStr := ‘Integer’;
    vtBoolean : TypeStr := ‘Boolean’;
    vtChar : TypeStr := ‘Char’;
    vtExtended : TypeStr := ‘Extended’;
    vtString : TypeStr := ‘String’;
    vtPointer : TypeStr := ‘Pointer’;
    vtPChar : TypeStr := ‘PChar’;
    vtObject : TypeStr := ‘Object’;
    vt ;
    vtW ;
    vtPW ;
    vtAnsiString : TypeStr := ‘AnsiString’;
    vtCurrency : TypeStr := ‘Currency’;
    vtVariant : TypeStr := ‘Variant’;
    vtInterface : TypeStr := ‘Interface’;
    vtW ;
    vtInt64 : TypeStr := ‘Int64’;
    end;
    ShowMessage( Format( ‘Array item %d is a %s’, [i, TypeStr] ) );
    end;
    end;

    Значения параметров по умолчанию

    В Delphi есть одна очень полезная возможность — использование значений параметров по умолчанию. Она позволяет установить принимаемое по умолчанию значение параметра процедуры или функции. Это значение будет использоваться в тех случаях, когда вызов процедуры или функции производится без указания значения данного параметра. В объявлении процедуры или функции принимаемое по умолчанию значение параметра указывается после знака равенства, следующего после его имени. Поясним это на следующем примере:

    procedure HasDefVal( s: string; i: integer = 0 );

    Подобное объявление означает, что процедура HasDefVal может быть вызвана двумя путями. В первом случае — как обычно, с указанием обоих параметров:

    procedure HasDefVal( ‘Hello’, 26 );

    Во втором случае можно задать только значение параметра s, а для параметра i использовать значение, установленное по умолчанию:

    procedure HasDefVal( ‘Hello’ );

    При использовании значении параметров по умолчанию следует помнить о нескольких приведенных ниже правилах:

    • Параметры, имеющие значения по умолчанию, должны располагаться в конце списка параметров. Параметр без значения по умолчанию не должен встречаться в списке после параметра, имеющего значение по умолчанию.
    • Значения по умолчанию могут присваиваться только параметрам обычных типов, указателям или множествам.
    • Значение по умолчанию может передаваться только по значению либо с модификатором const. Оно не может быть ссылкой или нетипизированным параметром.

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

    function Add( I1, I2: integer ): integer;
    begin
    Result := I1 + I2;
    end;

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

    function Add( I1, I2: integer; I3: integer = 0 ): integer;
    begin
    Result := I1 + I2 + I3;
    end;

    Директива

    Директива <$X->запрещает вызов функций как процедур (с игнорированием возвращаемого результата). По умолчанию этот режим включен (<$X+>). Так вот, запомните, использование переменной Result недопустимо при сброшенном флажке опции Extended Syntax, расположенном во вкладке Compiler диалогового окна Project Options, или при указании директивы компилятора <$X->.

    В каждой функции языка Objecl Pascal существует локальная переменная с именем Result, предназначенная для размещения возвращаемого значения. Кроме того, вернуть значение из функции можно также путем присвоения значения переменной, имеющей то же имя, что и данная функция. Это стандартный синтаксис языка Pascal, сохранившийся от его предыдущих версий. При использовании в теле функции переменной с ее именем не забывайте, что существуют большие отличия в обработке этого имени — все зависит от того, где она расположена — в левой части оператора присвоения или же в любом другом месте текста функции. Если имя функции указано в левой части оператора присвоения, то предполагается, что назначается возвращаемое функцией значение. Во всех других случаях предполагается, что осуществляется рекурсивный вызов этой функции.

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

    Блог GunSmoker-а

    . when altering one’s mind becomes as easy as programming a computer, what does it mean to be human.

    8 мая 2020 г.

    Windows Error Reporting и Delphi

    Windows Error Reporting (сокращённо: WER) — это набор технологий, встроенных в Windows, который собирает информацию о сбое в приложениях при их вылетах (а также о сбоях ядра) и отправляет её на сервера Microsoft. Разработчик программного обеспечения по этой информации может разработать и опубликовать соответствующее обновление. Затем конечный пользователь, отправляя отчёт, увидит, что для этой ошибки в программе доступно исправление, сможет скачать его и обновить программу.

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

    Содержание

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

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

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

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

    История Windows Error Reporting

    16-битные Windows

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

    К примеру, General Protection Fault (GPF, общее нарушение защиты, Unrecoverable Application Error, UAE) — это прерывание (fault), возникающее в случае попытки приложения получить доступ к не принадлежащей ему области памяти (сегодня известно как исключение Access Violation большинству разработчиков Delphi). При получении этого сигнала от процессора операционная система останавливает выполнение приложения, сообщает пользователю и продолжает выполнение других приложений. Но если в процессе обработки GPF (в обработчике GPF) будет возбуждено ещё одно GPF, процессор отправит сигнал «повторный GPF» (double fault), останавливая уже операционную систему. Если при этом снова произойдёт GPF (triple fault), процессор прекратит работу и его нельзя будет перезапустить (нужен будет перезапуск всего компьютера).

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

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

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

    Конечно же, Доктор Ватсон очень понравился разработчикам программ. Мэтт Питрек (автор «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 был предусмотрен новый механизм. С помощью ключа реестра HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\AeDebug ( HKLM\Software\Wow6432Node\Microsoft\Windows NT\CurrentVersion\AeDebug — для 32-битных программ в 64-битной системе) стало возможным указывать т.н. «посмертный отладчик» (postmortem debugger) или JIT-отладчик (Just-In-Time — «как раз вовремя») — для подключения к процессу при его вылете. Этот ключ реестра — документирован в MSDN и TechNet. Вы могли зарегистрировать Доктора Ватсона в качестве такого отладчика, вызвав:
    В результате чего Доктор Ватсон регистрировал сам себя:

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

    При этом, если параметр Auto ключа реестра AeDebug был равен True (1), то зарегистрированный отладчик запускался сразу, иначе — система выводила обычное сообщение, но с одной дополнительной кнопкой: «Отмена» — для запуска отладчика. Да, вот так коряво была добавлена эта возможность в Windows. Никто не удосужился даже сделать подходящий диалог.

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

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

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

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

    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) может не быть виной этого модуля. Быть может просто другой модуль неверно нас вызвал. Например, вылет проводника может быть из-за кривого расширения оболочки. Вылет игры может быть обусловлен глюком в видеодрайвере и т.д. Вот почему отчёты отправляются в централизованное хранилище. Там они сортируются и к отчётам допускаются все разработчики, чьи модули (exe или DLL) были упомянуты в отчёте.

    Microsoft не использует данные отчётов для каких-либо маркетинговых анализов, анализов частоты ошибок в различных программах т.п. Все данные пользователей защищаются от постороннего доступа и используются только для поиска причины ошибки. Отчёты отправляются по защищённому SSL соединению и хранятся на защищённых серверах, которые не используются ни для каких других целей. Для доступа к отчётам нужно предоставить логин и пароль (задаваемые при регистрации в WinQual). Любой разработчик может видеть только отчёты для своих программ. Все компании, использующие WER, обязуются следовать аналогичным политикам. Разумеется, нет гарантий, что небольшая компания из трёх человек, занимающаяся разработкой shareware-софта, заинтересована в соблюдении вашей конфиденциальности столько, сколько сама Microsoft (хотя она и согласилась следовать соответствующим политикам). Кроме того, по-умолчанию данные отчёта не содержат никаких данных пользователя, кроме тех, которые случайно попадут в дамп. Т.е. персональные данные специально не собираются. В отчёт они могут попасть только случайно. Тем не менее, вы можете посмотреть данные отчёта перед отправкой в случае, если вы работали с важными данными перед вылетом программы.

    А зачем вообще нужно отправлять отчёты о вылетах приложения? Дело в том, что в противном случае разработчик увидит только небольшую часть проблем в своём приложении, про большинство проблем он не будет знать (ведь про них не сообщают). Если разработчик не знает про проблему — он её и не исправит.

    Кроме того, автоматическая система сбора отчётов может сообщить вам, какая ошибка встречается в вашей программе чаще всего. Иными словами, вы можете сразу же исправить ту ошибку, которая действительно важная, которая мешает жить наибольшему количеству ваших пользователей. Т.е. вы можете исправить одну ошибку вместо сотни других — но эффект будет тот же самый.

    В частности, после того как Microsoft реализовала механизм отправки отчётов в Windows XP, она позднее провела широкомасштабный анализ присланных данных, который показал, что 80% пользовательских проблем могут быть решены исправлением 20% наиболее «популярных» ошибок. Даже исправление 1% самых частых ошибок устранит 50% пользовательских проблем!

    Windows Vista и позднее

    Неудивительно, что в Windows Vista служба отправки отчётов из Windows XP была снова обновлена. Она получила новое название Windows Error Reporting (WER) и представлена wermgr.exe и WER.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 — хотя всегда есть вариант использовать групповые политики.

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

    • HKCU\Software\Microsoft\Windows\Windows Error Reporting
    • HKLM\Software\Microsoft\Windows\Windows Error Reporting
      HKLM\Software\WOW6432Node\Microsoft\Windows\Windows Error Reporting

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

    Как я уже сказал, самое значительное изменение — появление локального центра сбора отчётов. Отчёты/дампы теперь хранятся локально в %ALLUSERSPROFILE%\Microsoft\Windows\WER\ (например: C:\ProgramData\Microsoft\Windows\WER\ ; а также они могут быть в %LOCALAPPDATA%\CrashDumps\ ) и могут быть отправлены в любой момент времени. Их можно просмотреть в так называемом «журнале стабильности работы»:

    Жирным отмечены отчёты, которые не были отправлены. Вы можете просмотреть технические сведения по любому отчёту:

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

    Замечу, что «журнал стабильности работы» — это отдельный, независимый журнал. Он никак не связан с системным логом «Приложения» (Event Log). События о вылетах добавляются и туда, и туда, но очищать журналы можно индивидуально.

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

    Вариант «Никогда не проверять решения» ( Windows Error Reporting / Disabled = 1), как и ожидается, покажет простое сообщение об ошибке — с единственной опцией: закрыть программу.

    Следующий вариант «Всегда спрашивать» ( Windows Error Reporting / Disabled = 0; Windows Error Reporting / Concent / DefaultConsent = 1) предложит или закрыть программу или отправить отчёт:

    Нажатие на кнопку отправки запустит отправку отчёта:

    Тут надо заметить, что, на самом деле, в этот момент идёт не отправка непосредственно отчёта, а только проверка, нужно ли его отправлять. Т.е. на сервер отправляются идентификационные данные — т.н. первый уровень (Level One), состоящий из основных параметров, идентифицирующих вылет (имя приложения, модуля, их версии, адрес/смещение, тип вылета, время и т.п.). Если сервер сообщит, что эту проблему ещё никто не встречал, либо если разработчик захотел собрать дополнительные отчёты, то вам будет предложено отправить сам отчёт (это называется «отправка дополнительной информации») — т.н. второй уровень (Level Two), состоящий из детализированного отчёта, включая слепки памяти (минидампы процесса):

    Заметьте, что в отличие от Windows XP, в Windows Vista помимо собственно лога прикладывается ещё и дамп процесса (.mdmp), который можно потом загрузить в отладчик Visual Studio или WinDbg, чтобы исследовать проблему.

    В противном случае (о сбое уже сообщили, уровень два не отправляется) программа просто завершается (возможно, с финальным диалогом — см. ниже).

    Соответственно, при выборе любой из двух «автоматических» опций ( DefaultConsent = 2 для «авто-проверять» / «Parameters only»; и = 3 для «и отправлять» / «Parameters and safe data»), начальный вопрос «отправлять» / «закрывать» пропускается, а сразу идёт отправка. Разница между 2 и 3 состоит в том, что при 2 Windows автоматически отправляет только данные «уровня один» — параметры для идентификации отчёта. Все данные «уровня два» (т.е. сам отчёт) при этом отправляются только после явного разрешения пользователя. Вариант 3 же автоматически отправляет и «уровень один» и «уровень два», без запроса разрешения пользователя — но только данные, которые система смогла опознать как, вероятно, не содержащие личную информацию. Для отправки любой другой дополнительной информации Windows всё же спросит разрешения пользователя. На практике эти два положения обычно не отличаются.

    Замечу, что DefaultConsent также может принимать значение 4 — автоматически отправлять вообще все данные без запроса пользователя. Это значение нельзя установить через UI в Windows Vista/7, хотя система его прекрасно понимает (т.е. диалог «отправка дополнительной информации» никогда не показывается). Но это значение стоит по умолчанию в последних версиях Windows (например, Windows 10).

    Отключение службы WerSvc может повлиять на поведение отчётов. Например, Windows не сможет определить, был ли уже отправлен отчёт и каждый отчёт будет считать новым. Отправка отчётов внешне будет успешна, но, похоже, отчёты просто встают в очередь на отправку.

    В некоторых случаях при вылете приложения Windows также может предложить поменять опции на «авто»:

    Важно: если вы разрабатываете невизуальное приложение (или если ваше приложение вылетает до показа первого окна), то в случае, если вы установите опцию WER в любую из двух (трёх) «авто» позиций, диалог не будет показан, если в системе уже есть точно такой же вылет и отчёт по нему был отправлен! Иными словами, если (невидимое) приложение вылетело в первый раз — система сделает локальный отчёт и отправит его (автоматически). Диалог будет показан. Если же то же самое приложение вылетает ровно на этом же месте с той же проблемой второй раз — система просто втихую закроет (невидимое) приложение без показа сообщения! Очистка старых отчётов приведёт к тому, что вылет снова будет считаться «новым» и приведёт к показу сообщения об ошибке. Визуальные (оконные) приложения показывают диалог всегда. Вы также можете форсировать диалог для невизуальных приложений сделав вызов WerSetFlags(WER_FAULT_REPORTING_ALWAYS_SHOW_UI); при старте своего приложения.

    (И наоборот, если вы сделаете вызов SetErrorMode(SEM_NOGPFAULTERRORBOX); или (недокументированный) WerSetFlags(WER_FAULT_REPORTING_NO_UI); , то ваше приложение не будет показывать диалог WER, даже если оно визуальное.)

    В любом случае, в конечном итоге — если финальный диалог о вылете должен быть показан, то он выглядит так:

    Если при этом вы зарегистрировали посмертный отладчик через AeDebug / Debugger , то будет показана дополнительная кнопка отладки:

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

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

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

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

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

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

    Примечание: в списке ниже ключ реестра Windows Error Reporting\что-то обозначает ключ HKCU\Software\Microsoft\Windows\Windows Error Reporting\что-то , а при его отсутствии — HKLM\Software\Microsoft\Windows\Windows Error Reporting\что-то , либо HKLM\Software\Wow6432Node\Microsoft\Windows\Windows Error Reporting\что-то (для 32-битных приложений на 64-битной машине).

    1. Попробуйте запустить приложение под отладчиком. Убедитесь, что в отладчике не отключены уведомления об исключениях. Если приложение под отладчиком не вылетает или подключить отладчик нет возможности — см. шаги ниже.
    2. Первым делом удалите регистрацию посмертного отладчика в ключе реестра AeDebug , либо хотя бы сбросьте параметр Auto в 0.
    3. [Vista+] Убедитесь, что «Служба регистрации ошибок Windows» («Windows Error Reporting Service», WerSvc ) не отключена (не находится в состоянии Disabled; по-умолчанию у неё тип запуска — Manual, но для надёжности вы можете её запустить вручную).
    4. Запустите Доктор Ватсон в Windows 2000, настройки отчётов в Windows XP, настройки WER в Windows Vista и позднее — и включите визуальные оповещения (Windows 2000), отчёты об ошибках (Windows XP), запрос согласия, т.е. не включайте автоматическую отправку (Windows Vista и выше).
    5. [Vista+] Проверьте настройки групповых политик WER. Убедитесь, что UI не отключен, логгинг не отключен, согласие (consent) не установлено в автоматическую отправку без запросов ( DefaultConcent = 1). Не забудьте проверить как политики машины, так и пользователя.
    6. [Vista+] Убедитесь, что ключа реестра Windows Error Reporting\DebugApplications\* нет или он установлен в 1.
    7. [Vista+] Убедитесь, что ключа реестра Windows Error Reporting\DontShowUI нет или он установлен в 0.
    8. [Vista+] Убедитесь, что ключа реестра Windows Error Reporting\LoggingDisabled нет или он установлен в 0.
    9. [Vista+] Очистите все отчёты в журнале стабильности системы.
    10. Убедитесь, что вы не вызываете SetErrorMode с одним из следующих флагов: SEM_FAILCRITICALERRORS , SEM_NOGPFAULTERRORBOX , SEM_NOOPENFILEERRORBOX . Для надёжности сделайте вызов SetErrorMode(0); первым действием при запуске своего приложения.
    11. [Win7+] Убедитесь, что вы не вызываете SetThreadErrorMode с одним из следующих флагов: SEM_FAILCRITICALERRORS , SEM_NOGPFAULTERRORBOX , SEM_NOOPENFILEERRORBOX для ваших потоков. Для надёжности сделайте вызов SetThreadErrorMode(0); первым действием ваших потоков.
    12. [Vista+] Убедитесь, что ваш код не делает вызов WerSetFlags(WER_FAULT_REPORTING_NO_UI); .
    13. [Vista+] Сделайте вызов WerSetFlags(WER_FAULT_REPORTING_ALWAYS_SHOW_UI); первым действием при запуске своего приложения.
    14. Убедитесь, что глобальная переменная System.JITEnable равна 0. Для надёжности присвойте её 0 первым действием при старте приложения.
    15. Запустите ваше приложение и дайте ему вылететь. Если никакого диалога так и не появилось, то выполните нижеследующие шаги.
    16. Проверьте, есть ли записи о вылете приложения в системном логе «Приложения».
    17. Проверьте, нет ли свежих записей в «журнале стабильности системы» или логов в %APPDATA%\Microsoft\Windows\WER\ReportArchive\ / %APPDATA%\CrashDumps\ .
    18. Попробуйте назначить свой глобальный обработчик необработанных исключений через системную функцию SetUnhandledExceptionFilter .
    19. Установите точки останова или хуки (в рамках вашего процесса) на TerminateProcess , ExitProcess , а если это не помогло — то и на TerminateThread и ExitThread .
    20. Установите точки останова или хуки на системную функцию kernel32.KiUserExceptionDispatcher — если эта функция будет вызвана непосредственно перед вылетом, то 99% за то, что у вас произошло крайне серьёзное необработанное исключение, при котором система даже не смогла показать сообщение.
    21. Наконец, попробуйте установить глобальный хук (все процессы) на TerminateProcess , TerminateThread , чтобы узнать, не завершает ли ваш процесс кто-то ещё.
    22. Также попробуйте пересобрать приложение под x86-64 или использовать другую версию Delphi (как более новую, так и более старую).

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

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

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

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

    Если VCL (или аналогичный фреймворк) не используется, либо если исключение происходит вне цикла выборки сообщений, то управление получает глобальный обработчик исключений RTL System._ExceptionHandler , который вызывает пользовательский обработчик из System.ExceptProc :
    В 99% случаев этот обработчик установлен в SysUtils.ExceptHandler , который показывает сообщение через SysUtils.ShowException , а затем завершает работу приложения с кодом возврата равным 1:
    SysUtils.ShowException показывает упрощённо-техническое сообщение:

    Если же модуль SysUtils не подключен, либо исключение возникло до инициализации модуля SysUtils , то System.ExceptProc будет не назначен (равен nil ), так что System._ExceptionHandler попытается обработать сообщение самостоятельно. Поскольку объект исключения определяется в том же модуле SysUtils , модуль System не может оперировать объектом исключения. Вместо этого он завершит приложение с «кодом ошибки» 217 (необработанное исключение) — через System._RunError :
    Что приведёт к завершению приложения. При этом System.Halt увидит, что установлен код ошибки, поэтому он сообщит об этом:

    Примечание: код 217, на самом деле, означает закрытие консольного приложения через Ctrl + Break . По совместительству он же используется для указания необработанного исключения. Также код может быть 230 — настоящий код для необработанных исключений, используется на не-Windows платформах. 204 — код для Invalid Pointer, вызывается менеджером памяти, если передать ему неверный указатель (например, указатель на уже удалённую память). А также частый код 216 — если необработанное исключения является аппаратным Access Violation. Для других аппаратных исключений также есть свои собственные коды, но на практике в 99% случаев вы увидите только 216, 217 или 204.

    Если ваш код будет создавать дополнительные фоновые потоки через 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 — зло, от которого нужно избавляться. Мы посмотрим на это чуть позже.

    Когда приложение Delphi может привести к вызову WER?

    А пока — посмотрим на то, как приложение Delphi может вызывать WER, даже хотя оно почти полностью завёрнуто в обработчики исключений, и необработанным исключениям возникнуть, вроде бы, неоткуда.

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

    Для начала, самый простой случай — код в потоке, создаваемом системной функцией CreateThread , по очевидным причинам не будет иметь обработчика исключений RTL (в отличие от RTL-функции BeginThread ).
    или Оба эти примера кода приведут к вылету приложения: будет вызван WER, создан/отправлен отчёт о вылете, приложение будет закрыто.

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

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

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

    Например:
    Ошибки переполнения буфера могут привести к перезаписи важных адресов на стеке потока. В результате блоки try будут испорчены. И если в этот момент произойдёт исключение, система увидит на стеке мусор вместо указателя на блок except . Обработчик вызвать не удастся, так что будет вызван только WER.

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

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

    Тем не менее, можно привести и аналогичные примеры. Например:
    Этот код вызывает просто переполнение стека (stack overflow). Часто — это обычное исключение, которое будет поймано ближайшим обработчиком и обработано. В данном случае — будет показано сообщение через Application.HandleException . Тем не менее, для обработки исключения тоже нужно свободное место на стеке. И если обработчик займёт слишком много места на стеке — получится исключение внутри обработчика исключения. Новое исключение будет передано выше, пока не дойдёт до самого верхнего уровня, где и будет обработано WER.

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

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

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

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

    Вызов UnhandledExceptionFilter

    Далее, поведение программы зависит и от версии Delphi. Указанная выше (в предыдущем разделе) логика с безусловным вызовом System.ExceptProc из System._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 ). Именно поэтому эта опция ничего не делает, если вы уже отлаживаете свою программу: в конце концов, задача опции — подключить отладчик, а если вы уже отлаживаетесь, то подключать ничего не надо.

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

    Далее, начиная с Delphi 5 в модуле System появляется новая глобальная переменная:
    по-умолчанию она равна 0 и означает, что все обработчики исключений работают как обычно. Если эта переменная отлична от нуля, то блоки except не будут вызываться если, во-первых, программа не отлаживается, во-вторых, исключение — аппаратное (для System.JITEnable = 1) или произвольное ( System.JITEnable = 2). Переменная System.JITEnable не изменяется кодом RTL/VCL и предназначена для изменения вами.

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

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

    Очевидно, что переменная System.JITEnable не предназначена для использования в production-версии вашего кода. По крайней мере, при значении 2 — точно. Ведь в вашем коде написаны какие-то обработчики исключений, стоят блоки try / except , ваш код предполагает, что обработчики будут выполняться, ведь они выполняют какую-то работу по откату. Но если вы включаете опцию System.JITEnable , то ни один из ваших обработчиков не будет вызван.

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

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

    Ручной вызов WER

    В конце концов, ваш код может просто вызвать WER вручную — через его API. Примеры вызова мы посмотрим ниже.

    Настройка Delphi-приложений для отправки отчётов

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

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

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

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

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

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

    • Не используйте CreateThread . Всегда используйте BeginThread .
    • Не изменяйте System.JITEnable .
    • Установите:
      и/или:
      • Заключайте каждую функцию потока, созданного CreateThread / BeginThread / QueueUserWorkItem (и аналогичными функциями), в явный блок try / except .
      • Заключайте в блоки try / except каждую секцию initialization , каждую секцию finalization и блок begin / end .dpr проекта.

      В блоке except вы можете вызывать SysUtils.ShowException или свой собственный код.

    • Вызовите SetErrorMode(SEM_NOGPFAULTERRORBOX); при старте процесса (опционально можно добавить и другие флаги). См. также.
    • Обработайте терминальное исключение главного потока:
      или:
      Заключайте в блоки try / except каждую секцию initialization , каждую секцию finalization и блок begin / end .dpr проекта. В блоке except вы можете вызывать SysUtils.ShowException или свой собственный код.
    • Опционально: замените все обработчики исключений на свои. Убедитесь, что вы дополнительно показываете в сообщении/диалоге: имя модуля, его версию и смещение исключения внутри этого модуля (см. ниже).

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

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

    Во-первых, в первом приближении очень хорошо сработала бы такая комбинация:
    В данном случае мы вызываем WER для всех перечисленных кодов аппаратных исключений и RTL обработчики — для всего остального. Список, конечно, приведён как пример, и его нужно изменить под ваше приложение. Например, если вы делаете определение запуска виртуальной машины через выполнение секретной инструкции, то код EXCEPTION_PRIV_INSTRUCTION (и, возможно, EXCEPTION_ILLEGAL_INSTRUCTION ) нужно убрать. Если вы проверяете аргументы выражений, то коды числовых исключений (деление на ноль, переполнение и т.п.) хорошо бы добавить. В тривиальном случае можно также считать что любое аппаратное исключение нужно передать WER. В сложном случае — фильтровать не только аппаратные, но и программные исключения (установив JITEnable в 2).

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

    К сожалению, в Delphi не предусмотрено никакой возможности отменить только «глобальные» блоки try / except , нет никакого аналога опции JITEnable . Вместо этого нам придётся назначить свой обработчик и вызывать WER вручную — через API. Сама Microsoft такой подход считает допустимым, но не рекомендованным. Рекомендованный — конечно же, просто не ловить исключения, которые вы не знаете как обрабатывать.

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

    Центральным местом тут является всего одна функция — ReportFault , которую и нужно нам вызвать. Проблема в том, что на вход функция просит пару ExceptionRecord и Context — это низкоуровневые понятия, которые недоступны из обычного блока except в Delphi. Поэтому большая часть кода посвящена преобразованию между этими двумя понятиями. К сожалению, весьма важный кусок информации — контекст процессора, не доступен. Чтобы его получить, нам придётся прибегнуть к нетривиальным ловушкам, рассмотрение которых выходит за рамки этой статьи.

    Очень важно, что этот код должен работать без выделения памяти (в куче). Во-первых, нам не известно состояние менеджера памяти в момент вызова нашего обработчика. Может быть, обработчик был вызван как раз потому, что произошло исключение в менеджере памяти. И что тогда? Снова выделять память? Во-вторых, менеджер памяти может быть уже финализирован к моменту вызова вашего обработчика. Например, если исключение возникло в секции initialization модуля, то сначала сработает блок except процедуры InitUnits модуля System , который завершит модули (и менеджер памяти — в том числе), а затем уже вызовет UnhandledExceptionFilter (и, следовательно — наш обработчик). Выделение памяти у уже отработавшего менеджера памяти, опять же, ничем хорошим не закончится.

    Именно по этой причине вызов GetProcAddress вынесен из кода UnhandledExceptionHandler — он выделяет память (см. реализацию GetProcAddress в модуле Windows ).

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

    (Это я ещё не про все подводные камни рассказал. Да, как видите, глобальная обработка исключений — не самое простое дело.)

    Если вызов WER был неудачен (например, WER отключен), либо WER вообще отсутствует (Windows 2000 и ранее), то мы пытаемся показать сообщение как оно показывалось бы ранее — методом ShowException модуля SysUtils . Проблема опять же в том, что делать, если на руках нет объекта исключения Delphi, который можно было бы показать. Напомню, что в этом случае модуль System показывает run-time ошибку (обычно — 217, 216 или 204, как мы обсуждали это выше) — что, на мой взгляд, достаточно бесполезно. Поэтому вместо этого я предлагаю показывать код исключения — что и делает функция ShowExceptionRecord , которая является слегка видоизменённой ShowException .

    Данный пример использует API уровня Windows XP (хотя будет работать в любой ОС), но вы также можете расширить этот пример на уровень Vista, чтобы полностью настроить поведение WER. В Vista вы можете изменять части диалога, добавлять в отчёт информацию и файлы и многое другое.

    Итак, имея на руках функцию UnhandledExceptionHandler , мы можем назначить её в качестве глобального обработчика исключений: В данном примере UnhandledExceptionHandler устанавливается обработчиком для оконных приложений VCL, а для остальных мест — мы подразумеваем, что будет использован стандартный обработчик модуля System , который должен вызвать (в не самых древних Delphi) стандартный системный UnhandledExceptionFilter . Соответственно, мы назначаем свой обработчик, чтобы перехватить этот вызов и показать WER.

    Конечно, в последнем случае гораздо проще просто ничего не делать, т.к., как мы помним из написанного выше, в не самых древних версиях Delphi при прогоне без отладчика исключение останется необработанным и будет поднято до WER. Именно поэтому этот код помечен «опциональным». Тем не менее, если вы захотите поменять WER на, скажем, свой собственный механизм обработки/логгирования исключений, то вам этот код будет нужен, так что я заодно его и показал.

    Для совсем старых версий Delphi вы можете заменить (или дополнить) вызов SetUnhandledExceptionFilter на назначение обработчика ExceptProc .

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

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

    Примеры выше были приведены для оконного VCL-приложения, где есть глобальный объект Application , предоставляющий событие OnException . Если вы пишите приложение с использованием другого фреймворка — вы должны адаптировать этот код, изменив назначение глобального обработчика. Например, FireMonkey тоже предоставляет свой собственный глобальный объект Application , у которого также есть событие OnException . Некоторые другие фреймворки предоставляют аналогичное событие. Например, IntraWeb предоставляет событие gServerController.OnLogException . Некоторые фреймворки в качестве такого события используют глобальную переменную Classes.ApplicationHandleException (например, OTL — Omni Thread Library) — так что вы можете назначить свой обработчик туда. Некоторые фреймворки предоставляют процедуру регистрации обработчика вроде RegisterExceptionFilter . Например — OTL и IntraWeb ( TIWServerControllerBase.RegisterExceptionCallback ). Некоторые фреймворки (в основном — многопоточные) не выпускают исключение наружу, а сохраняют его в свойстве типа .FatalException — например, TThread и OTL.

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

    К сожалению, не всегда можно назначить свой обработчик. К примеру, службы (сервисы Win32) в Delphi не предоставляют ни события, ни какого-либо иного способа зарегистрировать обработчик. В большинстве случаев для обработки исключений вызывается не виртуальный метод LogMessage . В TServiceApplication есть динамический метод DoHandleException, но заменить его нет никакой возможности, т.к. класс TServiceApplication создаётся безусловно, он не извлекается из какой-либо глобальной переменной, в которую мы могли бы записать свой класс. Есть и другие фреймворки, где не предусмотрена возможность указания своего обработчика исключений.

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

    И раз уж вы собираетесь вылетать, то сильно неплохо было бы настроить своё приложение на автоматический перезапуск. Делается это либо вручную (на Windows XP и младше), либо вы можете использовать Restart and Recovery API на Windows Vista и выше. Не забудьте только передавать флаг WER_SUBMIT_HONOR_RESTART . Рассмотрение Restart and Recovery API выходит за рамки этой статьи.

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

    Как я уже описывал, у вас есть возможность зарегистрировать любую программу на ваш выбор в качестве посмертного (postmortem) или JIT (Just-In-Time) отладчика. Когда приложение вылетает, система добавит кнопку «Отладка» («Debug») в диалог фатального вылета. Нажатие на эту кнопку запустит указанный вами отладчик, передав ему идентификатор процесса, в котором произошёл вылет. Отладчик сможет подключиться к процессу и исследовать его. Отладчик может быть классическим интерактивным — вроде отладчика Delphi. Или же это может быть автоматизированная утилита, которая просто соберёт информацию — вроде Доктора Ватсона.

    Посмертный отладчик регистрируется в ключе реестра HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\AeDebug (или HKLM\SOFTWARE\Wow6432Node\Microsoft\Windows NT\CurrentVersion\AeDebug — для 32-битных приложений в 64-битной системе). Для регистрации отладчика вам нужно создать или изменить значение Debugger в ключе AeDebug (строкового типа). Строка должна содержать корректную командную строку. Иными словами, если имя/путь отладчика включает в себя пробелы, его нужно заключать в кавычки. Путь обязательно должен быть полным и абсолютным.

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

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

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

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

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

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

    К примеру, для Delphi 5-7 установите Debugger в: только замените 70 (Delphi 7) на 50 (Delphi 5) или 60 (Delphi 6).

    Для более новых версий Delphi используйте: В настоящее время параметр %p не принимает ни одна версия Delphi.

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

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

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

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

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

    Если вы пользуетесь в основном Delphi, то вы регистрируете отладчик Delphi в качестве посмертного — и на этом всё. Если же вы используете несколько сред разработки, то, возможно, вы бы хотели переключаться между отладчиками. В этом случае вы можете написать свою утилиту-переходник и зарегистрировать её в качестве посмертного отладчика. Например:
    При запуске вы можете показать диалоговое окно со списком отладчиков, которые вы используете в работе. При выборе отладчика — запустите его через CreateProcess , передав ему отформатированные параметры командной строки (хотя бы так: «Args := StringReplace(StringReplace(StringReplace(Args, ‘%ld’, ParamStr(1), []), ‘%ld’, ParamStr(2), []), ‘%p’, ParamStr(3), []);»). Не забудьте наследовать описатель события (второй параметр %ld ) в целевой процесс. Дождитесь завершения процесса отладчика и выходите.

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

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

    Использование Threads Snapshot в качестве посмертного отладчика

    В качестве посмертного отладчика вы также можете использовать утилиту Threads Snapshot. Она входит в состав трейсера исключений EurekaLog. Если у вас нет EurekaLog, то вы можете скачать бесплатный Tools Pack.

    Для установки Threads Snapshot в качестве посмертного отладчика достаточно запустить её с параметром » /install «:

    Само собой, запускать нужно под администратором (и с элевацией прав).

    Когда утилита Threads Snapshot зарегистрирована в качестве посмертного отладчика, и вылетает любое приложение — вы можете нажать кнопку Debug для снятия снимка процесса:

    Нажатие на кнопку Debug запустит снятие снимка процесса:

    В конце утилита Threads Snapshot подготовит отчёт и спросит у вас, куда его сохранять. Отчёт будет сохранён в обычный .el формат (отчёт EurekaLog), который можно просмотреть в любом текстовом редакторе или в бесплатной утилите EurekaLog Viewer:

    Восстановить регистрацию предыдущего посмертного отладчика можно запуском Threads Snapshot с параметром » /uninstall «.

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

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

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

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

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

    Тип вылета

    Код исключения

    Далее, смотрим код ошибки. Он же — код исключения. Самые частые коды: $C0000005 — это Access Violation, $0EEDFADE — исключение-объект Delphi. Могут быть и другие коды. Если вы видите неопознанный код ошибки — попробуйте использовать утилиту Error Lookup. Она входит в состав трейсера исключений EurekaLog. Если у вас нет EurekaLog, то вы можете скачать бесплатный Tools Pack.

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

    Хорошо, с аппаратными исключениями понятно — они отличаются кодом. Но как отличить одно исключение Delphi от другого? Ведь любое исключение, представленное объектом Delphi, в результате будет иметь один и тот же код $0EEDFADE? Ну, в ситуации если WER вызван системой или через API уровня WinXP (т.е. функцию ReportFault , как мы сделали это в примере выше) — никак. Вам придётся исследовать слепок (дамп) процесса. Если же вы вызываете WER вручную (аналогично нашему примеру выше) и используете API уровня Vista+ ( WerCreateReport / WerSubmitReport ), то вы можете скопировать имя класса и/или сообщение в один из десяти произвольных строковых параметров отчёта (модифицированный код из примера выше):
    Да, ужасно много работы ради замены всего двух параметров. Получился не самый тривиальный код, который, к тому же работает без выделения памяти в куче стандартного менеджера памяти. Увы, вызвать WER — это вам не класс с перезаписью виртуального метода унаследовать. Вы можете модифицировать этот код под ваши нужды. Например, выводить имя класса и сообщение через (не используемые WER) параметры WER_P8 и WER_P9, или заменить один из стандартных параметров WER_P0-WER_P7.

    И насчёт сообщения — будьте аккуратны. Параметры отчёта используются WER для группировки и классификации отчётов, а сообщение об ошибке может быть локализовано, что приведёт к ненужному дублированию отчётов. Экспериментируйте.

    В любом случае, в результате имеем:

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

    Напомню, эта кастомизация возможна только на Windows Vista и выше. На Windows XP отчёт не изменится.

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

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

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

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

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

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

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

    Для этого вы можете использовать утилиту Address Lookup. Она входит в состав трейсера исключений EurekaLog. Если у вас нет EurekaLog, то вы можете скачать бесплатный Tools Pack. Запустите утилиту, укажите на модуль вылета и укажите смещение:

    Утилита покажет вам в каких модуле, классе, методе/функции и строке произошёл вылет. Чтобы это стало возможным утилите необходима отладочная информация. Она понимает множество форматов: map-файлы, TDS/TD32, JCL, DBG и другие. Но это также означает, что вам нужно заранее позаботится об этом. Включите генерацию map-файла (или используйте любой другой из поддерживаемых форматов). Разумеется, эти map-файлы (а также .tds, .dbg, .jdbg и т.п.) нужно будет хранить у себя. И не перепутать два разных файла от двух разных версий одной программы. Чтобы не перепутать — используйте поле версии в версионной информации модуля.

    Некоторые форматы поддерживают внедрение (собственный формат EurekaLog, TDS/TD32, JCL и т.п.) — вы можете использовать их. Тогда дополнительных файлов хранить не нужно (только сам модуль) и нельзя перепутать версии файлов, что тоже есть хорошо. Но увеличивается размер модуля, что не есть хорошо. Выбор за вами.

    Альтернативно, вы можете использовать и саму Delphi. Для этого запустите программу (ровно той версии, что упомянута в отчёте) и поставьте её на паузу (Run / Pause), затем откройте окно со списком модулей: View / Debug / Modules и выясните по какому адресу оказался загружен модуль на вашей машине.

    У вас есть базовый адрес, у вас есть смещение — вычислите полный адрес, сложив два значения. Теперь используйте Search / Go to address и введите полученный абсолютный адрес:

    Если вы всё сделали правильно, то IDE откроет нужный модуль и ткнёт вас в строчку с ошибкой.

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

    Во-вторых, IDE попросту не поддерживает сторонние отладочные форматы. Поэтому если вылет произошёл во внешнем модуле (в частном случае — в модуле ОС), то IDE откроет CPU-отладчик и ткнёт вас в конкретную ассемблерную инструкцию. Никакой дополнительной информации она показать не сможет.

    На практике хранить все копии исходников, восстанавливать их для исследования каждого вылета, пересобирать программу, гарантировать/проверять её совпадение — крайне трудоёмко и чревато ошибками, поэтому этот способ (с IDE) стоит приберечь только на самый последний случай (когда заранее к нему не подготовились), а сильно предпочтительнее просто генерировать отладочную информацию в одном из форматов и хранить её (либо ещё лучше — внедрять), а затем использовать Address Lookup.

    P.S. Аналогичную технику можно использовать и для анализа любых других адресов в сообщениях об ошибках. Например, «Runtime error 217 at 004060CD» или «Access Violation at address 005D2500. «. К сожалению, Delphi сделана в этом отношении не совсем грамотно: она показывает абсолютный адрес, а не смещение. В итоге получается, что если ошибка произошла в .exe — вам (возможно) повезло: .exe (почти) всегда грузится по фиксированному адресу $00400000. Если же исключение произошло в DLL — вы пролетели, если только вам сильно не повезёт (каким-то образом вы узнаете базовый адрес DLL, или же DLL на вашей машине окажется загруженной по тому же адресу).

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

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

    1. Vista и выше. На Windows XP дампы не создаются.
    2. Отчёт вылета должен содержать в себе дамп процесса (дамп процесса также называется мини-дампом, противопоставляя себя полному дампу режима ядра). Дамп процесса — это «слепок» памяти и, возможно, объектов процесса. Если вы вызываете WER вручную, то параметры создания дампа вы указываете самостоятельно — в вызове WER. См. код-пример выше, где мы вызывали WerReportAddDump для добавления мини-дампа в отчёт. Если вылет происходит под управлением системы, то мини-дамп создаётся (или нет) согласно настройке WER. В любом случае, файл мини-дампа будет упомянут в отчёте (файл .mdmp или, реже, .dmp), а ссылки на локальные хранилища я приводил выше (в описании WER).
    3. Отладочная информация в формате, которую может понять отладчик Microsoft. Для этого вам нужно создавать отладочную информацию в формате MAP и/или TDS/TD32, а затем использовать конвертер в DBG или PDB. Для этого подойдёт какой-либо из вариантов утилиты map2dbg:
      • https://code.google.com/p/map2dbg/
      • https://github.com/andremussche/map2dbg
      • https://github.com/garethm/map2dbg
      • https://bitbucket.org/wpostma/map2dbg

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

      • https://github.com/andremussche/map2dbg/tree/master/tds2pdb
      • https://sourceforge.net/projects/tds2pdb/
    4. Сама Visual Studio или WinDbg.

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

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

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

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

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

    К счастью, сегодня это не так. Вам подойдёт любой сертификат для цифровой подписи. Ключевые слова для поиска: code signing certificate или Microsoft Authenticode. Стоимость — что-то от $65 в год (при покупке на несколько лет) до $200 — в зависимости от поставщика. Остались и сертификаты за $500, но они теперь уже EV: «Extended Validation». Для них производится более тщательная проверка перед выдачей сертификата вас как компании. Такие подписи получают бонус от Smart-фильтров браузеров, а также требуются для подписи драйверов. Для обычного же разработчика вполне достаточно сертификатов за $65 в год. Только убедитесь, что вы покупаете сертификат именно для подписи исполняемых файлов Windows.

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

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

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

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

    Если вы не используете FinalBuilder, то вызывать SignTool вам придётся вручную. Как-то так:
    «C:\Program Files (x86)\Windows Kits\8.0\bin\x86\signtool.exe» sign /f «C:\путь-к-сертификату\сертификат.pfx» /p «пароль» /tr timestamp-сервер «C:\путь-к-программе\модуль.exe» /d «Краткое описание программы» /du «http://сайт-программы
    где » timestamp-сервер » может быть: http://timestamp.comodoca.com , http://sha256timestamp.ws.symantec.com/sha256/timestamp , http://tsa.starfieldtech.com/ или любым другим timestamp-сервером, поддерживающим RFC 3161.

    Примечание: все современные сертификаты используют SHA-256 и не распознаются Windows XP и ниже (которая поддерживает только SHA-1).

    Как только вы подписали файл — можно регистрировать его в Microsoft. Веб-сайт Windows Error Reporting (бывший WinQual) сейчас переехал на sysdev.microsoft.com (который, в свою очередь, в настоящее время находится в процессе переезда на https://developer.microsoft.com/dashboard/hardware — который пока только для разработчиков драйверов). Само собой, потребуется учётная запись Microsoft (Live), а также необходимо будет использовать только Internet Explorer.

    Регистрация происходит путём подписи файла. Вам предложат скачать .exe файл, вы должны подписать его своим сертификатом и закачать на сайт. Профиль компании будет создан автоматически по сертификату, вы станете администратором. Если же доступа к сертификату у вас нет, то вы можете выбрать одну из уже зарегистрированных компаний и сможете присоединиться к ней после одобрения заявки администратором компании.

    Выглядит это примерно так (на скриншотах показан процесс для драйверов на developer.microsoft.com, поэтому и упоминается EV; я не могу сделать скриншоты для обычного сертификата, поскольку этот процесс я уже прошёл; напомню, что в настоящее время вам нужно заходить на sysdev.microsoft.com, где произойдёт аналогичный процесс):

    Как только вы успешно зарегистрировались и вошли — вам будет доступна консоль (dashboard).

    Большинство опций в ней предназначены для разработчиков драйверов и сертификации приложений — и потому нам не интересны. Фактически, нас интересуют только отчёты (Reports). Но прежде нам нужно сделать ещё две вещи.

    Первое, что вам придётся в ней сделать — подписать соглашение. Для этого откройте Administration / Legal agreements:

    Как и ранее — тут куча всего для драйверов и сертификации, что нам не интересно. Выберите в фильтре тип «Signable» (т.е. что можно подписать, но ещё не подписано) и найдите в списке WER. Я не могу показать процесс подписи именно для этого соглашения (потому что у меня оно уже подписано), но вот скриншот для другого соглашения:

    Важно: вы должны использовать только Internet Explorer и у вас должен стоять Adobe Acrobat.

    Фактически, тут вам нужно продублировать ваше имя и дату ( Ctrl + C / Ctrl + V ) и нажать «Submit».

    Наконец, последнее, что нам осталось сделать — зарегистрировать свои программы. Делается это не тривиально. Откройте раздел Reports (где будет написано, что у вас отчётов нет) и проскрольте вниз, ищите ссылку на «Product mapping Tool»:

    Тут вас поджидает сюрприз, ибо установщик запросит установленный Windows Live Essentials, который. не доступен для скачивания. Придётся искать оффлайн установщик по неофициальным каналам. Короче, гуглите.

    В любом случае, текущая версия Product Mapping Tool называется аж Microsoft Ecosystem Metadata Exchange (или MEME). Она попросит вас войти в вашу учётку и синхронизируется с сервером Microsoft.

    Ну вот с помощью её мы и добавляем каждый выпуск (release) нашей программы. Само собой, добавляем Product, а не Driver. Ну и если у вас прям сложная программа, то можете создать ещё Product Group, но это не обязательно.

    Не забываем добавить и сами файлы (которые уже должны быть подписаны вашим сертификатом). Ещё раз: файл обязан быть подписан! И быть подписан именно вашим сертификатом, на аккаунт которого вы вошли в MEME. Если файл не будет подписан или будет подписан любым другим сертификатом (включая двойную подпись) — MEME покажет ошибку.

    Ещё один сюрприз тут — утилиту нужно вручную запускать с правами администратора (запускать из: C:\Program Files (x86)\Microsoft Ecosystem Metadata Exchange\MetadataExchange\ ). По крайней мере, на Windows 10. Иначе попытка добавления файла закончится ошибкой недостаточных привилегий (утилита выдаст общее сообщение «ошибка — см. лог», а подробности можно найти в системном логе «Приложения»).

    Я понятия не имею, что будет, если версионная информация в исполняемом модуле не будет совпадать с мета-информацией, которую вы вводите в MEME. Лучше бы, наверное, чтобы она совпадала как можно лучше — особенно номера версий.

    Не забудьте опубликовать внесённые изменения — ссылка «Publish Changes» в заголовке:

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

    После этого можно деплоить и крашиться! После регистрации вид раздела Reports изменится на рабочий:

    Учтите только, что отчётам нужно время, чтобы пройти отправку и сортировку, появляются они не сразу. Задержка будет от нескольких дней до месяца.

    Вот как будет выглядеть раздел отчётов при поступлении багов:

    Это общий вид «Product group summary», где создано две версии одного продукта, одна из которых раскрыта. Как вы можете видеть, отчёты сортируются по количеству возникновения. Чем больше произошло вылетов — тем выше в списке запись. Есть также «Company hotlist» — куда попадают самые «популярные» быги из всех ваших продуктов. Подразумевается, что «Company hotlist» будет служить своеобразным списком TO-DO для исправления багов.

    Вы можете раскрыть любую запись:

    Будут показана статистика и возможность скачать отчёты.

    У записей также может не быть никаких данных, кроме статистики. Это потому, что они не собраны. Вы должны щёлкнуть «Request more data»:

    В любом случае, когда вы скачаете отчёт (.cab или .zip-файл — в зависимости от ОС), в нём будет:

    Вот два отчёта с разных систем. В обоих случаях присутствует дамп процесса ( minidump.mdmp ), общая информация ( WERInternalMetadata.xml ) и пользовательский файл, присоединённый через WerReportAddFile ( dump.txt ) — куда вы можете сохранить информацию об исключении, стеки вызовов и т.п.

    WERInternalMetadata.xml выглядит примерно так (полный вариант из отчёта с 6-ю файлами; вариант из отчёта с тремя файлами существенно короче):

    sysinfo.txt содержит статистику работы системы (что-то вроде performance counters). WERDataCollectionStatus.txt — служебный. А memory.csv содержит список процессов (со статистикой).

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

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

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

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

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

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

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

    Что если я не хочу изобретать при этом велосипед?

    Тогда вам нужно использовать готовое решение. Для Delphi есть два трейсера исключений с поддержкой отправки отчётов в FogBugz: EurekaLog и madExcept. Конечно, они поддерживают не только FogBugz, но и другие трекеры.

    Более того, если вы хотите использовать WER, то EurekaLog поддерживает и его — присоединяя свой отчёт как дополнительный файл. (Я не уверен, но возможно, что madExcept в режиме Windows Logo Compliant Mode делает что-то аналогичное.)

    Ну и само собой, трейсеры исключений дадут вам и контекст процессора и стек вызовов.

    Обработка абстрактных методов в Delphi

    Автор: Антон Злыгостев
    НФК «Novosoft Inc»
    Источник: RSDN Magazine #2

    Опубликовано: 18.02.2003
    Исправлено: 13.03.2005
    Версия текста: 1.0

    Введение

    В языке Object Pascal, используемом средой разработки Delphi, существует механизм виртуальных конструкторов. Поклонникам C++ это кажется ужасной ересью, но виртуальные конструкторы очень удобны для создания экземпляров классов, которые еще не определены на этапе компиляции создающего кода. Такая технология позволяет разрабатывать компонентный код без необходимости реализации фабрик классов.

    Оборотной стороной этой гибкости является возможность случайно создать экземпляр абстрактного класса, что впоследствии почти неизбежно приведет к вызову одного из абстрактных методов. Если в C++ вызов чисто виртуальной функции требует изрядной ловкости, и практически невозможно произвести его нечаянно, то в Delphi это достигается одним неосторожным движением. К сожалению, встроенные в Delphi механизмы обнаружения и обработки абстрактных методов предоставляют лишь минимум информации об источнике ошибки.

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

    Виртуальные конструкторы

    Концепция виртуальных конструкторов в Delphi тесно связана с существованием специального типа «ссылка на класс». Совместное их использование позволяет создавать объекты классов, которые еще не существуют в момент компиляции кода, создающего экземпляр класса. Например, в стандартном классе TComponent объявлен виртуальный конструктор Create:

    ПРИМЕЧАНИЕ

    Все примеры в этой статье, если не указано иное, компилировались и тестировались на Borland Delphi 5.

    В сочетании со ссылкой на класс компонента:

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

    Такая возможность является ключевой для работы механизма чтения форм из DFM-файлов и ресурсов. Кроме того, она может быть полезной и для пользовательского кода, который не связан напрямую с VCL. Наиболее популярные области применения подобной функциональности – это сериализация объектов и регистрация plug-in. Кроме этого, на основе этого механизма и RTTI в Delphi 6 реализованы веб-сервисы.

    Абстрактные методы

    Рассмотрим теперь следующий код:

    На первый взгляд, все в порядке. Есть абстрактный класс, который декларирует некую функциональность, и есть его потомок, реализующий эту функциональность. Мы предполагаем использовать это примерно таким образом:

    (Реальный код, конечно, будет несколько сложнее. Скорее всего, объекты будут создаваться в одном месте, а использоваться в другом, но суть дела это не меняет.)

    В чем же проблема? А в том, что нерадивый прикладной программист запросто может передать в нашу процедуру в качестве параметра ссылку на абстрактный класс:

    Такой код вполне удачно скомпилируется. Что же произойдет во время работы приложения? Простейший эксперимент покажет, что результатом будет выдача исключения EAbstractError в момент вызова метода DoSomeJob.

    Казалось бы, все в порядке: нарушитель пойман, справедливость восстановлена. Ан нет. EAbstractError – на редкость неинформативный класс. Он не предоставляет никакой информации о контексте и причине ошибки. Если вы – разработчик, и приложение выдало соответствующее сообщение, то у вас есть шанс потратить некоторое время на общение с отладчиком и пошаговое выполнение, чтобы отловить класс-нарушитель. Но если вы скомпилировали свою библиотеку без отладочной информации и исходных текстов, то прикладной программист сможет только гадать, что же он сделал не так.

    Есть, конечно, весьма простой способ «обойти» проблему – никогда не объявлять абстрактных методов. VCL использует «пустые» определения методов сплошь и рядом. Однако это не путь для настоящих программистов. Хотя бы по той причине, что «пустая» реализация процедуры еще имеет какой-то смысл, но любая функция должна возвращать какое-то значение.

    Более естественным способом является запрет на создание экземпляров абстрактных классов, как это сделано, например, в C++. Увы, компилятор Delphi ограничится предупреждением: “constructing instance of class … containing abstract methods” . Вывод этого предупреждения можно подавить соответствующими опциями компилятора.

    Как правило, аккуратные программисты внимательно следят за предупреждениями, выдаваемыми компилятором. Но в ситуации, которая описана выше, «гром не грянет» и причин креститься у программиста не будет.

    Тестовое приложение

    Проиллюстрируем технику использования особенностей объектной модели Object Pascal на примере несложного приложения.

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

    Для этого мы будем использовать механизм пакетов времени выполнения (runtime packages). Разработчик дополнительных операторов должен будет реализовать свой класс-наследник и включить его в пакет. Наше приложение будет сканировать текущую папку в поисках файлов с расширением .bpl и динамически загружать их в свое адресное пространство.

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

    В классе TCnkOp мы «забудем» перекрыть один из абстрактных методов, объявленных в базовом классе. Мы убедимся, что стандартная обработка таких ошибок не дает никакой информации о причинах возникновения ошибки, и построим свою обработку так, чтобы можно было сразу определить, в каком классе и какой метод был оставлен абстрактным.

    Получение дополнительной информации

    Чтобы узнать больше о том, что привело к абстрактному вызову, необходимо разобраться с тем, как Delphi реализует обработку абстрактных методов.

    Стандартный обработчик

    Если протрассировать вызов абстрактного метода TAbstractObject.DoSomeJob, то выяснится интересная подробность: управление передается в системную процедуру _AbstractError:

    Эта процедура объявлена в секции implementation модуля System, то есть является недокументированной подробностью реализации Object Pascal и VCL. В ней проверяется, присвоено ли значение указателю AbstractErrorProc, и, если это так, то управление передается по этому адресу. Иначе приложение аварийно завершается с ошибкой 210. Если в проект включен модуль SysUtils (как правило, это так), то этому указателю будет присвоен адрес процедуры SysUtils.AbstractErrorHandler. Эта процедура и выбрасывает исключение EAbstractError, которое так мало говорит об источнике проблем.

    Усовершенствованный обработчик

    Из предыдущего раздела можно сделать два вывода:

    1. Существует документированный способ зарегистрировать свой обработчик абстрактных вызовов.
    2. Несмотря на то, что среда не передает в этот обработчик никаких параметров, функции, которые вызывают наш обработчик, никак не воздействуют на контекст вызова.

    Последствия, вытекающие из второго вывода, значительно менее «безопасны». Однако из него следует, что можно получить некоторую информацию о контексте, в котором произошла ошибка. Проще говоря, вывод 2 заявляет, что значение псевдопеременной self не изменилось и все еще доступно. Благодаря этому, мы можем произвести «подмену класса». То есть, для того, чтобы отвлечься от способа, которым Delphi передает в методы указатель на объект, мы просто зарегистрируем в качестве обработчика адрес метода объекта:

    Обратите внимание на код процедуры TAbstractHandler.HandleAbstract – он генерирует исключение с именем класса в качестве текста сообщения. На первый взгляд кажется, что он всегда будет возвращать строку “TAbstractHandler”, но это не так. Дело в том, что мы вызвали метод TAbstractHandler.HandleAbstract на объекте совсем другого класса! Фактически выполняющийся код очень похож на вот такой:

    В таком примере текст исключения будет содержать “TAbstractObject”. Обычно подобные вызовы приводят к ошибкам, но при соблюдении некоторых правил они вполне безопасны. «Пессимистическая» версия этих правил такова: вызывать «чужой» метод можно только в том случае, если он пользуется только полями и методами общего предка «своего» и «чужого» класса. На практике свободы больше, но для нашего случая ее уже вполне достаточно. Метод HandleAbstract пользуется только методом ClassName, доступным в TObject, который гарантированно является предком всех классов Delphi.

    ПРЕДУПРЕЖДЕНИЕ

    Эта методика не работает при вызове абстрактного метода класса. В методах класса self указывает на класс, а не на объект, и используемая подмена некорректна. К сожалению, надежного способа борьбы с этим я не вижу – довольно-таки сложно отличить указатель на VMT от указателя на указатель на VMT.

    Когда такое исключение возникает во время работы программы, можно посмотреть на объявление указанного класса и найти там метод, который остался абстрактным. Либо, если метод был оставлен абстрактным намеренно, в предположении, что он будет реализован в потомках, можно поискать то место в программе, которое приводит к созданию экземпляра абстрактного класса вместо класса-потомка. Подробнее об этом в следующем разделе.

    Раннее упреждение

    Чтобы предотвратить создание экземпляров абстрактных классов, надо, прежде всего, ответить на вопрос: «является ли данный класс абстрактным?». Ответ на этот вопрос прост: «класс является абстрактным, если он содержит абстрактные методы». Сама Delphi не содержит встроенных средств для проверки методов на абстрактность, поэтому такие средства придется изобрести самостоятельно.

    Чтобы узнать, абстрактен ли метод класса, придется немного покопаться в темных глубинах модуля System при помощи пошаговой отладки. Как мы уже знаем из предыдущего раздела, попытка вызвать абстрактный метод приводит нас в процедуру _AbstractError. Теперь нам необходимо проследить путь, ведущий в эту процедуру.

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

    Структура классов Delphi

    Устройство данных, размещенных по адресу, задаваемому TClass, является деталью реализации, скрытой от программиста. Относительно безопасно можно делать следующее:

    • Сравнивать ссылки на класс для проверки типа объекта.
    • Вызывать методы класса.
    • Вызывать конструкторы.

    К сожалению, этой функциональности недостаточно для поиска абстрактных методов. Для такого поиска нам придется заглянуть «под капот» класса, а именно – посмотреть, как работает метод TObject.ClassType. Реализация, конечно, может меняться от версии к версии. В Delphi 5 код предельно лаконичен:

    Delphi 6 не вносит ничего нового, хотя тот же код на Паскале читается легче, чем на ассемблере:

    Итак, этот метод возвращает адрес, на который указывают самые первые четыре байта в теле объекта. Нам это вряд ли помогло бы, если бы не знание о совместимости Delphi с COM. Как известно, структура COM-объектов строго стандартизована. В начале объекта должен быть расположен указатель на VMT. Дополнительным подтверждением этому служат константы с именами, начинающимися на vmt*, определенные в модуле System:

    Как интересно! Часть из них меньше нуля. Судя по именам констант, вплоть до vmtAfterConstruction (смещение -28) расположены указатели на различные интересные данные. Затем идут указатели на виртуальные методы, декларированные в самом TObject: AfterConstruction, BeforeDestruction, Dispatch, DefaultHandler, NewInstance, FreeInstance, Destroy. Затем идут методы с неотрицательными смещениями. Таким образом, указатель, расположенный в начале объекта, ссылается куда-то «в середину» VMT. И эта середина – ровно то место, с которого будут располагаться виртуальные методы, объявленные в классах-потомках. Из названий констант vmtQueryInterface, vmtAddRef и vmtRelease ясно, зачем так сделано – иначе в потомках TObject было бы невозможно реализовать интерфейс IUnknown.

    Итак, 4 байта, полученных при вызове TObject.ClassType, указывают в начало таблицы виртуальных методов, декларированных в потомках TObject. Этот вывод можно считать «безопасным» до тех пор, пока Delphi поддерживает совместимость с COM.

    Абстрактные методы

    Как нам уже известно, выполнение абстрактного вызова приводит нас в магическую процедуру System._AbstractError. Осталось понять, как это происходит. Внимательная трассировка покажет нам со всей неизбежностью, что адрес этой процедуры записывается в те позиции VMT, которые соответствуют абстрактным методам. Таким образом, для любой заданной позиции в VMT можно узнать, реализован ли соответствующий ей метод, сравнив ее значение с адресом процедуры _AbstractError.

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

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

    Этот код требует некоторых пояснений.

    Во-первых, наша процедура AbstractProc объявлена методом класса – это сделано для того, чтобы получить ее адрес без создания экземпляра класса TAbstractHandler. Это не влияет на структуру VMT – методы класса устроены точно так же, только у них self указывает на класс, а не на объект.

    Во-вторых, для получения адреса используется временная переменная типа procedure of object – указатель на метод. Это самый простой способ вынудить Delphi реально прочитать адрес метода из VMT – попытки взять адрес метода при помощи оператора @ не приведут к желаемому результату. Вместо адреса _AbstractProc будет получен адрес специально сгенерированного псевдометода, который состоит только из инструкции JMP на все тот же адрес _AbstractProc. Судя по всему, этот псевдометод нужен для того, чтобы компилятор мог встроить его вызов в случаях, когда он точно знает класс объекта. В таких ситуациях Delphi не делает косвенного вызова, а подставляет сразу абсолютный адрес метода.

    Получив указатель на метод класса в переменной TAP, мы выделяем из него указатель на код при помощи документированного приведения к типу SysUtils.TMethod.

    Однако эти эксперименты мы проводили над классом, который скомпилирован, как часть нашего приложения. В нашем же примере часть классов расположена в отдельном пакете, который компилируется в отдельный файл-библиотеку. Будет ли происходить вызов той же _AbstractProc из таких классов? И если будет, то как?

    Для получения ответа на эти вопросы необходимо знать о том, как Delphi реализует динамически подключаемые пакеты компонентов. Подробное рассмотрение этой темы выходит за пределы данной статьи. Поэтому я сразу предоставлю здесь результат, пропустив описание своих исследований .bpl-файлов.

    Да, Delphi строго следит за тем, чтобы в приложение нельзя было загрузить две версии одного и того же модуля в разных пакетах. То есть мы можем быть уверены, что любой абстрактный вызов приведет нас в единственную _AbstractProc. Для этого он пользуется механизмом таблиц импорта, предоставленным форматом PE-файлов Windows. На практике это означает, что соответствующая позиция в VMT будет указывать на фрагмент кода (thunk) следующего вида:

    Здесь addr – это адрес слота в таблице импорта. По этому адресу лежит настоящий адрес метода. Данная информация позволяет написать код, который сможет отличать указатели на «настоящие» методы от указателей на импортированные методы. Вот этот код:

    ПРИМЕЧАНИЕ

    Есть, конечно, определенный риск встретить «настоящий» метод, который будет начинаться с точно такой же инструкции косвенного перехода. Но вероятность этого весьма мала потому, что стандартный пролог метода (то, во что компилируется ключевое слово begin) выглядит по-другому. Для того, чтобы его изменить, от разработчика класса требуются специальные усилия. А реализация _AbstractProc начинается с инструкции CMP и тоже нас устраивает в смысле определения реального адреса.

    Соответственно этому, мы все указатели на код, который потенциально может быть импортирован, будем прогонять через этот метод. В частности, придется слегка модифицировать TAbstractHandler.AbsProcAddress:

    Итак, у нас есть образец позиции в VMT, которая соответствует абстрактным методам.

    Теперь можно оборудовать наш класс методом проверки на абстрактность:

    Абстрактные классы

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

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

    Чтобы это сделать, нужно как-то определить адрес конца VMT. Никаких стандартных способов это сделать не существует. Я потратил довольно много времени на анализ окрестностей VMT, но обнаружил только то, что в Delphi 5 различные RTTI-данные, относящиеся к классу, расположены в непосредственной близости от VMT. В частности, таблица имен полей, таблица имен методов, таблица динамических методов, имя класса, и информация о типе идут после VMT именно в порядке перечисления. А таблица интерфейсов, реализуемых классом, обычно расположена до начала VMT.

    Это не слишком-то надежные предположения, так что для определения конца VMT мы будем использовать наименьший из указателей, хранящихся в документированных полях VMT:

    При этом мы будем проверять, что значения этого указателя больше адреса VMT:

    После определения адресов начала и конца VMT проверка всех методов класса на абстрактность является тривиальной задачей. Осталось только добавить в наш код различные украшения типа определения класса-предка, в котором был декларирован абстрактный метод, форматирования текстов исключений по вкусу и так далее.

    Заключение

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

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

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

    Полный исходный код модуля приведен в файле

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

    Единственным улучшением, которое я бы внес в код обработчика абстрактных вызовов, является корректировка обработки абстрактных методов класса. Как я уже говорил, данная методика предполагает, что в переменной self хранится указатель на объект, и скорее всего приведет к AV, если на самом деле там хранится указатель на класс. Есть идея реализовать пару функций:

    основываясь на предположении о том, что в корректной VMT по смещению vmtSelfPtr должен лежать адрес ее начала:

    Проверку этой гипотезы и усовершенствование кода я оставляю читателям.

    Проверка боем

    Проверим работоспособность созданного обработчика на нашем примере. Архив AbstractCalc.zip содержит две версии приложения: SuperCalc.dpr – это первоначальный вариант. SmartCalc.dpr получен из него путем добавления AbstractHandler.pas.

    ПРИМЕЧАНИЕ

    Вы можете скомпилировать примеры, следуя инструкциям в файле ReadMe.txt.

    При запуске первой версии калькулятора попытка выбрать из списка оператор количества сочетаний приводит к появлению следующего сообщения:

    Рисунок 1: Краткость – сестра таланта

    Улучшенная версия сможет рассказать об ошибке более подробно:

    Рисунок 2: Действие ‘сыворотки правды’ на приложение-пример

    RunError — Процедура Delphi

    С целью поддержки структурной обработки исключительных ситуаций (exception) в Delphi введены новые расширения языка Pascal. В данной статье будет дано описание того, что из себя представляет такая обработка, почему она полезна, будут приведены соответствующий синтаксис Object Pascal и примеры использования исключительных ситуаций в Delphi.

    Структурная обработка исключительных ситуаций

    Структурная обработка исключительных ситуаций — это система, позволяющая программисту при возникновении ошибки (исключительной ситуации) связаться с кодом программы, подготовленным для обработки такой ошибки. Это выполняется с помощью языковых конструкций, которые как бы «охраняют» фрагмент кода программы и определяют обработчики ошибок, которые будут вызываться, если что-то пойдет не так в «охраняемом» участке кода. В данном случае понятие исключительной ситуации относится к языку и не нужно его путать с системными исключительными ситуациями (hardware exceptions), такими как General Protection Fault. Эти исключительные ситуации обычно используют прерывания и особые состояния «железа» для обработки критичной системной ошибки; исключительные ситуации в Delphi же независимы от «железа», не используют прерываний и используются для обработки ошибочных состояний, с которыми подпрограмма не готова иметь дело. Системные исключительные ситуации, конечно, могут быть перехвачены и преобразованы в языковые исключительные ситуации, но это только одно из применений языковых исключительных ситуаций.

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

    Такая «пожарная бригада» для обработки ошибок трудоемка, требует написания большого количества кода в котором можно легко ошибиться и который трудно отлаживать.

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

    Структурная обработка исключительной ситуации замещает ручную обработку ошибок автоматической, сгенерированной компилятором системой уведомления. В приведенном выше примере, процедура A установила бы «охрану» со связанным обработчиком ошибки на фрагмент кода, в котором вызывается B. B просто вызывает C. Когда C обнаруживает ошибку, то создает (raise) исключительную ситуацию. Специальный код, сгенерированный компилятором и встроенный в Run-Time Library (RTL) начинает поиск обработчика данной исключительной ситуации. При поиске «защищенного» участка кода используется информация, сохраненная в стеке. В процедурах C и B нет такого участка, а в A — есть. Если один из обработчиков ошибок, которые используются в A, подходит по типу для возникшей в C исключительной ситуации, то программа переходит на его выполнение. При этом, область стека, используемая в B и C, очищается; выполнение этих процедур прекращается.

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

    Без проверки возвращаемого кода после каждого вызова подпрограммы, код программы должен быть более простым, а скомпилированный код — более быстрым. При наличии исключительных ситуаций подпрограмма B не должна содержать дополнительный код для проверки возвращаемого результата и передачи его в A. B ничего не должна делать для передачи исключительной ситуации, возникшей в C, в процедуру A — встроенная система обработки исключительных ситуаций делает всю работу.

    Данная система называется структурной, поскольку обработка ошибок определяется областью «защищенного» кода; такие области могут быть вложенными. Выполнение программы не может перейти на произвольный участок кода; выполнение программы может перейти только на обработчик исключительной ситуации активной программы.

    Модель исключительных ситуаций в Delphi

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

    Синтаксис обработки исключительных ситуаций

    Теперь, когда мы рассмотрели, что такое исключительные ситуации, давайте дадим ясную картину, как они применяются. Новое ключевое слово, добавленное в язык Object Pascal — try. Оно используется для обозначения первой части защищенного участка кода. Существует два типа защищенных участков:

    • try..except
    • try..finally

    Первый тип используется для обработки исключительных ситуаций. Его синтаксис:

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

    Примеры обработки исключительных ситуаций

    Ниже приведены процедуры A,B и C, обсуждавшиеся ранее, воплощенные в новом синтаксисе Object Pascal:

    При ErrorCondition = True программа выдаст:

    Возможно вас удивила декларация типа ‘ESampleError = . Описание новой объектной модели дается в других уроках. Здесь же достаточно сказать, что исключительные ситуации (exceptions) являются классами, частью новой объектной модели.

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

    Процедура A помещает часть кода в блок try..except. Первая часть этого блока содержит часть кода, аналогично конструкции begin..end. Эта часть кода завершается ключевым словом except, далее следует один или более обработчиков исключительных ситуаций on xxxx do yyyy, далее может быть включен необязательный блок else, вся конструкция заканчивается end;. В конструкции, назначающей определенную обработку для конкретной исключительной ситуации (on xxxx do yyyy), после резервного слова on указывается класс исключительной ситуации, а после do следует собственно код обработки данной ошибки. Если возникшая исключительная ситуация подходит по типу к указанному после on, то выполнение программы переходит сюда (на код после do). Исключительная ситуация подходит в том случае, если она того же класса, что указан в on, либо является его потомком. Например, в случае on EFileNotFound обрабатываться будет ситуация, когда файл не найден. А в случае on EFileIO — все ошибки при работе с файлами, в том числе и предыдущая ситуация. В блоке else обрабатываются все ошибки, не обработанные до этого.

    Приведенные в примере процедуры содержат код (строка с writeln), который отображает путь выполнения программы. Когда C вызывает exception, программа сразу переходит на обработчик ошибок в процедуре A, игнорируя оставшуюся часть кода в процедурах B и C.

    После того, как найден подходящий обработчик ошибки, поиск оканчивается. После выполнения кода обработчика, программа продолжает выполняться с оператора, стоящего после слова end блока try..except (в примере — writeln(‘Exit A’)).

    Конструкция try..except подходит, если известно, какой тип ошибок нужно обрабатывать в конкретной ситуации. Но что делать, если требуется выполнить некоторые действия в любом случае, произошла ошибка или нет? Это тот случай, когда понадобится конструкция try..finally.

    Рассмотрим модифицированную процедуру B:

    Если C вызывает исключительную ситуацию, то программа уже не возвращается в процедуру B. А что же с теми 1000 байтами памяти, захваченными в B? Строка FreeMem(P,1000) не выполнится и Вы потеряете кусок памяти. Как это исправить? Нужно ненавязчиво включить процедуру B в процесс, например:

    Если в A поместить вызов NewB вместо B, то программа выведет сообщения следующим образом:

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

    Почему вызов GetMem не помещен внутрь блока try? Этот вызов может окончиться неудачно и вызвать exception EOutOfMemory. Если это произошло, то FreeMem попытается освободить память, которая не была распределена. Когда мы размещаем GetMem вне защищаемого участка, то предполагаем, что B сможет получить нужное количество памяти, а если нет, то более верхняя процедура получит уведомление EOutOfMemory.

    А что, если требуется в B распределить 4 области памяти по схеме все-или-ничего? Если первые две попытки удались, а третья провалилась, то как освободить захваченную область память? Можно так:

    Установив сперва указатели в NIL, далее можно определить, успешно ли прошел вызов GetMem.

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

    Вызов исключительной ситуации

    В процедуре C из примера мы уже могли видеть, как должна поступать программа при обнаружении состояния ошибки — она вызывает исключительную ситуацию:

    После ключевого слова raise следует код, аналогичный тому, что используется для создания нового экземпляра класса. Действительно, в момент вызова исключительной ситуации создается экземпляр указанного класса; данный экземпляр существует до момента окончания обработки исключительной ситуации и затем автоматически уничтожается. Вся информация, которую нужно сообщить в обработчик ошибки передается в объект через его конструктор в момент создания.

    Почти все существующие классы исключительных ситуаций являются наследниками базового класса Exception и не содержат новых свойств или методов. Класс Exception имеет несколько конструкторов, какой из них конкретно использовать — зависит от задачи. Описание класса Exception можно найти в on-line Help.

    Доступ к экземпляру объекта exception

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

    Как уже говорилось, при вызове исключительной ситуации (raise) автоматически создается экземпляр соответствующего класса, который и содержит информацию об ошибке. Весь вопрос в том, как в обработчике данной ситуации получить доступ к этому объекту.

    Рассмотрим модифицированную процедуру A в нашем примере:

    Здесь все изменения внесены в строку

    Пример демонстрирует еще одно новшество в языке Object Pascal — создание локальной переменной. В нашем примере локальной переменной является ESE — это тот самый экземпляр класса ESampleError, который был создан в процедуре C в момент вызова исключительного состояния. Переменная ESE доступна только внутри блока do. Свойство Message объекта ESE содержит сообщение, которое было передано в конструктор Create в процедуре C.

    Есть еще один способ доступа к экземпляру exception — использовать функцию ExceptionObject:

    Предопределенные обработчики исключительных ситуаций

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

    • Exception — базовый класс-предок всех обработчиков исключительных ситуаций.
    • EAbort — «скрытое» исключение. Используйте его тогда, когда хотите прервать тот или иной процесс с условием, что пользователь программы не должен видеть сообщения об ошибке. Для повышения удобства использования в модуле SysUtils предусмотрена процедура Abort, определенная, как:
    • EComponentError — вызывается в двух ситуациях:
      1. при попытке регистрации компоненты за пределами процедуры Register;
      2. когда имя компоненты не уникально или не допустимо.
    • EConvertError — происходит в случае возникновения ошибки при выполнении функций StrToInt и StrToFloat, когда конвертация строки в соответствующий числовой тип невозможна.
    • EInOutError — происходит при ошибках ввода/вывода при включенной директиве <$I+>.
    • EIntError — предок исключений, случающихся при выполнении целочисленных операций.
      • EDivByZero — вызывается в случае деления на ноль, как результат RunTime Error 200.
      • EIntOverflow — вызывается при попытке выполнения операций, приводящих к переполнению целых переменных, как результат RunTime Error 215 при включенной директиве <$Q+>.
      • ERangeError — вызывается при попытке обращения к элементам массива по индексу, выходящему за пределы массива, как результат RunTime Error 201 при включенной директиве <$R+>.
    • EInval — General Protection Fault. Соответствует RunTime Error 216.
    • EInval >Delphi, обладая прекрасными средствами доступа к данным, основывающимися на интерфейсе >Особенно важны два свойства класса EDBEngineError : Errors — список всех ошибок, находящихся в стеке ошибок BDE. Индекс первой ошибки 0;
      ErrorCount — количество ошибок в стеке.
      Объекты, содержащиеся в Errors, имеют тип TDBError. Доступные свойства класса TDBError:
      ErrorCode — код ошибки, возвращаемый Borland Database Engine;
      Category — категория ошибки, описанной в ErrorCode;
      SubCode — ‘субкод’ ошибки из ErrorCode;
      NativeError — ошибка, возвращаемая сервером БД. Если NativeError 0, то ошибка в ErrorCode не от сервера;
      Message — сообщение, переданное сервером, если NativeError не равно 0; сообщение BDE — в противном случае.
  • EDBEditError — наследник Exception ; вызывается, когда данные не совместимы с маской ввода, наложенной на поле. Объявлено в модуле Mask.
  • Заключение

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

    Процедуры Delphi

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

    Главное отличие процедуры от функции состоит в том, что есть связь между именем функции и значением. Таким образом, возможно использование имени функции в разнообразных выражениях. Организация процедуры Delphi по сути ничем не отличается от организации функции.

    Как и для функции, в начале каждой процедуры указывают ее заголовок. Затем называют эту процедуру каким-нибудь именем. После имени в скобках следует перечень параметров, заканчивающийся символом «;» и потом уже описывают разделы констант, типов, переменных и инструкций.

    Общий вид процедуры Delphi:

    Общий вид процедуры Delphi выглядит следующим образом:

    В приведенном выше примере, как мы видим, в состав заголовка процедуры сначала входит служебное словоprocedure. Далее за этим ключевым словом следом располагается имя процедуры, использующееся при вызове процедуры, обеспечивая активизацию ее выполнения.

    Если процедура обладает какими-либо параметрами, то программист их указывает в скобках, сразу после имени процедуры. В конце заголовка процедуры ставится символ «;». В случае, когда в процедуре имеются именованные константы, программист объявляет их в разделеconst.

    Далее за этим разделом идет раздел type, использующийся для объявления типов. После располагается раздел var, содержащий все переменные, которые впоследствии программист использует в своей программе. Затем следует раздел инструкций , которые включает в себя исполняемые инструкции данной процедуры.

    Примеры процедуры Delphi:

    Приведем пример процедуры Delphi, вычисляющей стоимость некоторой покупки:

    Илон Маск рекомендует:  Что такое код ignore_user_abort
    Понравилась статья? Поделиться с друзьями:
    Кодинг, CSS и SQL