Tutorial 2: MessageBox

W tym tutorialu, stworzymy w pełni funkcjonalny program pod Windows, który wyświetla okno z informacją
"Win32 assembly is great!".

Ściągnij przykładowy plik tutaj.

Teoria:

Windows zawiera bogactwo zasobów dla swoich programów. Centrum tego jest Windows'owe API (Interfejs Programowania Aplikacji, z ang. Application Programming Interface). API Windows'a jest olbrzymią kolekcją bardzo przydatnych funkcji, które zawarte są w samym systemie, gotowe do użycia przez jakikolwiek program pod Win32. Funkcje te są zawarte w kilku dynamicznie połączonych bibliotekach (z ang. Dynamic-Linked Libraries - DLLs), takich jak kernel32.dll, user32.dll i gdi32.dll. Kernel32.dll zawiera funkcje API odnoszące się do pamięci i zarządzania procesami. User32.dll kontroluje aspekty interfejsu użytkownika programu. Gdi32.dll jest odpowiedzialny za operacje graficzne. Poza "główną trójką" są inne DLL'e, które twój program może użyć, dzięki czemu zapewnia ci to dosyć informacji o pożądanych funkcjach API.
Programy pod Windows dynamicznie łączą się z tymi DLL'ami. Na przykład kody funkcji API nie są zamieszczone w windows'owym pliku wykonywalnym. Aby twój program wiedział, gdzie znaleźć potrzebne funkcje API podczas pracy, musisz osadzić te informacje w pliku twoim wykonywalnym. Te informacje znajdują się w bibliotekach importów. Musisz łączyć swóje programy z poprawnymi bibliotekami importów albo nie będą one w stanie zlokalizować potrzebnych funkcji API.
Kiedy program pod Win32 jest ładowany do pamięci, Windows czyta informacje zawarte w tym programie. Informacje te zawierają nazwy funkcji, których program używa i DLLe, w których te funkcje się znajdują. Kiedy Windows znajdzie takie informacje w programie, załaduje te DLLe i poprawi adresy wywołania funkcji w programie, tak aby te wywołania odnosiły się do poprawnych funkcji.
Są dwie kategorie funkcji API: Jedna dla ANSI, a druga dla Unicode. Nazwy funkcji API dla ANSI są zakończone literą "A", na przykład MessageBoxA. Te dla Unicod'u są zakończone na "W" (prawdopodobnie od Wide Char). Windows 95 obsługuje ANSI, natomiast Windows NT Unicode.
Normalnie obchodzimy się ze znakami ANSI, które są przedziałem znaków zakończonym zerem. Znak ANSI to 1 bajt w kodzie. Kiedy zbiór ANSI jest wystarczający dla języków Europejskich, to nie może sobie poradzić z językami orientalnymi, które zawierają tysiące unikalnych znaków. Tutaj wchodzi Unicode, znak Unicod'u to 2 bajty w kodzie, dzięki czemu może on zawierać 65536 unikalnych znaków.
Ale przez większość czasu będziesz używać bibliotekę importów, która potrafi zdeterminować i wybrać poprawną funkcję API dla twojej platformy. Po prostu odnosisz się do funkcji API bez przyrostka.

Przykład:

Poniżej zaprezentuje goły szkielet programu. Wypełnimy go później.

.386
.model flat, stdcall
.data
.code
start:
end start

Wykonywanie programu rozpoczyna się od pierwszej instrukcji zaraz pod etykietą określoną po dyrektywie end. W powyższym szkielecie, wykonywanie programu rozpoczyna się zaraz pod etykietą start. Proces ten będzie podążać instrukcja po instrukcji, aż pojawią się jakieś instrukcje kontrolujące bieg procesu. Instrukcjami tymi mogą być jmp, jne, je, ret itd. Te instrukcje przekierowują bieg procesu to jakiś innych instrukcji. Kiedy program musi wyjść do Windows'a, powinien wywołać funkcję API - ExitProcess.

ExitProcess proto uExitCode:DWORD

Powyższa linia to tzw. prototyp funkcji. Prototyp funkcji definiuje atrybuty tej funkcji assembler'owi/linker'owi więc może on wykonać dla ciebie weryfikację typów zmiennych. Format prototypu funkcji wygląda następująco:

