Impulzus

 
A Budapesti Műszaki és Gazdaságtudományi Egyetem Villamosmérnöki és Informatikai Kar Hallgatói Képviseletének lapja
Random cikkajánló

Rövid hírek házunk tájáról

"Feltörték a Balut!"

TDK Konferencia 1992. - Díjazási lista

Nőidomítás

–Jön a barátnőd este bulizni? –Nem, azt mondta, inkább otthon marad. –Jól van idomítva, tudja, hova nem kell jönnie

Alagút

A Dékán Úr köszöntője

Impresszum

Meghívó

Irodalmi Kör

NemKéne csapatinduló

Pondró II.

"Ezúttal a kisfiú az anyjával sétált kézenfogva."

C64 zóna

Egy korábbi számunkban esett már szó a 6502-es processzor mikroszkópi vizsgálatáról és fejtéséről, ehhez szeretnék némi adalékot nyújtani egy olyan témában, melyről az egyébként igen bőséges szakirodalom csak nagyon kevés információval szolgál.

A jól kiaknázható hardware-hibák kihasználásának elterjedésével egy időben kezdődött igazán a programvédelem lendületes fejlődése: a profi felhasználói programok, és főleg a játékok írói mindent megtettek, hogy az általuk "komponált", végletekig optimalizált full assembly kódból az avatatlan fejlesztő semmiképp ne tudja kihámozni az igazi nagy trükköket.

Módosították például a betöltés mechanizmusát is:

LOAD "GAME",8,1

hatására töltődik a megfelelő memóriaterületre a kód, és ezt a felhasználó a RUN paranccsal indíthatja. Vagy ha nem, hát debugolja. Ennek megakadályozását szolgálja az autostart, melynek tipikus megvalósítása, hogy a programot olyan kezdőcímre írják meg, hogy betöltés közben megszakításvektort, képernyőmemóriát, billentyűzetpuffert ír felül úgy, hogy ezt kihasználva elveszi a vezérlést a rendszertől. Ekkor indul be igazán az apparátus: például a gyorsabb lemezkezelés céljából saját gyorstöltő rutin folytatja a beolvasást, a RAM-mal átfedésben levő, és tőle területet elfoglaló ROM-ot kikapcsolja, hardware-hibát kihasználva nagyobb képernyőfelületet kezel (keretre ír), több színt jelenít meg, mint amire a gépet tervezték. A kikódolás lehet futásidejű is, például interruptból végez KIZÁRÓ VAGY műveleteket egy olyan kulccsal, amit a CIA chipből olvas ki, mint kvázi véletlen számot, és a helyes lefutás egyik feltétele a mikroszekundum pontosságú időzítés. Ettől akár egy tapasztaltabb kódtörő is zavarba jöhet. :-) Ő egy kicsi, egyszerű disassemblerrel esik neki a törésnek, értelmes utasításcsoportokat keres a memóriában, miután egy alkalmas helyen sikerült megakasztania a programot. Itt jöhet egy bökkenő: sehol nincs összefüggő kód, csak memóriaszemét. Ez esetben lehetséges, hogy a program írója erősen kihasználta a 65xx család egyik legérdekesebb tulajdonságát, az úgynevezett tiltott kódok létezését: ami értelmetlen byte-sorozatnak látszik, az valójában működő algoritmust takar, mely nemdokumentált utasításokkal operál.

A továbbiakban erről lesz szó, szeretnék rávilágítani a működésükre, valamint arra, hogy egyáltalán mi az oka annak, hogy léteznek. Érdemes megnézni a

http://impulzus.com/6502/

oldalt, mely most jelentősen bővült, a processzor blokkdiagramjának minden egységéből raktam ki megfejtett és értelmezett kapcsolásokat. A teljes layout képei is letölthetők!

