Rozdział XXVIII - Funkcje uruchomieniowe Win32 API Część 1


Na tej lekcji dowiesz się, co biblioteka Win32 oferuje twórcom oprogramowania w dziedzinie funkcji uruchomieniowych (debugging primitives). Gdy przebrniesz przez ten materiał, będziesz wiedział jak testować proces.

 

 

Załaduj {ten przykład}

 

Teoria

Biblioteka Win32 zawiera kilka funkcji API pozwalających programistom testować aplikacje. Nazywają się one funkcjami uruchomieniowymi Win32 API (Win32 Debug APIs). Dzięki ich pomocy możesz:

Mówiąc krótko, za pomocą tych funkcji API możesz zaprogramować prosty program uruchomieniowy (debugger). Ponieważ ten temat jest rozległy, podzieliłem go na trzy mniejsze części, które łatwiej objąć umysłem. W toku lekcji wyjaśnię podstawowe koncepcje oraz ogólny schemat roboczy zastosowania funkcji uruchomieniowych Win32 API.

Oto niezbędne kroki zastosowania funkcji uruchomieniowych Win32 API:

  1. Utwórz proces lub dołącz swój program do uruchomionego procesu. Jest to pierwszy krok w zastosowaniu funkcji uruchomieniowych Win32 API. Ponieważ twój program będzie pełnił rolę programu uruchomieniowego, musisz mieć jakiś program do testowania. Program ten możesz pozyskać na dwa sposoby:
  1. Zaczekaj na zdarzenia uruchomieniowe. Po uzyskaniu przez twój program procesu do testowania główny wątek tego procesu zostaje zawieszony aż do momentu, gdy twój program wywoła WaitForDebugEvent. Funkcja ta działa podobnie do innych funkcji typu WaitForXXX, tj. blokuje wywołujący ją wątek, aż wystąpi się oczekiwane zdarzenie. W tym przypadku oczekuje ona na wysłanie przez system Windows wiadomości o zdarzeniach uruchomieniowych. Zobaczmy na jej definicję:
WaitForDebugEvent PROTO lpDebugEvent:DWORD, dwMilliseconds:DWORD

Teraz zbadajmy szczegółowo strukturę DEBUG_EVENT:

DEBUG_EVENT STRUCT 
    dwDebugEventCode DD ? 
    dwProcessId      DD ? 
    dwThreadId       DD ? 
    u                DEBUGSTRUCT 
DEBUG_EVENT ENDS

Na tej lekcji nie będę wchodził w szczegóły tych wszystkich struktur, jedynie zajmiemy się strukturą CREATE_PROCESS_DEBUG_INFO.

Załóżmy, iż nasz program wywołuje funkcję WaitForDebugEvent, która zwraca sterowanie. Pierwszą rzeczą do zrobienia jest sprawdzenie wartości pola dwDebugEventCode, aby dowiedzieć się, który rodzaj zdarzenia uruchomieniowego wystąpił w testowanym procesie. Na przykład, jeśli pole dwDebugEventCode zawiera wartość CREATE_PROCESS_DEBUG_EVENT, możesz zinterpretować zawartość pola u jako strukturę CreateProcessInfo i mieć dostęp do niej przez adresowanie u.CreateProcessInfo.

  1. Wykonaj wszystko, co chcesz zrobić w odpowiedzi na wystąpienie danego zdarzenia uruchomieniowego. Gdy funkcja WaitForDebugEvent zwraca sterowanie, oznacza to, iż zdarzenie uruchomieniowe właśnie wystąpiło w testowanym procesie lub przekroczono zadany limit czasu. Twój program musi zbadać wartość pola dwDebugEventCode , aby poprawnie zareagować na dane zdarzenie. W tym sensie jest to podobne do przetwarzania wiadomości systemu Windows: wybierasz, na które reagować, a które zignorować.
  2. Pozwól programowi uruchomieniowemu kontynuować wykonywanie. Gdy wystąpi zdarzenie uruchomieniowe, system Windows zawiesi proces testowany. Gdy zakończysz obsługę zdarzenia, musisz wznowić pracę procesu testowanego. Dokonujesz tego poprzez wywołanie funkcji ContinueDebugEvent.
