Hlavní navigace

Co se Benderovi honí hlavou

24. 4. 2008
Doba čtení: 15 minut

Sdílet

V deváté části seriálu o architekturách počítačů dokončíme popis instrukční sady našeho jednoduchého cvičného mikroprocesoru. Popíšeme si také instrukce pro manipulaci se zásobníkem, instrukce NOP, HALT a IRET. Nakonec si také vysvětlíme princip přerušovacího systému mikroprocesoru.

Obsah

1. Manipulace s hodnotami uloženými na zásobníku
2. Instrukce PUSH a POP
3. Instrukce NOP
4. Instrukce HALT
5. Přerušovací systém mikroprocesoru
6. Celá instrukční sada cvičného mikroprocesoru
7. Obsah další části seriálu

1. Manipulace s hodnotami uloženými na zásobníku

Již v předchozích částech tohoto seriálu jsme si řekli, že náš cvičný mikroprocesor obsahuje kromě běžných pracovních registrů A a B i čítač instrukcí představovaný registrem PC (program counter). Navíc je programátorům (i když nepřímo) dostupný i takzvaný ukazatel na vrchol zásobníku, jenž je uložený v registru nazvaném SP (stack pointer). V tomto šestnáctibitovém registru je umístěna adresa, na které se nachází takzvaný vrchol zásobníku (top of stack). Jinými slovy to znamená, že samotný zásobník není uložen v mikroprocesoru (ten by pro něj musel obsahovat specializovanou paměť), ale přímo v operační paměti, což na jednu stranu zjednoduší a zlevní mikroprocesor (teoreticky totiž může zásobník obsáhnout celou paměť), na stranu druhou je manipulace s hodnotami uloženými na zásobníku poněkud pomalejší, protože mikroprocesor musí při těchto operacích komunikovat s relativně pomalou operační pamětí.

pc0901

Obrázek 1: Registr představující ukazatel na vrchol zásobníku SP

Zásobník má svoje dno (první volné místo, do kterého je možné uložit hodnotu) u popisované architektury umístěné překvapivě na předposlední adrese operační paměti, tj. při šestnáctibitovém adresování se jedná o adresu 0×fffe. Při uložení hodnoty na zásobník instrukcí PUSH nebo instrukcí CALL (zde se jedná o zapamatování návratové adresy) se po zkopírování dané hodnoty na zásobník, tj. na adresu SP, adresa uložená v registru SP zmenší o hodnotu 2, protože se vždy pracuje se šestnáctibitovými hodnotami. Naopak při vyzvednutí hodnoty ze zásobníku instrukcí POP či vyzvednutí návratové adresy instrukcí RET se hodnota v registru SP o dvojku zvýší. Toto chování ukazatele na vrchol zásobníku je typická pro velkou řadu mikroprocesorů – někdy se také charakterizuje slovy „zásobník roste směrem dolů“, tj. od nejvyšší adresy k adrese nejnižší.

pc0902

Obrázek 2: Základní operace se zásobníkem

Obsah zásobníku je tedy (kromě své inicializace) ovlivňován čtyřmi instrukcemi: PUSH, POP, CALL a RET. Poslední dvě instrukce jsme si popsali v předchozí části tohoto seriálu, zbývá nám popsat instrukce PUSH a POP, které mají všestranné využití: od volání funkcí s parametry (hodnoty parametrů jsou uloženy na zásobník, protože tímto způsobem lze zabezpečit jejich rekurentnost, tj. možnost vícenásobného volání), přes tvorbu rekurzivních algoritmů v assembleru až k tvorbě složitějších datových struktur, jejichž prvky jsou mnohdy uložené právě na zásobníku. Bližší informace o funkci zásobníku (i odkazy na další informační zdroje) lze nalézt například v mých předchozích seriálech Programovací jazyk Forth a zásobníkové procesory či Zásobníkové programovací jazyky.

2. Instrukce PUSH a POP