NazwaFunkcji PROTO [NazwaParametru]:TypDanych,[NazwaParametru]:TypDanych,...

Krótko, nazwa funkcji, następnie słowo PROTO, a potem lista typów danych i parametrów oddzielonych przecinakmi. W powyższym przykładzie ExitProcess jest zdefiniowane jako funkcja, która przybiera tylko jeden parametr typu DWORD (z ang. Double Word - podwójne słowo). Prototypy funkcji są bardzo przydatne kiedy używasz składnię wysokiego poziomu - invoke. Invoke to po prostu call ze sprawdzaniem typów zmiennych. Na przykład jeżeli użyjesz:

call ExitProcess

bez włożenia na stos parametru DWORD to assembler/linker nie będzie w stanie poinformować cię o błędzie. Zauważysz to później, gdy program się zwiesi :) Ale kiedy użyjesz:

invoke ExitProcess

Linker poinformuje cię, że zapomniełeś ułożyć na stosie parametr DWORD w celu uniknięcia błędu. Polecam ci używanie invoke zamiast prostego call. Składnia invoke jest następująca:

INVOKE  wyrażenie [,argumenty]

Wyrażeniem może być nazwa funkcji lub wskaźnik funkcji. Parametry funkcji są oddzielone przecinkami.

Większość protoypów funkcji API jest zawartych w plikach include. Jeżeli używasz MASM'a hutch'a będą one w folderze MASM/include/ . Pliki include mają rozszerzenie .inc, a prtotypy funkcji zawartych w poszczególnych DLL-ach są zawarte w pliku .inc o nazwie tej samej co DLL. Na przykład ExitProcess jest ekspotowany przez kernel32.dll więc prototyp ExitProcess jest zawarty w kernel32.inc.
Możesz też tworzyć prototypy dla twoich własnych funkcji.
W moim przykładzie użyję windows.inc hutch'a, które możesz ściągnąć z http://win32asm.cjb.net

teraz z powrotem do ExitProcess, parametr uExitCode jest wartością, która powoduje, że program powróci do Windows po zakończeniu. Możesz wywołać ExitProcess w ten sposób:

invoke ExitProcess, 0

Umieść tą linijkę zaraz pod etykietą startową, a uzyskasz program pod Win32, który natychmiast się zamyka, ale jest on poprawny...

.386
.model flat, stdcall
option casemap:none
include \masm32\include\windows.inc
include \masm32\include\kernel32.inc
includelib \masm32\lib\kernel32.lib
.data
.code
invoke ExitProcess,0
start:
end start

Opcja casemap:none mówi MASM'owi, żeby "uważał na zmianę wielkości liter" tzn. że ExitProcess i exitprocess to nie to samo. Zwróć uwagę na nową dyrektywę, include. Po tej dyrektywie występuje nazwa pliku którą chciałbyś wstawić w miejsce tej dyrektywy. W powyższym przykładzie kiedy MASM zacznie przetwarzać linię include \masm32\include\windows.inc, otworzy plik windows.inc, który znajduje się w folderze \MASM32\include i przetworzy zawartość windows.inc tak jakbyś ją tam (do swojego programu) wkleił. Plik windows.inc hutch'a zawiera definicje stałych i struktur, które będą ci potrzebne w programowaniu pod Win32. Nie zawiera on żadnego prototypu funkcji. Hutch i ja próbujemy umieścić tam tyle definicji stałych i struktur ile się da, ale jeszcze wiele zostało do wliczenia. Plik windows.inc jest cały czas update'owany. Sprawdź strony hutch'a i moje dla nowszych wersji.
Z windows.inc twój program ma definicje stałych i struktur. Dla prototypów funkcji musicz wliczyć inne pliki include. Możesz wygenerować takie pliki, które będą zawierały tylko prototypy funkcji z bibliotek importów. Aby wygenerować takie pliki musisz postępować następująco:

  1. Ściągnij pakiet bibliotek importowych dla MASM'a ze strony hutch'a lub mojej. Zawiera on kolekcję bibliotek jakie będą ci potrzebne pod Win32. Ściągnij także program l2inc.
  2. Rozpakuj obie paczki do tego samego folderu. Jeżeli zainstalowałeś już MASM32 rozpakuj je do folderu MASM32\Lib
  3. Uruchom l2inca.exe z następującymi przełącznikami:

  4. l2inca /M *.lib

    Program l2inca.exe wyciągnie informacje z bibliotek importów i utworzy pliki include pełne prototypów funkcji.

  5. Przenieś te pliki do folderu MASM32\include.
