(ASM_014_DLL_Simple.asm)
Amikor egy alkalmazás elindul, akkor az esetek többségében nincs szükség a programozáskor elkészített, vagy felhasznált összes folyamatra és kódra. Például, ha egy alkalmazásunk mondjuk egy vonalkód olvasótól is képes adatokat fogadni (mondjuk egy raktári nyilvántartó rendszer), de általában a billentyűzetről, vagy egy listából választ a felhasználó, akkor felesleges lenne a vonalkód olvasóhoz tartozó nagyméretű programmodult betölteni, és ezzel a drága memória-erőforrást terhelni.
Szintén gazdaságosabb lenne, ha ezt az egyszer megírt vonalkód olvasó kódot más alkalmazások is használhatnák, úgy, mintha az csak őket szolgálná ki. Vagyis dinamikusan csak akkor kellene a memóriában megjelennie ennek a kódnak, ha éppen szükség van rá, sőt ha már nincs, akkor fel kellene szabadítani a foglalt memória területet.
A virtuális memória tárgyalásánál említettük, hogy az azonos folyamathoz tartozó szálak ugyanarra a címtartományra képeződnek le, vagyis „látják” egymás változóit és kódjait is. Ha most lenne arra mód, hogy olyan programmodulokat is lehessen fejleszteni, melyek tetszőleges virtuális címre tölthetők, és ott az egyes alkalmazások igényeinek megfelelő számú adatterülettel, de egy kódbázissal szolgálnák ki az igényeket, majd használat után „eldobhatók” lennének, akkor a programok mindenkori futási helyigénye a minimumon lenne tartható, és az operációs rendszer egy nagyon takarékos és gazdaságos működést valósíthatna meg. Ez a dinamikusan betölthető könyvtárak koncepciója, amit „DLL”-nek vagy („Dynamic Link Library”) nevezünk, és ami épp a fenti célokat szolgálja.
Szokás ezeket az állományokat dinamikusan csatolt könyvtáraknak, vagy alkalmazás-kiterjesztésnek („application extension”) nevezni. Lényeges különbség azonban, hogy ezek a futtatható függvény-könyvtárak nem szerkesztődnek statikusan az eredményként keletkező „exe” fájlhoz, hanem attól különálló állományként „dll” kiterjesztéssel készülnek el, és a fájlrendszerben tárolódnak (általában az alkalmazás saját alkönyvtárában). Az elnevezés tehát arra utal, hogy amikor egy „dll”-ben tárolt függvényre szükség van, akkor azt az operációs rendszer az operatív memóriába tölti, és az adott függvény belépési pontjára adja a vezérlést.
Itt emlékeztetünk a „reentrant” függvény hívás előzetesen felvetett igényére. Ez úgy valósul meg, hogy minden, a „dll”-ben tárolt függvényt a hívó folyamat saját címtartományon létrehozott „stack”-en keresztül paraméterezheti, így az egy időben külön adatterületek használatával megvalósuló kiszolgálás azt az „érzést” biztosítja a folyamatoknak, mintha a „dll” csak őket szolgálná ki.
Készítsünk tehát egy olyan modult, ami dinamikusan betölthető a memóriába, és ugyanakkor az éppen futó folyamat képes a benne kódolt függvényeket meghívni, azoknak paramétereket átadni, és az eredményeket átvenni. A „dll” fejlesztés valójában nagyon hasonlít az alkalmazásfejlesztéshez, csak a végeredmény nem egy futtatható „exe” kiterjesztésű fájl lesz. A fordítás és szerkesztés után egy „dll” kiterjesztésű fájlt kapunk, amit az őt használó alkalmazásnak el kell tudnia érni. A VS2010 beállításaiban csak kevés változást eszközlünk, elsősorban az alábbi helyeken:
A képen láthatóak alapján
a „Configuration Properties/General” tulajdonságok közül a „Target Extension”-t „.dll”-re, a „Configuration Type”-okból pedig „Dynamic Library (.dll)„-t válasszuk.
a „System” tulajdonságok közül a „SubSystem” tulajdonságot állítsuk be „Not Set”-re.
Az „Input” csoportban a „Modul” definíciós fájlt állítsuk be a fájl nevével megegyező és „def” kiterjesztésű fájlra.
Az „Advanced” fülön a start címkét hagyjuk üresen.
A definíciós fájl, melynek a kiterjesztése „def”, arra hivatott, hogy a készülö „dll”-ben a nyilvános hatókörű függvényeket megjelöljük. Ennek a fájlnak a feldolgozásával állítja be a hívható függvények körét a „linker”. Az ASM_014_DLL_Simple.def nevű definíciós fájl tartalma tehát az alábbi:
A forráslista, mely tulajdonképpen a hívható függvények kódjait tartalmazza, szintén egy külön fájlban kap helyet, és az alábbiakban részletezett:
Ebben a forráslistában hagyományos és már jól ismert függvény definíciókat találunk, és ezek egy kivételével mind egyenrangúak. A „LIBMain” címkével jelzett függvény a „dll” belépési pontja, melynek a „dll” használatával kapcsolatban van jelentősége, most csak a legegyszerűbb kialakításban tárgyaljuk, és a következő példán részletezzük. Fontos tudnunk, hogy ha ez a függvény „0”-val (false) tér vissza, akkor a „dll” nem töltődik be.
A példa „dll”-ünk két szabadon hívható funkciót tartalmaz, ezek a „MsgFromDll”, és a „DLLTask” nevűek. Ebben a példában csak az elsőt fogjuk meghívni. Ez a függvény egy üzenetablakot dob a képernyőre, amiben üdvözöl bennünket.
A befogadó alkalmazás, ami a „dll”-t használni fogja, egy külön projektben kerül fejlesztésre, melynek neve „ASM_015_DLL_Simple_Test”, és az alábbiakban látható:
A „dll”-t közvetlenül nem futtathatjuk, hogy használhassuk, ahhoz egy olyan programot is írnunk kell, melyben az adott funkcionalitásokat meghívjuk. Fent úgy mondjuk, „hosztoló alkalmazás” fogja a „dll”-t betölteni magának, amikor szüksége van rá, és meghívja az „MsgFromDLL” függvényt. Érdemes megfigyelni, hogy a „dll” használata tulajdonképpen három fázisból áll, az elsőben a „LoadLibrary”-val betöltjük a „dll”-t:
A fenti kódrészletből most azt az operációt látjuk, amikor a „dll” betöltését valósítjuk meg. A „LoadLibrary” függvény paramétere egy nullával terminált sztring, ami a „dll” teljes nevét tartalmazza. Ha a betöltés sikeres, akkor a betöltött kódmodul „handle”-jét, vagyis azonosítóját kapjuk. Ez annak a memóriaterületnek a kezdő címe, ahol a „dll” a betöltés után található. A visszatérésként kapott „eax” tartalom tehát egy virtuális memóriacím, ahol a betöltött „dll” tárolása kezdődik. A hívás után a regisztertartalom az alábbi:
Vagyis a betöltött „dll” tárfoglalása a 74330000h virtuális címen kezdődik. A következő lépés az, hogy a használni kívánt függvény belépési pontját meghatározzuk. Erre szolgál a „GetProcAddress” függvény, melynek paraméterei a „dll” korábban megszerzett „handler”-e, valamint a függvény nevét tartalmazó sztring címe. Az itt kapott visszatérési érték már közvetlenül a „dll”-ben található függvény-belépési pont virtuális címe.
A regisztertartalmak ablakában láthatjuk, hogy a függvény címe 7433100FH ami éppen 100F=4096+15 = 4111 bájttal van magasabban mint a „handler”-ként kapott 74330000h cím. Ha a hívás után a „Dissasembly” ablakra váltunk, akkor láthatjuk, hogy valóban itt van a függvényünk belépési pontja és érthetővé válik a vezérlés átadás folyamata.
Vagyis a vezérlésátadás úgy valósul meg, hogy először egy ugrási táblára kapunk címet, ahonnan egy relatív „jump” utasítás viszi el vezérlést a 7433103ch címre, vagyis a meghívott függvényhez.
Figyelemmel arra, hogy a főprogramban az egyes „dll” használati fázisokat folyamatosan kiírtuk, a futtatás kép az alábbiakban látható, ahol a konzol ablakban a hívást megelőző kiírások látszanak, valamint a már „dll”-ből származó üzenetablak.
Természetesen az egyes futások alkalmával a fenti virtuális címek eltérhetnek a példában látottaktól, de a viszonyuk és a betöltés elve mindig azonos.