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](https://i.iinfo.cz/urs/pc_09_01-120890143369681.png)
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](https://i.iinfo.cz/urs/pc_09_02-120890144405990.png)
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](https://i.iinfo.cz/urs/pc_09_03-120890145387141.png)
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](https://i.iinfo.cz/urs/pc_09_04-120890147720115.png)
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](https://i.iinfo.cz/urs/pc_09_05-120890149424878.png)
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:
- Příchodem přerušení na vstup IRQ (bude vysvětleno v následující kapitole).
- Příchodem signálu RESET (ve své podstatě jde o přerušení nejvyšší úrovně).
- 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](https://i.iinfo.cz/urs/pc_09_06-120890151204565.png)
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:
- Na zásobník se uloží obsah příznakových bitů Cary flag a Zero flag
- Na zásobník se uloží aktuální obsah registru PC
- Do registru PC je uložena nějaká předem známá hodnota specifikovaná výrobcem, například 0×0008
- 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](https://i.iinfo.cz/urs/pc_09_07-120890154397750.jpg)
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](https://i.iinfo.cz/urs/pc_09_08-120890157552468.jpg)
Obrázek 8: Srdce mnoha výkonných serverů – mikroprocesor Super SPARC
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ů.