Использование SEH в 32 разрядных приложениях Windows с компилятором Mingw-W64

Что такое SEH


Из всех механизмов, предоставляемых операционными системами семейства Windows, возможно наиболее широко используемым, но не полностью документированным, является механизм структурной обработки исключений (он же Structured Exception Handling, или просто — SEH). Структурная обработка исключений — это сервис, предоставляемый операционной системой, механизм обработки программных и аппаратных исключений в операционной системе Microsoft Windows, позволяющий программистам управлять обработкой исключений. Исключение — это событие при выполнении программы, которое приводит к её ненормальному или неправильному поведению.

Вся документация по SEH, которую вы, вероятно, найдете, описывает одну лишь компиляторно-зависимую оболочку, созданную функциями библиотеки времени выполнения (Run-Time-Library, RTL) вокруг реализации SEH операционной системы. В ключевых словах _try, _finally, или _except, нет ничего магического. Группы разработчиков из Microsoft, занимающиеся разработкой операционных систем и компиляторов, определили эти ключевые слова, и то, что они делают. Другие поставщики компиляторов просто поддержали эту семантику. Видимые программисту инструменты SEH уровня компилятора призваны скрыть базовый механизм SEH уровня операционной системы, что позволяет не обнародовать детали функционирования последнего. Основные детали базового механизма SEH уровня ОС будут рассмотрены в этой статье. В статье отражены личные взгляды и предпочтения автора.
Основной смысл SEH состоит в следующем: когда выполняющийся поток совершает ошибку, ОС дает возможность узнать об этом. Точнее, когда поток совершает ошибку, ОС приостанавливает поток, сохраняет все регистры процессора в специальной структуре и из служебного потока, связанного с тем, который вызвал ошибку, вызывает определенную пользователем callback-функцию. Функциональность этой callback-функции в значительной степени не регламентирована, т.е. в процессе своей работы она может делать все, что угодно. Например, она может устранить причину ошибки, если это возможно. Независимо оттого, что эта callback-функция делает, она должна возвратить значение, которое скажет системе, что делать дальше. Эта callback-функция называется exception handler (обработчик исключения). Для того, чтобы быть вызванной:

  1. Функция обработчик исключения должна соответствовать прототипу
  2. Адрес функции обработчика исключения должен быть указан в специальной структуре данных EXCEPTION_REGISTRATION
  3. Структура данных EXCEPTION_REGISTRATION должна располагаться в стеке потока, который вызвал исключение с выравниванием адреса структуры и ее компонентов на адрес, кратный 4 (биты 0 и 1 адреса равны нулю)
  4. Тело функции обработчика исключения не должно располагаться в стеке потока, который вызвал исключение


Значение, которое возвращает обработчик исключения, указывает ОС обработано ли исключение или нет.

Технические детали SEH


Прототип функции обработчика исключения:

EXCEPTION_DISPOSITION
 __cdecl _except_handler(
     struct _EXCEPTION_RECORD *ExceptionRecord,
     void * EstablisherFrame,
     struct _CONTEXT *ContextRecord,
     void * DispatcherContext
     );


EXCEPTION_DISPOSITION это макроопределение, имеющее тип int, _EXCEPTION_RECORD содержит информацию о произошедшем исключении, EstablisherFrame — адрес структуры EXCEPTION_REGISTRATION в стеке потока, вызвавшего исключение, _CONTEXT — адрес структуры содержащей все регистры процессора в момент возникновения исключения, DispatcherContext — служебая информация

Структура данных EXCEPTION_REGISTRATION

_EXCEPTION_REGISTRATION struc
     prev    dd      ?
     handler dd      ?
 _EXCEPTION_REGISTRATION ends


image

Как видно из определения, структура состоит из 2х элементов по 32 бита, prev — адрес структуры с предыдущим обработчиком, handler — адрес функции обработчика исключения. Наличие элемента prev позволяет выстраивать цепочку из обработчиков, адрес последней структуры с актуальным обработчиком доступен через регистр fs:0, отсюда и начинается просмотр и вызов последовательности обработчиков, до тех пор, пока один из них не вернет значение ExceptionContinueExecution (0). Если ни один обработчик из цепочки обработчиков не может обработать исключение, то его обрабатывает первый элемент цепочки — обработчик исключений по умолчанию (стандартный обработчик), у которого нет варианта отказаться от обработки исключения.