W naszym przykładzie wywołujemy funkcję eksportowaną przez kernel32.dll, więc musimy wliczyć prototypy funkcji zawartych w kernel32.dll. Ten plik to kernel32.inc. Jeżeli otworzysz ten plik edytorem tekstu, to zobaczysz że jest on pełny prototypów funkcji dla kernel32.dll. Jeżeli nie wliczysz kernel32.inc, nadal możesz wywołać ExitProcess, ale tylko prostą składnią call. Nie będziesz mógł użyć składni invoke. Wniosek jest następujący: aby móc użyc invoke, musisz wliczyć prototyp użytej funkcji gdzieś w kodzie źródłowym. W powyższym przykładzie, jeżeli nie wliczysz kernel32.inc, możesz zdefiniować prototyp funkcji dla ExitProcess gdziekolwiek w kodzie, ale wcześniej niż komenda invoke, a invoke będzie działać. Pliki include są po to aby zaoszczędzić ci pracy z wpisywaniem wszystkich prototypów funkcji samemu, więc używaj ich kiedy tylko chcesz.
Teraz poznamy nową dyrektywę, includelib. includelib nie robi tego samego co include. To tylko sposób na poinformowanie assemblera jaką bibliotekę importów twój program używa. Kiedy assembler zauważy dyrektywę includelib, umieszcza komendę łączącą do pliku obiektu, więc linker będzie wiedział z jakimi bibliotekami importów trzeba połączć twój program. Nie musisz jednak używać includelib. Możesz okreslić biblioteki importów w linii poleceń linkera, ale uwierzcie mi, jest to nudne, a linia poleceń może pomieścić tylko 128 znaków.

Teraz zapisz przykład jako msgbox.asm. Przypuszczając, że ml.exe jest w twojej ścieżce, zassembluj msgbox.asm wyrażeniem:

    ml  /c  /coff  /Cp msgbox.asm
  • /c mówi MASM'owi by tylko zassemblował i nie uruchamiał jeszcze link.exe. Przez większośc czasu nie chciałbyś uruchamiać link.exe automatycznie, ponieważ może będziesz musiał wykonać kilka rzeczy ważniejszych niż link.exe.
  • /coff mówi MASM'owi by utworzył plik .obj w formacie COFF MASM używa wariacji COFF (Potoczny format pliku obiektu
    z ang. - Common Object File Format), który jest używany pod Unix'em jako jego własny obiekt i format pliku wykonywanlego.
  • /Cp mówi MASM'owi, aby przestrzegał wielkości liter. Jeżeli używasz MASM32 hutch'a, to możesz umieścić "option casemap:none" na początku twojego kodu, zaraz pod dyrektywą .model, a da to ten sam efekt.
Po zassemblowaniu msgbox.asm, dostaniesz msgbox.obj. msgbox.obj jest plikiem obiektu. Plik obiektu jest tylko o krok od pliku wykonywalnego. Zawiera on instrukcje/dane w formie binarnej. Plikowi temu brakuje tylko kilku poprawek adresów, które to zrobi linker.

Teraz możesz uruchomić linkera:

    link /SUBSYSTEM:WINDOWS  /LIBPATH:c:\masm32\lib  msgbox.obj
/SUBSYSTEM:WINDOWS  informuje linker'a jakim typem programu wykonywalnego jest ten program
/LIBPATH:<ścieżka do biblioteki importów> mówi linker'owi, gdzie znajdują się biblioteki importów. Jeżeli używasz MASM32, będą one w folderze MASM32\lib.
Linker czyta plik obiektu i poprawia go o adresy bibliotek importów. Kiedy proces ten zostanie zakończony dostaniesz msgbox.exe.

Teraz masz msgbox.exe. Nie bój się, uruchom go :) Dojdziesz do tego, że nic nie robi. Hmmm... nie umieściliśmy jeszcze w nim nic ciekawego. Ale to wciąż program Win32. A popatrz na jego rozmiar ! Na moim PC to 1,536 bajtów.

