Assembler

W tym tutorialu stworzymy w pełni funkcjonalny windows'owy program, który wyświetli następującą informację w okienku: "Win32 assembly is great!".

Część 2: MessageBox (Okienko informacyjne)

Ściągnij przykładowy plik stąd.

Teoria:

Windows przygotowuje odpowiednie zasoby dla programów windowsowych. Centralnym zbiorem zasobów jest Windows API (Application Programming Interface). Windows API jest dużą kolekcją bardzo użytecznych funkcji, które rezydują wewnątrz Windows, gotowe do użycia przez jakikolwiek windowsowy program. Te funkcje są zapisane w kilku dynamicznie przyłączanych bibliotekach (DLLs), takich jak kernel32.dll, user32.dll and gdi32.dll. Kernel32.dll zawiera funkcje API, które wykonują działania na pamięci i zarządzają procesami. User32.dll zarządzają interfejsem użytkownika programu. Gdi32.dll jest odpowiedzialne za graficzne operacje. Są również inne biblioteki DLL, których twój program może używać, dostarczając ci odpowiednich informacji o żądanych funkcjach API.
Programy Windows dynamicznie przyłączane są do tych funkcji DLL. Kod funkcji API nie jest przyłączany do pliku wykonywalnego programu Windows. Aby twój program mógł wiedzieć gdzie szukać wymaganych funkcji API podczas uruchamiania, musisz dołączyć tą informację do pliku. Ta informacja jest w importowanych bibliotekach. Musisz łączyć swój program z odpowiednimi bibliotekami, w przeciwnym przypadku nie będzie można zlokalizować funkcji API.
Kiedy program Windows jest ładowany do pamięci, Windows czyta informację zapisaną w programie. Ta informacja zawiera nazwy funkcji, których program używa i biblioteki DLL tych funkcji. Gdy Windows znajdzie te informacje w programie, załaduje odpowiednie DLL i odda sterowanie odpowiedniej funkcji programu.
Są dwa rodzaje funkcji API: Jedna dla ANSI, druga dla Unicode. Nazwy funkcji API dla ANSI mają przyrostek "A", np. MessageBoxA. Te dla Unicode posiadają przyrostek "W". Windows 95 dostarcza ANSI a Windows NT Unicode.
Zwykle jesteśmy przyzwyczajeni do łancuchów ANSI, które mają tablice zakończone przez NULL. Znak ANSI posiada rozmiar 1 bajt. Kod ANSI jest wystarczający dla języków Europejskich, języki orientalne jednak posiadają kilka tysięcy unikalnych znaków. Dlatego używają UNICODE, który ma rozmiar 2 bajtów, umożliwiając zapis 65536 unikalnych znaków w łańcuchu.
Lecz przez większość czasu, będziesz używał plików dołączanych (include), które mogą określić i wybrać odpowiednią funkcję API dla twojej platformy. Dlatego odnoś się do nazwy funkcji API bez przyrostka.

Przykład:

Przedstawię teraz szablon programu, który później zostanie omówiony.

.386
.model flat, stdcall

.data
.code
start:
end start

Wykonanie zaczyna się od pierwszej instrukcji poniżej etykiety o nazwie określonej po słowie kluczowym end. W powyższym szablonie, wykonanie rozpocznie się od pierwszej instrukcji poniżej etykiety start . Wykonywanie będzie następowało instrukcja po instrukcji aż wystąpią instrukcje sterujące, takie jak jmp, jne, je, retitp. Te instrukcje zmieniają kolejność wykonywania programu przekazując sterowanie do innych instrukcji. Kiedy program potrzebuje wyjść do Windows, powinna być wywołana funkcja API: ExitProcess.

ExitProcess proto uExitCode:DWORD

Powyższa linia jest nazywana prototypem funkcji. Prototyp funkcji definiuje atrybuty funkcji dla linkera w celu umożliwienia wykonania sprawdzenia typów. Format prototypu funkcji jest następujący:

FunctionName PROTO [ParameterName]:DataType,[ParameterName]:DataType,...

W skrócie, nazwa funkcji następująca po słowie PROTO a następnie lista parametrów typów danych oddzielonych przecinkami. W przykładzie powyższym ExitProcess jest definiowane jako funkcja, która potrzebuje tylko jednego parametru typu DWORD. Prototypy funkcji są bardzo pożyteczne, kiedy używasz składni dalekiego wywołania: invoke. Możesz uważać invoke jako przykład wywołania ze sprawdzaniem typów. Na przykład, jeżeli wykonasz:

call ExitProcess

bez położenia podwójnego słowa (dword) na stos, assembler/linker nie będzie mógł wyłapać tego błędu dla ciebie. Zauważysz to później kiedy program się "wysypie". Ale jeżeli użyjesz :

invoke ExitProcess

linker poinformuje cię że zapomniałeś odłożyć na stos słowa i w ten sposób unikniesz błędu. Polecam używać invoke zamiast po prostu call. Składnia polecenia invoke jest następująca:

