Эксперименты с POSIX сигналами в Windows

от автора

Всем здравствуйте. Я расскажу о различных, подчас интересных, эффектах, которые возникают при работе с POSIX сигналами в Windows. Затаривайтесь Вискасом и вперёд.

Полёт будет долгий
Полёт будет долгий

Конечно же не могу не упомянуть гибкость механизма сигналов POSIX. Этот механизм реализует подобие парадигмы событийного программирования в C#, чем он, собственно, и интересен. Обычно, в коде, написанном на C/C++ мы вызываем функции ядра, например внутри getch(). А вот когда ядро вызывает нас, происходят всякие интересные эффекты: такие как асинхронные вызовы и произвольная логика программы, изменение способа обработки исключительных ситуаций. Чем то напоминает callback.

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

Я продемонстрирую несколько экспериментов с сигналами, в которых обнаруживаются не совсем очевидные вещи. В качестве примеров я выбрал обработчики сигналов SIGINT и SIGABRT. Обработчик SIGINT вызывается асинхронно, в ответ на нажатие клавиш CTRL+C. Обработчик SIGABRT вызывается в ответ на необработанную исключительную ситуацию, но не всегда. Я объясню особенности, возникающие в зависимости от способа генерации сигналов и наличия отладчика. Обратите внимание на то, что все эксперименты проводились в Windows, а исходный код компилируется как код C++.

Содержание

  1. Обработчики сигналов запускаются в разном контексте в зависимости от того, каким образом сгенерирован сигнал.

  2. Обработка исключения, в зависимости от контекста обработчика сигнала.

  3. Обработчик сигнала SIGABRT вызывается, если программа выполняется не под отладчиком.

Исходный код программы

#include <windows.h>  #include <stdlib.h> #include <stdio.h> #include <conio.h>  #include <signal.h> #include <errno.h>  #define SIGINT_RAISE  int _get_thread_id() { HANDLE hThread = 0; DWORD dwId = 0; hThread = GetCurrentThread(); dwId = GetThreadId(hThread); return (int)dwId; }  void funcabrt(int sig) // обработчик неперехваченного исключения { _set_abort_behavior(0, _CALL_REPORTFAULT); perror("funcabrt"); printf("Thread ID: %d\n", _get_thread_id()); _getch(); };  void funcint(int sig) // обработчик нажатия клавиш CTRL+C { try { printf("funcint\n"); printf("Thread ID: %d\n", _get_thread_id()); _getch(); throw (int)ERANGE; } catch (int err) { _set_errno(err); throw; }  };  int main() { #if defined(SIGINT_RAISE) printf("Raise SIGINT mode on\n"); #else printf("Raise SIGINT mode off\n"); #endif  printf("main\n"); printf("Thread ID: %d\n", _get_thread_id()); try { if (signal(SIGABRT, funcabrt) == SIG_ERR) exit(EXIT_FAILURE); if (signal(SIGINT, funcint) == SIG_ERR) exit(EXIT_FAILURE); #if defined(SIGINT_RAISE) raise(SIGINT); #endif  while (1); // нажмите CTRL+C вылетит птенец } catch (int err) { printf("catch\n"); _getch(); }; };

Обработчики сигналов запускаются в разном контексте в зависимости от того, каким образом сгенерирован сигнал.

Обработчик сигнала SIGINT
void funcint(int sig) // обработчик нажатия клавиш CTRL+C {     try     {         printf("funcint\n");         printf("Thread ID: %d\n", _get_thread_id());         _getch();         throw (int)ERANGE;     }     catch (int err)     {         _set_errno(err);         throw;     }      };

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

Вывод

Raise SIGINT mode on main Thread ID: 5380 funcint Thread ID: 5380

Смотрим стек вызовов

ntdll.dll!775270f4() ntdll.dll![Указанные ниже кадры могут быть неверны или отсутствовать, символы для ntdll.dll не загружены] ntdll.dll!775264a4() kernel32.dll!76054b6e() kernel32.dll!760acf97() kernel32.dll!760ad071() ucrtbase.dll!0fb5ed35() ucrtbase.dll!0fb5ec74() experiment.exe!funcint(int sig) ucrtbase.dll!0fb2ca9a() experiment.exe!main() [Внедренный фрейм] experiment.exe!invoke_main() experiment.exe!__scrt_common_main_seh() kernel32.dll!7604ed6c() ntdll.dll!775437eb() ntdll.dll!775437be()

