Snad každý procesor osmibitové éry v sobě ukrýval nějaké překvapení, o kterém se v manuálu nepsalo. Nejčastěji to byly instrukce s kódy, které byly v oficiálním seznamu instrukcí označeny cudným „- – -„.
Procesor 8085 měl několik šikovných instrukcí, o nichž se nemluví, stejně jako Z80 – tam se před člověkem otevřel velmi široký svět, ve kterém šlo pracovat s polovinami indexových registrů, posouvat obsah apod.
Naprostým přeborníkem v nedokumentovaných instrukcích je ale procesor 6502. Zatímco výše zmíněné procesory působí dojmem, že „nedokumentované“ někdo opravdu navrhl, zabudoval, ale pak se rozhodli, že o nich nikomu neřeknou, tak u 6502 působí nedokumentované („ilegální“) operační kódy jako směs instrukcí s naprosto nahodilými efekty, od použitelných přes obskurní až k naprosto ujetým.
Použitelná by mohla být (s trochou fantazie) třeba instrukce ANC #imm, která provede operaci AND mezi obsahem registru A a přímým operandem a nastaví hodnotu příznaku C podle nejvyššího bitu výsledku. Trošku divočejší instrukce jsou třeba LAX (která funguje jako LDA a LDX najednou, tj. vloží operand do registrů A a X) nebo XAA (přenese obsah registru X do registru A a pak provede logický součin [AND] registru A s operandem). Naprosté obskurity jsou instrukce jako TAS nebo SAY.
Kupříkladu taková instrukce TAS. Ta má tvar TAS $nnnn, Y – tedy operační kód a dva bajty adresy. Jako příklad dejme TAS $1234, Y:
- Instrukce udělá logický součin (AND) obsahu registrů A a X (oba registry ponechá nezměněné) a uloží výsledek do ukazatele zásobníku S.
- S výsledkem udělá logický součin s hodnotou $13 (tj. vyšší byte adresy, zvýšený o 1) a výsledek uloží do paměti na adresu $1234
Vzbuzuje to několik otázek. Například: Jak na to, proboha, někdo mohl přijít? Nebo: K čemu to je? Nebo: Proč?
Na tu poslední si odpovíme právě v tomto článku. Budu vycházet z famózního How MOS 6502 Illegal Opcodes really work a přidám drobné úpravy.
Jak 6502 dekóduje instrukce?
Na rozdíl od procesorů s mikrokódem, ve kterých je každá instrukce přeložena interně do posloupnosti jednoduchých operací, které jsou provedeny jakýmsi „vnitřním procesorem“, používá 6502 kombinační logiku, jejímž srdcem je PLA – dekódovací paměť. PLA je organizována jako 130 řádků (údajů), každý po 21 bitech. Každý řádek udává operaci (či operace), co se pro daný operační kód (či sadu operačních kódů) provedou v určitém taktu.
Každý údaj lze rozdělit na tři části: ON bity, OFF bity a časování. Zjednodušeně si je můžeme představit takto:
ON bity | |||||||
7 | 6 | 5 | 4 | 3 | 2 | 1 | |
OFF bity | |||||||
7 | 6 | 5 | 4 | 3 | 2 | 1 | |
časování | |||||||
T6 | T5 | T4 | T3 | T2 | T1 |
Pozornému čtenáři neuniklo, že nesedí počet bitů (22) s deklarovaným. To je proto, že ve skutečnosti vypadá řádek o něco složitěji – ale k tomu se hned dostanu.
Části ON a OFF určují, jak má vypadat instrukční kód. Pokud má mít nastaven bit 4, bude nastaven bit 4 v části ON. Pokud má mít bity 3 a 2 nulové, budou nastaveny bity 3 a 2 v části OFF. Na hodnotě bitů, které nejsou nastaveny ani v ON, ani v OFF, nezáleží a může být jakákoli. Poslední, co se kontroluje, je takt. V prvním taktu instrukce (po jejím vyzvednutí) je procesor v taktu T1 a s každým dalším se zvětšuje o 1, tj. posouvá doleva v poli „časování“.
Pokud všechny bity, na kterých záleží, odpovídají dané hodnotě, a pokud sedí i číslo taktu, je daný řádek „platný“ a předává se kombinační logice, která podle toho vykoná nějaké jednoduché úkony. Pro jednu instrukci může být platných řádků i víc najednou.
Skládání instrukcí
Ve skutečnosti je v PLA uloženo pouze šest ON bitů a šest OFF bitů pro bity 2-7. Hodnoty bitů 0 a 1 se kódují trochu jinak, pomocí trojice signálů G1, G2 a G3. G1 platí, pokud je nastavený bit 0, G2 pro nastavený bit 1 a G3 pro oba bity nulové. Vidíte, že logika není úplná, že chybí ošetření stavu pro oba bity nastavené. A je to opravdu tak, při pohledu do tabulky instrukcí vidíme, že instrukce, které mají nastavené oba nejnižší bity (tj. končí na x3, x7, xB a xF) jsou „ilegální“. Ve skutečnosti takové instrukce spustí řádky s G1 i s G2 a fungují tak jako kombinace dvou předchozích instrukcí.
(Když se ponoříte do výpisu PLA, zjistíte, že jsou bity v naprosto odlišném pořadí, proto opakuju: výše zmíněné je pouze ilustrační!)
Zjednodušeně řečeno: pokud má instrukce oba nejnižší bity nastavené, chová se, díky neúplnému dekodéru, jako dvě instrukce, G1 a G2. Například instrukce s kódem $AF se chová jako instrukce s kódem $AE – LDX i s kódem $AD – LDA (nikoli $AC, ta má oba bity nulové a je správně dekódována jako G3). U prvních dvou taktů to nevadí, v nich se pouze čte absolutní adresa, ve třetím taktu se obsah této adresy čte do registru. Do kterého? To se nastavuje v prvním taktu. Pro G1 se aktivuje signál, uvolňující zápis do registru A, pro G2 zápis do registru X. V našem případě se tedy uvolní oba, a hodnota se zapíše do obou.
Obdobně kombinace STA a STX vytvoří složenou instrukci SAX, která do paměti uloží hodnoty obou registrů A a X najednou – totiž výsledek AND obou hodnot (tipuju, že jsou interně použity otevřené kolektory, které vytvoří „montážní AND“).
Takto lze vystopovat mnoho „ilegálních instrukcí“. Dokonce i TAS…
Kill ‚em!
V PLA je zakódováno nejen to, kolik taktů která instrukce trvá a kde bere operandy, ale i načtení kódu další instrukce v posledním taktu. Což s sebou nese další zajímavý jev.
Některé kódy nedělají nic (NOP). Některé NOPy ještě předtím načtou jeden nebo dva bajty. No a některé instrukce prostě zaseknou celý procesor (většina kódů, které končí dvojkou). Nejpravděpodobnější vysvětlení je, že se u těchto kódů v PLA nenajde právě to načtení další instrukce, které mj. vynuluje počítadlo taktů, a instrukce se dostane do smrtícího „osmého taktu“, pro který už neexistuje žádný záznam v PLA. A protože se požadavky na přerušení řeší až na konci instrukce, tj. v okamžiku, kdy se nuluje počítadlo taktů, tak ani přerušení nenastane. Procesor se zkrátka „ocitne mimo šachovnici“ a vzpamatuje ho až RESET.
K dalšímu studiu doporučuju: