Aleš Keprt - Elektronická učebnice Assembleru
http://student.inf.upol.cz/keprt/vyuka/asm/asm1.aspx
Assembler - 1.část poslední změna této stránky: 9.2.2007 Zpět
Vítejte u první části učebních materiálů k Assembleru. Tyto učební texty vznikly na jaře roku 2000 jako doprovodný materiál ke cvičením. Původní texty nebyly určeny k samostudiu, ale jen jako doplňkový text k tomu, co se studenti dověděli na hodinách. Během let jsem Assembler několikrát střídavě učil a neučil v denním i kombinovaném studiu a tyto neustálé změny způsobily i postupné úpravy těchto Současná verze stále nemůže být pokládána za dostačující primární zdroj samostudia, nicméně alespoň je text stále aktualizován, aby reflektoval nové prvky a změny vždy v nejnovější verzi překladače Assembleru. Seznam doporučené studijní literatury (čili něco ke čtení :-) je uveden na hlavní stránce, dole pod nabídkou, ze které jste se zřejmě dostali na tuto stránku.
1. Úvodní seznámení s assemblerem Protože všichni by měli znát C++, nejsnazší cesta k assembleru je ukázat si rozdíly oproti C++. Hned na úvod je také potřeba si říci o nejzákladnější a největší a nejčastější chybě, kterou programátoři v Assembleru dělají: Píší podivné konstrukce, jakoby neznali "normální" styl programování v C++, Pascalu nebo jiném jazyku. Přitom v Assembleru byste měli 90% kódu napsat úplně stejným způsobem jako v jiném programovacím jazyku, pouze to má jinou syntaxi. :-) Pojďme tedy na rozdíly Assembleru oproti C++. Ten základní je už v samotném psaní příkazů. V assembleru se totiž píše každý příkaz na samostatný řádek. Každý takový příkaz má následující podobu: instrukce operand1,operand2. Čili na začátku řádku je příkaz, kterému se říká instrukce a za mezerou jsou parametry, zvané operandy. Počet operandů u příkazu je 0-3. 1. 2. 3. 4.
instrukce bez operandu: př.: cld instrukce s 1 operandem - operand se používá jako parametr i pro zápis výsledku: př.: inc eax instrukce se 2 operandy - oba operandy jsou parametry, výsledek se uloží do 1.operandu: add eax,1 instrukce se 3 operandy - 1.operand je vyhrazen pro výsledek, zbylé dva jsou parametry: imul eax,ebx,34
Nejjednodušší zápis assembleru je přímo do kódu C/C++. Většina překladačů C++ to umožňuje, ovšem ne všechny mají stejnou syntaxi. Zde je příklad, který si můžete vyzkoušet ve Visual C++:
1 z 13
19.2.2007 7:49
Aleš Keprt - Elektronická učebnice Assembleru
http://student.inf.upol.cz/keprt/vyuka/asm/asm1.aspx
void main() { _asm { imul eax,ebx,34 add eax,1 inc eax cld } }
Všimněte si červeně vyznačených řádků. Příkazy v assembleru (dále jen asm) se píší do deklarace _asm {...}. Nutno dodat, že výše uvedený program nemá žádný efekt - je to ukázka, jak se zapisují instrukce se třemi, dvěma, jedním a žádným operandem. V následující tabulce je srovnání konstrukcí a operací v C++ a assembleru.
2 z 13
C++
asm
operandy název instrukce
poznámka
=
mov
2
move
+=
add
2
add
-=
sub
2
subtract
*=
mul (u) imul (s)
1 1,2,3
multiply
u=unsigned s=signed
/=
div (u) idiv (s)
1 1
divide
u=unsigned s=signed
++
inc
1
increment
--
dec
1
decrement
>>=
shr (u) sar (s)
2 2
shift right u=unsigned shift arithmetic right s=signed
<<=
shl
2
shift left
&=
and
2
(bitwise) and
|=
or
2
(bitwise) or
^=
xor
2
(bitwise) xor
~=
not
1
(bitwise) not
-
neg
1
negate
změní znaménko čísla
19.2.2007 7:49
Aleš Keprt - Elektronická učebnice Assembleru
1
http://student.inf.upol.cz/keprt/vyuka/asm/asm1.aspx
goto
jmp
jump
return
ret
label:
@@label:
-
//
;
-
jednořádková poznámka
&
offset
-
reference (vrací pointer na proměnnou)
*
[]
-
dereference (vrací proměnnou, na kterou ukazuje pointer)
*(char*)& byte ptr
-
(tvrdé) přetypování na 1bajtovou hodnotu
*(short*)& word ptr
-
(tvrdé) přetypování na 2bajtovou hodnotu
*(long*)& dword ptr
-
(tvrdé) přetypování na 4bajtovou hodnotu
V tabulce nejsou podmíněné příkazy (if) a cykly (for, while, do). Důvodem je to, že v assembleru se tyto věci dělají úplně jinak než v C++.
Pozor! V jedné istrukci můžete pouze jedenkrát adresovat paměť. To znamená, že např. příkaz a=33; zapíšete normálně jako mov a,33, ale příkaz a=b; takto jednoduše napsat nejde. Byly by zde dvě proměnné, čili dvojí adresace paměti v jedné instrukci. Tyto případy jsou poměrně časté a vyřešíte je pomocí registrů (viz následující kapitola). Přitom dvojí přístup do paměti v jedné instrukci je možný, pokud nejde o dvojí adresaci. Např. příkaz a++; se zapíše jednoduše inc a. Tato instrukce je provedena ve třech krocích: 1. CPU přečte z paměti hodnotu proměnné a. 2. CPU zvýší hodnotu o jedničku. 3. CPU zapíše novou hodnotu proměnné a zpět do paměti. Pozn.: Tento příklad taky ukazuje, že inc a není atomická instrukce ve víceprocesorových systémech. Velká a malá písmena se v assembleru standardně nerozlišují, ale lze to zapnout. Pokud chcete kombinovat assembler s C++, bez rozlišování velkých a malých písmen by to ani nešlo. Proto překladače obvykle nabízejí možnost velká a malá písmena rozlišovat jen u exportovaných funkcí, zatímco interně je assembler nerozlišuje. Možné kombinace operandů při násobení a dělení: násobení unsigned: mul r/m8
3 z 13
ax=al*op1
dělení unsigned: div r/m8 al=ax/op1, ah=zbytek
19.2.2007 7:49
Aleš Keprt - Elektronická učebnice Assembleru
mul r/m16 mul r/m32
dx:ax=ax*op1 edx:eax=eax*op1
násobení signed: imul r/m8 ax=al*op1 imul r/m16 dx:ax=ax*op1 imul r/m32 edx:eax=eax*op1 imul reg16,r/m16 op1=op1*op2 imul reg32,r/m32 op1=op1*op2 imul reg16,imm8/16 op1=op1*op2 imul reg32,imm8/32 op1=op1*op2 imul reg16,r/m16,imm8/16 op1=op2*op3 imul reg32,r/m32,imm8/32 op1=op2*op3
http://student.inf.upol.cz/keprt/vyuka/asm/asm1.aspx
div r/m16 div r/m32 dělení signed: idiv r/m8 idiv r/m16 idiv r/m32
ax=dx:ax/op1, dx=zbytek eax=edx:eax/op1, edx=zbytek
al=ax/op1, ah=zbytek ax=dx:ax/op1, dx=zbytek eax=edx:eax/op1, edx=zbytek
před dělením: cbw - rozšíří al->ax cwd - rozšíří ax->dx:ax cdq - rozšíří eax->edx:eax cwde - rozšíří ax->eax
vysvětlivky: reg = registr s uvedeným počtem bitů mem = paměť (proměnná) s uvedeným počtem bitů r/m = registr nebo paměť s uvedeným počtem bitů imm = konstanta (immediate) s uvedeným počtem bitů
Pozor! Instrukce násobení se třemi operandy (poslední dva řádky této tabulky) vyžadují, aby třetím operandem byla přímá hodnota (tj. konstntna-číslo, ne proměnná, ne registr). Pokud tam všam napíšete něco neplatného, překladač ve Visual C++ nenapíše chybu. Program však sprvně fungovat nebude. (Je to chyba Visual C++.)
2. Registry Registry jsou paměťové buňky přímo v CPU. Práce s nimi je rychlejší a nemusí se při tom adresovat paměť. Tzn. příklad z předchozího odstavce (příkaz a=b; v C++) můžeme v assembleru napsat pomocí registru eax (je to jeden z mnoha registrů, klidně můžete použít i jiný - záleží především na typu proměnné). mov eax,b mov a,eax
A je to. ---
4 z 13
19.2.2007 7:49
Aleš Keprt - Elektronická učebnice Assembleru
http://student.inf.upol.cz/keprt/vyuka/asm/asm1.aspx
Ale registry je potřeba se naučit podrobně. Na přednášce struktury počítačů nebo operačních systémů byste měli probírat všechny podrobnosti. Zde se podívámě na to důležité. Základní aritmetické registry (použitelné pro počítání) jsou eax, ebx, ecx, edx. Adresovací registry jsou esi, edi, ebp, esp. Segmentové registry jsou cs, ds, es, fs, gs, ss. Registry s trojpísmenným jménem e.. jsou 32bitové. Segmenty jsou 16bitové a používají se většinou jen v 16bit programech (tj. v DOSu). Ve Widnows 95/NT (dále jen Win32) a Linuxu je 32bitový kód a segmenty nejsou většinou potřeba, protože paměť je podle potřeby operačním systémem mapována do virtuálního prostoru každého procesu. Podíváme se nejprve na registr eax, totéž platí pro ebx, ecx, edx. eax ax ah
al
Celý registr eax má 32 bitů (4 bajty). Registr ax odpovídá zápisu (short)eax v C++, je to tedy spodních 16 bitů z registru eax (dva bajty ze čtyř, tedy polovina). Horní polovinu eax takto adresovat nelze. Můžete však příkazem rol eax,16 přehodit horní a dolní polovinu registru. Dále můžete použít čtvrtinu, tj. 8 bitů (čili jeden bajt) pod názvem al. Druhý bajt (horní polovina ax) je přístupný jako ah. Adresovací registry esi, edi, ebp, esp fungují podobně, ovšem nemají 8bitové ekvivalenty. Tzn. jsou to 32bitové registry a mají 16bitové ekvivalenty si, di, bp, sp. Registr ebp má specifickou úlohu při adresování lokálních proměnných, takže ho nebudete ve svých programech nijak zvlášť měnit. Registr esp (stack pointer) ukazuje na vrchol programového zásobníku, takže jeho použití v programu je už prakticky úplně vyloučeno. Další dva jsou volně použitelné registry.
3. Programový zásobník Každý mikroprocesor používá při své činnosti zásobník. Zásobník (datová struktura typu LIFO) je umístěn někde v paměti a na jeho vrchol ukazuje registr esp. Zásobník vždy roste dolů, tj. přidáváním hodnot na zásobník se snižuje hodnota esp. Můžete použít tyto jednoduché instrukce: push op1 - uloží hodnotu op1 na zásobník (vždy 4 bajty) pop op1 - vytáhne hodnotu ze zásobníku a uloží do op1 (vždy 4 bajty) mov op1,[esp] - uloží do op1 hodnotu na vrcholu zásobníku (4 bajty), zásobník se nezmění Zásobník vždy pracuje po 4 bajtech (v 16bitovém režimu v DOSu po 2 bajtech).
Teď si můžeme ukázat malý příklad. Co program dělá, zkuste poznat sami. 5 z 13
19.2.2007 7:49
Aleš Keprt - Elektronická učebnice Assembleru
http://student.inf.upol.cz/keprt/vyuka/asm/asm1.aspx
#include <math.h> int pythagoras(int a,int b) { int result; _asm { mov eax,a ;eax=a imul eax,a ;eax*=a mov ebx,b ;ebx=b imul ebx,b ;ebx*=b add eax,ebx ;eax+=ebx mov result,eax ;result=eax } return (int)sqrt((double)result); }
Všechny instrukce mají dva operandy, použité registry jsou (32bitové) eax a ebx. Kvůli tomu, že jsme použili tyto dva registry, C++ je už nemůže použít pro sebe. Kdyby v nich něco na začátku bylo, náš asm program by to zrušil. Proto je vhodné program upravit následujícím způsobem, aby hodnoty registrů byly zachovány. #include <math.h> int pyth(int a,int b) { int result; _asm { push eax push ebx mov eax,a ;eax=a imul eax,a ;eax*=a mov ebx,b ;ebx=b imul ebx,b ;ebx*=b add eax,ebx ;eax+=ebx mov result,eax ;result=eax pop ebx pop eax } return (int)sqrt((double)result); }
Instrukce push a pop pracují se zásobníkem CPU a sdílejí tak paměť s voláním funkce. Pokud tedy zapomenete zavolat pop přesně odpovídající k předchozímu push, program se zhroutí (buďte rádi, že máte Windows NT a nespadne celý počítač...).
Pozor! Ve Visual C++ můžete libovolně používat registry eax, ebx, ecx, edx, esi, edi. Překladač sám přidá potřebné instrukce push a pop. V 6 z 13
19.2.2007 7:49
Aleš Keprt - Elektronická učebnice Assembleru
http://student.inf.upol.cz/keprt/vyuka/asm/asm1.aspx
Borland C++ 3.1 (DOS) můžete používat pouze 16bit registry ax, bx, cx, dx. Indexové registry můžete použít, vypnete-li optimalizace. Anebo prostě musíte použít push-pop. Registr ebp se VŽDY používá pro adresování lokálních proměnných, takže ten nemůžete použít nikde, kde jsou nějaké lokální proměnné. Nemáte-li lokální proměnné, můžete ebp použít, ale musíte jej chránit pomocí push-pop. Segmentové registry zatím na nic nepotřebujete, aspoň tedy dodám, že jejich použití je možné jen opatrně (musíte také použít push-pop).
4. Přímé adresování Přímé adresování je tam, kde jsou adresy známé přímo při překladu, tzn. při práci se statickými nebo globálními proměnnými. Přímé adresování je naprosto jasné a velmi se podobná C++. V asm máte ale ještě nějaké možnosti navíc. static int thevar,thevar2; _asm { mov eax,thevar ;eax=thevar mov eax,thevar+4 ;eax=thevar2 mov eax,[offset thevar+4] ;eax=thevar2 }
Z příkladu je vidět, že názvy proměnných se v asm vyhodnocují vždy jako adresy (jakoby pointery), se kterými ještě můžete provádět základní aritmetické operace. Příklad počítá s tím, že proměnné thevar a thevar2 jsou v paměti bezprostředně za sebou. Proměnné jsou deklarovány static, protože s lokálními proměnnými ještě pracovat neumíme.
5. Nepřímé adresování Jednou ze základních konstrukcí C++ jsou pointery, bez kterých byste mnohé věci ani nemohli napsat. V assembleru tomu odpovídá nepřímé adresování. Zatímco u přímého adresování známe přesnou adresu proměnné již při překladu, u nepřímého adresování máme jen pointer, přesněji řečeno známe vzdálenost proměnné od nějakého pevného bodu. Přímé adresování paměti je tedy práce s běžnými proměnnými. mov eax,thevar mov eax,[thevar]
7 z 13
;přímé adresování nějaké globální proměnné jménem thevar ;totéž jako předchozí řádek
19.2.2007 7:49
Aleš Keprt - Elektronická učebnice Assembleru
http://student.inf.upol.cz/keprt/vyuka/asm/asm1.aspx
Pokud je thevar běžná globální proměnná typu int, pak oba tyto příkazy dělají totéž, čili do eax přiřadí hodnotu thevar. Proč tam jsou ty hranaté závorky: Pomůckou k pochopení, proč se název proměnné píše takto do hranatých závorek, může být představa paměti počítače jako jednoho velkého pole, do kterého přistupujeme pomocí těchto hranatých závorek stejně, jak to děláme ve vyšších programovacích jazycích při práci s poli. Toto srovnání můžete chápat doslova. Za "přímé adresování" pak označujeme situaci, kdy v době překladu programu víme, kde přesně je ona "proměnná" thevar v paměti. To u globální proměnné thevar víme vždy. Do oněch hranatých závorek se tedy při překladu dosadí nějaké číslo, které udává doslova "kolikátý bajt od začátku paměti" chceme načíst do registru eax. Za "nepřímé adresování" označujeme situaci, kdy v době překladu programu ještě přesnou pozici oné "proměnné" nevíme. To platí například pro situace, kdy k datům přistupujeme přes pointery. Mějme tedy nějakou globální proměnnou typu pointer jménem thepointer a zkusme následující kód. mov ebx,thepointer mov esi,2 mov al,[ebx+esi]
;ebx=thepointer ;esi=2 ;ax=*(bx+si)=*(thepointer+2)=thepointer[2]
mov ah,thepointer[esi] ;toto nefunguje!
Tato pro začátečníky méně jasná konstrukce je příkladem nepřímého adresování. Nejprve si načteme pointer do registru ebx - toto je opět přímé adresování. Jenže místo hodnoty proměnné, na kterou thepointer ukazuje, máme v ebx pouze její adresu. Do registru esi dáme číslo 2. Potom načteme do al hodnotu na adrese ebx+esi, což odpovídá práci s pointerem nebo prostě čtení druhého prvku pole charů, na které ukazuje thepointer. Ano, první řádky výše uvedeného kódu odpovídají tomu, když ve vyšším jazyku máme pointer na pole charů a přečteme z něj buňku pomocí thepointer[2]. Poslední řádek ukazuje alternativní možnost, kdy adresa pointeru není načtena do ebx. Tento příkaz je neplatný, protože má dvě adresování paměti. Nejprve požaduje načíst obsah thepointer a potom ještě obsah vypočítané adresy thepointer+esi. Toto je tedy příklad dvojí adresace paměti v jedné instrukci, o kterém již víme, že prostě není možná.
Instrukce lea lea reg,address "load effective address" Instrukce lea vypočítá efektivní adresu a její hodnotu uloží do nějakého registru. Čili třetí řádek posledního příkladu můžete nahradit dvojicí:
8 z 13
19.2.2007 7:49
Aleš Keprt - Elektronická učebnice Assembleru
http://student.inf.upol.cz/keprt/vyuka/asm/asm1.aspx
lea ebx,[ebx+esi] ;base=ebx, index=esi, scale=1 mov al,[ebx] ;base=ebx
V uvedeném příkladě to samozřejmě nemá smysl, jelikož by stačilo napsat add ebx,esi, ale při složitějším adresování může být použití lea výhodné. Podíváme se tedy na to, co všechno lze v adresování využít. Obecný tvar nepřímé adresy je base+(index*scale)+displacement kde scale je konstanta (může být pouze 1, 2, 4, 8). Jednotlivé položky adresy jsou pochopitelně nepovinné, viz. poznámky u výše uvedeného příkladu.
Pozor! V 16bitovém řežimu (v DOSu) máte ovšem daleko horší situaci: bx+index+displacement, kde index může být jedině si nebo di. Všechny položky jsou pochopitelně nepovinné. Instrukci lea můžete také použít pro jednoduché násobení. Takové násobení je nejrychlejší možné rychlé (1 takt). Násobit můžete 2x, 3x, 4x, 5x, 8x, 9x. lea eax,[eax+8*eax]
;eax=9*eax (unsigned)
Při práci s nepřímými adresami musíte dávat pozor na datové typy. V C++ takové problémy nejsou: long pole[20]; long *pointer=pole+12; _asm { lea ebx,[pole+12] mov eax,[ebx] }
//pointer na prvek pole[12] //ebx=pointer na prvek pole[3] = 12 bajtů za začátkem pole //eax=pole[3]
Co z toho plyne? Při nepřímém adresování pokud možno využívejte násobení konstantami vždy, když typ prvku pole je short nebo long. long pole[20]; _asm { mov esi,3 lea ebx,[pole+esi*4] lea ebx,[pole+3*4] mov eax,[ebx] }
//ebx=pointer na prvek pole[3] = 12 bajtů za začátkem pole //totéž //eax=pole[3]
Na obrázku vidíte oficiální popis výpočtu efektivní adresy: offset=base+(index*scale)+displacement. Uvedené hodnoty scale 1,2,3,4 jsou 9 z 13
19.2.2007 7:49
Aleš Keprt - Elektronická učebnice Assembleru
http://student.inf.upol.cz/keprt/vyuka/asm/asm1.aspx
bohužel chybné - správně má být 1,2,4,8 (jak vidět i mistr tesař se někdy utne - obrázek je z "Intel Architecture Software Developer's Manual").
Další obrázek ukazuje seznam datových typů podporovaných procesory x86 a jejich umístění v paměti (tzv. LSB = lowest significat byte first důležité u vícebajtových proměnných). Proměnné mohou začínat na libovolné adrese, ovšem pokud budou patřičně zarovnány, vykonávaný program se tím výrazně zrychlí. Všechny tyto vlastnosti se 100% shodují s chováním C++ a dalších vyšších programovacích jazyků, proto nebudu zacházet do podrobností.
10 z 13
19.2.2007 7:49
Aleš Keprt - Elektronická učebnice Assembleru
http://student.inf.upol.cz/keprt/vyuka/asm/asm1.aspx
Poznámka: Ačkoliv procesory Pentium a novější jsou hardwarově 64bitové, pouze malá část instrukční sady je skutečně 64bitová a většina instrukcí pracuje se 32 nebo méně bity. Tento zdánlivý paradox je způsoben tím, že činnost RISC jádra je nezávislá na činnosti 64bitové paměťové jednotky (na obrázku jako "bus interface unit"). Viz následující velmi stručný diagram:
11 z 13
19.2.2007 7:49
Aleš Keprt - Elektronická učebnice Assembleru
http://student.inf.upol.cz/keprt/vyuka/asm/asm1.aspx
6. Disassembler Vaše dotazy často zodpoví disassembler. Disassembler je opak assembleru - je to program, který převádí binární strojový kód do symbolického tvaru v assembleru.
12 z 13
19.2.2007 7:49
Aleš Keprt - Elektronická učebnice Assembleru
http://student.inf.upol.cz/keprt/vyuka/asm/asm1.aspx
Napište příkaz ve Visual C++, potom ho spusťte pomocí F10 a stiskem Ctrl+Alt+D zobrazte dissassembly window (dříve Alt+8). Můžete se tak například podívat, jak se pracuje s poli. Uvědomte si ale, že pokud nekompilujete váš program s optimalizacemi (release build), tak často uvidíte poměrně krkolomný kód, který byste správně v assembleru nikdy neměli napsat (dlouhý a pomalý).
7. Inline a external assembler Inline assembler je ten, který píšete do souborů C++ (nebo jiných vyšších jazyků). External assembler je ten, který je v samostatných souborech s příponou ASM. Zatím se budeme věnovat pouze inline assembleru, protože se tím výhodně vyhneme některým potížím. Ukázka malého dema v assembleru je tady (100 řádků) a zkompilovaný program je tady (200 bajtů). Je to program pro MS-DOS, takže ve Windows nemusí běžet stoprocentně dobře. Zpět
13 z 13
19.2.2007 7:49