Rozdział II - Okno informacyjne


W tym rozdziale utworzymy w pełni funkcjonalny program Windows, który wyświetla poniższe okienko informacyjne

 

obrazek

 

Pobierz plik z przykładem {z tego archiwum}.

 

Teoria

System Windows przygotowuje dla uruchomionych w nim programów mnóstwo różnych zasobów, których sercem jest Windows API (Application Programming Interface - Interfejs Programowy Aplikacji). Jest to olbrzymi zbiór bardzo użytecznych funkcji, które znajdują się w samym Windows gotowe do wykorzystania przez każdy uruchomiony w tym środowisku program. Funkcje te przechowywane są w kilku dynamicznie dołączanych bibliotekach (DLL - Dynamic Linked Libraries) takich jak kernel32.dll, user32.dll i gdi32.dll.

Oprócz tych "trzech głównych" istnieje wiele innych bibliotek dynamicznych, z których może korzystać twój program, o ile posiadasz wystarczającą ilość informacji o zawartych w nich funkcjach API.

Programy Windows dynamicznie łączą się z tymi bibliotekami DLL, tj. kody funkcji API nie są dołączane do pliku wykonywalnego Windows. Aby twój program wiedział, gdzie znaleźć pożądane funkcje API w czasie swojego wykonania, musisz osadzić tę informację w pliku wykonywalnym. Informacja ta znajduje się w bibliotekach importu. Musisz połączyć swoje programy z właściwymi bibliotekami importu, albo nie będą one potrafiły zlokalizować funkcji API.

Gdy program Windows jest ładowany do pamięci, system Windows odczytuje informację zawartą w jego pliku wykonywalnym. Informacja ta obejmuje nazwy używanych w programie funkcji oraz bibliotek DLL, w których funkcje te są przechowywane. Gdy system Windows odnajdzie taką informację w programie, załaduje biblioteki DLL i dokona uaktualnienia adresów wywołań funkcji API, tak aby wywołanie przekazywało sterowanie do właściwej funkcji.

Istnieją dwie kategorie funkcji API: jedna dla ANSI i druga dla Unicode. Nazwy funkcji API dla ANSI mają przyrostek "A", np. MessageBoxA. Dla Unicode przyrostek ma postać literki "W" (sądzę, że pochodzi to od słówek Wide Char). Windows 95 standardowo obsługuje kodowanie ANSI, natomiast Windows NT Unicode.

Właściwie jesteśmy przyzwyczajeni do tekstów ANSI, które są ciągami znaków zakończonych kodem NULL. Znak ANSI ma rozmiar 1 bajtu. Kod ANSI jest wystarczający dla języków europejskich, jednakże nie potrafi obsługiwać języków dalekowschodnich, w których występuje po kilka tysięcy znaków. Dlatego wymyślono UNICODE. Znak w Unicode ma rozmiar 2 bajtów, co daje 65536 różnych znaków.

Jednakże w większości przypadków będziesz stosował plik dołączalny, który potrafi określić i wybrać odpowiednie funkcje API dla twojej platformy. Po prostu odwołuj się do nazw funkcji API bez przyrostka.

Przykład:

Poniżej przedstawiam czysty szkielet programu. Wypełnimy go treścią za chwilę.

 

.386

.MODEL FLAT, STDCALL

.DATA

.CODE

start:

END start

 

Wykonanie programu rozpoczyna się od pierwszej instrukcji umieszczonej bezpośrednio za etykietą określoną po dyrektywie end. W powyższym szkielecie wykonanie rozpocznie się od pierwszej instrukcji za etykietą start. Wykonanie będzie postępować instrukcja po instrukcji aż do napotkania instrukcji zmieniającej przepływ sterowania, takiej jak jmp, jne, je, ret itp. Instrukcje te przenoszą wykonanie do innej instrukcji w programie. Gdy program musi zakończyć swoje działanie i powrócić do Windows, powinien wywołać funkcję API o nazwie ExitProcess.


ExitProcess PROTO uExitCode:DWORD 