ContinueDebugEvent PROTO dwProcessId:DWORD,\
                         dwThreadId:DWORD,\
                         dwContinueStatus:DWORD

Funkcja ta wznawia wykonywanie wątku, który został poprzednio zawieszony w wyniku wystąpienia zdarzenia uruchomieniowego.

Podsumowując, jeśli zdarzenie uruchomieniowe odnosi się do wyjątku w procesie testowanym, powinieneś wywołać ContinueDebugEvent ze znacznikiem DBG_CONTINUE, jeśli twój program usunął już przyczynę powstania wyjątku. W przeciwnym razie twój program musi wywołać ContinueDebugEvent ze znacznikiem DBG_EXCEPTION_NOT_HANDLED. Odstępstwem jest pojedynczy przypadek, gdy musisz zawsze stosować znacznik DBG_CONTINUE: pierwszy wyjątek EXCEPTION_DEBUG_EVENT posiadający wartość EXCEPTION_BREAKPOINT w polu ExceptionCode. Gdy testowany proces ma zamiar wykonać swoją pierwszą instrukcję, twój program otrzyma zdarzenie uruchomieniowe wyjątku (exception debug event). W rzeczywistości jest to przerwa uruchomieniowa (debug break). Jeśli odpowiesz wywołując ContinueDebugEvent ze znacznikiem DBG_EXCEPTION_NOT_HANDLED, to system Windows NT/2000/XP odmówi uruchomienia testowanego procesu (ponieważ nikt się o niego nie troszczy). W takim przypadku zawsze musisz używać znacznika DBG_CONTINUE, aby system Windows wiedział, iż chcesz kontynuacji tego wątku.

  1. Kontynuuj ten cykl w nieskończonej pętli, aż testowany proces zakończy swoje działanie. Pętla ta wygląda następująco:
.WHILE TRUE
        INVOKE WaitForDebugEvent, ADDR DebugEvent, INFINITE
        .BREAK .IF DebugEvent.dwDebugEventCode==EXIT_PROCESS_DEBUG_EVENT
        ...
        <Obsłóż zdarzenia uruchomieniowe>
        ...
        INVOKE ContinueDebugEvent, DebugEvent.dwProcessId,\
                                   DebugEvent.dwThreadId,\
                                   DBG_EXCEPTION_NOT_HANDLED 
    .ENDW

Jest tutaj jeden kruczek. Gdy już rozpoczniesz testowanie jakiegoś programu, nie możesz się po prostu odłączyć od niego aż do momentu, gdy zakończy on swoje działanie.

 

Podsumujmy jeszcze raz powyższe kroki:

  1. Utwórz proces lub dołącz swój program do uruchomionego procesu.
  2. Zaczekaj na zdarzenia uruchomieniowe.
  3. Wykonaj wszystko co należy w odpowiedzi na zdarzenie uruchomieniowe.
  4. Pozwól testowanemu programowi kontynuować swoje działanie.
  5. Powtarzaj ten cykl w nieskończonej pętli, aż testowany proces zakończy się.

Przykład

Ten przykład uruchamia i testuje aplikację Win32 oraz wyświetla istotne informacje, takie jak uchwyt procesu, numer ID procesu, adres bazowy obrazu procesu itd.

 

.386

.MODEL FLAT, STDCALL

OPTION CASEMAP:NONE

INCLUDE    \masm32\include\windows.inc
INCLUDE    \masm32\include\kernel32.inc
INCLUDE    \masm32\include\comdlg32.inc
INCLUDE    \masm32\include\user32.inc
INCLUDELIB \masm32\lib\kernel32.lib
INCLUDELIB \masm32\lib\comdlg32.lib
INCLUDELIB \masm32\lib\user32.lib

.DATA