INVOKE  wyrażenie [,arguments]

wyrażenie może być nazwą funkcji lub wskaźnikiem do funkcji. Parametry funkcji są oddzielone przecinkami.

Większość prototypów funkcji dla funkcji API są przechowywane w plikach include. Jeżeli używasz MASM32 (Hutch'a), będą one w folderze MASM32/include. Pliki include mają rozszerzenie .inc i prototypy funkcji dla funkcji DLL są zapisane w pliku .inc z taką samą nazwą jak DLL. Na przykład ExitProcess jest eksportowana przez kernel32.lib więc prototyp funkcji dla ExitProcess jest zapisany w kernel32.inc.
Możesz również tworzyć prototypy funkcji dla własnych funkcji.
W moich przykładach użyję pliku windows.inc (Hutch'a), który możesz ściągnąć z http://win32asm.cjb.net

Wracając do ExitProcess, parametr uExitCode jest wartością, którą chcesz, aby program zwrócił do Windows po zatrzymaniu programu. Możesz wywołąć ExitProcess nastąpująco:

invoke ExitProcess, 0

Wpisz tą linię dokładnie poniżej etykiety start, uzyskasz program win32, który natychmiast wyjdzie do Windows, niemniej będzie to program działający.

.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

opcja casemap:none mówi MASM aby rozróżniał wielkość liter więc ExitProcess i exitprocess różnią się. Zauwasz nową dyrektywę, include. Ta dyrektywa zawiera nazwę pliku, który chcesz dołączyć w miejsce gzie występuje dyrektywa. W powyższym przykładzie, kiedy MASM odczytuje linię include \masm32\include\windows.inc, zostanie otwarty plik windows.inc ,który znajduje się w folderze \MASM32\include i wykona zawartość tego pliku jak gdybyś go tam wkleił. Plik windows.inc Hutch'a zawiera definicje i struktury programu win32. Nie zawiera żadnych prototypów funkcji. Zawartość pliku windows.inc jest zrozumiała. Hutch i ja staramy się dopisać tak wiele stałych i struktur jak to możliwe, ale jest jeszcze dużo do dołączenia. Będę go stale uaktualniał. Sprawdzaj nasze strony domowe w celach uaktualnień.
Właśnie zpliku windows.inc, twój program pobiera definicje stałych i struktur. A teraz o prototypach funkcji; będziesz potrzebował dołączać inne pliki. Są one wszystkie zapisane w folderze \masm32\include.

W naszym przykładzie powyżej, wywołujemy funkcję eksportowaną przez kernel32.dll, więc będziemy potrzebować dołaczyć tę funkcję z kernel32.dll. Tym plikiem jest kernel32.inc. Jeżeli otworzysz go z edytora tekstu, zobaczysz, że jest pełen prototypów funkcji dla kernel32.dll. Jeżeli nie dołaczysz kernel32.inc, możesz nadal wywoływać ExitProcess ale tylko według prostej składni call. Nie będzieszmógł skorzystać z funkcjiinvoke . Różnica jest następująca: W celu wywołania funkcji invok, musisz dodać prototyp funkcji gdzieś w kodzie źródłowym. W powyższym przykładzie, jeżli nie dołączysz kernel32.inc, możesz zdefiniować prototyp funkcji dla ExitProcess gdziekolwiek w kodzie źródłowym powyżej polecenia invoke i to będzie działać. Pliki dołączane poleceniem includes służą oszczędzaniu pracy potrzebnej na wpisywanie prototypów więc należy z nich korzystać jak najczęściej.
Teraz zajmiemy się nową dyrektywą , includelib. includelib nie działa tak samo jak include. To tylko sposób na powiedzienie assembler'owi, że program będzie używał bibliotek. Kiedy assembler widzi dyrektywę includelib, dodaję komendę do obiektu z pliku, tak, że linker wie, że program potrzebuje importować biblioteki w celu przyłączenia do pliku . Nie musisz jednak się męczyć i używać includelib. Możesz wyspecyfikować nazwy importowanych bibliotek w komendzie wywołującej linker, lecz wierz mi , to trudne i linia komendy może zawierać tylko 128 znaków.

Teraz zapisz przykład pod nazwą msgbox.asm. Przyjmujemy, że ml.exe jest w ścieżce dostępu, zasembluj msgbox.asm z parametrami:

    ml  /c  /coff  /Cp msgbox.asm
  • /c znaczy,że MASM tylko asembluje. Nie wywołuje linkera. Przez większość czasu nie będziesz wywoływał automatycznie link.exe, więc może się okazać, że będziesz musiał wykonać jakie inne polecenia w celu wywołania linkera (link.exe).

  • /coff mówi MASM aby utworzył plik .obj w formacie COFF . MASM używa różnych formatów COFF (Common Object File Format) które są wykorzystywane pod Unix'em jako jego własny format obiektu i wykonywanego pliku.
    /Cp mówi MASM aby zachował identyfikatory użytkownika. Jeżeli używasz pakietu Hutch'a MASM32 , możesz wpisać "option casemap:none" w nagłówku swojego kodu źródłowego, dokładnie poniżej dyrektywy.model w celu uzyskania tego samego efektu.
Po bezbłędnym zasemblowaniu pliku msgbox.asm, uzyskasz plik msgbox.obj. msgbox.obj jest plikiem obiektu. Plik obiektu jest tylko jednym krokiem od pliku wykonywalnego. Zawiera on dane/instrukcje w formacie binarnym. To czego mu brakuje to powiązania adresowe dokonane przez linker.

Następnie przystąp do łączenia (link) :

    link /SUBSYSTEM:WINDOWS  /LIBPATH:c:\masm32\lib  msgbox.obj
/SUBSYSTEM:WINDOWS  Powyższe polecenia informują linker jakiego rodzaju plikiem wykonywalnym jest ten program.
/LIBPATH:<ścieżka do importowanej biblioteki> mówi linkerowi gdzie są importowane biblioteki. Jeżeli używasz MASM32, będą one w folderze MASM32\lib.
Linker czyta plik obiektowy i dokonuje jego powiązania z importowanymi bibliotekami. Kiedy ten proces jest zakończony uzyskujesz plik msgbox.exe.

Teraz już masz msgbox.exe. Dalej, uruchom go. Przekonasz się, że niczego on nie wykonuje. Cóż, jeszcze niczego interesującego nie wplisaliśmy do niego. Niemniej jest to windows'owy program. I zobacz jaki ma rozmiar! Na moim PC, to jest 2,560 bytes.

Następnie wprowadzimy okienko informacyjne (message box). Prototyp funkcji dla tego okienka jast następujący:

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

hwnd jest uchwytem (handle) do okna macierzystego (parent window). Możesz uważać uchwyt za numer, który reprezentuje okno do którego się odwołujesz . Jego wartość nie jest ważna. Musisz tylko pamiętać, że reprezentuje okno. Kiedy zechcesz cokolwiek zrobić z tym oknem, musisz się do niego odwołać poprzez jego uchwyt.
lpText jest wskaźnikiem do tekstu, jaki chcesz aby wyswietlił się w obszarze roboczym (client area) tego okienka. Wskaźnik jest tak naprawdę adresem czegoś. Wskaźnik do łancucha tekstu ==adres tego łańcucha.
lpCaption jest wskaźnikiem do opisu (caption) tego okienka informacyjnego
uType określa ikonę oraz liczbę i typ przycisków okienka
Zmodyfikuj plik msgbox.asm aby uzyskać 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 "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

Zasembluj i uruchom go . Zobaczysz okienko informacyjne wyświetlające tekst "Win32 Assembly is Great!".

Popatrz jeszcze raz na kod źródłowy.
Definiujemy dwa zakończone zerem łańcuchy w sekcji .data. Pamiętaj, że każdy ciąg znaków ANSI w Windows musi być zakończony znakiem NULL (0 szesnastkowo).
Używamy dwóch stałych, NULL i MB_OK. Te stałe są opisane w windows.inc. Możesz odnosić się do nich przez nazwę zamiast przez wartość. To poprawia czytelność twojego kodu źródłowego.
addr ten operator jest używany do przekazania adresu etykiety do funkcjii. To działa wyłącznie w kontekście dyrektywy invoke. Nie możesz na przykład używać go do oznaczenia adresu etykiety do rejestru/zmiennej. Możesz użyć offset zamiast addr w powyższym przykładzie. Jednak, jest kilka różnic pomiędzy nimi:

  1. addr nie może wyprzedzać odwołania, podczas gdy offset może. Na przykład, jeżeli etykieta jest zdefiniowana gdzieś dalej w kodzie źródłowym niż linia invoke, addr nie zadział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 wygeneruje błąd. Jeżeli użyjesz offset zamiast addr w powyższym kodzie, MASM zasembluje go bezbłędnie.
  3. addr może odwoływać się do lokalnych zmiennych, podczas gdy offset nie może. Lokalna zmienna jest tylko zarezerwowanym miejscem w obszarze stosu . Możesz tylko znać jej adres podczas wykonywania programu. offset jest interpretowany podczas asemblowania przez asembler. Więc jest naturalne, że offsetnie funkcjonuje ze zmiennymi lokalnymi. addr może odnosić się do lokalnych zmiennych ponieważ asembler sprawdza najpierw czy zmienna do której się odwołujeaddr jest globalna czy lokalna. Jeżeli jest zmienną globalną, przekaże adres tej zmiennej do obiektu w pliku. W tym sensie, to działa jak offset. Jeżeli to jest lokalna zmienna, generuje sekwencje następujących instrukcji zanim wywoła funkcję:
  4. lea eax, LocalVar
    push eax


    Ponieważ lea może określać adres etykiety podczas uruchamiania, działa prawidłowo.


[Tutorial część 3]