Teraz umieścimy tam okno z wiadomością. Jego prototyp funkcji to:

MessageBox PROTO hwnd:DWORD, lpText:DWORD, lpCaption:DWORD, uType:DWORD

hwnd jest zaczepieniem do okna macierzystego. Możesz myśleć o tym zaczepieniu jako o numerze, który reprezentuje okno, do którego się odnosisz. Jego wartość nie jest dla ciebie ważna. Tylko pamiętasz, że reprezentuje ono okno. Kiedy chcesz zrobić coś z tym oknem, musisz się do niego odnieść poprzez punkt zaczepienia.
lpText jest wskaźnikiem do tekstu, który chcesz wyświetlić w okienku. Wskaźnik jest tak naprawdę adresem czegoś. Wskaźnik do tekstu=adres linii z tym tekstem.
lpCaption jst wskaźnikiem do nagłówka naszego okienka
uType określa ikonę oraz typ i ilość przycisków na okienku
Zmodyfikujmy msgbox.asm, aby dodać tam okienko z naszą informacją.
 

.386
.model flat,stdcall
option casemap:none
include \masm32\include\windows.inc
include \masm32\include\kernel32.inc
includelib \masm32\lib\kernel32.lib
include \masm32\include\user32.inc
includelib \masm32\lib\user32.lib

.data
MsgBoxCaption  db "Iczelion Tutorial No.2",0
MsgBoxText       db "Win32 Assembly is Great!",0

.code
start:
invoke MessageBox, NULL, addr MsgBoxText, addr MsgBoxCaption, MB_OK
invoke ExitProcess, NULL
end start

Zassembluj to i uruchom. Powinieneś zobaczyć okienko z informacją "Win32 Assembly is Great!".

Przyjżyjmy się jeszcze kodowi żródłowemu.
W sekcji .data definiujemy dwa zakończone zerem linie. Pamiętaj, że każda linia ANSI w Windows musi być zakończona przez NULL (heksdecymalnie 0).
Używamy dwóch stałych, NULL i MB_OK. Stałe te są udokumentowane w pliku windows.inc. Więc możesz się odnosić do nich po nazwie, a nie po wartości. Poprawia to czytalność twojego kodu.
Operator addr jest użyty do podania adresu etykiedy do funkcji. Jest on poprawny tylko przy wywołaniu invoke. Możesz go używać do przyporządkowania adresu etykiety do rejestru/zmiennej, na przykład. Możesz użyć offset zamiast addr w powyższym przykładzie. Aczkolwiek jest kilka różnic pomiędzy tymi dwoma dyrektywami:

  1. addr nie może obsłużyć przyszłej referencji, kiedy offset może. Na przykład, jeżeli etykieta jest zdefiniowana gdzieś poniżej w kodzie niż invoke, to addr nie będzie działać :
  2. invoke MessageBox,NULL, addr MsgBoxText,addr MsgBoxCaption,MB_OK
    ......
    MsgBoxCaption  db "Iczelion Tutorial No.2",0
    MsgBoxText       db "Win32 Assembly is Great!",0
    MASM poinformuje cię o błędzie. Jeżeli użyjesz offset zamiast addr w powyższym wycinku kodu, MASM zassembluje to bez problemu.
  3. addr może obsłużyć lokalne zmienne, kiedy offset nie może. Będziesz znał jej adres tylko podczas uruchamiania. offset jest interpretowane podczas assemblowania przez assembler. Więc to jest naturalne, że offset nie będzie działał z lokalnymi zmiennymi. addr jest w stanie obsłużyć lokalne zmienne ponieważ assembler sprawdza pierwsze czy zmienna referowana przez addr jest lokalna czy globalna. Jeżeli jest zmienną globalną, assembler umieszca adres tej zmiennej w pliku obiektu. W tej formie działa to jak offset. Jeżeli jest to zmienna lokalna, assembler generuje sekwencje instrukcji taką jak ta, zanim wywoła funkcję:
  4. lea eax, LocalVar
    push eax


    Kiedy tylko lea może zdeterminować adres etykiety podczas pracy programu, to działa to dobrze.


[Strona główna Iczelion's Win32 Assembly]

[Text został przetłumaczony przez WercY]