Nás cvičný mikroprocesor obsahuje instrukce PUSH (uložení obsahu pracovního registru na zásobník) a POP (obnovení obsahu pracovního registru), které jako svůj operand vyžadují jeden z pracovních registrů A či B. Není tedy možné přímo na zásobník uložit konstantu nebo obsah nějaké adresy operační paměti (i tuto instrukci některé mikroprocesory, zejména ty mající architekturu CISC, obsahují), veškeré operace je nutné provádět přes pracovní registry. Místo neimplementované instrukce:

PUSH #1234 

se tedy musí provést dvojice instrukcí:

LD A,#1234
PUSH A 

To, který pracovní registr bude pro operaci PUSH či POP použit, závisí na obsahu adresní části instrukce, která následuje ihned za jejím operačním kódem, což je ostatně patrné z následující tabulky:

Strojový kód Zápis v assembleru Význam
14 00 PUSH A uložení obsahu pracovního registru A na zásobník
14 01 PUSH B uložení obsahu pracovního registru B na zásobník
15 00 POP A obnovení obsahu pracovního registru A ze zásobníku
15 01 POP B obnovení obsahu pracovního registru B ze zásobníku
pc0903

Operace PUSH a POP znázorněné na mechanické analogii zásobníku

Prohození obsahu obou pracovních registrů se dá (poměrně naivním způsobem) implementovat pomocí zásobníku:

; první varianta prohození obsahu dvou pracovních registrů
PUSH A
PUSH B
POP A
POP B 

Ve skutečnosti však bývá kratší a rychlejší použití následujícího idiomu (stříškou je zde naznačena operace XOR, nikoli umocnění):

; druhá varianta prohození obsahu dvou pracovních registrů
XOR A,B        ; A=A^B
XOR B,A        ; B=B^(A^B)=A^(B^B)=A^0=A
XOR A,B        ; A=(A^B)^A=(A^A)^B=0^B=B 

Při volání funkcí se používá postup, při kterém se na zásobník nejprve uloží všechny potřebné parametry a potom i návratová hodnota (parametry jsou očíslované stejně, jako je tomu například v programovacím jazyku Pascal, tj. přesně naopak, než je tomu u céčka):

; sekvence instrukcí volající funkci
LD A,param1
PUSH A         ; načtení a uložení prvního parametru funkce
LD A,param2
PUSH A         ; načtení a uložení druhého parametru funkce
CALL funkce    ; zavolání funkce (a uložení návratové adresy na zásobník)

; tělo funkce
funkce:
POP A          ; získání hodnoty druhého parametru
POP B          ; získání hodnoty prvního parametru
...
...
...
RET            ; získání návratové hodnoty a provedení zpětného skoku 

V případě, že je zásobník prázdný, tj. registr SP obsahuje hodnotu 0×fffe, je zakázáno volat instrukce POP či RET, protože ze zásobníku není možné žádnou hodnotu získat a vrátit. Situace, kdy dojde k tomu, že je jedna z těchto instrukcí volána nad prázdným zásobníkem, je nazývána stack underflow. Při volání či provádění nějaké nekorektně zapsané funkce může dojít k jiné podobně zapeklité situaci, ve které zásobník obsahuje o jednu hodnotu více, než programátor předpokládal. Za této situace se při návratu z funkce pomocí instrukce RET ze zásobníku získá špatná adresa a provádění programu pokračuje na zcela jiném místě, než by se ze zápisu algoritmu zdálo. Této situaci se někdy říká buffer overflow, protože ono nežádoucí uložení více hodnot na zásobník je možné provést například kvůli nekorektní práci s řetězci nebo poli. Z tohoto důvodu je vhodné, aby programátoři píšící své programy v programovacích jazycích typu C, C++ či Pascal, věděli, jakým způsobem je vlastně volání funkcí a předávání parametrů funkcím implementováno.

pc0904

Obrázek 4: Mikroprocesor Intel 486DX

3. Instrukce NOP

Poslední dvě instrukce, které jsme si ještě nepopsali, jsou instrukce NOP a instrukce HALT. Instrukce NOP, jejíž mnemotechnická zkratka je odvozena od anglického sousloví No OPeration, je velmi jednoduchá; jak z pohledu programátora, tak i z hlediska její implementace v mikroprocesoru. Mikroprocesor při přijetí operačního kódu instrukce NOP prostě žádnou výraznou operaci neprovede a pokračuje v provádění následující instrukce.

