Assembler (Jazyk symbolických adres)

(6. část)

Další instrukce přesunů dat

Přesuny port - registr

Na port můžeme zapisovat (nebo z něj číst) osm, šestnáct i 32 bitů. Porty na architektuře PC číslujeme od 0 do 1024. Číslo portu se v následujících instrukcích zadává přímo (0 až 255) nebo v registru DX. Instrukce pro práci s porty pracují vždy s registrem AL, AX nebo EAX:

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.

Instrukce dosazení adresy

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?

Segmentace pamě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í):

Segmenty jako bloky paměti měnit nelze, ale obsah segmentových registrů (segmentovou část adresy segmentu) měnit lze (jen v 16-bitovém režimu, viz dále, v 32-bitovém ne, navíc se to rozhodně nedoporučuje, pokud člověk naprosto přesně neví, co dělá). Do segmentových registrů nejde dosadit hodnota přímo. Tu dosadíme například tak, že ji vložíme do některého univerzáního registru a z něj pak do segmentového registru, nebo ji uložíme na zásobník a pak ji z něj vyjmeme do segmentového registru.

16-bitový režim

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í:

Otázka: Proč se zde (v 16-bitovém režimu) pro získání adresy prvního parametru přičítá k BP právě 6? Jaké jsou adresy dalších parametrů a lokálních proměnných?

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).

Další instrukce přesunů dat - pokračování

Instrukce pro práci s řetězci

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):

Následující instrukce existují ve variantách pracujících s různě velkou hodnotou. Instrukce končící B operují se slabikou (1 byte), W se slovem (2 byty) a D s dvojslovem (4 byty). Po provedení operace se automaticky zvýší hodnota adresy v registrovém páru (popř. v obou registrových párech) o tuto velikost.

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í

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.

Prohození hodnot

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*.



Jan Outrata
outrataj@phoenix.inf.upol.cz