Iczelion - 17 - Dynamic Link Libraries

Tutorial 17: Dynamic Link Libraries


In diesem Tutorial werden wir etwas DLLs lernen, was sie sind und wie sie erstellt werden.

Sie können das Beispiel hier herunterladen.

Theorie:

Wenn Sie lange genug programmieren, werden Sie feststellen, dass die Programme die Sie normalerweise schreiben einige Code Routinen gemeinsam haben. Es ist reine Zeitverschwendung diese immer wieder neu zu schreiben, wenn Sie ein neues Programm programmieren. In der guten alten Zeit von DOS speicherten Programmierer diese gemeinsam genutzten Routinen in einer oder mehr Libraries (~Bibliothek). Wenn sie die Funktion benutzen wollten, haben sie die Library nur in die Objekt-Datei gelinkt und der Linker extrahierte diese Funktionen aus der Library und fügte sie in die endgültige ausführbare Datei ein. Dieser Prozess wird statisches Linken genannt. C Laufzeit Bibliotheken sind ein gutes Beispiel. Der Nachteil dieser Methode ist, dass sie identische Funktionen in jedem Programm haben, die diese aufrufen. Ihr Festplattespeicher wird unnötig verschwendet, indem identische Kopien der Funktion gespeichert werden. Für DOS Programme war diese Methode noch ziemlich akzeptabel, da in der Regel sowieso nur ein Programm im Speicher aktiv war. So gab's keine Speicherverschwendung.

Unter Windows wird die Situation wesentlich kritischer, da Sie verschiedene Programme simultan laufen lassen können. Speicher wird ziemlich schnell gefressen, wenn ihr Programm ziemlich groß ist. Windows hat die Lösung für diesen Typen von Problem: Dynamic Link Libraries(=DLL=dynamisch gelinkte Bibliothek). Eine DLL ist eine Art gemeinsamer Pool von Funktionen. Windows lädt nicht mehrere Kopien einer DLL in den Speicher, so dass, selbst wenn mehrere Instanzen ihres Programmes laufen, nur eine Kopie der DLL im Speicher von den Programmen verwendet wird. Und ich sollte diesen Punkt ein wenig erklären. In Wirklichkeit haben alle Prozesse, die die selbe DLL benutzen, ihre eigene Kopie der DLL. Es sieht so aus, als ob mehrere Kopien der DLL im Speicher wären. Aber in Wirklichkeit zaubert Windows ein wenig mit Paging und alle Prozesse teilen den selben DLL-Code. So ist im physikalischen Speicher nur eine Kopie des DLL-Codes. Wie auch immer, jeder Prozess wird seine eigene Daten-Sektion der DLL haben.

Das Programm linkt die DLL während der Laufzeit, anders als die alten statischen Libraries Das ist auch der Grund warum es Dynamic Link Library heißt. Sie können eine DLL während der Laufzeit auch aus dem Speicher entfernen, wenn Sie sie nicht mehr brauchen. Wenn das Programm das einzige ist, dass diese DLL benutzt, wird sie unverzüglich aus dem Speicher entfernt. Wenn die DLL aber noch von anderen Programmen benutzt wird, verbleibt die DLL im Speicher bis das letzte Programm, dass sie auch benutzt, sie nicht mehr benötigt.

Wie auch immer, der Linker hat ein schwierigeren Job, wenn er Adressen für die endgültige ausführbare Datei korrigieren muss. Da er die Funktionen nicht "extrahieren" kann und sie in die endgültige ausführbare Datei einfügen kann, muss irgendwie genügend Informationen über die DLL und Funktionen in der endgültige ausführbare Datei gespeichert werden, damit die richtige DLL zu Laufzeit lokalisiert und geladen werden kann.

Hier kommen Import Libraries ins Spiel. Eine Import Library enthält Informationen über die DLL, die sie repräsentiert. Der Linker kann die Infos aus den Import Libraries extrahieren, die er benötigt und in die ausführbare Datei schreiben. Wenn der Windows-Loader das Programm in den Speicher lädt, sieht er, dass das Programm eine DLL linkt und sucht danach und mapped sie in den Speicherbereich des Prozesses und korrigiert die Adressen für die aufzurufenden Dateien in der DLL.

