Tutorial 2: MessageBox

V tomto tutorialu vytvoříme plně funkční Windows program, který zobrazí MessageBox, který nám oznámí skvělou zprávu: "Win32 assembly is great!" :).

Stáhněte si kód zde: tady.

Teorie:

Windows nabízí množství programových zdrojů pro své programy. Jejich středem je API (Application Programming Interface). Windows API je sbírka mnoha velice užitečných funkcí, které sídlí v samotném Windows, připravena k použití v jakémkoliv Windows programu. Tyto funkce jsou uloženy v několika dynamicky-linkovaných knihovnách (DLLs) jako je kernel32.dll, user32.dll a gdi32.dll. Kernel32.dll obsahuje API funkce, které se starají o paměť a správu procesů. User32.dll kontroluje aspekty uživatelského rozhraní vašeho programu a gdi32.dll je zodpovědná za operace s grafikou. Kromě těchto třech hlavních existují další DLL knihovny, které může váš program využívat za předpokladu, že víte jak a které API funkce chcete použít.
Windows programy se dynamicky (za běhu programu) odkazují k těmto DLL knihovnám, tj. samotný kód API funkcí není přítomen v exe souboru. Aby program věděl, kde najít potřebné API funkce za běhu, musíte mu tu informaci vložit přímo do exe souboru. Tyto informace jsou uloženy v importních knihovnách (*.lib). Vaše programy tedy musíte slinkovat se správnými importními knihovnami nebo nebudete schopni použít API funkce.
Když je program nahrán do paměti, Windows přečte informace uložené v programu. Ty obsahují jména funkcí, které program využívá a jména DLL knihoven, ve kterých jsou tyto funkce uloženy. Když Windows nalezne takové informace, nahraje DLL knihovny a provede takové operace v programu, aby jednotlivá volání v programu volala správné funkce.
Existují dvě kategorie API funkcí: Jedna je pro ANSI a ta druhá pro Unicode. Ke jménu API funkce pro ANSI je přidáno "A", např. MessageBoxA. Ke jménu funkce pro Unicode je přidáno "W" (zkratka Wide Char, myslím). Windows 95 přirozeně podporuje ANSI a Windows NT Unicode.
My jsme obvykle zvyklí na ANSI řetězce, což jsou pole znaků ukončené NULL. ANSI znak(character) je 1 byte veliký. Zatímco ANSI je dostatečné pro Evropské jazyky, nemůže zvládnout několik orientálních jazyků, které mají několik tisíc jedinečných znaků. Proto existuje UNICODE. UNICODE znak je 2 byty veliký, což umožňuje mít až 65536 jedinečných znaků v řetězcích.
Ale většinu času budete používat hlavičkové soubory, které sami určí a vyberou vhodné varianty funkcí pro vaši platformu. Prostě používejte API funkce bez přidávaného "A" nebo "W".

Příklad:

Zde je základní kostra programu: Probereme ji později.

.386
.model flat, stdcall

.data
.code
start:
end start

Provádění programu začíná od první instrukce pod labelem se jménem, které je uvedeno po end direktivě. V předchozí kostře programu, provádění začíná první instrukcí pod labelem start. Provádění bude pokračovat instrukci po instrukci dokud nenarazí na instrukce změny směru provádění programu jako jsou jmp, jne, je, ret apod. Tyto instrukce změní směr provádění na nějaké další instrukce. Když se program potřebuje vrátit do Windows, měl by zavolat API funkci, ExitProcess.

ExitProcess proto uExitCode:DWORD

Řádek výše je nazýván funkčním prototypem. Funkční prototyp určuje parametry funkce pro assembler/linker, který pak může za vás zkontrolovat, jestli funkci užíváte se správnými parametry. Formát funkčního prototypu vypadá takto:

JmenoFunkce PROTO [JmenoParametru]:TypParametru,[JmenoParametru]:TypParametru,...

Krátce řečeno, jméno funkce, za kterým následuje klíčové slovo PROTO a potom seznam datových typů parametrů funkce, oddělený čárkou. V případě funkce ExitProcess definuje ExitProcess jako funkci, která má pouze jediný parametr typu DWORD. Funkční prototypy jsou velice užitečné, když používáte syntaxi volání z vysšších programovacích jazyků, invoke. Můžeme brát invoke jako jednoduché volání s kontrolou typů. Když např. použijete:

call ExitProcess

aniž by jste PUSHli DWORD do zásobníku, assembler/linker nebude schopen pro vás zachytit chybu. Že jste chybu udělali si všimnete až ve chvíli, kdy program spadne. Ale když použijete:

invoke ExitProcess

, tak vás bude linker informovat, že jste zapomněli PUSHnout DWORD do zásobníku a chybu jednoduše odstraníte. Doporučuji užívat invoke namísto prostého call. Syntaxe invoke je:

INVOKE  výraz [,argumenty]

Výrazem může být jméno funkce nebo ukazatel na funkci. Parametry funkce jsou odděleny čárkami.

Většina funkčních prototypů pro API funkce je uložena v hlavičkových souborech. Pokud použijete Hutchovo MASM32, budou v MASM32/include adresáři s koncovkou .inc . Funkční prototypy pro funkce v DLL jsou uloženy v .inc souboru se stejným jménem jako DLL. Např. ExitProcess je exportován knihovnou kernel32.lib, takže funkční prototyp je v kernel32.inc.
Můžete také vytvářet své vlastní funkční prototypy svých funkcí.
V mých tutoriálech budu používat Hutchův windows.inc, který si můžete stáhnout z http://win32asm.cjb.net

Zpět k ExitProcess, uExitCode parametr je hodnota, kterou chcete aby váš program vrátil Windows poté co skončí. Můžete zavolat ExitProcess takto:

invoke ExitProcess, 0

Dejte tento řádek hned po label start a budete mít win32 program, který dělá jen to, že se vrátí do Windows, poté co se spustí. Nic extra, ale přesto to je korektní 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 říká MASM, aby rozlišoval malá a velká písmena v labelech, takže ExitProcess a exitprocess jsou rozdílné. Všiměte si nové direktivy, include. Po ní následuje jméno souboru, který chcete vložit na místo, kde jste direktivu napsali. V příkladu výše, když MASM dojde na řádek include \masm32\include\windows.inc, otevře windows.inc, který je v \MASM32\include adresáři a začne zpracovávat obsah windows.inc jako kdyby jste jeho obsah přímo přepsali do našeho kódu. Hutchův windows.inc obsahuje definice konstant a struktur, které potřebujete v programování pod win32. Neobsahuje funkční prototypy. Windows.inc není v žádném případě kompletní. Hutch a já jsme se snažili vložit do něj co nejvíce konstant a struktur, ale ještě mnoho chybí. Bude průběžně vylepšován. Sledujte Hutchovi a moje stránky kvůli updatům.
Z windows.inc váš program obdrží konstanty a definice struktur. Pro funkční prototypy musíte zahrnout(include) další hlavičkové soubory. Všechny jsou v \masm32\include adresáři.

V našem příkladu, voláme funkci, která je v kernel32.dll, takže potřebujeme zahrnout funkčí prototypy z kernel32.dll. Ty jsou v kernel32.inc. Jestliže si ho otevřete v textovém editoru, uvidíte, že je plný funkčních prototypů pro kernel32.dll. Pakliže nezahrnete kernel32.inc, můžete i přesto ExitProcess zavolat, ale pouze pomocí CALL. Nebudete moci užít invoke. Pointa je jasná: aby jste mohli použít invoke, musíte vložit prototyp někam do kódu. V našem případě výše, jestliže nezahrnete kernel32.inc, můžete definovat prototyp pro ExitProcess kdekoliv v kódu a invoke bude také pracovat. Hlavičkové soubory existují proto, aby vám ušetřili práci s psaním vlastních prototypů, takže je užívejte kdykoliv můžete.
Nyní přišel čas na další direktivu, includelib. Includelib nepracuje jako include. Je to pouze způsob jak říci assembleru, jakou importní knihovnu program používá. Když assembler uvidí includelib direktivu, vloží linkovací příkaz do object souboru (*.obj) , takže linker pak pozná s jakými knihovnami má slinkovat váš program. Nejste nuceni tuto direktivu používat. Máte možnost určit jména knihoven v příkazové řádce linkeru, ale věřte mi, je to zdlouhavé a navíc příkazová řádka pobere pouze 128 znaků.