Powyższy wiersz przedstawia prototyp funkcji definiujący atrybuty tej funkcji dla asemblera / linkera, aby możliwe było dokonanie sprawdzenia typów. Format prototypu funkcji jest następujący:


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


W skrócie nazwa funkcji, za którą następuje słowo kluczowe PROTO i lista typów danych parametrów, rozdzielonych przecinkami. W próbce ExitProcess podanej powyżej prototyp definiuje ExitProcess jako funkcję, która przyjmuje tylko jeden parametr typu DWORD (32-bitowe słowo). Prototypy funkcji są bardzo użyteczne, gdy korzystasz ze składni wywołań wysokiego poziomu, INVOKE. Możesz potraktować INVOKE jako proste wywołanie ze sprawdzaniem typów. Na przykład, jeśli zastosujesz:

 

   call ExitProcess


bez umieszczenia na stosie podwójnego słowa, to asembler / linker nie będzie w stanie wyłapać ten błąd dla ciebie. Zauważysz go później, gdy program się zawiesi. Lecz jeśli użyjesz:

 

    INVOKE ExitProcess

 

Linker poinformuje cię, iż zapomniałeś umieścić na stosie podwójne słowo, zapobiegając w ten sposób błędowi. Zalecam ci stosowanie INVOKE zamiast prostego wywołania. Składnia polecenia INVOKE jest następująca:

 

    INVOKE wyrażenie [,argumenty] 

 

wyrażenie może być nazwą funkcji lub wskazaniem funkcji. Parametry funkcji są rozdzielone przecinkami.

Większość prototypów funkcji dla funkcji API przechowywana jest w plikach dołączanych. Jeśli korzystasz z MASM32 przystosowanego przez Hutcha, to znajdziesz je w katalogu /masm32/Include. Pliki dołączane posiadają rozszerzenie inc a prototypy dla funkcji z bibliotek DLL są umieszczone w plikach o tej samej nazwie co nazwa biblioteki DLL. Na przykład, ExitProcess udostępniana jest przez bibliotekę kernel32.lib, więc prototyp tej funkcji będzie umieszczony w kernel32.inc.

Możesz również tworzyć prototypy dla swoich własnych funkcji.

We wszystkich moich przykładach korzystam z pliku windows.inc przygotowanego przez Hutcha, który możesz załadować z http://win32asm.cjb.net

Wracając do ExitProcess, parametr uExitCode jest wartością, którą program zwraca do Windows po swoim zakończeniu. Funkcję ExitProcess możesz wywołać następująco:

 

    INVOKE ExitProcess, 0

 

Umieść ten wiersz bezpośrednio pod etykietą start, a otrzymasz program win32, który natychmiast wraca do Windows, jednakże jest to całkiem poprawny program.

 

.386

.MODEL FLAT, STDCALL

OPTION CASEMAP:NONE

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

.DATA
 
.CODE

start: 
    INVOKE ExitProcess, 0 
END start

 

OPTION CASEMAP:NONE każe asemblerowi MASM rozróżniać duże i małe litery w etykietach, więc ExitProcess i exitprocess są dla niego różne. Zwróć uwagę na nową dyrektywę, include. Za dyrektywą tę następuje nazwa pliku, który ma zostać wstawiony w miejscu dyrektywy. W powyższym przykładzie, gdy MASM przetwarza wiersz

 

INCLUDE \masm32\include\windows.inc

 

Otworzy on plik windows.inc, który znajduje się w katalogu \masm32\include i przetworzy jego zawartość tak, jakbyś ją wstawił w tym miejscu. Plik windows.inc Hutcha zawiera definicje stałych i struktur niezbędnych do programowania win32. Nie zawiera on żadnego prototypu funkcji. W żaden sposób plik ten nie jest wyczerpujący. Hutch i ja staraliśmy się umieścić w nim jak najwięcej stałych i struktur, lecz wciąż wiele jest to dołączenia. Będzie on ciągle uaktualniany. Sprawdzaj od czasu do czasu strony domowe Hutcha i moją.