Sie können die DLL auch von Ihnen selbst laden lassen ohne sich auf den Windows-Loader zu verlassen. Diese Methode hat seine Vor- und Nachteile:

  • Es wird keine Import Library benötigt, demnach können sie jede DLL laden und benutzen, selbst wenn sie keine Import Library haben. Sie müssen aber dennoch über die Funktionen bescheid wissen und die Parameter und solche Dinge.
  • Wenn Sie den Loader die DLL für ihr Programm laden lassen und der Loader kann die DLL nicht finden, wird er eine Fehlermeldung "Eine benötigte DLL Datei, xxxx.dll wurde nicht gefunden" und zack! ihr Programm hat keine Chance zu laufen, auch wenn die DLL nicht essentiell für den Programmablauf ist. Wenn Sie die DLL selbst laden und die DLL kann nicht gefunden werden und sie enthält keine wichtigen Informationen, so kann ihr Programm den Benutzer darüber unterrichten und mit der Ausführung fortfahren.
  • Sie können *undokumentierte* Funktionen benutzen, die nicht in Import Libraries enthalten sind. Vorausgesetzt, dass Sie genügend über die Funktion wissen.
  • Wenn Sie LoadLibrary benutzen, müssen Sie GetProcAdress für jede Funktion, die Sie benutzen wollen, aufrufen. GetProcAdress liefert den Einstiegspunkt-Adresse eine Funktion in einer DLL. Demnach könnte ihr Code ein wenig größer und langsamer werden, aber nur unwesentlich.
Nachdem wir Vor-/Nachteile eines LoadLibrary Aufrufes gesehen haben, gehen wir ins Detail wie wir eine DLL nun erzeugen.

Der folgende Code ist ein DLL-Skelet.

;-------------------------------------------------------------------------------------- ; DLLSkeleton.asm ;-------------------------------------------------------------------------------------- .386 .model flat,stdcall option casemap:none include \masm32\include\windows.inc include \masm32\include\user32.inc include \masm32\include\kernel32.inc includelib \masm32\lib\user32.lib includelib \masm32\lib\kernel32.lib .data .code DllEntry proc hInstDLL:HINSTANCE, reason:DWORD, reserved1:DWORD mov eax,TRUE ret DllEntry Endp ;--------------------------------------------------------------------------------------------------- ; Dies ist eine Dummy-Funktion ; Sie macht nichts. Ich füge sie hier ein, um zu zeigen wo sie ihre ; Funktionen in eine DLL einfügen könne. ;---------------------------------------------------------------------------------------------------- TestFunction proc ret TestFunction endp End DllEntry ;------------------------------------------------------------------------------------- ; DLLSkeleton.def ;------------------------------------------------------------------------------------- LIBRARY DLLSkeleton EXPORTS TestFunction


Das obere Programm ist das DLL-Skelett. Jede DLL muss eine Einsprungs-Funktion haben. Windows ruft diese Einsprungs-Funktion jedesmal auf, wenn:

  • Die DLL das erste Mal geladen wird
  • Die DLL aus dem Speicher geladen wird.
  • Ein Thread im selben Prozess erzeugt wird.
  • Ein Thread im selben Prozess zerstört wird.
DllEntry proc hInstDLL:HINSTANCE, reason:DWORD, reserved1:DWORD mov eax,TRUE ret DllEntry Endp


Sie können die Einsprungs-Funktion so benennen wie Sie möchten, so lange Sie ein passendes END <Einsprungs-Funktions-Namen> haben. Diese Funktion benötigt drei Parameter, wobei nur die ersten beiden von Bedeutung sind.

hInstDLL ist das Modul Handle der DLL. Es ist nicht das selbe wie das Instanz-Handle eines Prozesses. Sie sollten diesen Wert speichern, falls Sie ihn für später benötigen. Sie können ihn nicht so einfach wieder erfragen.
reason kann einer der folgenden vier Werte sein:

  • DLL_PROCESS_ATTACH
  • Die DLL erhält diesen Wert, wenn sie das erste Mal in den Adressraum des Prozesses kommt. Sie können diese Gelegenheit nutzen um Initialisierungen vorzunehmen.
  • DLL_PROCESS_DETACH
  • Die DLL erhält diesen Wert, wenn sie aus dem Speicherbereich des Prozesses entfernt wird. Sie können diese Gelegenheit nutzen, um Aufräumarbeiten zu verrichten, wie Speicher de-allozieren und so weiter.
  • DLL_THREAD_ATTACH
  • The DLL erhält diesen Wert, wenn der Prozess einen neuen Thread erzeugt.
  • DLL_THREAD_DETACH
  • The DLL erhält diesen Wert, wenn ein Thread aus dem Prozess zerstört wird.
Sie geben TRUE in EAX zurück, wenn sie DLL weiterlaufen lassen wollen. Wenn Sie FALSE zurückgeben, wird die DLL nicht geladen. Zum Beispiel, wenn Ihr Initialisierungs-Code Speicher alloziieren muss und das nicht erfolgreich verläuft, sollte die Einsprungs-Funktion FALSE zurückliefern, um zu indizieren, dass die DLL nicht laufen kann.

Sie können Ihre Funktionen in der DLL nach oder vor der Einsprungs-Funktion plazieren. Wenn Sie sie aber für andere Programme verfügbar sein sollen, müssen Sie die Namen in die Export-Liste der Modul-Definitions-Datei (.def) schreiben.