AppName      DB "Przykład nr 1 testowania Win32", 0
ofn          OPENFILENAME <>
FilterString DB "Pliki wykonywalne", 0, "*.exe", 0, "Wszystkie pliki", 0, "*.*", 0, 0
ExitProc     DB "Program testowany kończy działanie", 0      
NewThread    DB "Tworzony jest nowy wątek", 0
EndThread    DB "Usuwany jest wątek", 0
ProcessInfo  DB "Uchwyt pliku: %lx ", 0dh, 0Ah,
                "Uchwyt procesu: %lx", 0Dh, 0Ah,
                "Uchwyt wątku: %lx", 0Dh, 0Ah,
                "Podstawa obrazu: %lx", 0Dh, 0Ah,
                "Adres startowy: %lx", 0

.DATA?

buffer    DB 512 DUP(?)
startinfo STARTUPINFO <>
pi        PROCESS_INFORMATION <>
DBEvent   DEBUG_EVENT <>

.CODE

start:
    mov    ofn.lStructSize, SIZEOF ofn
    mov    ofn.lpstrFilter, OFFSET FilterString
    mov    ofn.lpstrFile, OFFSET buffer
    mov    ofn.nMaxFile, 512
    mov    ofn.Flags, OFN_FILEMUSTEXIST OR OFN_PATHMUSTEXIST OR \
                      OFN_LONGNAMES OR OFN_EXPLORER OR OFN_HIDEREADONLY
    INVOKE GetOpenFileName, ADDR ofn

    .IF eax==TRUE
        INVOKE GetStartupInfo, ADDR startinfo
        INVOKE CreateProcess, ADDR buffer, NULL, NULL, NULL, FALSE,
                              DEBUG_PROCESS + DEBUG_ONLY_THIS_PROCESS,
                              NULL, NULL, ADDR startinfo, ADDR pi
        .WHILE TRUE
            INVOKE WaitForDebugEvent, ADDR DBEvent, INFINITE
            .IF DBEvent.dwDebugEventCode==EXIT_PROCESS_DEBUG_EVENT
                INVOKE MessageBox, 0, ADDR ExitProc, ADDR AppName,
                                   MB_OK + MB_ICONINFORMATION
                .BREAK
            .ELSEIF DBEvent.dwDebugEventCode==CREATE_PROCESS_DEBUG_EVENT
                INVOKE wsprintf, ADDR buffer, ADDR ProcessInfo,
                                 DBEvent.u.CreateProcessInfo.hFile,
                                 DBEvent.u.CreateProcessInfo.hProcess,
                                 DBEvent.u.CreateProcessInfo.hThread,
                                 DBEvent.u.CreateProcessInfo.lpBaseOfImage,
                                 DBEvent.u.CreateProcessInfo.lpStartAddress
                INVOKE MessageBox, 0, ADDR buffer, ADDR AppName,
                                   MB_OK + MB_ICONINFORMATION
            .ELSEIF DBEvent.dwDebugEventCode==EXCEPTION_DEBUG_EVENT
                .IF DBEvent.u.Exception.pExceptionRecord.ExceptionCode==\
                    EXCEPTION_BREAKPOINT
                    INVOKE ContinueDebugEvent, DBEvent.dwProcessId,
                                               DBEvent.dwThreadId,
                                               DBG_CONTINUE   
                    .CONTINUE
                .ENDIF
            .ELSEIF DBEvent.dwDebugEventCode==CREATE_THREAD_DEBUG_EVENT
                INVOKE MessageBox, 0, ADDR NewThread, ADDR AppName,
                                   MB_OK + MB_ICONINFORMATION           
            .ELSEIF DBEvent.dwDebugEventCode==EXIT_THREAD_DEBUG_EVENT
                INVOKE MessageBox, 0, ADDR EndThread, ADDR AppName,
                                   MB_OK + MB_ICONINFORMATION           
            .ENDIF
            INVOKE ContinueDebugEvent, DBEvent.dwProcessId,
                                       DBEvent.dwThreadId,
                                       DBG_EXCEPTION_NOT_HANDLED
       
        .ENDW
    .ENDIF
    INVOKE CloseHandle, pi.hProcess
    INVOKE CloseHandle, pi.hThread
    INVOKE ExitProcess, 0