Z pliku windows.inc twój program otrzymał stałe i definicje struktur danych. Teraz dla prototypów funkcji musisz dołączyć inne pliki. Przechowywane są one wszystkie w katalogu \masm32\include.

W naszym przykładzie powyżej wywołujemy funkcję eksportowaną przez kernel32.dll, więc musimy dołączyć prototypy funkcji z kernel32.dll. Znajdują się one w pliku kernel32.inc. Jeśli otworzysz ten plik za pomocą edytora tekstu, to zobaczysz mnóstwo prototypów funkcji dla kernel32.dll. Jeśli nie dołączysz pliku kernel32.inc, to wciąż możesz wywołać ExitProcess, ale tylko za pomocą składni prostego wywołania. Nie będziesz mógł zastosować INVOKE. Chodzi tutaj o to, iż użycie INVOKE wymaga umieszczenia w kodzie źródłowym prototypu funkcji. W powyższym przykładzie, jeśli nie dołączysz kernel32.inc, możesz sam zdefiniować prototyp tej funkcji gdziekolwiek w kodzie źródłowym przed zastosowaniem polecenia INVOKE i będzie to działać. Pliki dołączane pozwalają ci zaoszczędzić pracy przy wpisywaniu prototypów, więc korzystaj z nich tak często, jak tylko możesz.

Teraz napotkaliśmy nową dyrektywę INCLUDELIB. Nie pracuje ona w ten sam sposób jak include. Pozwala ona jedynie poinformować asembler, z jakich bibliotek importu korzysta twój program. Gdy asembler napotka dyrektywę includelib, umieszcza w pliku pośrednim (obj) polecenie dla linkera, aby wiedział, z jaką biblioteką importu należy skonsolidować twój program. Jednakże nie masz obowiązku stosować INCLUDELIB. Możesz określić nazwy bibliotek importu w wierszu poleceń wywołania linkera, ale uwierz mi, jest to niewygodne, a wiersz poleceń nie może zawierać więcej niż 128 znaków.

Teraz zapisz ten przykład pod nazwą msgbox.asm. Zakładając, iż ml.exe jest w twojej ścieżce katalogów, dokonaj asemblacji msgbox.asm za pomocą polecenia:


ml /c /coff /Cp msgbox.asm


Gdy proces asemblacji msgbox.asm zakończy się sukcesem, otrzymasz plik msgbox.obj., który jest plikiem obiektowym.. Plik obiektowy jest już tylko o jeden krok od pliku wykonywalnego. Zawiera on dane oraz instrukcje w postaci binarnej.. Brakuje jedynie ustalenia adresów dokonywanego przez linkerlink.exe.

Teraz dokonujemy konsolidacji za pomocą polecenia:


link /SUBSYSTEM:WINDOWS /LIBPATH:c:\masm32\lib msgbox.obj


Linkerlink.exe wczytuje plik obiektowy i łączy go z adresami pochodzącymi z bibliotek importu. Gdy proces się zakończy, otrzymujesz msgbox.exe.

Masz swój program msgbox.exe. Dalej, uruchom go. Stwierdzisz, że nic nie robi. No..., jeszcze nic interesującego w nim nie umieściliśmy. Jednakże jest to program dla Windows. A spójrz na jego rozmiar! Na moim PC ma 1536 bajtów.

Teraz umieścimy w nim okienko informacyjne. Jego prototyp funkcji ma postać:

 

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

Zmodyfikujmy msgbox.asm, aby umieścić tu okienko informacyjne.


.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 "Kurs Iczeliona. Rozdział nr 2",0 
MsgBoxText    DB "Asembler Win32 jest Wspaniały!",0

.CODE 

start: 
    INVOKE MessageBox, NULL, ADDR MsgBoxText, ADDR MsgBoxCaption, MB_OK 
    INVOKE ExitProcess, NULL 
END start

 

Dokonaj asemblacji i uruchom program. Na ekranie powinno pojawić się następujące okienko informacyjne:


obrazek

 

Spójrzmy ponownie na kod źródłowy.