Eine DLL benötigt in der Entwicklungsphase eine Modul-Definititions-Datei. Wir werfen nun ein Blick drauf.

LIBRARY DLLSkeleton
EXPORTS TestFunction

Normalerweise werden die ersten beiden Zeilen benötigt. Das LIBRARY Statement definiert den internen Modul-Namen der DLL. Er sollte identisch mit dem Dateinamen der DLL sein.
Das EXPORTS Statement teilt dem Linker mit, welche Funktionen aus der DLL exportiert werden sollen, sprich, welche andere Programme auch nutzen sollen können. In diesem Beispiel wollen wir anderen Modulen die Möglichkeit bieten TestFunction aufzurufen, weswegen wir diesen Namen beim EXPORTS Statement einfügen.
Eine weitere Änderung ist in der Linker Parameter-Liste. Sie müssen /DLL und /DEF:<ihr def Dateiname> ihrem Linker als Parameter übergeben, ungefähr so:

link /DLL /SUBSYSTEM:WINDOWS /DEF:DLLSkeleton.def /LIBPATH:c:\masm32\lib DLLSkeleton.obj

Die Assembler Parameter bleiben die gleichen, nämlich /c /coff /Cp. Nachdem Sie die Objekt-Datei gelinkt haben, erhalten Sie eine .dll und eine .lib Datei. Die .lib Datei ist die Import Library, die Sie benutzen können, um andere Programme zu linken, die die Funktionen aus der DLL benutzen wollen.

Als nächstes zeige ich Ihnen wie man LoadLibrary benutzt, um eine DLL zu laden.

;--------------------------------------------------------------------------------------------- ; UseDLL.asm ;---------------------------------------------------------------------------------------------- .386 .model flat,stdcall option casemap:none include \masm32\include\windows.inc include \masm32\include\user32.inc include \masm32\include\kernel32.inc includelib \masm32\lib\kernel32.lib includelib \masm32\lib\user32.lib .data LibName db "DLLSkeleton.dll",0 FunctionName db "TestHello",0 DllNotFound db " Konnte Library nicht laden",0 AppName db "Lade Library",0 FunctionNotFound db "TestHello Funktion nicht gefunden",0 .data? hLib dd ? ; das Handle der Library (DLL) TestHelloAddr dd ? ; die Adresse der TestHello Funktion .code start: invoke LoadLibrary,addr LibName ;--------------------------------------------------------------------------------------------------------- ; Rufen Sie LoadLibrry mit dem Namen der gewünschten DLL auf. Wenn der Aufruf erfolgreich war, ; wird das Handle der Library (DLL) zurückgeliefert. Wenn nicht, wir NULL zurückgeliefert. ; Sie können das Library-Handle GetProcAdress oder jeder anderen Funktion die ein Library- ; Handle benötigt, als Parameter übergeben. ;------------------------------------------------------------------------------------------------------------ .if eax==NULL invoke MessageBox,NULL,addr DllNotFound,addr AppName,MB_OK .else mov hLib,eax invoke GetProcAddress,hLib,addr FunctionName ;------------------------------------------------------------------------------------------------------------- ; Wenn Sie das Library Handle erhalten, übergeben Sie es GetProcAddress mit der Adresse ; des Namen der Funktion in der DLL die Sie aufrufen möchten. Sie gibt die Adresse der Funktion ; zurück, wenn sie erfolgreich ausgeführt wird. Ansonsten NULL. ; Funktionsadressen ändern sich nicht, solange Sie die Library nicht aus dem Speicher entfernen ; und neu laden. Deswegen können Sie diese in globale Variablen für den späteren Gebrauch speichern. ;------------------------------------------------------------------------------------------------------------- .if eax==NULL invoke MessageBox,NULL,addr FunctionNotFound,addr AppName,MB_OK .else mov TestHelloAddr,eax call [TestHelloAddr] ;------------------------------------------------------------------------------------------------------------- ; Als nächstes können Sie die Funktion mit einem einfach Call und der Variable, die die Adresse enthält aufrufen. ;------------------------------------------------------------------------------------------------------------- .endif invoke FreeLibrary,hLib ;------------------------------------------------------------------------------------------------------------- ; Wenn Sie die Library nicht mehr benötigen, entfernen Sie sie aus dem Speicher mit FreeLibrary. ;------------------------------------------------------------------------------------------------------------- .endif invoke ExitProcess,NULL end start


Wie Sie sehen können, ist die Benutzung von LoadLibrary ein klein wenig mehr Aufwand, aber es ist auch flexibler.


Deutsche Übersetzung: Joachim Rohde
Die original Win32Asm-Tutorials stammen von Iczelion's Win32 Assembly HomePage