image

SEH это ошибка


Я совершенно уверен в том, что сама изначальная идея SEH ошибочна. Операционная система будет сообщать программе о возникновении незапланированных «исключительных» ситуаций, таких как «деление на 0», «неверный HANDLE», «попытка исполнения неверной инструкции процессора» «попытка доступа к памяти при отсутствии соответствующих разрешений» и так далее. Совершенно нереально исправить такие ошибки во время исполнения программы, ошибка уже случилась, программа уже пошла по неверному пути, который привел ее к этой ошибке. Нельзя уже сделать ничего разумного, но SEH дает возможности:

  1. Скрыть ошибку, заменив стандартный обработчик ошибок своим, и «обработать» исключение
  2. Позволить программе «прожить» немного дольше, сделав вид, что «ничего не случилось»


И первое, и второе очень плохо.

К сожалению, стандартные обработчики ошибок современных ОС семейства Windows не показывают пользователю ничего, что могло бы информировать его о том, что случилось и что надо делать, после паузы и показа неинформативного окна приложение аварийно закрывается. Моя идея состоит в том, чтобы при возникновении исключения программист должен иметь возможность информировать пользователя об ошибке с помощью специально сформированных сообщений, после чего выполнить минимально возможную «приборку за собой» и постараться корректно закрыть программу. Еще раз напоминаю, что в большинстве случаев нормальное продолжение выполнения программы после возникновения исключения невозможно.

Компилятор GCC


GCC является главным компилятором для сборки ряда операционных систем; среди них — различные варианты Linux и BSD, а также ReactOS, Mac OS X, OpenSolaris, NeXTSTEP, BeOS и Haiku.

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

GCC является лидером по количеству процессоров и операционных систем, которые он поддерживает. Имеет развитые средства для программирования на встроенном ассемблере (ассемблерные вставки). Генерирует компактный и быстрый исполняемый код. Имеет возможности оптимизации, в том числе с учетом ассемблерных вставок. К относительным недостаткам GCC можно отнести непривычный синтаксис AT&T используемый во встроенном ассемблере.

GCC/MinGW является полностью надежным качественным компилятором, который, на мой взгляд, превосходит любой доступный на сегодняшний день компилятор языка Си по качеству сгенерированного кода. Это несколько менее выражено с самыми последними версиями MSVC, но все еще заметно. Особенно для всего, что связано с inline assembly, GCC на мой взгляд, превосходит MSVC.

Соответствие стандартам, как мне кажется, также намного лучше в GCC.

MinGW-W64 компилятор


MinGW— набор инструментов разработки программного обеспечения для создания приложений под Windows. Включает в себя компилятор, родной программный порт GNU Compiler Collection (GCC) под Windows вместе с набором свободно распространяемых библиотек импорта и заголовочных файлов для Windows API. В MinGW включены расширения для библиотеки времени выполнения Microsoft Visual C++ для поддержки функциональности C99.

В связи с тем, что в рамках изначального проекта MinGW не обещалось, что в его кодовую базу будут вноситься обновления, связанные с добавлением некоторых новых ключевых элементов Win32 API, а также наиболее необходимой поддержки 64-битной архитектуры, был создан проект MinGW-w64. Он является новой чистой реализацией портирования GNU Compiler Collection (GCC) под Microsoft Windows, осуществленной изначально компанией OneVision и переданной в 2008 году в общественное пользование (Public Domain). Сначала он был предложен на рассмотрение для интеграции с оригинальным проектом MinGW, но был отклонен в связи с подозрением на использование несвободного или проприетарного кода. По многим серьезным причинам этического характера, связанным с отношением со стороны авторов MinGW, ведущие разработчики кода MinGW-w64 решили больше не пытаться кооперироваться с проектом MinGW.

MinGW-w64 обеспечивает более полную реализацию Win32 API, включая:

  • лучшую поддержку стандарта C99
  • лучшую поддержку pthreads (включая возможность задействовать функциональность стандарта C++11 библиотеки libstdc++ компилятора GCC)
  • GCC multilib
  • точки входа в программу с поддержкой Unicode (wmain/wWinMain)
  • DDK (из проекта ReactOS)
  • DirectX (из проекта WINE)
  • поддержку больших файлов
  • поддержку 64-битной архитектуры Windows


