Ve čtvrté části článku o použití assembleru (nejenom) v Linuxu se budeme zabývat technikami využívanými pro tvorbu podmínek a taktéž různých typů programových smyček v assembleru, což je problematika, která na většině v současnosti používaných mikroprocesorových architektur vyžaduje práci s takzvanými příznaky (či příznakovými bity).
Obsah
1. Podmíněné a nepodmíněné skoky jako základ pro realizaci rozvětvení a programových smyček
2. Strojové instrukce určené pro provedení skoku na architekturách i386 a x86_64
3. Základní příznakové bity na architekturách i386 a x86_64
4. Vybrané instrukce pro podmíněné skoky založené na testování příznakových bitů
5. Použití instrukcí DEC a JZN pro implementaci počítané programové smyčky
6. Odlišná realizace počítané smyčky s instrukcemi DEC, JZ a JMP
7. Praktický příklad – jednoduchá počítaná programová smyčka
8. Další praktický příklad – počítaná programová smyčka s testem na začátku
9. Použití instrukce CMP v součinnosti s podmíněným skokem
10. Praktický příklad – počítaná smyčka s volitelným začátkem a koncem
11. Repositář s demonstračními příklady
1. Podmíněné a nepodmíněné skoky jako základ pro realizaci rozvětvení a programových smyček
Velmi důležitým typem strojových instrukcí, které v různé podobě najdeme prakticky u všech modelů mikroprocesorů (resp. přesněji řečeno u mikroprocesorů všech dnes rozšířených mikroprocesorových architektur), jsou instrukce provádějící skoky na nějakou adresu v operační paměti. Implementace skoku není, alespoň na první pohled a u jednodušších architektur bez instrukční pipeline, vlastně nijak složitá, protože se v případě použití absolutní adresy dosadí hodnota z operačního kódu instrukce do registru PC a v případě použití relativní adresy se tato hodnota (nazývaná někdy poněkud nepřesně offset) přičte k aktuální hodnotě registru PC. Relativní adresa je v tomto případě v kódu instrukce uložena se znaménkem, proto se skok může provést dozadu i dopředu (ostatně právě použití relativní adresy uvidíme v dále popisovaných demonstračních příkladech).
Skoky většinou dělíme podle jednoho kritéria (formy zápisu adresy) na absolutní a relativní a podle kritéria druhého (za jakým okolností se skok provede) na skoky podmíněné a nepodmíněné. V závislosti na použité instrukční sadě jsou možné různé kombinace, typicky však u většiny mikroprocesorů nalezneme kombinace nepodmíněný absolutní skok, nepodmíněný relativní skok a podmíněný relativní skok. Skoky nepodmíněné jsou jednodušší a svou podstatou odpovídají příkazu goto známého z některých programovacích jazyků a také z mnoha článků, ve kterých autoři mnohdy bez hlubšího zamyšlení se nad původní myšlenkou opakují, že by se goto nemělo při strukturovaném programování používat :-). V assembleru se však skoky vesele používají, neboť právě pomocí nich se vytváří základní konstrukce strukturovaného programování – podmínky a programové smyčky.
2. Strojové instrukce určené pro provedení skoku na architekturách i386 a x86_64
U architektury mikroprocesorů 32bitové řady i386 a taktéž 64bitové řady x86-64 je základní strojovou instrukcí určenou pro provedení nepodmíněného skoku instrukce nazvaná jednoduše a přímočaře JMP (což je, jak jste zajisté zjistili, mnemotechnická zkratka slova jump). V assembleru většinou za mnemotechnickou zkratkou jména instrukce následuje návěští (label), z něhož assembler odvodí reálnou adresu. Ukažme si typický špagetový kód vznikající nadbytečným použitím této instrukce:
# asmsyntax=as # Ukazka spagetoveho kodu s instrukci typu JMP # - pouzita je "Intel" syntaxe. # # Autor: Pavel Tisnovsky .intel_syntax noprefix # Linux kernel system call table sys_exit=1 #----------------------------------------------------------------------------- .section .data #----------------------------------------------------------------------------- .section .bss #----------------------------------------------------------------------------- .section .text .global _start # tento symbol ma byt dostupny i linkeru _start: jmp l1 l4: mov eax, sys_exit # cislo sycallu pro funkci "exit" mov ebx, 0 # exit code = 0 int 0x80 # volani Linuxoveho kernelu l2: jmp l3 l1: jmp l2 l3: jmp l4
Překlad vypadá takto (skoky jsou zde relativní a tudíž velmi krátké – celá instrukce má délku pouhé dva bajty):
0000000000400078 <.text> 400078: eb 0e jmp 0x400088 ; relativní skok vpřed 40007a: b8 01 00 00 00 mov eax,0x1 40007f: bb 00 00 00 00 mov ebx,0x0 400084: cd 80 int 0x80 400086: eb 02 jmp 0x40008a ; relativní skok o 3 adresy dál 400088: eb fc jmp 0x400086 ; relativní skok zpět (druhá hodnota je se znaménkem) 40008a: eb ee jmp 0x40007a ; relativní skok zpět (druhá hodnota je se znaménkem)
3. Základní příznakové bity na architekturách i386 a x86_64
Alternativně je možné použít i další způsoby adresování, čímž se například implementuje tabulka skoků (jedna z možných realizací stavového automatu) atd., ovšem tyto techniky pro účely dnešního článku prozatím nepotřebujeme znát. Mnohem zajímavější jsou podmíněné skoky, které se při programování v assembleru či ve strojovém kódu používají pro implementaci programových smyček while, do-while, for a taktéž konstrukcí typu if-then-else atd. Podmíněný skok je proveden či naopak neproveden na základě nějaké podmínky. Vzhledem k tomu, že pracujeme na té nejnižší programové úrovni, tj. na úrovni strojových instrukcí, není samozřejmě možné podmínku definovat nějakým složitým a sofistikovaným způsobem – musí se jednat o operaci, kterou mikroprocesor dokáže jednoduše a především dostatečně rychle zpracovat (i přesto představují skoky úzké místo v programech).
Z tohoto prostého důvodu – podmínky musí být realizovány dostatečně jednoduchým způsobem pro snadnou implementaci na čipu – jsou na mikroprocesorových architekturách i386 a x86-64 podmínky založeny na testování jednoho z takzvaných příznakových bitů, negací těchto bitů či dokonce jejich kombinací. Pokud z důvodu zjednodušení výkladu celé relativně rozsáhlé problematiky budeme ignorovat některé speciálnější příznaky a především pak rozdíly mezi hodnotami bez znaménka (unsigned) a se znaménkem (signed), můžeme zpočátku použít především příznaky nazvané Carry flag, Sign flag a Zero flag, tj. příznak přenosu, příznak záporného výsledku a příznak nulovosti. Význam těchto příznakových bitů se shrnut v následující tabulce:
Příznak | Význam zkratky | Poznámka |
---|---|---|
ZF | zero flag | výsledek předchozí operace je nulový |
CF | carry flag | přenos (bezznaménková aritmetika) |
SF | sign flag | výsledek je záporný (nastaven nejvyšší bit bajtu či slova) |
V případě, že alespoň prozatím nebudeme brát v úvahu další příznakové bity, existuje šest základních variant podmíněného skoku, které jsou vypsány v tabulce v navazující kapitole. Ve chvíli, kdy podmínka není splněna (tj. testovaný příznakový bit má opačnou hodnotu než očekávanou), není skok proveden, tj. mikroprocesor instrukci skoku v podstatě ignoruje a pokračuje v načtení instrukce uvedené ihned za skokem (to, že mikroprocesor instrukci skoku ignoruje samozřejmě platí jen z pohledu logiky vytvářeného programu; samotné provedení instrukce muselo proběhnout a tudíž se program o několik strojových taktů pozdržel – i neprovedení podmíněného kroku si tedy vyžádalo svou cenu).
4. Vybrané instrukce pro podmíněné skoky založené na testování příznakových bitů
Strojové instrukce určené pro provedení podmíněných skoků jsou ve své základní variantě (existují pro ně totiž i jmenné aliasy) pojmenovány jednoduše a přímočaře – začínají písmenem J (jump), za nímž následuje volitelné písmeno N (negace) a jednoznaková zkratka příznaku. Instrukce JNC tedy znamená „proveď skok, pokud příznak Carry není nastaven“, zatímco instrukce JZ znamená „proveď skok pouze při nastavení příznaku Zero“:
Mnemotechnická zkratka instrukce | Význam instrukce podmíněného skoku |
---|---|
JC | podmíněný skok za předpokladu, že je nastaven příznak přenosu (Carry flag) |
JNC | podmíněný skok za předpokladu, že je vynulován příznak přenosu (Carry flag) |
JZ | podmíněný skok za předpokladu, že je nastaven příznak nulovosti (Zero flag) |
JNZ | podmíněný skok za předpokladu, že je vynulován příznak nulovosti (Zero flag) |
JS | podmíněný skok za předpokladu, že je nastaven příznak záporného výsledku (Sign flag) |
JNS | podmíněný skok za předpokladu, že je vynulován příznak záporného výsledku (Sign flag) |
Jmenné aliasy:
Instrukce | Alias |
---|---|
JZ | JE |
JNZ | JNE |
JC | JB, JNAE |
JNC | JNB, JAE |
JS | nemá alias |
JNS | nemá alias |
5. Použití instrukcí DEC a JZN pro implementaci počítané programové smyčky
V předchozí kapitole jsme si řekli, že podmíněné relativní skoky jsou použity pro implementaci programových smyček a podmínek. K tomuto účelu se podmíněné skoky prakticky vždy vhodně kombinují s některou instrukcí, která modifikuje jeden příznakový bit či dokonce větší množství příznakových bitů. Například jednoduchou počítanou programovou smyčku by bylo možné implementovat s využitím kombinace strojových instrukcí DEC a JNZ. Instrukce nazvaná DEC snižuje obsah specifikovaného pracovního registru o jedničku a současně nastavuje příznak nuly, tj. Zero flag (ZF), samozřejmě ovšem pouze za předpokladu, že ten pracovní registr, jehož hodnota se snižuje, skutečně dosáhl nulové hodnoty. Instrukce JNZ znamená podmíněný skok, který je proveden pouze tehdy, pokud příznak nuly není nastaven („jump if not zero“, viz též předchozí kapitolu).
Před samotným začátkem programové smyčky je samozřejmě nutné do pracovního registru nastavit potřebný počet opakování, který by měl být ideálně odlišný od nuly. Ostatně zkuste si sami odpovědět na otázku, co by se stalo v případě, kdy by pracovní registr použitý jako počitadlo, byl před vstupem do smyčky vynulovaný. Podívejme se nyní na způsob použití 32bitového registru EAX ve funkci počitadla. Vzhledem k tomu, že se testuje pouze nulovost registru po snížení jeho hodnoty o jedničku, může tato smyčka být provedena 0× až 232-1× (ignoruje se znaménko):
MOV EAX, počet_opakování ; počet opakování smyčky SMYCKA: příkaz 1 ; libovolné instrukce, jejichž celková příkaz 2 ; celková délka bloku musí být menší než cca 120 bytů ... ; (kvůli omezení relativního skoku, pokud se délka překročí, použije se delší instrukce) ... ... příkaz X DEC EAX ; snížení čítače smyčky o jedničku JNZ SMYCKA ; přeloží se jako relativní skok
6. Odlišná realizace počítané smyčky s instrukcemi DEC, JZ a JMP
Podmínku ovšem můžeme naopak přesunout i na samotný začátek smyčky. Nyní se ovšem namísto instrukce pro podmíněný skok JNZ použije instrukce JZ v kombinaci s instrukcí nepodmíněného skoku JMP:
MOV EAX, počet_opakování ; počet opakování smyčky SMYCKA: DEC EAX ; snížení čítače smyčky o jedničku JZ KONEC ; přeloží se jako relativní skok příkaz 1 ; libovolné instrukce, jejichž celková příkaz 2 ; celková délka bloku musí být menší než cca 120 bytů ... ; (kvůli omezení relativního skoku, pokud se délka překročí, použije se delší instrukce) ... ... příkaz X JMP SMYCKA ; nepodmíněný skok na začátek smyčky
7. Praktický příklad – jednoduchá počítaná programová smyčka
Podívejme se nyní na způsob realizace velmi jednoduché počítané programové smyčky naprogramované v assembleru mikroprocesorů s architekturou i386 a x86_64 (používají se však pouze 32bitové registry). V tomto příkladu je dynamicky vytvořen buffer pro čtyřicet ASCII znaků. Do tohoto bufferu se v programové smyčce následně vypíše čtyřicet hvězdiček. Počitadlo smyčky je uloženo v 32bitovém pracovním registru EBX, adresa začátku bufferu v registru ECX a zapisovaný znak v osmibitovém pracovním registru AL (součást registru EAX). Řetězec čtyřiceti znaků je následně vypsán na standardní výstup nám již důvěrně známou funkcí sys_write:
# asmsyntax=as # Testovaci program naprogramovany v assembleru GNU as # - pocitana programova smycka # - pouzita je "Intel" syntaxe. # # Autor: Pavel Tisnovsky .intel_syntax noprefix # Linux kernel system call table sys_exit=1 sys_write=4 # pocet opakovani znaku rep_count=40 #----------------------------------------------------------------------------- .section .data #----------------------------------------------------------------------------- .section .bss .lcomm buffer, rep_count # rezervace bufferu pro vystup #----------------------------------------------------------------------------- .section .text .global _start # tento symbol ma byt dostupny i linkeru _start: mov ecx, offset buffer # zapis se bude provadet do tohoto bufferu mov ebx, rep_count # pocet opakovani znaku mov al, '*' # zapisovany znak loop: mov [ecx], al # zapis znaku do bufferu inc ecx # uprava ukazatele do bufferu dec ebx # zmenseni pocitadla jnz loop # pokud jsme se nedostali k nule, skok na zacatek smycky mov eax, sys_write # cislo syscallu pro funkci "write" mov ebx, 1 # standardni vystup mov ecx, offset buffer # adresa retezce, ktery se ma vytisknout mov edx, rep_count # pocet znaku, ktere se maji vytisknout int 0x80 # volani Linuxoveho kernelu mov eax, sys_exit # cislo sycallu pro funkci "exit" mov ebx, 0 # exit code = 0 int 0x80 # volani Linuxoveho kernelu
Překlad se provede takto:
as loop1.s -o loop1.o ld -s loop1.o
Zajímavé bude se podívat na způsob překladu aplikace a na realizaci podmíněného skoku:
objdump -M intel-mnemonic -f -d -t -h a.out
a.out: file format elf64-x86-64 architecture: i386:x86-64, flags 0x00000102: EXEC_P, D_PAGED start address 0x00000000004000b0 Sections: Idx Name Size VMA LMA File off Algn 0 .text 00000037 00000000004000b0 00000000004000b0 000000b0 2**0 CONTENTS, ALLOC, LOAD, READONLY, CODE 1 .bss 00000030 00000000006000e8 00000000006000e8 000000e8 2**3 ALLOC SYMBOL TABLE: no symbols Disassembly of section .text: 00000000004000b0 <.text>: 4000b0: b9 e8 00 60 00 mov ecx,0x6000e8 4000b5: bb 28 00 00 00 mov ebx,0x28 4000ba: b0 2a mov al,0x2a 4000bc: 67 88 01 mov BYTE PTR [ecx],al 4000bf: ff c1 inc ecx 4000c1: ff cb dec ebx 4000c3: 75 f7 jne 0x4000bc 4000c5: b8 04 00 00 00 mov eax,0x4 4000ca: bb 01 00 00 00 mov ebx,0x1 4000cf: b9 e8 00 60 00 mov ecx,0x6000e8 4000d4: ba 28 00 00 00 mov edx,0x28 4000d9: cd 80 int 0x80 4000db: b8 01 00 00 00 mov eax,0x1 4000e0: bb 00 00 00 00 mov ebx,0x0 4000e5: cd 80 int 0x80
Poznámky:
- Podmíněný skok ve zdrojovém kódu realizujeme instrukcí JNZ, která se ve výpisu zobrazuje jako JNE, což je ovšem jen jmenný alias.
- Strojová instrukce pro podmíněný skok má délku pouhé dva bajty: 75 f7. První bajt je operační kód, druhý bajt pak číslo se znaménkem, které je větší než 0x7f, což naznačuje skok zpět.
- Pokud by tělo smyčky překročilo přibližně 120 bajtů, byla by instrukce pro skok delší (některé assemblery ovšem vypíšou varování či dokonce chybu a vyžadují explicitní použití „dlouhého“ skoku).
8. Další praktický příklad – počítaná programová smyčka s testem na začátku
V dalším demonstračním příkladu je ukázána počítaná programová smyčka, v níž se test na ukončení provádí na jejím začátku po odečtení jedničky od počitadla. Tento příklad je vlastně v mnoha ohledech totožný s příkladem předchozím, ovšem vzhledem k tomu, že změna stavu počitadla (snížení jeho hodnoty o jedničku) a následný test je proveden na začátku smyčky, je nutné při inicializaci počitadla do něj vložit hodnotu 41 a nikoli 40, jinak by se vytisklo pouze 39 hvězdiček následovaných ASCII znakem s kódem nula:
# asmsyntax=as # Testovaci program naprogramovany v assembleru GNU as # - pocitana programova smycka s testem na zacatku # - pouzita je "Intel" syntaxe. # # Autor: Pavel Tisnovsky .intel_syntax noprefix # Linux kernel system call table sys_exit=1 sys_write=4 # pocet opakovani znaku rep_count=40 #----------------------------------------------------------------------------- .section .data #----------------------------------------------------------------------------- .section .bss .lcomm buffer, rep_count # rezervace bufferu pro vystup #----------------------------------------------------------------------------- .section .text .global _start # tento symbol ma byt dostupny i linkeru _start: mov ecx, offset buffer # zapis se bude provadet do tohoto bufferu mov ebx, rep_count+1 # pocet opakovani znaku+1 (protoze se dec+test provadi na zacatku) mov al, '*' # zapisovany znak loop: dec ebx # zmenseni pocitadla jz konec # pokud jsme se dostali k nule, konec smycky mov [ecx], al # zapis znaku do bufferu inc ecx # uprava ukazatele do bufferu jmp loop # nepodmineny skok na zacatek smycky konec: mov eax, sys_write # cislo syscallu pro funkci "write" mov ebx, 1 # standardni vystup mov ecx, offset buffer # adresa retezce, ktery se ma vytisknout mov edx, rep_count # pocet znaku, ktere se maji vytisknout int 0x80 # volani Linuxoveho kernelu mov eax, sys_exit # cislo sycallu pro funkci "exit" mov ebx, 0 # exit code = 0 int 0x80 # volani Linuxoveho kernelu
Překlad se provede takto:
as loop2.s -o loop2.o ld -s loop2.o
Opět se podívejme na zpětný překlad (disassembling), aby bylo patrné, jakým způsobem se přeloží podmíněný i nepodmíněný skok:
objdump -M intel-mnemonic -f -d -t -h a.out
a.out: file format elf64-x86-64 architecture: i386:x86-64, flags 0x00000102: EXEC_P, D_PAGED start address 0x00000000004000b0 Sections: Idx Name Size VMA LMA File off Algn 0 .text 00000039 00000000004000b0 00000000004000b0 000000b0 2**0 CONTENTS, ALLOC, LOAD, READONLY, CODE 1 .bss 00000030 00000000006000f0 00000000006000f0 000000f0 2**3 ALLOC SYMBOL TABLE: no symbols Disassembly of section .text: 00000000004000b0 <.text>: 4000b0: b9 f0 00 60 00 mov ecx,0x6000f0 4000b5: bb 29 00 00 00 mov ebx,0x29 4000ba: b0 2a mov al,0x2a 4000bc: ff cb dec ebx 4000be: 74 07 je 0x4000c7 4000c0: 67 88 01 mov BYTE PTR [ecx],al 4000c3: ff c1 inc ecx 4000c5: eb f5 jmp 0x4000bc 4000c7: b8 04 00 00 00 mov eax,0x4 4000cc: bb 01 00 00 00 mov ebx,0x1 4000d1: b9 f0 00 60 00 mov ecx,0x6000f0 4000d6: ba 28 00 00 00 mov edx,0x28 4000db: cd 80 int 0x80 4000dd: b8 01 00 00 00 mov eax,0x1 4000e2: bb 00 00 00 00 mov ebx,0x0 4000e7: cd 80 int 0x80
Poznámky:
- Podmíněný skok ve zdrojovém kódu realizujeme instrukcí JZ, která se ve výpisu zobrazuje jako JE, což je opět pouze jmenný alias.
- Strojová instrukce pro podmíněný skok má znovu délku pouhé dva bajty a z druhého bajtu (0x07) vidíme, že je proveden skok vpřed (o osm bajtů počítáno od začátku skoku).
- Pokud by tělo smyčky překročilo přibližně 120 bajtů, byla by instrukce pro skok delší (některé assemblery ovšem vypíšou varování či dokonce chybu a vyžadují explicitní použití „dlouhého“ skoku).
- Nepodmíněný skok je taktéž realizován s použitím relativní adresy, takže délka této instrukce je pouhé dva bajty. Druhý bajt instrukce je větší než 0x7f, takže se jedná o skok zpět.
9. Použití instrukce CMP v součinnosti s podmíněným skokem
Společně s podmíněnými skoky se velmi často používá doposud nepopsaná instrukce nazvaná CMP (mnemotechnická zkratka od slova compare). V podstatě se jedná o běžné celočíselné odečítání, tj. o instrukci nazvanou SUB, ovšem s tím důležitým rozdílem, že výsledek, tj. samotný rozdíl, není nikam zapsán, což znamená, že se obsah běžných pracovních registrů provedením této instrukce nezmění. Na první pohled může vypadat tato instrukce nesmyslně – proč se vůbec mají odečítat dvě hodnoty, když se výsledek hned zapomene? Ovšem samotný výsledek není ve skutečnosti vše, protože při odečítání si mikroprocesor v příznakových registrech zapamatuje i to, zda byla odečítaná čísla shodná (tehdy se nastaví Zero flag na jedničku) či zda byla druhá hodnota větší než hodnota první (příznak Carry flag bude v tomto případě roven jedné).
Instrukce CMP akceptuje různé typy operandů; může se jednat o běžné pracovní registry, ovšem použít lze i konstanty či obsah získaný ze zvolené adresy operační paměti. V následující tabulce si ukážeme, jaké příznaky se nastaví při provedení různých variant funkce CMP:
První operand | Druhý operand | Zero flag | Carry flag | Význam |
---|---|---|---|---|
00 | 00 | 1 | 0 | obě hodnoty jsou shodné |
42 | 42 | 1 | 0 | obě hodnoty jsou shodné |
20 | 10 | 0 | 0 | první hodnota je větší (nedošlo k přenosu) |
10 | 20 | 0 | 1 | druhá hodnota je větší (došlo k přenosu) |
Poznámka: ve skutečnosti by v tomto případě (odečítání a porovnávání) bylo korektnější mluvit o takzvané výpůjčce (borrow) a nikoli o přenosu (carry).
Z těchto příkladů současně nepřímo vyplývá i to, jak můžeme příznakové bity použít. Jestliže je zapotřebí testovat dvě hodnoty na rovnost, postačí zjistit, zda je Zero flag nastavený na jedničku (ostatně i z tohoto důvodu má podmíněný skok JZ alias JE – jump if equal). Pokud potřebujeme otestovat, jestli je první hodnota menší než druhá, lze zjistit hodnotu příznaku Carry flag atd. Test na nulovou hodnotu lze provést odečtením nuly – výsledek bude uložen v Zero flagu (ve skutečnosti se ovšem tento test většinou neprovádí, protože i některé další instrukce dokážou nastavit tento příznak automaticky a vlastně „mimochodem“). Podobně je tomu u testování, zda je hodnota kladná či záporná. Podívejme se na několik možností použití:
; test na rovnost dvou hodnot MOV EAX, hodnota 1 MOV EBX, hodnota 2 CMP EAX, EBX JZ JE_ROVNO ; skok na kód provedený v případě rovnosti JNZ NENI_ROVNO ; skok na kód provedený v případě nerovnosti
; zjištění relace dvou čísel MOV EAX, hodnota 1 MOV EBX, hodnota 2 CMP EAX, EBX JNC EAX_JE_VETSI_NEBO_ROVNO_EBX JC EAX_JE_MENSI_NEZ_EBX
; test na nulovost MOV EAX, hodnota SUB EBX,EBX ; vynulování registru EBX (lze i XOR EBX,EBX) CMP EAX,EBX JZ EAX_JE_NULOVE
; test na nulovost MOV EAX, hodnota CMP EAX,0 JZ EAX_JE_NULOVE
; test na kladnou či zápornou hodnou MOV EAX, hodnota CMP EAX,0 JS EAX_JE_ZAPORNE
10. Praktický příklad – počítaná smyčka s volitelným začátkem a koncem
Dnešní poslední demonstrační příklad opět ukazuje realizaci počítané programové smyčky, tentokrát se ovšem ve smyčce vytváří řetězec obsahující znaky od 'a' do 'z'. Pro test na ukončení smyčky se používá kombinace instrukcí CMP+JNZ pro porovnání, zda aktuálně zapisovaný znak nepřekročil hodnotu 'z' (v předchozích příkladech se prováděl test na nulu). Povšimněte si, že se ve zdrojovém kódu mohou zapisovat i složitější konstantní výrazy typu 'z'+1 či dokonce 'z'-'a'+1:
# asmsyntax=as # Testovaci program naprogramovany v assembleru GNU as # - smycka vyuzivajici instrukci CMP # - pouzita je "Intel" syntaxe. # # Autor: Pavel Tisnovsky .intel_syntax noprefix # Linux kernel system call table sys_exit=1 sys_write=4 # pocet opakovani znaku rep_count='z'-'a'+1 #----------------------------------------------------------------------------- .section .data #----------------------------------------------------------------------------- .section .bss .lcomm buffer, rep_count # rezervace bufferu pro vystup #----------------------------------------------------------------------------- .section .text .global _start # tento symbol ma byt dostupny i linkeru _start: mov ecx, offset buffer # zapis se bude provadet do tohoto bufferu mov al, 'a' # kod prvniho zapisovaneho znaku loop: mov [ecx], al # zapis znaku do bufferu inc al # ASCII kod dalsiho znaku inc ecx # uprava ukazatele do bufferu cmp al, 'z'+1 # ma se smycka ukoncit? jnz loop # pokud jsme neprekrocili kod 'z', opakovat smycku mov eax, sys_write # cislo syscallu pro funkci "write" mov ebx, 1 # standardni vystup mov ecx, offset buffer # adresa retezce, ktery se ma vytisknout mov edx, rep_count # pocet znaku, ktere se maji vytisknout int 0x80 # volani Linuxoveho kernelu mov eax, sys_exit # cislo sycallu pro funkci "exit" mov ebx, 0 # exit code = 0 int 0x80 # volani Linuxoveho kernelu
Překlad se provede následovně:
as loop3.s -o loop3.o ld -s loop3.o
Zpětný překlad (disassembling) prozradí způsob překladu:
objdump -M intel-mnemonic -f -d -t -h a.out
a.out: file format elf64-x86-64 architecture: i386:x86-64, flags 0x00000102: EXEC_P, D_PAGED start address 0x00000000004000b0 Sections: Idx Name Size VMA LMA File off Algn 0 .text 00000034 00000000004000b0 00000000004000b0 000000b0 2**0 CONTENTS, ALLOC, LOAD, READONLY, CODE 1 .bss 00000020 00000000006000e8 00000000006000e8 000000e8 2**3 ALLOC SYMBOL TABLE: no symbols Disassembly of section .text: 00000000004000b0 <.text>: 4000b0: b9 e8 00 60 00 mov ecx,0x6000e8 4000b5: b0 61 mov al,0x61 4000b7: 67 88 01 mov BYTE PTR [ecx],al 4000ba: fe c0 inc al 4000bc: ff c1 inc ecx 4000be: 3c 7b cmp al,0x7b 4000c0: 75 f5 jne 0x4000b7 4000c2: b8 04 00 00 00 mov eax,0x4 4000c7: bb 01 00 00 00 mov ebx,0x1 4000cc: b9 e8 00 60 00 mov ecx,0x6000e8 4000d1: ba 1a 00 00 00 mov edx,0x1a 4000d6: cd 80 int 0x80 4000d8: b8 01 00 00 00 mov eax,0x1 4000dd: bb 00 00 00 00 mov ebx,0x0 4000e2: cd 80 int 0x80
Díky tomu, že zapisovaný znak je uložený v osmibitovém registru AL, je zvýšení tohoto registru o jedničku a test na mezní hodnotu realizován velmi krátkými strojovými instrukcemi inc al a cmp al, 0x7b.
11. Repositář s demonstračními příklady
Všechny čtyři dnes popisované demonstrační příklady byly společně s podpůrnými skripty určenými pro překlad či naopak pro disassembling uloženy do GIT repositáře dostupného na adrese https://github.com/tisnik/presentations/. Všechny dnešní příklady jsou, na rozdíl od některých příkladů vysvětlených minule a předminule, určené pro GNU Assembler a používají „Intel“ syntaxi. Následuje tabulka s odkazy na zdrojové kódy příkladů i na již zmíněné podpůrné skripty:
Špagetový kód realizovaný instrukcí nepodmíněného skoku JMP
# | Soubor | Popis | Odkaz do repositáře |
---|---|---|---|
1 | jumps.s | program pro GNU Assembler | https://github.com/tisnik/presentations/blob/master/assembler/14_spaghetti_code/jumps.s |
2 | assemble | skript pro překlad na i386/x86_64 | https://github.com/tisnik/presentations/blob/master/assembler/14_spaghetti_code/assemble |
3 | disassemble | skript pro disassembling | https://github.com/tisnik/presentations/blob/master/assembler/14_spaghetti_code/disassemble |
Počítaná programová smyčka s testem prováděným na začátku
# | Soubor | Popis | Odkaz do repositáře |
---|---|---|---|
1 | loop1.s | program pro GNU Assembler | https://github.com/tisnik/presentations/blob/master/assembler/11_gas_loop/loop1.s |
2 | assemble | skript pro překlad na i386/x86_64 | https://github.com/tisnik/presentations/blob/master/assembler/11_gas_loop/assemble |
3 | disassemble | skript pro disassembling | https://github.com/tisnik/presentations/blob/master/assembler/11_gas_loop/disassemble |
Počítaná programová smyčka s testem prováděným na konci
# | Soubor | Popis | Odkaz do repositáře |
---|---|---|---|
1 | loop2.s | program pro GNU Assembler | https://github.com/tisnik/presentations/blob/master/assembler/12_gas_loop/loop2.s |
2 | assemble | skript pro překlad na i386/x86_64 | https://github.com/tisnik/presentations/blob/master/assembler/12_gas_loop/assemble |
3 | disassemble | skript pro disassembling | https://github.com/tisnik/presentations/blob/master/assembler/12_gas_loop/disassemble |
Počítaná smyčka s volitelným začátkem a koncem
# | Soubor | Popis | Odkaz do repositáře |
---|---|---|---|
1 | loop3.s | program pro GNU Assembler | https://github.com/tisnik/presentations/blob/master/assembler/13_gas_loop/loop3.s |
2 | assemble | skript pro překlad na i386/x86_64 | https://github.com/tisnik/presentations/blob/master/assembler/13_gas_loop/assemble |
3 | disassemble | skript pro disassembling | https://github.com/tisnik/presentations/blob/master/assembler/13_gas_loop/disassemble |
12. Odkazy na Internetu
- Intel x86 JUMP quick reference
http://unixwiz.net/techtips/x86-jumps.html - Linux assemblers: A comparison of GAS and NASM
http://www.ibm.com/developerworks/library/l-gas-nasm/index.html - Programovani v assembleru na OS Linux
http://www.cs.vsb.cz/grygarek/asm/asmlinux.html - Is it worthwhile to learn x86 assembly language today?
https://www.quora.com/Is-it-worthwhile-to-learn-x86-assembly-language-today?share=1 - Why Learn Assembly Language?
http://www.codeproject.com/Articles/89460/Why-Learn-Assembly-Language - Is Assembly still relevant?
http://programmers.stackexchange.com/questions/95836/is-assembly-still-relevant - Why Learning Assembly Language Is Still a Good Idea
http://www.onlamp.com/pub/a/onlamp/2004/05/06/writegreatcode.html - Assembly language today
http://beust.com/weblog/2004/06/23/assembly-language-today/ - Assembler: Význam assembleru dnes
http://www.builder.cz/rubriky/assembler/vyznam-assembleru-dnes-155960cz - Assembler pod Linuxem
http://phoenix.inf.upol.cz/linux/prog/asm.html - AT&T Syntax versus Intel Syntax
https://www.sourceware.org/binutils/docs-2.12/as.info/i386-Syntax.html - Linux Assembly website
http://asm.sourceforge.net/ - Using Assembly Language in Linux
http://asm.sourceforge.net/articles/linasm.html - vasm
http://sun.hasenbraten.de/vasm/ - vasm – dokumentace
http://sun.hasenbraten.de/vasm/release/vasm.html - The Yasm Modular Assembler Project
http://yasm.tortall.net/ - 680x0:AsmOne
http://www.amigacoding.com/index.php/680x0:AsmOne - ASM-One Macro Assembler
http://en.wikipedia.org/wiki/ASM-One_Macro_Assembler - ASM-One pages
http://www.theflamearrows.info/documents/asmone.html - Základní informace o ASM-One
http://www.theflamearrows.info/documents/asminfo.html - Linux Syscall Reference
http://syscalls.kernelgrok.com/ - Programming from the Ground Up Book - Summary
http://savannah.nongnu.org/projects/pgubook/ - IBM System 360/370 Compiler and Historical Documentation
http://www.edelweb.fr/Simula/ - IBM 700/7000 series
http://en.wikipedia.org/wiki/IBM_700/7000_series - IBM System/360
http://en.wikipedia.org/wiki/IBM_System/360 - IBM System/370
http://en.wikipedia.org/wiki/IBM_System/370 - Mainframe family tree and chronology
http://www-03.ibm.com/ibm/history/exhibits/mainframe/mainframe_FT1.html - 704 Data Processing System
http://www-03.ibm.com/ibm/history/exhibits/mainframe/mainframe_PP704.html - 705 Data Processing System
http://www-03.ibm.com/ibm/history/exhibits/mainframe/mainframe_PP705.html - The IBM 704
http://www.columbia.edu/acis/history/704.html - IBM Mainframe album
http://www-03.ibm.com/ibm/history/exhibits/mainframe/mainframe_album.html - Osmibitové muzeum
http://osmi.tarbik.com/ - Tesla PMI-80
http://osmi.tarbik.com/cssr/pmi80.html - PMI-80
http://en.wikipedia.org/wiki/PMI-80 - PMI-80
http://www.old-computers.com/museum/computer.asp?st=1&c=1016