Van, amikor az ember mélyebbre ásna. Nem elég a kényelmes, felhasználóbarát operációs rendszer, ami mindent elrejt előlünk. Néha visszavágyunk abba az időbe, amikor a programozó még úr volt a hardver felett, és minden egyes bitet személyesen vezényelt. Ma egy ilyen, már-már „retró” kalandra invitállak titeket: hogyan kérdezzük le az egér X/Y elmozdulását közvetlenül a hardverből, Assembly nyelven. Készüljetek, mert belemászunk a bitek közé, és megértjük, mi történik valójában, amikor megrántjuk az egeret! 🚀
A múlt ködébe burkolózó szükségesség: Miért Assembly?
Kezdjük az alapoknál: miért akarná bárki is ezt csinálni? Nos, ha nem a DOS korszakban nőttél fel, ahol a géped egyedüli uralkodója te voltál, és nem egy operációs rendszer, akkor talán furcsának tűnik. De képzeld el a ’90-es éveket! Volt egy DOS-os játékod, ami abszolút precizitást és minimális késleltetést igényelt. Az operációs rendszerek (már ha volt egyáltalán valami a DOS-on felül, mint pl. a Windows 3.1) még nem kínáltak olyan kifinomult hardver-absztrakciót, mint ma. Sőt, sokszor közvetlenül kellett „beszélgetnünk” a perifériákkal, és erre az Assembly volt a legalkalmasabb, sőt, sokszor az egyetlen ésszerű választás. 🧐
Az Assembly nem más, mint a processzor „anyanyelve”. Minden utasítás, amit egy magasabb szintű nyelvben (C++, Python stb.) leírunk, végül Assemblyvé, majd gépi kóddá alakul. Ha közvetlenül Assemblyben dolgozunk, az azt jelenti, hogy teljes kontrollt gyakorolunk a CPU felett, és szó szerint minden egyes műveletet mi diktálunk. Ez egyszerre felszabadító és borzasztóan felelősségteljes. Kicsit olyan, mintha a Forma-1-es autót nem a kormányról, hanem közvetlenül a motorvezérlőről vezetnénk. Izgalmas, de egy rossz mozdulat, és falnak megyünk! 💥
Az egér: Több, mint egy egyszerű mutatóeszköz
Mielőtt belevetnénk magunkat a hardveres mélységekbe, értsük meg, hogyan működik egy egér a legalapvetőbb szinten. Az egér nem egy abszolút pozíciót küld a gépnek (hacsak nem egy nagyon speciális grafikustábláról van szó), hanem elmozdulásokat. Amikor elhúzod az egeret jobbra, az nem azt mondja, hogy „most az X=500-nál vagyok”, hanem azt, hogy „X irányban elmozdultam +5 egységet”. Ez az inkrementális mozgás sokkal hatékonyabb a hardver és szoftver számára, hiszen csak a változást kell kezelni. Gondolj bele: ha minden egyes egérmozdulatnál az abszolút koordinátákat küldené, az feleslegesen sok adatforgalmat generálna, és bonyolítaná a feldolgozást. 🧠
PS/2 kontra USB: A „közvetlen hardver” dilemma
Napjainkban szinte minden egeret USB-n keresztül csatlakoztatunk. Az USB (Universal Serial Bus) egy rendkívül komplex protokoll. Ahhoz, hogy USB egér adatait olvassuk, már USB host controllerekkel, HID (Human Interface Device) protokollokkal és rengeteg réteggel kellene foglalkoznunk, ami már nem igazán a „közvetlenül a hardverből” érzést adná. Sokkal inkább az operációs rendszer rétegeinek megkerüléséről szólna, ami egy külön tudomány. 🤯
Ezért a mi kalandunkhoz egy igazi klasszikust választunk: a PS/2 egeret. A PS/2 port, bár már ritkán található meg a modern alaplapokon, egykor a billentyűzet és az egér szabványos csatlakozója volt. Azért tökéletes a célra, mert a PS/2 perifériák közvetlenül egy dedikált vezérlőhöz, a 8042 Keyboard Controllerhez kapcsolódnak, ami pedig közvetlenül az I/O (Input/Output) portokon keresztül érhető el a CPU számára. Ez az igazi „drótos” kapcsolat, ahogy mi azt szeretjük! ❤️
Merüljünk el a PS/2 protokollban (és a bitek táncában!)
A PS/2 egér kommunikációja a 8042 Keyboard Controlleren keresztül zajlik. Ennek a vezérlőnek két fő I/O portja van, amivel foglalkoznunk kell:
60h
(hexadecimális): Ez az adatport. Ide írunk parancsokat a vezérlőnek (és az egérnek), és innen olvassuk az érkező adatokat (billentyűleütéseket, egérmozdulatokat).64h
(hexadecimális): Ez a parancs/állapot port. Ezt használjuk a vezérlő állapotának lekérdezésére (pl. van-e adat az adatporton), és a vezérlőnek szóló parancsok küldésére (pl. billentyűzet engedélyezése/tiltása, egér engedélyezése).
Az egér alapvetően egy három bájtos adatcsomagot küld minden elmozduláskor vagy gombnyomásra. Ez a csomag így néz ki:
- 1. Bájt (Állapot bájt):
- Bit 0: Bal gomb állapota (0 = felengedve, 1 = lenyomva)
- Bit 1: Jobb gomb állapota (0 = felengedve, 1 = lenyomva)
- Bit 2: Középső gomb állapota (0 = felengedve, 1 = lenyomva)
- Bit 3: Mindig 1 (ez egy „szinkron bit” a csomag elejének felismerésére)
- Bit 4: X-túlcsordulás (X overflow) – ha az X elmozdulás túl nagy volt
- Bit 5: Y-túlcsordulás (Y overflow) – ha az Y elmozdulás túl nagy volt
- Bit 6: X elmozdulás előjele (0 = pozitív, 1 = negatív)
- Bit 7: Y elmozdulás előjele (0 = pozitív, 1 = negatív)
- 2. Bájt (X elmozdulás): Egy 8 bites előjeles szám (-128 és +127 között), ami az X irányú elmozdulást jelöli.
- 3. Bájt (Y elmozdulás): Egy 8 bites előjeles szám (-128 és +127 között), ami az Y irányú elmozdulást jelöli.
Láthatjuk, hogy az X és Y elmozdulások előjele (pozitív vagy negatív irány) a harmadik, azaz a legfelső bitből (MSB) derül ki, ha az a bájt önmagában előjeles. Kicsit trükkös, nemde? 🤔
Ahhoz, hogy az egér elkezdje küldeni az adatokat, először is engedélyeznünk kell a kommunikációt. Ez egy sor parancs küldésével történik a 64h
és 60h
portokra. Például, a 0xA8
(Enable Auxiliary Device Interface) parancs küldése a 64h
portra engedélyezi az egér interfészt, majd az egérnek is kell egy 0xF4
(Enable Data Reporting) parancsot küldeni a 60h
porton keresztül. A kommunikáció pontos lépései bonyolultak, és a vezérlő állapotának ellenőrzésével járnak, hogy készen áll-e az adatok fogadására, vagy épp van-e adat, amit kiolvashatunk. Ez egy állapotgépes folyamat, ahol a programnak türelmesen várnia kell a vezérlő válaszára, mielőtt továbblépne. Mintha egy nagyon régi rádiót akarnánk beállítani, ahol minden gombnyomásra várni kell egy picit, mielőtt válaszolna. 📻
IRQ12: Az egér suttogása a processzornak
Rendben, az egér küldi az adatokat, de hogyan értesül erről a processzor? Hát persze, megszakítások (Interrupts) segítségével! A PS/2 egér a IRQ12 (Interrupt Request Line 12) vonalat használja. Amikor az egérnek új adatcsomagja van, „aktiválja” az IRQ12 vonalat, ami jelzi a PIC-nek (Programmable Interrupt Controller), hogy van egy megszakítási kérés. A PIC ezután továbbítja ezt a kérést a CPU felé, amely félbeszakítja éppen futó feladatát, és átadja a vezérlést egy speciális függvénynek: az Interrupt Service Routine (ISR)-nek.
Nekünk, Assembly programozóknak, fel kell készülnünk erre! Ez azt jelenti, hogy írnunk kell egy saját ISR-t, és annak címét be kell jegyeznünk az Interrupt Vector Table (IVT)-be, a 0x0000:0x0000
memóriacímen kezdődő táblázatba, pontosan a 0x2C
offsetre (mert az IRQ12 a 0x2C
vektorhoz van társítva az alapértelmezett konfigurációban). Ez a táblázat tartalmazza az összes megszakítási rutinhoz tartozó memória címeket. Képzeld el, mintha a telefonkönyvbe írnád be a saját számodat, hogy téged hívjanak, ha valaki az „egér” szót kimondja. 📞
Az Assembly kód lélektana (nem is olyan rémisztő, ígérem!)
Most jön a lényeg! A következő lépések vázolják fel, mit kell tennünk Assemblyben egy egyszerű egérkezelő rutinhoz. Fontos megjegyezni, hogy ez a leírás a valós módú (Real Mode) DOS-környezetet feltételezi, ahol a közvetlen hardverhozzáférés a legegyszerűbb. A modern, védett módú (Protected Mode) operációs rendszerekben ez a megközelítés közvetlenül nem működne, hiszen az OS szigorúan kezeli a hardverhozzáférést. 🚨
Egy tipikus ISR a következőképpen nézne ki:
- Kontextus mentése: Mivel az ISR bármikor megszakíthatja a processzor aktuális működését, elengedhetetlen, hogy mentsük az összes regiszter értékét a veremre (stack), amit az ISR fel fog használni. Így az ISR befejeztével vissza tudjuk állítani az eredeti állapotot, és a megszakított program ott folytathatja, ahol abbahagyta. (pl.
PUSH AX
,PUSH BX
,PUSH CX
,PUSH DX
,PUSH ES
,PUSH DS
,PUSH SI
,PUSH DI
,PUSH BP
,PUSH SP
,PUSH FLAGS
). - Várakozás az adatokra: Először is meg kell győződnünk róla, hogy van adat a
60h
porton. Ehhez folyamatosan olvassuk a64h
port állapotregiszterét. Addig várunk, amíg a kimeneti puffer tele (Output Buffer Full) bit (Bit 0) be nem áll. (pl.IN AL, 64h
,TEST AL, 01h
,JZ wait_loop
) - Adatok olvasása: Ha van adat, kiolvassuk mindhárom bájtot a
60h
adatportról.IN AL, 60h
(első bájt: állapot és gombok)MOV [gomb_allapot], AL
IN AL, 60h
(második bájt: X elmozdulás)MOV [egér_x], AL
IN AL, 60h
(harmadik bájt: Y elmozdulás)MOV [egér_y], AL
Ezeket az értékeket eltárolhatjuk memóriában lévő változókban, hogy a fő programunk hozzáférhessen. Fontos az előjeles számok kezelése: ha az X vagy Y elmozdulás bájtja negatív érték, azt előjelkiterjesztéssel (
CBW
,CWD
,CDQ
utasításokkal) kell 16 vagy 32 bites regiszterekbe mozgatni, ha azzal dolgoznánk. - PIC jelzése: Miután feldolgoztuk az adatokat, rendkívül fontos, hogy jelezzük a PIC-nek (Programmable Interrupt Controller), hogy befejeztük a megszakítás kezelését, és a processzor visszatérhet a normális működéshez. Ezt egy EOI (End of Interrupt) paranccsal tesszük, amit a PIC
20h
portjára küldünk. (MOV AL, 20h
,OUT 20h, AL
). Ha ezt elfelejtenénk, a rendszer „lefagyna”, mert a PIC folyamatosan megpróbálná feldolgozni ugyanazt a megszakítást. 🥶 - Kontextus visszaállítása: Visszaállítjuk az összes elmentett regiszter értékét a veremből. (pl.
POP FLAGS
,POP SP
,POP BP
, stb.) - ISR kilépés: Végül a
IRET
(Interrupt Return) utasítással térünk vissza a megszakítás előtti program futásához. AzIRET
automatikusan visszaállítja a CS és IP regisztereket, valamint a FLAGS regisztert a veremből, így a program pontosan ott folytatja, ahol a megszakítás érte.
Természetesen az egér inicializálását is el kell végezni, ami magában foglalja az egér engedélyezését, a mintavételezési gyakoriság beállítását, és az IVT módosítását, hogy a mi ISR-ünk címe legyen az IRQ12-höz rendelve. Ez általában a program indításakor egyszeri feladat. 😊
Kihívások és buktatók: Miért nem a mindennapok hőse?
Bár a fenti leírás elméletileg egyszerűnek tűnik, a gyakorlatban rengeteg buktatóval járhat, különösen modern környezetben:
- Operációs Rendszer Dominancia: A mai OS-ek (Windows, Linux, macOS) teljes mértékben átveszik a hardverek irányítását. Ők állítják be az IVT-t, ők kezelik a megszakításokat, és ők kommunikálnak az egérrel a saját driver rétegeiken keresztül. Ha megpróbáljuk felülírni az IVT-t vagy közvetlenül I/O portokra írni egy modern OS alatt, az nagy valószínűséggel rendszerösszeomláshoz (BSOD vagy Kernel Panic) vezet. Nem vicc, ezt csak nagyon körültekintően, kernelmódban, vagy virtuális gépen próbáljátok ki! 💀
- USB bonyolultság: Mint említettük, az USB egerekhez sokkal komplexebb szoftveres rétegek szükségesek, mint a PS/2-esekhez.
- Időzítés és Versenyhelyzetek: A hardverrel való közvetlen kommunikáció rendkívül érzékeny az időzítésre. Egy rossz várakozás, egy elfelejtett EOI, és máris problémáink vannak. A több magos processzorok és a modern memóriakezelés tovább bonyolítja a helyzetet.
- Biztonság: A közvetlen hardverhozzáférés egy operációs rendszeren belül hatalmas biztonsági kockázatot jelentene. Ezért tiltja a legtöbb modern OS ezt a fajta tevékenységet a felhasználói programok számára.
Mégis, miért érdekes ez ma? A retro feeling és a mélyebb megértés
Akkor miért foglalkozunk mégis ezzel a „régimódi” dologgal? Van néhány nyomós ok:
- Retro Számítástechnika és Emulátorok: Ha DOS-os játékokat fejlesztenél, vagy régi rendszereket tanulmányoznál, ez a tudás felbecsülhetetlen. Sőt, az emulátorok (pl. DOSBox) is ezt a logikát emulálják a háttérben. Egy kis nosztalgia sosem árt! 🎮
- Beágyazott Rendszerek: Egy mikrokontrolleren vagy egy beágyazott Linux rendszeren, ahol nincs teljes értékű OS, ott még ma is ez a közvetlen hardveres kommunikáció a mindennapos. Például egy Raspberry Pi Zero-n, vagy egy Arduino-n egészen másképp néz ki a dolog. Itt a hardverprogramozás a király! 👑
- Mélyebb Megértés: Nincs jobb módja annak, hogy megértsük, hogyan működik egy számítógép a legalapvetőbb szinten, mint ha belemászunk az Assemblybe és a hardver interfészekbe. Ez a tudás segít abban, hogy jobb, hatékonyabb kódokat írjunk, még magasabb szintű nyelveken is. Tudni, hogy mi történik a motorháztető alatt, mindig előnyt jelent. 💡
- A „Mágia” Eloszlatása: Amikor az ember látja, hogy valójában mi zajlik a kulisszák mögött, megszűnik a „fekete doboz” érzés. Ami korábban varázslatnak tűnt (egy egér mozgatja a kurzort!), az hirtelen logikus, mérnöki megoldássá válik. Ez egy egészen elképesztő felismerés! ✨
Záró gondolatok
Ahogy a „hackelés” szó eredeti értelmében is a dolgok működési elvének megértését és azokon való ügyeskedést jelentette, úgy az Assembly szintű hardverprogramozás is a számítástechnika lelkébe enged bepillantást. Bár a modern alkalmazásfejlesztésben ritkán van rá szükségünk, a PS/2 egér X/Y elmozdulásának közvetlen lekérdezése Assemblyből egy lenyűgöző utazás a processzor és a perifériák párbeszédébe. Ez nem csak egy technikai feladat, hanem egyfajta tisztelgés a számítástechnika múltja előtt, és egy ablak a jelen mélyebb rétegeibe. Szóval, ha legközelebb kattintasz egyet, gondolj arra, mennyi mindennek kell a háttérben zajlania, hogy az megtörténhessen! 😉 Ki tudja, talán pont most ébredt fel benned egy régi vágású hardverhacker! Hajrá, kísérletezzetek, de óvatosan! 😉