Собственно, компилятор используемый в статье доступен по 
ссылке

Поддержка SEH в Mingw-w64


Совершенно неудовлетворительна поддержка SEH, проще сказать — ее нет, несмотря на наличие макросов __try1() и __except1. Мало того, что ни синтаксически (поскольку синтаксис __try __except () не поддерживается), ни семантически (макросы __try1 __except1 только позволяют установить/удалить обработчик исключений) невозможно программирование SEH, как с компиляторами Microsoft или Borland/Embarcadero, даже само использование макросов __try1 __except1 может приводить к проблемам

Проблемы


Использование стандартных макросов __try1 и __except1 может приводить к проблемам, так как в этих макросах явно изменяется состояние регистра ESP, но компилятор, который должен вести учет использования стека (что означает, что компилятор всегда должен «знать», каково значение регистра ESP), об этом не уведомляется. Может быть, стандартные макросы будут работать, а может быть и нет.

Решение


Во-первых, однозначно придется отказаться от использования стандартных макросов __try1 и __except1, вместо них будут ассемблерные вставки, не затрагивающие регистр ESP и локальная переменная типа EXCEPTION_REGISTRATION в стеке. Приблизительно вот так:

static int exception=0;

EXCEPTION_REGISTRATION seh_ex_reg;
seh_ex_reg.handler = (PEXCEPTION_ROUTINE) exception_handler;
/*ассемблерная вставка, которая в поле prev записывает адрес предыдущей структуры из fs:0 и устанавливает новый адрес этой структуры в fs:0 */
asm ("\t movl %%fs:0, %%eax; movl %%eax, %0 \n" : "=r" (seh_ex_reg.prev) : : "%eax");
asm volatile("\t movl %0,%%eax; movl %%eax, %%fs:0 \n"::"r" (&seh_ex_reg) : "%eax");
// блок try
	{
	// code
	if(exception == 0)
		{
		//ассемблерная вставка: убираем обработчик исключений 
		asm (	// restore previous handler, ESP is not modified 
		"\t movl %0, %%eax \n"
		"\t movl %%eax, %%fs:0 \n" 
		: 
		:"r" (seh_ex_reg.prev) 
		: "%eax"
		);
		goto end;
		}
	}
// блок except
	{
	trusted_code://сюда мы попадаем при исключении
	//ассемблерная вставка: убираем обработчик исключений
	//выполняем очистку
	}
end:
exit();


Во-вторых, необходимо отделить доверяемый код (т.е., отлаженный, не генерирующий исключений) от недоверяемого (где возможны исключения). Это достаточно стандартная практика, где за блоком try следует блок except С помощью нескольких глобальных переменных, ассемблерных вставок и имеющего дурную славу опрератора goto удалось решить такую задачу. Решение не претендует на универсальность, над этим еще надо думать, пока это выглядит как сваленный в кучу набор трюков. Недостатки: много ассемблерного кода, используются статические переменные, не производится «размотка» стека, состояние локальных переменных в стеке try блока теряется безвозвратно. Корректное завершение программы возможно, используя только переменные и функции из except блока.

От использования статических переменных, впрочем, легко отказаться, для чего придется «расширить» структуру EXCEPTION_REGISTRATION, дополнив ее недостающими элементами, перенести туда все бывшие ранее статическими переменные. Это сделает код примера не намного сложнее, но зато код будет по-настоящему структурным, способным обрабатывать исключения на нескольких уровнях вложенности. Доступ к дополнительным элементам расширенной структуры EXCEPTION_REGISTRATION_EX из функции обработчика исключения осуществляется через параметр EstablisherFrame который нужно преобразовать к соответствующему типу. Заодно сохраним значения регистров ESP и EBP, чтобы восстановить стек фрейм к тому состоянию, которое было в начале блока try. Теперь наш пример обретает законченную форму. К сожалению, при возникновении исключения, содержимое локальных переменных блока try безвозвратно теряется.

Ссылка на исходный текст

© Habrahabr.ru