Nézzük először, hogy hogyan zajlik az utasítások dekódolása: a beolvasott opkód az utasításregiszterbe kerül, és ott is marad a végrehajtás teljes ideje alatt, folyamatosan bemenetet szolgáltatva a dekódolónak, melynek emellett még azt is tudnia kell, hogy éppen hányadik óraciklusban jár a végrehajtás. Ezek az információk először egy rendezett struktúrán haladnak át, egy ROM-on, mely sokbemenetű NOR kapukból áll, a mátrix oszlopai a kimenetek, a sorai a bemenetek. Az opkód minden bitjének a ponáltja és a negáltja is a bemenetre jut, valamint az óraciklus információ is, mely a Timing Control-tól jön (ez nemes egyszerűséggel egy shift regiszter: opkód betöltésekor kap egy 1-es bitet, mely végigfut rajta). A ROM kitöltésekor úgy jártak el, hogy minden oszlophoz a kívánt opkód bitjeinek negáltjait és valamelyik ciklusjelzőt kontaktusozták le. Például a 130 kimenet egyike: "TXS utasítás a második óraciklusban". Ezt a jelet némi erősítés, invertálgatás után közvetlenül rá lehet kötni 8 transzfer gate-re, melyek a regiszterblokkban egy belső sín tartalmát (az X értékével) beírják a stack pointerbe. Ez egy egyszerű eset, a kimenetek jelentős része nem ilyen: tipikus, hogy nem minden bit van lekötve, így egy kimenet egyszerre jelenti 15 utasítás különböző címzésmódjait, melyből talán 5-10-nek van tényleges hatása, a többi nem is tart annyi óraciklusig, amennyit az oszlop előír. A ROM-hoz csatlakozó igen kusza kombinációs hálózat ráadásul még sokbemenetű kapukba is összefogja a kimeneteket. Miért van szükség ekkora kavarásra? Egyrészt azért, mert bizonyos eseteket tényleg nem lehet egyszerűen kikapuzni (mikor kell egyik belső sín tartalmát egy másikra átírni, honnan kapja az ALU a bemeneteit stb.), másrészt az utasításdekódoló optimalizálva van. Gondoljunk bele, hány kimenetre lenne szükség, ha csak úgy, első nekifutásra le akarnánk kezelni az 56 utasítás, 13 címzésmód, 2-től 7 óraciklusig terjedő végrehajtás minden esetét! Ilyen szemszögből a 130 már nem is tűnik soknak... Ezek a jelek szinte mindenhova eljutnak: regiszterek írását, olvasását, növelését vezérlik, megmondják az ALU-nak, hogy az egy menetben előállított logikai műveletek közül melyiknek az eredményét kell felhasználni, befolyásolják a flagek és az interrupt logika működését (SEI, CLI – tiltás/engedélyezés, BRK – szoftvermegszakítás). Az optimalizálás és takarékoskodás nem csak a ROM oszlopaira terjedt ki, hanem a sorokra is: az opkód első bitjének a ponáltja például sehova nincs kötve, az ő sora hiányzik, a helyén óraciklus információ van.

Mindezekből következik, hogy a 6502 (és a tulajdonságait öröklő 6510 is) csak a dokumentált utasítások esetén működik logikusan, egyébként azt csinálja, ami a kapcsolásból szinte véletlenszerűen, előre nem megtervezetten adódik (nyilván nem volt benne a projekt specifikációban, hogy legyen tízféle utasításunk, melyek fagyást csinálnak, és nem 100% valószínűséggel).

A tiltott kódokat három csoportra lehet osztani:

1. Üres utasítás, mint a NOP: beolvassa az operandusát, egy sínre kiírja, ám ekkor be is fejezi a működését, az eredményt "ott felejti". Valamely későbbi utasítás fogja felülírni, így elvész. Ha lenne olyan kód, amely belső sín tartalmát használja fel úgy, hogy előtte nem ír rá semmit, akkor az egy plusz regisztert jelentene, amit persze szoftveresen kéne frissíteni, és csak olyan műveletek futhatnának mellette, melyek épp nem használják az adott vezetékeket. Igazi súlyos trükk lenne.:-)

2. Értelmes utasítás, mely komplex is lehet, több műveletet is elvégezhet, mint például az

ANE ($8B): A = (A | #$EE) & X & #byte

A beépített konstans sem garantált, az adatbusz tartalmától függ, melyhez a videochip is hozzáfér a végrehajtás alatt!

3. Fagyást okozó utasítás

Ha több regiszter tartalmát ugyanarra a sínre írjuk, vagy az ALU-ban előállított logikai függvények közül többet is a kimenetére kényszerítünk, akkor logikai kapuk egymás ellen dolgozhatnak, nagyon lassan vagy egyáltalán nem alakul ki éles 0 vagy 1 szint, amit ugyan a következő kapuk elfogadnak, csinálnak belőle valamit, de mindenképpen lassabban kapcsolnak. Az előírt időzítések felborulnak, a processzor működésképtelen állapotba kerül. Ennek a bekövetkezése a belső tárolók tartalmától is függ: néhány NOP-szerű utasítás gyakran fagyaszt, de nem mindig...

Az elvetemült kóderek képesek voltak órákon keresztül futtatni olyan algoritmusokat, amelyek tiltott kódokat teszteltek, és a kapott eredményhalmazból próbáltak következtetni arra, hogy a flagek milyen logika szerint működnek a nem definiált esetekben. Még mindig vannak, akik demókat fejlesztenek C64-re, és nagyon komoly fejlődést tudtak felmutatni az elmúlt években: 5 éve teljesen jogosan gondolhattuk, hogy már abszolút kicsikartak mindent az alig 1 MHz-es rendszerből, de nem. Ők azok, akik nem holmi PC-t nyomkodva, egyébként igen korrekt emulátorban fejlesztenek, hanem személyesen A GÉP előtt ülnek...

Bereg