W sekcji .DATA definiujemy dwa łańcuchy znakowe zakończone kodem zero. Pamiętaj, iż w Windows każdy łańcuch znakowy ANSI musi się kończyć znakiem o kodzie zero.

Używamy dwóch stałych: NULL oraz MB_OK. Są one udokumentowane w pliku windows.inc. Więc możesz się do nich odwoływać poprzez nazwę zamiast poprzez wartość. Zwiększa to czytelność twojego kodu źródłowego.

Operator ADDR użyty jest do przekazania adresu etykiety do funkcji. Działa on jedynie w obrębie dyrektywy INVOKE. Nie możesz na przykład używać go do przypisania adresu etykiety rejestrowi lub zmiennej. Zamiast tego możesz użyć operatora OFFSET. Jednakże pomiędzy ADDR i OFFSET istnieją pewne różnice:

  1. ADDR nie może obsługiwać odwołań w przód, natomiast OFFSET radzi sobie z tym doskonale. Na przykład, jeśli dana etykieta jest zdefiniowana gdzieś dalej w tekście źródłowym programu, za wierszem polecenia INVOKE, to ADDR nie zadziała:
    ...
    INVOKE MessageBox, NULL, ADDR MsgBoxText, ADDR MsgBoxCaption, MB_OK 
    ...
MsgBoxCaption DB "Kurs Iczeliona. Rozdział nr 2",0 
MsgBoxText    DB "Asembler Win32 jest Wspaniały!",0

MASM zgłosi błąd. Jeśli zamiast addr umieścisz w podanym fragmencie programu operator offset, to MASM dokona asemblacji bez błędów.

  1. ADDR może obsługiwać lokalne zmienne, natomiast OFFSET nie. Zmienna lokalna jest jedynie pewnym miejscem zarezerwowanym na stosie procesora. Jej adres będzie znany tylko w czasie wykonywania się programu. Operator OFFSET przetwarzany jest w czasie procesu asemblacji przez asembler. Więc zupełnie naturalnym jest, iż nie działa on ze zmiennymi lokalnymi. Operator ADDR może obsłużyć zmienne lokalne z uwagi na fakt, iż asembler sprawdza najpierw, czy wskazana zmienna jest globalna, czy lokalna. Jeśli jest to zmienna globalna, to umieszcza adres tej zmiennej w pliku obiektowym. W tym sensie działa to identycznie jak OFFSET . Jeśli jest to zmienna lokalna, to generuje poniższy ciąg instrukcji zanim wywoła daną funkcję:
    lea  eax, LocalVar 
    push eax


Ponieważ instrukcja lea (load effective address - załaduj adres efektywny argumentu) może określić adres etykiety w czasie pracy programu, będzie to działać wspaniale.

Dodatek w Pascalu

Aby udowodnić wam, iż wiadomości o programowaniu systemu Windows przydają się w innych językach programowania, pod wieloma programami asemblerowymi umieściliśmy ich dokładne odpowiedniki w języku Pascal, który jest ogólnie dostępny w sieci Internet. Przykłady zostały uruchomione i przetestowane w środowisku DevPascal firmy Bloodshed Software Inc, które również można bezpłatnie pobrać z sieci (pakiet ten wyposażony jest w kompilator FreePascala). Oczywiście program asemblerowy jest bardziej efektywny, a program wynikowy zajmuje dużo mniej miejsca.

 

{********************************
**  I Liceum Ogólnokształcące  **
**           w Tarnowie        **
**       mgr Jerzy Wałaszek    **
********************************}

program MsgBox;

uses Windows;

begin
  MessageBox(0,'Okienko informacyjne w Pascalu',
             'Kurs Iczeliona, Rozdział nr 2',MB_OK);
  ExitProcess(0);
end.

 

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.


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

©2024 mgr Jerzy Wałaszek

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

Pytania proszę przesyłać na adres email: i-lo@eduinf.waw.pl

W artykułach serwisu są używane cookies. Jeśli nie chcesz ich otrzymywać,
zablokuj je w swojej przeglądarce.
Informacje dodatkowe