K jakému účelu je možné tuto instrukci použít? První způsob použití spočívá ve vytvoření „rezervované“ paměti, popř. k přepisu nějaké instrukce bez nutnosti přemístění zbytku programu v operační paměti. Jedná se tedy o podobnou operaci, jakou je zakomentování části zdrojového kódu programu, ovšem s tím rozdílem, že instrukce NOP stále zabírá jeden byte v operační paměti. Druhý nejčastější způsob použití spočívá v tvorbě zpožďovacích smyček, protože je prakticky na všech mikroprocesorech známá doba trvání provedení této instrukce (například 2 takty). Třetí oblastí použití je „zarovnání“ instrukcí na některých architekturách tak, aby instrukce začínaly například na násobku 16 bitů či 32 bitů. To značným způsobem zrychlí načítání instrukcí a jejich operandů z operační paměti. Většinou tuto výplň generují přímo překladače z vyšších programovacích jazyků. Následují dva jednoduché příklady:

; v původním programu nahradíme instrukci skoku instrukcí NOP
CMP A,B                   ; porovnání pracovních registrů A a B
JNZ password_incorrect    ; v případě rozdílu se provede skok část programu, který uživatele nepustí dále
...                       ; část programu prováděná v případě korektního hesla

; upravený program (velmi jednoduchý, ale mnohdy účinný "crack")
CMP A,B                   ; porovnáme pracovní registry A a B
NOP                       ; instrukce JNZ měla délku 2 byty, tj. potřebujeme zapsat 2xNOP
NOP
...                       ; část programu prováděná v případě korektního hesla



; použití instrukce NOP ve zpožďovací smyčce
LD A, #delay
opak:
    DEC A
    NOP                   ; přidáním instrukcí NOP můžeme dobu trvání smyčky prodloužit
    NOP                   ; (nemusí se použít například v sobě zanořená smyčka)
    JNZ opak 

Ve strojovém kódu vypadá instrukce NOP také jednoduše. Důležité je, že její délka je pouze jeden byte a proto se může tato instrukce použít všude tam, kde je zapotřebí vytvořit nějakou paměťovou výplň. Kdyby byla její délka větší, mohlo by to představovat problém.

Strojový kód Zápis v assembleru Význam
1e NOP neprovádí se žádná operace, mikroprocesor přejde na další instrukci
pc0905

Obrázek 5: Mikroprocesor IBM Blue Lightning DX2 (ten mi ještě doma v jednom „psacím stroji“ bez problémů funguje, i když chladič se pravděpodobně už poněkolikáté spálil)

4. Instrukce HALT

Instrukce HALT má sice také délku pouze jednoho bytu, tj. obsahuje operační kód, ale adresní část už nikoli, ovšem její funkce je poněkud odlišná od výše uvedené instrukce NOP. Po načtení této instrukce se mikroprocesor zastaví – to sice neznamená, že by přestal přijímat hodinové impulsy, ale z hlediska okolního prostředí se začne jednat o zcela pasivní součástku: mikroprocesor se odpojí od všech sběrnic, tj. uvede své vstupní i výstupní vodiče (kromě dvou vodičů zmíněných dále) do stavu vysoké impedance a přestane s načítáním dalších instrukcí, tj. zastaví se zvyšování hodnoty registru PC. V této chvíli mohou operační paměť i samotné sběrnice začít používat další aktivní obvody, typicky řadič přímého přístupu do paměti (DMA), matematický koprocesor či grafický čip.

Mikroprocesor se z tohoto stavu, ve kterém se snaží hrát na „mrtvého brouka“ může dostat několika způsoby:

  1. Příchodem přerušení na vstup IRQ (bude vysvětleno v následující kapitole).
  2. Příchodem signálu RESET (ve své podstatě jde o přerušení nejvyšší úrovně).
  3. Odpojením mikroprocesoru od napájení.
Strojový kód Zápis v assembleru Význam
1f HALT mikroprocesor se zastaví a čeká na příchod externího přerušení
pc0906

