Использование MessageBox в отладке программ (черновик статьи)

Тема в разделе "WASM.ZEN", создана пользователем Y_Mur, 16 апр 2007.

  1. Y_Mur

    Y_Mur Active Member

    Публикаций:
    0
    Регистрация:
    6 сен 2006
    Сообщения:
    2.494
    Когда в программе происходит ошибка и нужно проинформировать об этом факте себя любимого, то первая мысль - послать MessageBox. Именно так и поступает большинство новичков в программировании, однако очень скоро они убеждаются в многопоточном коварстве этой с виду безобидной функции и приходят к убеждению, что использование MessageBox для отладки - "плохой стиль программирования", а хороший стиль - писать лог файл или использовать вывод с помощью внешнего отладчика (OutputDebugString). На самом деле отладочный MessageBox, это прежде всего удобно, а лог файл и OutputDebugString, просто другие инструменты, которые в некоторых случаях удобнее MessageBox, а в некоторых нет. Но рассмотрим суть проблемы по порядку.
    Вызвав MessageBox, программист ожидает, что выполнение программы продолжится с того места, откуда вызван MessageBox. В программах, не имеющих собственного окна, всё именно так и происходит, даже несмотря на то что на самом деле API функция MessageBox выполняется в отдельном потоке.
    Код (Text):
    1. invoke GetModuleHandle, 0 ; Определить handle процесса
    2. mov [h_proc], eax
    3. .if eax == 0
    4.    invoke MessageBox, 0, zSTR(<'не удалось определить handle процесса', 13, 10, 'продолжить ?'>), addr szError, MB_YESNO
    5.    .if eax == IDNO  ; Выбрано завершение программы
    6.         invoke ExitProcess, 0   ; Выход из программы
    7.     .endif
    8. .endif
    Врождённая многопоточность MessageBox проявляется когда он вызван в оконном обработчике, к примеру, в событии WM_MOUSEMOVE или WM_KEYDOWN. В этом случае повторное сообщение вызовет новый MessageBox, несмотря на то, что старый ещё не закрыт. Любой мало-мальски опытный программер сразу же найдёт решение этой проблемы - достаточно указать MessageBox-у handle родительского окна, и сообщения мыши и клавиатуры не будут попадать в родительское окно до тех пор, пока MessageBox не будет закрыт.
    Код (Text):
    1. invoke MessageBox, [hwnd], zSTR('ошибка при обработке WM_KEYDOWN'), addr szError, MB_OK
    Ура! проблема решена? Ан нет, не тут то было - MessageBox по прежнему не блокирует сообщения таймера и если информация об ошибке пришла оттуда, то опять получаем серию дублированных MessageBox-ов, способных даже безнадёжно завесить программу.
    Выход прост - использовать флаг:
    Код (Text):
    1. .if [wmsg] == WM_TIMER
    2. ; Безопасный код
    3. inc [TimerCount]
    4. ; Код защищённый флагом
    5. .if [MessageErrorOn] == False
    6.     …
    7.     invoke … ; Вызов API
    8.     .if eax == 0
    9.        mov [MessageErrorOn], True
    10.        invoke MessageBox, [hwnd], zSTR(<'Ошибка в WM_TIMER', 13, 10 ,'продолжить ?'>), addr szError, MB_YESNO
    11.        mov [MessageErrorOn], False
    12.     .endif
    13. .endif      ; конец защиты флагом
    Но и это не всё! Кроме WM_TIMER, есть ещё более коварное сообщение WM_PAINT. Дело в том, что прежде чем появиться на экране MessageBox сам посылает это сообщение родительскому окну и наотрез отказывается показываться, если оно сообщение не будет обработано!
    Код (Text):
    1. .if [wmsg] == WM_PAINT
    2.   ; Код защищённый флагом
    3.   .if [MessageErrorOn] == False
    4.       invoke BeginPaint, [hwnd], addr PAST
    5.       invoke … ; Вызов API
    6.       .if eax == 0
    7.          mov [MessageErrorOn], True
    8.          invoke MessageBox, [hwnd], zSTR(<'Ошибка в WM_PAINT', 13, 10 ,'продолжить ?'>), addr szError, MB_YESNO
    9.          mov [MessageErrorOn], False
    10.       .endif
    11.       invoke EndPaint, [hwnd], addr PAST
    12.   .endif        ; конец защиты флагом
    Этот пример приведёт к тому, что MessageBox будет создан с характерным звуком, но останется невидимкой, причём, нажимая Пробел, Enter и стрелки управления курсором можно будет нажать невидимые экранные кнопки и продолжить выполнение с виду зависшей программы. А попытка совсем убрать проверку флага [MessageErrorOn] вызовет "самогенерирующуюся" бесконечную серию MessageBox-ов.
    Поэтому правильно будет переписать пример так:
    Код (Text):
    1. .if [wmsg] == WM_PAINT
    2.   ; Код защищённый флагом
    3.   invoke BeginPaint, [hwnd], addr PAST
    4.   .if [MessageErrorOn] == False
    5.       invoke … ; Вызов API
    6.       .if eax == 0
    7.          mov [MessageErrorOn], True
    8.          invoke MessageBox, [hwnd], zSTR(<'Ошибка в WM_PAINT', 13, 10 ,'продолжить ?'>), addr szError, MB_YESNO
    9.          mov [MessageErrorOn], False
    10.       .endif
    11.   .endif        ; конец защиты флагом
    12.   invoke EndPaint, [hwnd], addr PAST
    или так:
    Код (Text):
    1. .if [wmsg] == WM_PAINT
    2.   ; Код защищённый флагом
    3.   .if [MessageErrorOn] == False
    4.       invoke BeginPaint, [hwnd], addr PAST
    5.       invoke … ; Вызов API
    6.       .if eax == 0
    7.          mov [MessageErrorOn], True
    8.          invoke MessageBox, [hwnd], zSTR(<'Ошибка в WM_PAINT', 13, 10 ,'продолжить ?'>), addr szError, MB_YESNO
    9.          mov [MessageErrorOn], False
    10.       .endif
    11.       invoke EndPaint, [hwnd], addr PAST
    12.   .else ; стандартная обработка
    13.       invoke DefWindowProc, [hwnd], [wmsg], [wparam], [lparam]
    14.   .endif        ; конец защиты флагом
    или если BeginPaint \ EndPaint не нужны, то:
    Код (Text):
    1. .if [wmsg] == WM_PAINT
    2.   ; Код защищённый флагом
    3.   .if [MessageErrorOn] == False
    4.       invoke … ; Вызов API
    5.       .if eax == 0
    6.          mov [MessageErrorOn], True
    7.          invoke MessageBox, [hwnd], zSTR(<'Ошибка в WM_PAINT', 13, 10 ,'продолжить ?'>), addr szError, MB_YESNO
    8.          mov [MessageErrorOn], False
    9.       .endif
    10.   .endif        ; конец защиты флагом
    11.   invoke ValidateRect, [hwnd], 0
    Теперь кажется всё! Осталось оформить обработчики ошибок в удобные макросы и можно храбро использовать.
    Полный файл примера прилагается, поэтому здесь большая часть кода пропущена.
    Обратите внмание при генераци тестовой ошибки по клавише "пробел" или "enter", счётчик таймера продолжает увеличиваться, поскольку inc [TimerCount] и его вывод на экран не защищены флагом, что в данном случае не вляет на "устойчвость" программы.
    Код (Text):
    1. .686
    2. .model flat, stdcall
    3. option   casemap: none  ; Различать регистр букв
    4. ...
    5. DEBUG_MODE      ; Включить режим отладки
    6. ; RELEASE_MODE  ; Выключить режим отладки.
    7. .code
    8. start:
    9.   Create_Log_File zSTR('MyLog.log') ; создаём лог-файл.
    10.   ...
    11.   mov [h_proc], @FUNC(GetModuleHandle, 0) ; Определить handle процесса
    12.   TestError 0, zSTR(szNoWin, 'Окно ещё не создано')
    13.   ; === Initialize GDI+ ===
    14.   @invoke GdiplusStartup, addr hGDIplus, addr GdiPlusSI, NULL
    15.   Test_GDIp_Error zSTR('Инициализация GDI+')
    16.   ...
    17. WinProc PROC uses ebx edi esi, hwnd:DWORD, wmsg:DWORD, wparam:DWORD, lparam:DWORD
    18. LOCAL   PAST:PAINTSTRUCT  ; Структура для работы с экраном
    19.   ...
    20.     .ELSEIF [wmsg] == WM_PAINT  ; === Прорисовка окна ===
    21.     ...
    22.     ; Конструктор сплошной кисти
    23.     @invoke GdipCreateSolidFill, %Color(255, 225,  50,   0), addr hGrBrush
    24.     Test_GDIp_Error <>, [hwnd]
    25.     ; Залитый эллипс
    26.     @invoke GdipFillEllipse, [memGraphics], [hGrBrush],  @REAL4(35.0),  @REAL4(35.0), @REAL4(25.0), @REAL4(25.0)
    27.     Test_GDIp_Error <>, [hwnd]
    28.     ...
    29.     ; --- вычисляем график синуса ---
    30.     FLD @REAL8(100.0)   ; Y0
    31.     FLD @REAL8(10.0)    ; X0
    32.     FLD @REAL8(-20.0)   ; масштаб Y (перевернуть график по Y)
    33.     FLD @REAL8(15.0)    ; масштаб X
    34.     FLDPI
    35.     FMUL @REAL8(0.01)   ; Шаг по X
    36.     FLDZ
    37.     mov ecx, 201
    38.     mov edi, offset lpzsBuf ; временный буфер
    39.     @@:
    40.       ; Y0, X0, Масштаб Y, Масштаб X, Шаг, X
    41.       FLD ST
    42.       FMUL ST, ST3  ; * масштаб X
    43.       FADD ST, ST5  ; + X0
    44.       FSTP REAL4 ptr [edi]  ; X
    45.       add edi, 4
    46.       FLD ST
    47.       FSIN
    48.       FMUL ST, ST4  ; * масштаб Y
    49.       FADD ST, ST6  ; + Y0
    50.       FSTP dword ptr [edi]  ; Y
    51.       add edi, 4
    52.       FADD ST, ST1  ; X + шаг
    53.       dec ecx
    54.     jnz @B
    55.     FCOMPP
    56.     FCOMPP
    57.     FCOMPP
    58.     Test_FPU_Errors zSTR(<'График синуса'>), FPU_Err_Without_Accurace, True, [hwnd]
    59.     ...
    60.     ; --- Построение графка sin ---
    61.     @invoke GdipDrawCurve, [memGraphics], [hGrPen], addr lpzsBuf, 201
    62.     Test_GDIp_Error <>, [hwnd]
    63.     ...
    В 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".
     
  2. Y_Mur

    Y_Mur Active Member

    Публикаций:
    0
    Регистрация:
    6 сен 2006
    Сообщения:
    2.494
    И поскольку инклюды для работы с GDI+ опять благополучно канули в лету, возвращаю их в обращение форума.
    (вариант без call через jmp)
     
  3. Atlantic

    Atlantic Member

    Публикаций:
    0
    Регистрация:
    22 июн 2005
    Сообщения:
    322
    Адрес:
    Швеция
    Все верно, за исключением того, что MessageBox не многопоточный (сейчас быстро склепал тест - число потоков при вызове MessageBox не изменяется) - он просто содержит свой цикл обработки сообщений, и передает их вызвавшему приложению. В результате, например, при обработке сообщений таймера цепочка MessageBox'ов - фактически цепочка рекурсивных вызовов. Попробуй в обработчике таймера выделить стека побольше, и очень скоро получишь Stack Overflow :)
     
  4. IceStudent

    IceStudent Active Member

    Публикаций:
    0
    Регистрация:
    2 окт 2003
    Сообщения:
    4.300
    Адрес:
    Ukraine
    Y_Mur
    Мы не ищем лёгких путей, правда? Мы будем обходить кучу проблем и неудобств, упорно затачивая пресловутый MessageBox для отладки :)
     
  5. rain

    rain New Member

    Публикаций:
    0
    Регистрация:
    22 апр 2006
    Сообщения:
    976
    уже только за то что человек собрался писать статью ему можно сказать спасибо :)
    IceStudent угу так и есть.
    Я не так уж часто ими пользуюсь (int 3 рулит), и зачастую играет роль появится ли вообще этот мессадж бокс, а не то что он написал :)
    ЗЫ Кто-нибудь эти лог файлы вообще использует?
     
  6. IceStudent

    IceStudent Active Member

    Публикаций:
    0
    Регистрация:
    2 окт 2003
    Сообщения:
    4.300
    Адрес:
    Ukraine
    rain
    Ну, не логами едиными живы отладка и тестирование. А вообще, используются активно никсоидами, да и в виндовых проектах имеют место быть, тем более, в режиме ядра проблематично MessagoBox показывать :)
     
  7. nitrotoluol

    nitrotoluol New Member

    Публикаций:
    0
    Регистрация:
    5 сен 2006
    Сообщения:
    848
    Y_Mur
    Не. Статья не рулит. Хотя бы потому что при исключении можно посмотреть технический отчет и узнать адресс, где произошла ошибка. Тот же месджбок так сказать. Но видно и модуль и содержание регистров.

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

    Вообщем статья конечно же имеет место быть. Но практического применения в реальной жизни не вижу.
     
  8. G13

    G13 New Member

    Публикаций:
    0
    Регистрация:
    24 мар 2006
    Сообщения:
    499
    1. “VKDEBUG v1.1, September 2002.”. Ищем в папочке с masm32 и медитируем… ;)

    2. МessageBox использую, но не в критических участках программы. Когда таскать с собой кучу отладочных библиотек напряжно, но об ошибках сигнализировать необходимо – юзается простенькая процедурка (GetLastError –› FormatMessage –› MessageBox).

    3. За GDI+ инклюды — спасибо! ;))
     
  9. Y_Mur

    Y_Mur Active Member

    Публикаций:
    0
    Регистрация:
    6 сен 2006
    Сообщения:
    2.494
    Atlantic
    да действительно нет нового потока.

    rain
    дык я и пишу про то как добиться гарантированного появления мессадж бокса на экране :)

    IceStudent
    эт оно конечно верно, но в юзверь моде, как раз в GDI+, которая частенько падает вместе с прогой (а то и вместе с системой) при "незамеченных" кодах ошибки, имхо MessagoBox как раз удобно.

    nitrotoluol
    не так уж и геморойно - достаточно один раз разобраться что к чему, а вот GDI+ под отладчиком может преподносить каверзные сюрпризы побеждать которые куда геморойнее.

    G13
    видел - не радует..., хотя конечно дело вкуса ;)
    Дык именно это у меня и запрятано в макрос TestError, плюс аналогичные макросы для тестирования состояния FPU и результатов GDI+ для которых системные FormatMessage не предусмотрены.


    В общем, макросы эти юзаю давно, (правда в теперешнем сильно переработанном варианте они достаточно свежие :) и однозначно убедился в их удобстве, а вот статья про них явно требует серьёзной переработки - буду медитировать и ждать вдохновения :)
     
  10. spencer

    spencer New Member

    Публикаций:
    0
    Регистрация:
    15 авг 2005
    Сообщения:
    277
    мне кажется удобней был бы тот же int 3 и логи нежели напрягатся с месаджбоксами.
     
  11. Y_Mur

    Y_Mur Active Member

    Публикаций:
    0
    Регистрация:
    6 сен 2006
    Сообщения:
    2.494
    spencer
    При условии, что прога адекватно ведёт себя под дебаггером, что не всегда бывает с GDI+ ;)
     
  12. UbIvItS

    UbIvItS Well-Known Member

    Публикаций:
    0
    Регистрация:
    5 янв 2007
    Сообщения:
    6.077
    табличка в первую очередь нужна юзверю, что б он бедный хоть примерно знал от чего прога отбросила копыта и сообщил об ентом разработчику - лог послал.