END start

Analiza

Program wypełnia strukturę OPENFILENAME, a następnie wywołuje GetOpenFileName, aby pozwolić użytkownikowi wybrać program do testowania.

 

    INVOKE GetStartupInfo, ADDR startinfo
    INVOKE CreateProcess, ADDR buffer, NULL, NULL, NULL, FALSE,
                          DEBUG_PROCESS + DEBUG_ONLY_THIS_PROCESS,
                          NULL, NULL, ADDR startinfo, ADDR pi

 

Gdy użytkownik wybierze jakiś program, wywołana zostaje funkcja GetStartupInfo do wypełnienia struktury STARTUPINFO standardowymi wartościami, a następnie CreateProcess do załadowania wybranego programu. Zwróć uwagę, iż w celu testowania tylko tego programu bez jego procesów potomnych stosujemy znacznik DEBUG_PROCESS w połączeniu ze znacznikiem DEBUG_ONLY_THIS_PROCESS.

 

    .WHILE TRUE
        INVOKE WaitForDebugEvent, ADDR DBEvent, INFINITE

 

Gdy testowany proces zostanie załadowany, wchodzimy do nieskończonej pętli wywołując na jej początku funkcję WaitForDebugEvent. Funkcja WaitForDebugEvent nie zwróci sterowania aż do wystąpienia zdarzenia uruchomieniowego (debug event) w testowanym programie, ponieważ określiliśmy w drugim jej parametrze wartość INFINITE (w nieskończoność). Gdy wystąpi jakieś zdarzenie uruchomieniowe, funkcja WaitForDebugEvent zwróci sterowanie do naszego programu, a struktura DBEvent zostanie wypełniona informacją o zaistniałym zdarzeniu.

 

    .IF DBEvent.dwDebugEventCode==EXIT_PROCESS_DEBUG_EVENT
        INVOKE MessageBox, 0, ADDR ExitProc, ADDR AppName,
                           MB_OK + MB_ICONINFORMATION
        .BREAK

 

Najpierw sprawdzamy wartość w dwDebugEventCode. Jeśli jest nią EXIT_PROCESS_DEBUG_EVENT, wyświetlamy okienko wiadomości z napisem "Program testowany kończy działanie", a następnie wychodzimy z pętli.

 

    .ELSEIF DBEvent.dwDebugEventCode==CREATE_PROCESS_DEBUG_EVENT
        INVOKE wsprintf, ADDR buffer, ADDR ProcessInfo,
                         DBEvent.u.CreateProcessInfo.hFile,
                         DBEvent.u.CreateProcessInfo.hProcess,
                         DBEvent.u.CreateProcessInfo.hThread,
                         DBEvent.u.CreateProcessInfo.lpBaseOfImage,
                         DBEvent.u.CreateProcessInfo.lpStartAddress
        INVOKE MessageBox, 0, ADDR buffer, ADDR AppName,
                           MB_OK + MB_ICONINFORMATION

 

Jeśli wartością w dwDebugEventCode jest CREATE_PROCESS_DEBUG_EVENT, to wyświetlamy kilka interesujących informacji o testowanym procesie w oknie wiadomości. Informacje te pobieramy ze unii u.CreateProcessInfo. CreateProcessInfo jest strukturą typu CREATE_PROCESS_DEBUG_INFO. Więcej informacji o niej znajdziesz w pliku pomocy dla biblioteki Win32.

 

    .ELSEIF DBEvent.dwDebugEventCode==EXCEPTION_DEBUG_EVENT
        .IF DBEvent.u.Exception.pExceptionRecord.ExceptionCode==\
            EXCEPTION_BREAKPOINT
            INVOKE ContinueDebugEvent, DBEvent.dwProcessId,
                                       DBEvent.dwThreadId,
                                       DBG_CONTINUE    
            .CONTINUE
        .ENDIF

 