Teď uložte příklad pod jménem msgbox.asm. Za předpokladu, že ml.exe je ve vaší cestě, spusťte msgbox.asm s:

Po úspěšném sestavení(assembling) msgbox.asm, dostanete msgbox.obj. Msgbox.obj je objektový soubor, což je již jen jeden krůček k spustitelnému exe souboru. Obsahuje instrukce/data v binární formě. Chybí mu pouze některé úpravy adres, což dělá linker.

Tedy napište:

/SUBSYSTEM:WINDOWS  informuje linker, jaký druh spustitelného souboru je váš program
/LIBPATH:<cesta k importní knihovně> říká linkeru kde jsou importní knihovny. Jestliže požíváte MASM32, budou v MASM32\lib adresáři.
Linker čte .obj soubor a upravuje ho adresami z importních knihoven. Když skončí dostanete msgbox.exe.

Teď ho spusťte. Zjistíte, že nic nedělá :). Nu což, nedali jsem do něj ještě nic zajímavého. Ale přesto je to program pod Windows. A podívejte se na tu velikost! Na mém PC to je 1,536 bytů.

Teď zapojíme Message Box. Jeho funkční prototyp je:

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

hwnd je handle(rukojeť) k rodičovskému oknu. O handelu můžete přemýšlet jako o číslu, které reprezentuje okno na které poukazujete. Jeho hodnota není pro vás důležitá. Pouze si pamatujte, že představuje okno. Když budete chtít dělat cokoliv s oknem, budete na něj v programu poukazovat právě pomocí handelu.
lpText je ukazatel na text, který chcete zobrazit v klientské oblasti messageboxu. Ukazatel(pointer) je jen adresa něčeho. Ukazatel na textový řetězec=adresa toho řetězce.
lpCaption je ukazatel na nadpis messageboxu.
uType určuje ikonu a číslo a typ tlačítek(button) v messageboxu.
Pojďme tedy změnit msgbox.asm, aby obsahoval messagebox.
 

.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ův Tutorial 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

Zkompilujte a spusťte. Uvidíte messagebox zobrazující text "Win32 Assembly is Great!".

Pojďme prozkoumat kód.
Definujeme nulou ukončené řetězce (zero-terminated strings) v .data sekci. Pamatujte, každý ANSI řetězec ve Windows musí být ukončen pomocí NULL (0 hexadecimalně).
Použijeme dvě konstanty, NULL a MB_OK. Jsou ve windows.inc a můžete je užít na místo hodnot, což zlepšuje čitelnost kódu.
Addr operátor je užit k předání adresy labelu funkci. Je platný pouze v rámci invoke direktivy. Nemůžete ho například použít k předání adresy labelu registeru/proměnné. K tomu použijte offset namísto addr v našem příkladu. Jsou však mezi nimi rozdíly:

  1. addr nemůže zvládnout zpracovat forward(následně) deklarovaný např. label, zatímco offset může. Např. jestliže je label definován v kódu někde dále než na řádku s invoke, addr nebude fungovat.
  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 ohlásí chybu. Pokud použijete offset místo addr v předchozí části kódu, MASM si stěžovat již nebude.

  3. addr zvládá lokální proměnné, zatímco offset ne. Lokální proměnná je pouze rezervovaný prostor v zásobníku. Během běhu programu budete znát pouze její adresu. Offset je ale interpretován během sestavování přímo assemblerem. Takže přirozeně offset nebude s lokálními proměnnými pracovat. Addr je schopen pracovat s lokálními proměnnými díky skutečnosti, že assembler nejprve kontroluje, jestliže proměnná se kterou pracujete addr je globální nebo lokální. Jestliže globální, vloží přímo její adresu do .obj souboru. V tomto případě pracuje jako offset. Pakliže je ale proměnná lokální, vygeneruje sekvenci instrukcí jako je tato předtím než zavolá funkci:
  4. lea eax, LocalVar
    push eax


    Poněvadž lea může určit adresu labelu za běhu, bude to fungovat.


[Iczelion's Win32 Assembly HomePage]
Překlad Shaldan 2006, fx.s@seznam.cz - při četbě a využívání nových poznatků z četby opravdu za nic neručím :))