Obrázek 6: Mikroprocesor MIPS 3000 (jeden z nejlépe navržených mikroprocesorů)

5. Přerušovací systém mikroprocesoru

V předchozí kapitole jsme se lehce dotkli tématu přerušovacího systému mikroprocesoru. O co se vlastně jedná? Mikroprocesor při běžné práci postupně načítá instrukce z operační paměti a následně je provádí (vykonává). Pokud se nejedná o instrukci skoku, je v dalších taktech načtena a provedena následující instrukce, skoky a instrukce RET povedou ke změně adresy uložené v registru PC atd. Program je tedy prováděn na základě algoritmu převedeného do formy programu zapsaného ve strojovém kódu. V některých případech je však vhodné, aby byla tato poklidně prováděná sekvence instrukcí přerušena na základě nějakého vnitřního či vnějšího podnětu. Typicky se jedná o žádost periferního zařízení o to, aby mu procesor poslal, či z něj naopak vyčetl nějaká data.

Typickým příkladem takového zařízení je klávesnice (předpokládejme pro tuto chvíli, že nemá žádnou hardwarovou vyrovnávací paměť). Ve chvíli, kdy uživatel stiskne či pustí nějakou klávesu, je nutné, aby mikroprocesor kód této klávesy načetl a nějakým způsobem zpracoval, jinak se může stát, že se informace o stisku zapomene ve chvíli, kdy uživatel klávesu opět pustí. Sledování stisků kláves je možné provádět buď aktivně či pasivně.

Aktivní způsob spočívá v neustálém načítání kódů stisknuté klávesy z obvodu, který klávesnici obsluhuje, což však vede k tomu, že je celý program zaplněn několika smyčkami, které se neustále volají a i při různých výpočtech je nutné často provádět odskoky do těchto smyček. Pasivní režim je mnohem jednodušší – mikroprocesor vykonává program, který byl uložen v operační paměti a pokud dojde ke stisku klávesy, je generováno takzvané přerušení, tj. signál, který je přiveden na jeden ze vstupů mikroprocesoru a způsobí násilné (a především asynchronní) porušení běhu programu a většinou odskok na předem zadanou adresu. Přitom je adresa, na které se program nacházel v době příchodu přerušení, uložena na zásobník, spolu s informacemi o nastavení příznakových bitů Zero flag a Carry flag.

Způsobů, jakým je přerušovací systém v mikroprocesorech implementován, je několik. Buď mají mikroprocesory specializované registry obsahující adresy, na které se při výskytu přerušení provede skok, nebo se jedná o instrukce, které řadič přerušení mikroprocesoru „vnutí“ na datovou sběrnici atd. Také zdrojů přerušení může být několik, přičemž se zavádí takzvané priority – v případě, že přijde více přerušení ve stejnou chvíli, rozhodně prioritní dekodér o tom, které přerušení má přednost. Většinou jsou s vyšší prioritou obsloužena pomalejší zařízení typu klávesnice, sériového portu či disketové jednotky oproti zařízením rychlejším (koprocesor, síťová karta), ovšem konkrétní přiřazení priorit se systém od systému liší.

Náš mikroprocesor má pouze jeden vstup pro přerušení, který je označen symbolem IRQ. Jde o takzvané nemaskovatelné přerušení, protože programátor nemá žádnou možnost, jak programově toto přerušení zakázat. V případě, že na tento vstup mikroprocesoru přijde úroveň logické jedničky (přerušení lze podle povahy mikroprocesoru spouštět buď hranou nebo úrovní), je dokončena právě probíhající instrukce a ihned poté se provedou následující kroky:

  1. Na zásobník se uloží obsah příznakových bitů Cary flag a Zero flag
  2. Na zásobník se uloží aktuální obsah registru PC
  3. Do registru PC je uložena nějaká předem známá hodnota specifikovaná výrobcem, například 0×0008
  4. S příchodem dalšího hodinového taktu mikroprocesor začne provádět instrukce uložené právě na této adrese (zde by tedy měl být program pro obsluhu přerušení, který například zjistí, které zařízení přerušení vyvolalo, provede příslušnou obsluhu a nakonec návrat zpět do původního programu).