Мы видим, что вызов обработчика происходит в основном потоке, посредством библиотеки выполнения C Runtime

Случай, когда сигнал возбужден ядром. Это стандартная ситуация, когда сигнал SIGINT возбуждается при нажатии на клавиатуре CTRL+C при работе консольных приложений. Обработчик запускается в контексте ядра в отдельном потоке.

Вывод

Raise SIGINT mode off main Thread ID: 976 funcint Thread ID: 2848

Смотрим стек вызовов потока 2848

ntdll.dll!775270f4() ntdll.dll![Указанные ниже кадры могут быть неверны или отсутствовать, символы для ntdll.dll не загружены] ntdll.dll!775264a4() kernel32.dll!76054b6e() kernel32.dll!760acf97() kernel32.dll!760ad071() ucrtbase.dll!0fb5ed35() ucrtbase.dll!0fb5ec74() experiment.exe!funcint(int sig) ucrtbase.dll!0fb2c72d() kernel32.dll!7607e3d8() kernel32.dll!7604ed6c() ntdll.dll!775437eb() ntdll.dll!775437be()

Мы видим, что вызов обработчика происходит в потоке kernel32.dll, посредством библиотеки выполнения C Runtime

Обработка исключения, в зависимости от контекста обработчика сигнала.

int main() { #if defined(SIGINT_RAISE) printf("Raise SIGINT mode on\n"); #else printf("Raise SIGINT mode off\n"); #endif  printf("main\n"); printf("Thread ID: %d\n", _get_thread_id()); try { if (signal(SIGABRT, funcabrt) == SIG_ERR) exit(EXIT_FAILURE); if (signal(SIGINT, funcint) == SIG_ERR) exit(EXIT_FAILURE); #if defined(SIGINT_RAISE) raise(SIGINT); #endif  while (1); // нажмите CTRL+C вылетит птенец } catch (int err) { printf("catch\n"); _getch(); }; };

Здесь в том случае, если сигнал сгенерирован программой, возбужденное исключение попадёт в блок try/catch. Причина следует из особенностей, которые я описал в п.1. При возбуждении исключения происходит раскрутка стека, т.е. поиск обработчиков в генерирующей сигнал (и фактически вызвавшей её обработчик) функции.

Вывод

Raise SIGINT mode on main Thread ID: 4676 funcint Thread ID: 4676 catch

А вот если SIGINT сгенерирован ядром, исключение не найдёт своего обработчика и отправится в отладчик, либо будет сгенерирован SIGABRT. Точнее найдёт, возможно, в неграх ядра есть SEH обработчик верхнего уровня (системный), до которого исключение в конце концов добирается и уже этот обработчик производит фактически вызов обработчика SIGABRT, который находится в нашей программе. Однако, зафиксировать этот факт при отладке не представляется возможным.

Обработчик сигнала SIGABRT вызывается, если программа выполняется не под отладчиком.

Возбуждаем пустое исключение.

throw;

Если программа выполняется под отладчиком, неперехваченное исключение попадает в отладчик, т.к. никаких обработчиков для такого исключения не предусмотрено.

Вывод

Raise SIGINT mode off main Thread ID: 2940 funcint Thread ID: 5372

Отладчик

Если программа выполняется без присоединённого отладчика, происходит генерация SIGABRT

Обработчик сигнала SIGABRT
void funcabrt(int sig) // обработчик неперехваченного исключения { _set_abort_behavior(0, _CALL_REPORTFAULT); perror("funcabrt"); printf("Thread ID: %d\n", _get_thread_id()); _getch(); };

Вывод

Raise SIGINT mode off main Thread ID: 5748 funcint Thread ID: 2732 funcabrt: Result too large Thread ID: 2732

Итоги:

  1. Если мы генерируем сигнал в программе, то обработчик сигнала вызывается как обычная функция Если сигнал генерирует ядро, то обработчик сигнала вызывается в отдельном потоке

  2. Если программа выполняется под отладчиком, все сгенерированные и необработанные исключения перехватывает отладчик. Если программа выполняется без отладчика, вызывается обработчик сигнала SIGABRT



ссылка на оригинал статьи https://habr.com/ru/articles/593395/


Комментарии

Добавить комментарий

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