Когда в программе происходит ошибка и нужно проинформировать об этом факте себя любимого, то первая мысль - послать MessageBox. Именно так и поступает большинство новичков в программировании, однако очень скоро они убеждаются в многопоточном коварстве этой с виду безобидной функции и приходят к убеждению, что использование MessageBox для отладки - "плохой стиль программирования", а хороший стиль - писать лог файл или использовать вывод с помощью внешнего отладчика (OutputDebugString). На самом деле отладочный MessageBox, это прежде всего удобно, а лог файл и OutputDebugString, просто другие инструменты, которые в некоторых случаях удобнее MessageBox, а в некоторых нет. Но рассмотрим суть проблемы по порядку. Вызвав MessageBox, программист ожидает, что выполнение программы продолжится с того места, откуда вызван MessageBox. В программах, не имеющих собственного окна, всё именно так и происходит, даже несмотря на то что на самом деле API функция MessageBox выполняется в отдельном потоке. Код (Text): invoke GetModuleHandle, 0 ; Определить handle процесса mov [h_proc], eax .if eax == 0 invoke MessageBox, 0, zSTR(<'не удалось определить handle процесса', 13, 10, 'продолжить ?'>), addr szError, MB_YESNO .if eax == IDNO ; Выбрано завершение программы invoke ExitProcess, 0 ; Выход из программы .endif .endif Врождённая многопоточность MessageBox проявляется когда он вызван в оконном обработчике, к примеру, в событии WM_MOUSEMOVE или WM_KEYDOWN. В этом случае повторное сообщение вызовет новый MessageBox, несмотря на то, что старый ещё не закрыт. Любой мало-мальски опытный программер сразу же найдёт решение этой проблемы - достаточно указать MessageBox-у handle родительского окна, и сообщения мыши и клавиатуры не будут попадать в родительское окно до тех пор, пока MessageBox не будет закрыт. Код (Text): invoke MessageBox, [hwnd], zSTR('ошибка при обработке WM_KEYDOWN'), addr szError, MB_OK Ура! проблема решена? Ан нет, не тут то было - MessageBox по прежнему не блокирует сообщения таймера и если информация об ошибке пришла оттуда, то опять получаем серию дублированных MessageBox-ов, способных даже безнадёжно завесить программу. Выход прост - использовать флаг: Код (Text): .if [wmsg] == WM_TIMER ; Безопасный код inc [TimerCount] ; Код защищённый флагом .if [MessageErrorOn] == False … invoke … ; Вызов API .if eax == 0 mov [MessageErrorOn], True invoke MessageBox, [hwnd], zSTR(<'Ошибка в WM_TIMER', 13, 10 ,'продолжить ?'>), addr szError, MB_YESNO mov [MessageErrorOn], False .endif .endif ; конец защиты флагом Но и это не всё! Кроме WM_TIMER, есть ещё более коварное сообщение WM_PAINT. Дело в том, что прежде чем появиться на экране MessageBox сам посылает это сообщение родительскому окну и наотрез отказывается показываться, если оно сообщение не будет обработано! Код (Text): .if [wmsg] == WM_PAINT ; Код защищённый флагом .if [MessageErrorOn] == False invoke BeginPaint, [hwnd], addr PAST invoke … ; Вызов API .if eax == 0 mov [MessageErrorOn], True invoke MessageBox, [hwnd], zSTR(<'Ошибка в WM_PAINT', 13, 10 ,'продолжить ?'>), addr szError, MB_YESNO mov [MessageErrorOn], False .endif invoke EndPaint, [hwnd], addr PAST .endif ; конец защиты флагом Этот пример приведёт к тому, что MessageBox будет создан с характерным звуком, но останется невидимкой, причём, нажимая Пробел, Enter и стрелки управления курсором можно будет нажать невидимые экранные кнопки и продолжить выполнение с виду зависшей программы. А попытка совсем убрать проверку флага [MessageErrorOn] вызовет "самогенерирующуюся" бесконечную серию MessageBox-ов. Поэтому правильно будет переписать пример так: Код (Text): .if [wmsg] == WM_PAINT ; Код защищённый флагом invoke BeginPaint, [hwnd], addr PAST .if [MessageErrorOn] == False invoke … ; Вызов API .if eax == 0 mov [MessageErrorOn], True invoke MessageBox, [hwnd], zSTR(<'Ошибка в WM_PAINT', 13, 10 ,'продолжить ?'>), addr szError, MB_YESNO mov [MessageErrorOn], False .endif .endif ; конец защиты флагом invoke EndPaint, [hwnd], addr PAST или так: Код (Text): .if [wmsg] == WM_PAINT ; Код защищённый флагом .if [MessageErrorOn] == False invoke BeginPaint, [hwnd], addr PAST invoke … ; Вызов API .if eax == 0 mov [MessageErrorOn], True invoke MessageBox, [hwnd], zSTR(<'Ошибка в WM_PAINT', 13, 10 ,'продолжить ?'>), addr szError, MB_YESNO mov [MessageErrorOn], False .endif invoke EndPaint, [hwnd], addr PAST .else ; стандартная обработка invoke DefWindowProc, [hwnd], [wmsg], [wparam], [lparam] .endif ; конец защиты флагом или если BeginPaint \ EndPaint не нужны, то: Код (Text): .if [wmsg] == WM_PAINT ; Код защищённый флагом .if [MessageErrorOn] == False invoke … ; Вызов API .if eax == 0 mov [MessageErrorOn], True invoke MessageBox, [hwnd], zSTR(<'Ошибка в WM_PAINT', 13, 10 ,'продолжить ?'>), addr szError, MB_YESNO mov [MessageErrorOn], False .endif .endif ; конец защиты флагом invoke ValidateRect, [hwnd], 0 Теперь кажется всё! Осталось оформить обработчики ошибок в удобные макросы и можно храбро использовать. Полный файл примера прилагается, поэтому здесь большая часть кода пропущена. Обратите внмание при генераци тестовой ошибки по клавише "пробел" или "enter", счётчик таймера продолжает увеличиваться, поскольку inc [TimerCount] и его вывод на экран не защищены флагом, что в данном случае не вляет на "устойчвость" программы. Код (Text): .686 .model flat, stdcall option casemap: none ; Различать регистр букв ... DEBUG_MODE ; Включить режим отладки ; RELEASE_MODE ; Выключить режим отладки. .code start: Create_Log_File zSTR('MyLog.log') ; создаём лог-файл. ... mov [h_proc], @FUNC(GetModuleHandle, 0) ; Определить handle процесса TestError 0, zSTR(szNoWin, 'Окно ещё не создано') ; === Initialize GDI+ === @invoke GdiplusStartup, addr hGDIplus, addr GdiPlusSI, NULL Test_GDIp_Error zSTR('Инициализация GDI+') ... WinProc PROC uses ebx edi esi, hwnd:DWORD, wmsg:DWORD, wparam:DWORD, lparam:DWORD LOCAL PAST:PAINTSTRUCT ; Структура для работы с экраном ... .ELSEIF [wmsg] == WM_PAINT ; === Прорисовка окна === ... ; Конструктор сплошной кисти @invoke GdipCreateSolidFill, %Color(255, 225, 50, 0), addr hGrBrush Test_GDIp_Error <>, [hwnd] ; Залитый эллипс @invoke GdipFillEllipse, [memGraphics], [hGrBrush], @REAL4(35.0), @REAL4(35.0), @REAL4(25.0), @REAL4(25.0) Test_GDIp_Error <>, [hwnd] ... ; --- вычисляем график синуса --- FLD @REAL8(100.0) ; Y0 FLD @REAL8(10.0) ; X0 FLD @REAL8(-20.0) ; масштаб Y (перевернуть график по Y) FLD @REAL8(15.0) ; масштаб X FLDPI FMUL @REAL8(0.01) ; Шаг по X FLDZ mov ecx, 201 mov edi, offset lpzsBuf ; временный буфер @@: ; Y0, X0, Масштаб Y, Масштаб X, Шаг, X FLD ST FMUL ST, ST3 ; * масштаб X FADD ST, ST5 ; + X0 FSTP REAL4 ptr [edi] ; X add edi, 4 FLD ST FSIN FMUL ST, ST4 ; * масштаб Y FADD ST, ST6 ; + Y0 FSTP dword ptr [edi] ; Y add edi, 4 FADD ST, ST1 ; X + шаг dec ecx jnz @B FCOMPP FCOMPP FCOMPP Test_FPU_Errors zSTR(<'График синуса'>), FPU_Err_Without_Accurace, True, [hwnd] ... ; --- Построение графка sin --- @invoke GdipDrawCurve, [memGraphics], [hGrPen], addr lpzsBuf, 201 Test_GDIp_Error <>, [hwnd] ... В RELEASE_MODE код и данные для ведения лога и вывода тестовых MessageBox-ов полностью исключаются из исполнимого модуля, остается только обработка SEH. Макросы DEBUG_MODE и RELEASE_MODE могут быть вызваны в любом месте программы, что позволяет переводить в режим отладки только нужную часть программы. Макросы @invoke и @FUNC помимо вызова функции запоминают служебную информацию о ней. Если вместо них использовать invoke и FUNC, то в MessageBox не будут отраженны имя и расположение функции, вызвавшей ошибку. Обратите внимание, что макрофункция, имеющая несколько параметров color() в данном случае должна быть передана в макрос @invoke как значение %color(), в то время как в обычный invoke её нужно передавать без символа %. Собственно это глюк масма - при небольшом количестве макросов символ % не обязателен, но раньше или позже он потребуется. TestError ErrorCode, lpzString, h_parent_wnd расшифровывает сообщение об ошибке в API функции, параметры: ErrorCode - код ошибки может быть любым числом или NONZERO, lpzString - адрес строки с дополнительной информацией, h_parent_wnd - handle родительского окна Макрос TestError должен непосредственно следовать за @invoke, поскольку анализирует на наличие кода ошибки регистр eax. Test_GDIp_Error lpzString, h_parent_wnd расшифровывает сообщение об ошибке в GDI+ функции. параметры: lpzString - адрес строки с дополнительной информацией, h_parent_wnd - handle родительского окна Test_FPU_Errors lpzString, FPU_Err_Flags, Reset_Flag, h_parent_wnd параметры: lpzString - адрес строки с дополнительной информацией, FPU_Err_Flags - флаги анализируемых FPU ошибок: FPU_Err_All - Все ошибки, включая "не пустой стек" FPU_Err_IE - Недопустимая операция FPU_Err_DE - Операция с "не числом" FPU_Err_ZE - Деление на ноль FPU_Err_OE - Переполнение FPU_Err_UE - Антипереполнение FPU_Err_PE - Потеря точности FPU_Err_SF - Ошибка в стеке сопроцессора FPU_Err_Nonzero_Stek - Не пустой стек FPU_Err_Without_Accurace - Все ошибки кроме потери точности FPU_Err_Base - Основные ошибки FPU_Err_Accurace - Потеря точности Reset_Flag - Флаг сброса FPU в случае ошибки (True\False) h_parent_wnd - handle родительского окна Использовать этот макрос следует только после достаточно больших логически завершённых блоков FPU команд. А место нахождения ошибки внутр блока лучше обнаруживать в отладчике или через SEH. Все параметры макросов являются необязательными: TestError 0, <>, [hwnd] TestError , , [hwnd] TestError допустимые конструкции. Ну и в заключение стоит отметить, что соблюдая описанные меры предосторожности ничего не мешает помещать MessageBox в SEH. Для тестирования SEH в программе-примере нажатие "enter" генерирует деление на ноль в обработчике WM_PAINT. Благодарности: Oleg_SK, bogrus, Four-F за интересное обсуждение темы. В аттаче к статье: Исправил странный баг - FormatMessage почему то не всегда срабатывала при выходном буфере 4кБ вместо 1кБ, хотя казалось бы больше не меньше Поместил в обработчк таймера реальную, а не ложную ошбку и вывел тестироване SEH на "enter".
И поскольку инклюды для работы с GDI+ опять благополучно канули в лету, возвращаю их в обращение форума. (вариант без call через jmp)
Все верно, за исключением того, что MessageBox не многопоточный (сейчас быстро склепал тест - число потоков при вызове MessageBox не изменяется) - он просто содержит свой цикл обработки сообщений, и передает их вызвавшему приложению. В результате, например, при обработке сообщений таймера цепочка MessageBox'ов - фактически цепочка рекурсивных вызовов. Попробуй в обработчике таймера выделить стека побольше, и очень скоро получишь Stack Overflow
Y_Mur Мы не ищем лёгких путей, правда? Мы будем обходить кучу проблем и неудобств, упорно затачивая пресловутый MessageBox для отладки
уже только за то что человек собрался писать статью ему можно сказать спасибо IceStudent угу так и есть. Я не так уж часто ими пользуюсь (int 3 рулит), и зачастую играет роль появится ли вообще этот мессадж бокс, а не то что он написал ЗЫ Кто-нибудь эти лог файлы вообще использует?
rain Ну, не логами едиными живы отладка и тестирование. А вообще, используются активно никсоидами, да и в виндовых проектах имеют место быть, тем более, в режиме ядра проблематично MessagoBox показывать
Y_Mur Не. Статья не рулит. Хотя бы потому что при исключении можно посмотреть технический отчет и узнать адресс, где произошла ошибка. Тот же месджбок так сказать. Но видно и модуль и содержание регистров. int3 - так же как вариант. При отлаке на критических участках вызывать мсджбокс - слишком гемморно. И то, что написанна целая статья - тому подтверждение. Кстати, если правильно подкорректироват стек, мсджбокс будет вызываться самой системой и писать, что произошло исключение по адрессу такому-то. Вообщем статья конечно же имеет место быть. Но практического применения в реальной жизни не вижу.
1. “VKDEBUG v1.1, September 2002.”. Ищем в папочке с masm32 и медитируем… 2. МessageBox использую, но не в критических участках программы. Когда таскать с собой кучу отладочных библиотек напряжно, но об ошибках сигнализировать необходимо – юзается простенькая процедурка (GetLastError –› FormatMessage –› MessageBox). 3. За GDI+ инклюды — спасибо! )
Atlantic да действительно нет нового потока. rain дык я и пишу про то как добиться гарантированного появления мессадж бокса на экране IceStudent эт оно конечно верно, но в юзверь моде, как раз в GDI+, которая частенько падает вместе с прогой (а то и вместе с системой) при "незамеченных" кодах ошибки, имхо MessagoBox как раз удобно. nitrotoluol не так уж и геморойно - достаточно один раз разобраться что к чему, а вот GDI+ под отладчиком может преподносить каверзные сюрпризы побеждать которые куда геморойнее. G13 видел - не радует..., хотя конечно дело вкуса Дык именно это у меня и запрятано в макрос TestError, плюс аналогичные макросы для тестирования состояния FPU и результатов GDI+ для которых системные FormatMessage не предусмотрены. В общем, макросы эти юзаю давно, (правда в теперешнем сильно переработанном варианте они достаточно свежие и однозначно убедился в их удобстве, а вот статья про них явно требует серьёзной переработки - буду медитировать и ждать вдохновения
табличка в первую очередь нужна юзверю, что б он бедный хоть примерно знал от чего прога отбросила копыта и сообщил об ентом разработчику - лог послал.