Jeśli wartością w dwDebugEventCode jest EXCEPTION_DEBUG_EVENT, musimy dalej sprawdzić dokładny typ wyjątku, który odczytasz z ExceptionCode. Jeśli zawartość ExceptionCode wynosi EXCEPTION_BREAKPOINT i zdarza się to po raz pierwszy (lub jesteśmy pewni, iż w testowanym procesie nie osadzono przerwania 3h), możemy bezpiecznie założyć, iż wyjątek ten wystąpił, gdy testowany proces próbował uruchomić swoją pierwszą instrukcję. Gdy skończymy obsługę wyjątku, musimy wywołać ContinueDebugEvent ze znacznikiem DBG_CONTINUE, aby pozwolić testowanemu programowi uruchomić się. Później wracamy z powrotem do oczekiwania na następne zdarzenie uruchomieniowe.

 

    .ELSEIF DBEvent.dwDebugEventCode==CREATE_THREAD_DEBUG_EVENT
        INVOKE MessageBox, 0, ADDR NewThread, ADDR AppName,
                           MB_OK + MB_ICONINFORMATION            
    .ELSEIF DBEvent.dwDebugEventCode==EXIT_THREAD_DEBUG_EVENT
        INVOKE MessageBox, 0, ADDR EndThread, ADDR AppName,
                           MB_OK + MB_ICONINFORMATION            
    .ENDIF

 

Jeśli w dwDebugEventCode jest CREATE_THREAD_DEBUG_EVENT lub EXIT_THREAD_DEBUG_EVENT, wyświetlamy okienko wiadomości z odpowiednimi informacjami o otworzeniu lub zakończeniu wątku w testowanym procesie.

 

        INVOKE ContinueDebugEvent, DBEvent.dwProcessId,
                                   DBEvent.dwThreadId,
                                   DBG_EXCEPTION_NOT_HANDLED
    .ENDW

 

Aby wznowić działanie procesu testowanego, wywołujemy ContinueDebugEvent ze znacznikiem DBG_EXCEPTION_NOT_HANDLED.

 

    INVOKE CloseHandle, pi.hProcess
    INVOKE CloseHandle, pi.hThread
    INVOKE ExitProcess, 0

END start

 

Gdy testowany proces zakończy się, jesteśmy poza pętlą uruchomieniową i musimy zamknąć zarówno uchwyt procesu jak i wątku. Zamknięcie uchwytów nie oznacza usunięcie procesu lub wątku. Znaczy jedynie, iż nie potrzebujemy już więcej tych uchwytów do odwoływania się do procesu lub wątku.

 

Autorem kursu jest Iczelion. Kurs programowania Windows znalazł się na serwerze I LO w Tarnowie za pisemną zgodą autora.
Tłumaczenie z języka angielskiego, opracowanie HTML i konwersję przykładów programów wykonał mgr Jerzy Wałaszek.



List do administratora Serwisu Edukacyjnego Nauczycieli I LO

Twój email: (jeśli chcesz otrzymać odpowiedź)
Temat:
Uwaga: ← tutaj wpisz wyraz  ilo , inaczej list zostanie zignorowany

Poniżej wpisz swoje uwagi lub pytania dotyczące tego rozdziału (max. 2048 znaków).

Liczba znaków do wykorzystania: 2048

 

W związku z dużą liczbą listów do naszego serwisu edukacyjnego nie będziemy udzielać odpowiedzi na prośby rozwiązywania zadań, pisania programów zaliczeniowych, przesyłania materiałów czy też tłumaczenia zagadnień szeroko opisywanych w podręcznikach.



   I Liceum Ogólnokształcące   
im. Kazimierza Brodzińskiego
w Tarnowie

©2017 mgr Jerzy Wałaszek

Dokument ten rozpowszechniany jest zgodnie z zasadami licencji
GNU Free Documentation License.