Příkladem práce s porty může být např. čtení informací z paměti CMOS. Provádí se to tak, že na port 70h vyšleme číslo čtené slabiky v CMOS. Někdy může být potřeba na CMOS paměť chvilku počkat, a potom z portu 71h přečteme hodnotu žádané slabiky.
V dnešních OS bývají instrukce pro práci s porty privilegované, takže je v nich nemůžeme vyzkoušet. Pro toto opatření je celkem rozumný důvod, protože bychom mohli ovlivnit hardware, který se ovládá přes porty (např. síťové karty) a OS by tak nad ním mohl ztratit kontrolu.
Při nepřímém adresování si můžeme vypočítanou adresu uložit do registru pomocí instrukce LEA.
LEA se dá použít i pro jednoduché násobení. Takové násobení je nejrychlejší možné (na 486 až 1 takt), např. LEA eax, [eax+8*eax] znamená eax = 9*eax.
Otázka: Jaké násobky lze uskutečnit? Je možné takto vynásobit např. 5-ti nebo 6-ti?
Vzhledem k tomu, že 16-bitový obvod 8086 je schopen práce s pamětí o velikosti 1 MB, pro jejíž adresaci je potřeba 20 bitů (2^20 = 1 MB), a obsahuje jen šestnáctibitové registry, je nutný přístup k této paměti po logických blocích velikosti maximálně 64 kB (2^16). Intel pro to vytvořil schéma tzv. segmentace paměti. Bloku říkáme segment a jeho adresa (adresa začátku segmentu) v paměti je násobkem šestnácti. Tyto logické bloky (segmenty) se mohou překrývat a ve skutečnosti se překrývají téměř vždy. Umístění jednotlivých slabik v segmentu určuje číslo offsetu, zkráceně offset. Logická adresa slabiky v paměti se potom skládá ze dvou částí: segmentové a offsetové, které se při zápisu adresy oddělují dvojtečkou a do hranatých závorek se píše jen offset. Obě tyto části jsou vyjádřeny šestnáctibitovým číslem. Segmentová část adresy je adresa segmentu vydělená šestnácti a offsetová část je rovna offsetu. (Lineární) adresa slabiky v paměti se z těchto dvou částí spočítá následovně: segmentová část je vynásobena šestnácti (v binárním vyjádření jsou přidány čtyři bity s hodnotou nula) a k tomuto dvacetibitovému číslu je potom přičten offset. Pro zmatení uživatelů/programátorů se pro segmetovou část adresy také často nesprávně používá označení ,,adresa segmentu'' nebo i ,,segment'' (i když už víte, že skutečná adresa segmentu je šestnáctinásobek a segment je blok paměti s touto adresou). Pro výrazné zjednodušení výkladu budu i já používat místo sousloví ,,segmentová část adresy bloku paměti'' jen ,,segment'', zda je myšlena část adresy nebo blok paměti vyplyne z kontextu. Např. (lineární) adresa místa v paměti na segmentu 0xAB1E a offsetu 0x1111 je 0xAB1E0 + 0x1111 = 0x0xAC2F1.
Důsledky segmentace:
Operační systém vyčlení pro běh programu při jeho spuštění segmenty (logické bloky paměti) pro jeho strojový kód (instrukce), data a zásobník a program je (samozřejmě) nemůže měnit. Segmenty se výrazně (nebo i úplně) překrývají a segment pro data a zásobník bývá většinou společný. Pro adresaci každého tohoto segmentu jsou určeny speciální registry.
Segmentové registry - 16-bitové, určené pro uložení segmentové části adresy (segmentu, viz výkladové ujednání):
Dnes v době 32-bitových operačních systémů pracujeme a programujeme (téměř) výhradně v 32-bitovém režimu procesoru. Z důvodu zpětné kompatibility ale dokáží i dnešní procesory pracovat v 16-bitovém režimu procesoru 8086 (přímo v tzv. reálném režimu nebo nepřímo v tzv. virtuálním režimu). V tomto režimu máme oproti 32-bitovému tato omezení:
Kromě instrukce LEA lze v 16-bitovém režimu použít i instrukce LDS, LES, LFS, LGS a LSS, které fungují stejně jako LEA, navíc však nastaví příslušný segmentový registr. V 32-bitovém režimu (obecně v tzv. chráněném režimu) jsou to ale privilegované instrukce, tudíž je zde použít nelze.
Příklad (v 16-bitovém režimu):
unsigned long val1, val2;
_asm {
lfs si, val1
les di, val2
mov ax, fs:[si+2]
cmp ax, es:[di+2]
jb mensi
ja vetsi
mov ax, fs:[si]
cmp ax, es:[di]
jb mensi
ja vetsi
jmp konec
mensi:
push fs
mov fs, es
pop es
mov ax, si
mov si, di
mov di, ax
vetsi:
mov ax, fs:[si+2]
mov es:[di+2], ax
mov ax, fs:[si]
mov es:[di], ax
konec:
}
Ekvivalentní příklad v 32-bitovém režimu:
unsigned long val1, val2;
_asm {
lea esi, val1
lea edi, val2
mov ax, [esi+2]
cmp ax, [edi+2]
jb mensi
ja vetsi
mov ax, [esi]
cmp ax, [edi]
jb mensi
ja vetsi
jmp konec
mensi:
mov eax, esi
mov esi, edi
mov edi, eax
vetsi:
mov ax, [esi+2]
mov [edi+2], ax
mov ax, [esi]
mov [edi], ax
konec:
}
Pokud v zápisu adresy neuvedeme segment, defaultně se použije registr DS. Použití jiného segmentového registru lze zadat i jinak než jeho uvedením spolu s dvojtečkou - pomocí instrukčních prefixů SEGDS, SEGES, SEGCS a SEGSS. Tedy např. MOV AX, ES:[BX] je to samé jako SEGES MOV AX, [BX].
Segmentace se ale netýká jen 16-bitového režimu, existuje i na dnešních 32-bitových procesorech a operačních systémech (M$ Windows, Linux a další). Na programové úrovni ji ale bylo nutné používat jen v programech pracujících v 16-bitovém režimu, v dobách 16-bitových OS (např. M$ DOS) nebo dnes v emulaci 16-bitového operačního prostředí (např. M$ DOS v M$ Windows). V tomto režimu mají segmenty velikost typicky 64 kB. Jelikož dnešní 32-bitové operační systémy pracují v (chráněném) 32-bitovém režimu, nemusíme se v programu při práci s pamětí segmentací vůbec zabývat, protože s 32-bitovými registry jsme schopni adresovat celých 4 GB paměti jen pomocí offsetové části adresy a segmentaci transparentně (pro program) řeší OS, tzn. automaticky nastavuje segmentové registry podle přístupu do paměti a program to nemůže ovlivnit. Segmenty jsou potom velké i několik GB (překrývají se). Segmentace nás tedy zajímá, jen pokud programujeme v 16-bitovém režimu procesoru (procesor po resetu pracuje v reálném režimu a 32-bitové OS provádějí nějaký kód ještě před přepnutínm do chráněného 32-bitového režimu, nebo např. tzv. bootloadery, tj. zavaděče OS).
Assembler má velmi silný nástroj v řetězcových instrukcích. Za řetězec je v Assembleru považován libovolný blok dat v paměti o libovolné délce, avšak v těchto instrukcích se řetězcem myslí posloupnost znaků, slov nebo dvojslov. Řetězcové instrukce nemají operandy, pracují s hodnotami na adresách určených vždy těmito registry (registrovými páry):
Př. Zjednodušte předchozí příklad použitím některých z těchto instrukcí.
Příklad:
void *memcpy(void *dest, void *src, unsigned long n)
{
_asm {
mov esi, src
mov edi, dest
mov ecx, n
cykl:
movsb
loop cykl
mov eax, edi
}
}
Prefix opakování se velmi často používá před řetězcovými instrukcemi a umožňuje tak jejich podmíněné i nepodmíněné opakování. Jejich společným použitím se program zrychlí a hlavně zjednoduší. Nepodmíněným prefixem je:
Instrukční prefix REP rozšiřuje použití předchozích řetězcových instrukcí na řetězce jako posloupnosti jednotek dat. Jestliže máme nastavený registr ECX na počet slabik řetězce a adresy zdrojového a cílového řetězce, zajistí REP např. jejich zkopírování na jednom řádku programu (dalo by se říci jedinou instrukcí), REP MOVSB.
Př. Zjednodušte předchozí příklad použitím prefixu opakování. Použijte řetězcovou instrukci pracující s dvojslovem (končící D).
Příklad, jak nejrychleji vynulovat blok paměti:
XOR EAX, EAX
MOV ECX, čtvrtina_velikosti_bloku
REP STOSD
Řetězcové instrukce porovnávání nastavují mimo jiné i vlajku ZF. Proto Assembler obsahuje navíc prefixy podmíněného opakování:
Opakování je tedy přerušeno nejen při nulovém ECX, ale i při nastavení ZF do log. 1 nebo 0.
K prohození dvou hodnot je potřeba jednu z nich uložit na pomocné místo a navíc to nejde méně než třemi instrukcemi. Instrukční sada procesoru však obsahuje i jedinou instrukci na prohození obsahu registrů nebo registru a paměti (která samozřejmě nepotřebuje pomocné místo, v paměti):
Př. Přepište bloky Assembleru provádějící funkce strcpy, strcmp a strchr jazyka C z jednoho z minulých cvičení tak, aby řetězce a znak byly parametry funkcí obsahujících blok Assembleru. Jednodušeji řečeno, napište tyto funkce celé v Assembleru, tzn. tělo funkcí bude obsahovat jen blok _asm. K parametrům přistupujte nepřímou adresací pomocí EBP a výsledek vracejte v EAX. Navíc v Assembleru nahraďte cykly řetězcovými instrukcemi s prefixy opakování REP*.