O onen návrat z obsluhy přerušení zpět do původního programu se stará instrukce IRET, jež obnoví původní obsah příznakových bitů Carry flag a Zero flag i obsah čítače instrukcí PC. Po provedení této instrukce se (pokud programátor neudělal úmyslně či neúmyslně nějaké modifikace s obsahem zásobníku) řízení vrací zpět do původního programu, který v mnoha případech není příchodem přerušení žádným způsobem ovlivněn.

Povšimněte si však jedné zdánlivé „maličkosti“ – při vstupu do přerušení se na zásobník automaticky neuloží obsah pracovních registrů a ani při návratu z přerušení se neprovede jejich obnova. To je (ostatně jako u mnoha skutečných mikroprocesorů) zcela ponecháno na programátorovi, který přerušovací rutinu vytváří. Důvod je ten, že v obsluze přerušení se například nemusí s některými pracovními registry vůbec manipulovat a není tedy vhodné, aby se vždy automaticky ukládaly na zásobník a posléze zase obnovovaly – to by stálo mnoho cyklů (tiků hodinového signálu), se kterými je nutné při programování obslužné přerušovací rutiny co nejvíce šetřit. Přerušovací rutina, která modifikuje obsah obou pracovních registrů tedy musí začínat a končit takto:

; příklad přerušovací rutiny
PUSH A   ; úschova aktuálního obsahu obou pracovních registrů
PUSH B
...      ; obsluha přerušení
POP  B
POP  A   ; obnova obsahu pracovních registrů
IRET     ; obnova příznakových bitů a čítače instrukcí 

Samotná instrukce IRET má délku jednoho bytu, protože obsahuje pouze operační kód a nikoli adresní část.

Strojový kód Zápis v assembleru Význam
19 IRET návrat z přerušení
pc0907

Obrázek 7: Mikroprocesor StrongARM firmy Digital

6. Celá instrukční sada cvičného mikroprocesoru

Nyní již známe celou instrukční sadu našeho cvičného mikroprocesoru, tj. jak operační kódy všech instrukcí, tak i obsahy jejich adresních částí. Vzhledem k tomu, že se ještě k programování ve strojovém kódu i assembleru vrátíme, uvedu zde celou tabulku instrukcí se všemi kombinacemi operačních kódů a adres. Podobnou tabulku vydává každý výrobce mikroprocesorů, protože se jedná (samozřejmě spolu se signálovými charakteristikami) o jednu z nejdůležitějších informací o tomto složitém integrovaném obvodu:

Strojový kód Zápis v assembleru Význam
Aritmetické instrukce
00 00 ADD A,A A←A+A
00 01 ADD A,B A←A+B
00 10 ADD B,A B←A+B
00 11 ADD B,B B←B+B
01 00 ADC A,A A←A+A+CF
01 01 ADC A,B A←A+B+CF
01 10 ADC B,A B←A+B+CF
01 11 ADC B,B B←B+B+CF
02 00 SUB A,A A←A-A
02 01 SUB A,B A←A-B
02 10 SUB B,A B←B-A
02 11 SUB B,B B←B-B
03 00 SBB A,A A←A-A-CF
03 01 SBB A,B A←A-B-CF
03 10 SBB B,A B←B-A-CF
03 11 SBB B,B B←B-B-CF
04 00 INC A inkrementace registru A
04 01 INC B inkrementace registru B
05 00 DEC A dekrementace registru A
05 01 DEC B dekrementace registru B
Logické instrukce
06 00 AND A,A A←A & A
06 01 AND A,B A←A & B
06 10 AND B,A B←A & B
06 11 AND B,B B←B & B
07 00 OR A,A A←A | A
07 01 OR A,B A←A | B
07 10 OR B,A B←A | B
07 11 OR B,B B←B | B
08 00 XOR A,A A←A ^ A
08 01 XOR A,B A←A ^ B
08 10 XOR B,A B←A ^ B
08 11 XOR B,B B←B ^ B
09 00 COM A A←~A
09 01 COM B B←~B
Posuvy a rotace
0a 00 RL A bitová rotace registru A doleva
0a 01 RL B bitová rotace registru B doleva
0b 00 RLC A bitová rotace registru A doleva přes příznak přenosu
0b 01 RLC B bitová rotace registru B doleva přes příznak přenosu
0c 00 RR A bitová rotace registru A doprava
0c 01 RR B bitová rotace registru B doprava
0d 00 RRC A bitová rotace registru A doprava přes příznak přenosu
0d 01 RRC B bitová rotace registru B doprava přes příznak přenosu
0e 00 ASR A aritmetický posuv registru A doprava
0e 01 ASR B aritmetický posuv registru B doprava
0f 00 CMP A,A A-A (ovlivní se Carry flag a Zero flag)
0f 01 CMP A,B A-B (ovlivní se Carry flag a Zero flag)
0f 10 CMP B,A B-A (ovlivní se Carry flag a Zero flag)
0f 11 CMP B,B B-B (ovlivní se Carry flag a Zero flag)
Testování a porovnání
10 00 TEST A,A A & A (ovlivní se Zero flag)
10 01 TEST A,B A & B (ovlivní se Zero flag)
10 10 TEST B,A B & A (ovlivní se Zero flag)
10 11 TEST B,B B & B (ovlivní se Zero flag)
Přesuny mezi pamětí a registry
11 00 xx yy LD A,#konstanta načtení konstanty xxyy do pracovního registru A
11 01 xx yy LD B,#konstanta načtení konstanty xxyy do pracovního registru B
11 10 xx yy LD A,[adresa] načtení hodnoty uložené v operační paměti do pracovního registru A
11 11 xx yy LD B,[adresa] načtení hodnoty uložené v operační paměti do pracovního registru B
12 00 xx yy ST A,[adresa] uložení hodnoty z pracovního registru A do operační paměti na zadanou adresu
12 01 xx yy ST B,[adresa] uložení hodnoty z pracovního registru B do operační paměti na zadanou adresu
13 00 MOV A,B přesun hodnoty z registru B do registru A
13 01 MOV B,A přesun hodnoty z registru A do registru B
14 00 PUSH A uložení obsahu registru A na zásobník
14 01 PUSH B uložení obsahu registru B na zásobník
15 00 POP A obnovení obsahu registru A ze zásobníku
15 01 POP B obnovení obsahu registru B ze zásobníku
Skokové a návratové instrukce
16 xx yy JMP nepodmíněný skok na zadanou adresu xxyy
17 xx yy CALL adr skok na adresu xxyy s uložením stavu PC na zásobník
18 RET návrat na adresu, jež je uložena na vrcholu zásobníku (obnovení PC)
19 IRET návrat z přerušení (obnovení PC a příznakových bitů)
1a rr JC podmíněný relativní skok za předpokladu, že je nastaven příznak přenosu (Carry flag)
1b rr JNC podmíněný relativní skok za předpokladu, že je vynulován příznak přenosu (Carry flag)
1c rr JZ podmíněný relativní skok za předpokladu, že je nastaven příznak nulovosti (Zero flag)
1d rr JNZ podmíněný relativní skok za předpokladu, že je vynulován příznak nulovosti (Zero flag)
Nezařazené zbývající instrukce
1e NOP neprovádí se žádná operace, mikroprocesor přejde na další instrukci
1f HALT mikroprocesor se zastaví a čeká na příchod externího přerušení
pc0908

Obrázek 8: Srdce mnoha výkonných serverů – mikroprocesor Super SPARC

CS24_early

7. Obsah další části seriálu

V následující části tohoto seriálu si popíšeme některé typické architektury mikroprocesorů, zejména architektury označované téměř magickými zkratkami RISC, CISC, VLIW a MISC. Řekneme si, jaké jsou přednosti i zápory jednotlivých architektur, především s ohledem na budoucí vývoj mikroprocesorů.

Byl pro vás článek přínosný?

Autor článku

Vystudoval VUT FIT a v současné době pracuje na projektech vytvářených v jazycích Python a Go.