Canvas not supported
Canvas not supported
Suchen
Bookmark
Canvas not supported
Shine Effekt
Direkt zum Seiteninhalt

Hauptmenü:

Windows Libraries II

 

Autor:

Oscar Kohler

Version:

0.11

Level:

Mittel

Sprache:

C/C++, Assembler


 
Haftungsausschluss / Disclaimer
Kontakt / Contact
Drucken / Print

Programm Beispiele:


Als C / C++ Code Beispiele verwenden wir bei allen folgenden Fällen eine simple Lib, die nur eine Funktion mit dem sprechenden Namen "Mul" zur Verfügung stellt, die 2 integer Zahlen multipliziert und das Ergebnis als integer Wert zurück gibt.
Zum Einsatz kommt der Visual C / C++ Compiler von Microsoft.

Statische Bibliothek erzeugen und anwenden


Um eine statische Lib zu erzeugen geht man prinzipiell folgend vor:


  • Projekt mit der Konfiguration "Static library (.lib)" angelegen

  • Funktionen definieren und schreiben

  • zu einer .lib Datei kompilieren


staticlib.h

int Mul( int a, int b );

staticlib.cpp

#include "stdafx.h"
#include "staticlib.h"

int Mul( int a, int b ) {
  return a*b;
}


Nun können wir unsere Lib bereits verwenden:

  • Lib im Projekt als reference angeben,

  • Funktions Prototypen ( Signaturen ) einbinden,

  • Lib - Funktionen wie "normale" Funktionen verwenden.


test.cpp

#include "staticlib.h"
#include

int _tmain( int argc, _TCHAR* argv[] ) {
  std::wcout <<
"3 * 4 = " << Mul( 3, 4 );
  return 0;
}


Der Linker verbindet die einzelnen .obj und .lib Dateien zu einer gemeinsamen Executable.


Statische Bibliothek Linker / Static library link


Zum "Beweis" sehen wir uns den verlinkten Assembler Code an:


00C512F0  push        4  

00C512F2  push        3  
00C512F4  call        Mul (0C51320h)
...
00C51323  mov         eax,dword ptr [a]  
00C51326  imul        eax,dword ptr [b]


Zuerst werden die beiden Werte auf den Stack gelegt und dann die Lib wie eine interne Funktion aufgerufen.

Anschliessend werden die beiden übergebenen Werte in der Funktion multipliziert.

Aktiviert man allerdings die Option "Whole Program Optimization" dann wird nach dem Linkvorgang nochmals der Compiler aufgerufen, der das Ganze gnadenlos optimiert.
Aus dem Sprung und der Multiplikation bleibt nur noch folgendes übrig:


00C512F0  push 12

Der Compiler hat mit bekommen, dass die Funktion nur einmal mit den Werten 4 und 3 aufgerufen wird und daraus einfach die Konstante 12 abgeleitet :-).

Dynamische Lib ( DLL ) erzeugen


Erstellen wir wieder unsere Beispiel ^ DLL, dazu benötigen wir folgende Schritte:


  • DLL initialisieren
  • Funktionen definieren und ausprogrammieren
  • Funktionen bekannt geben ( exportieren )

Zuerst wird ein neues Projekt angelegt, mit der Konfiguration: "Dynamic Library (.dll)"

Anschliessend definieren wir den Entry-Point:

#include "stdafx.h"

BOOL APIENTRY DllMain( HMODULE hModule,
                      DWORD  ul_reason_for_call,
                      LPVOID lpReserved
  ) {
  switch (ul_reason_for_call) {
     case DLL_PROCESS_ATTACH:
     
case DLL_THREAD_ATTACH:
     
case DLL_THREAD_DETACH:
     
case DLL_PROCESS_DETACH:
        
break;
  }
  
return TRUE;
}


DllMain ist der sogenannte Entrypoint. Bei bestimmten Ereignissen wie z.b: Laden oder Entladen der DLL wird dieser Entrypoint vom System aufgerufen. Anhand des "Reason" können wir entsprechend darauf reagieren und z.B.: Resourcen initialisieren oder freigeben.

Nun schreiben wir unsere gewünschten Funktionen. In unserem Fall wieder die "Mul" Funktion:


int Mul(int a, int b) {
  return a * b;
}


Die Funktion nimmt wieder zwei integer Werte entgegen und gibt uns den multiplizierten integer Wert zurück.

Damit wir diese Funktionen von "aussen" benutzen können, muss die Funktion exportiert, sozusagen von aussen sichtbar gemacht werden.

Nun kommt ein sehr wichtiger Aspekt ins Spiel:

Calling Conventions ( Aufruf Konventionen )


Calling Conventions / Aufruf KonditionenBeim Funktionsaufruf werden oft Parameter übergeben und / oder ein Rückgabewert entgegen genommen. Worüber man sich bei internen Funktionen kaum Gedanken macht, ist beim Aufruf von Funktionen einer DLL jedoch elementar.

  • Werden Parameter in CPU Register übergeben ?
  • Welche Parameter werden wie auf dem Stack abgelegt ?
  • Wer räumt den Stack auf, die Funktion oder der Aufrufer ?

Um die Übergabe zu "standardisieren" hat man sich auf
^ Calling Conventions geeinigt.

Die üblichsten sind folgende:

  • __cdecl ... Parameter werden reverse auf dem Stack abgelegt,
  • __stdcall ... Parameter werden ebenso reverse auf dem Stack abgelegt, der Cleanup unterscheidet sich jedoch,
  • __fastcall ... Parameter werden erst in Registern übergeben, anschliessend auf dem Stack.

Wichtiger ist hier weniger welche Konvention benutzt wird, sondern viel mehr dass Aufrufer und Funktion die gleiche Sprache sprechen und somit dieselbe Konvention benutzen.

Um die Konvention anzuzeigen wird die exportierte Funktion durch Visual Studio entsprechend "dekoriert".
Unsere "Mul" Funktion sieht nach dem Export, je nach gewählter Konvention ( und extern "C" ) folgend aus:

  • __cdecl -> Mul
  • __stdcall -> _Mul@8
  • __fastcall -> @Mul@8

Wobei das Prefix ( _ / @ ) die Konvention, und das Postfix ( @8 ) die Anzahl der übergebenen Bytes anzeigt. 

Eine "Dekoration" ermöglicht auch, die Calling Convention einer "unbekannten" DLL zu erkennen.

DLL Funktionen exportieren


Konsequenterweise müsste man den Begriff: ^ Symbole exportieren verwenden, da es prinzipiell auch möglich ist Variablen und Klassen zu exportieren. Da aber generell vom Export von Variablen abgeraten wird und der Export von Klassen eher unüblich ist, beschränke ich mich hier auf den Export von Funktionen.


Um Funktionen zu exportieren gibt es zwei Möglichkeiten:


Modul Definitionsfile .def


Hierbei wird einfach ein File mit dem Namen der dll + .def angelegt, in unserem Fall dynalib.def und die Funktionen in der Export Section definiert.

EXPORTS
  Mul


Der Funktionsname ist nun "aussen" sichtbar.

Will man nach "aussen" einen anderen Namen sichtbar machen, kann man das folgend angeben:

EXPORTS
  _Mul=Mul


Nun wird die interne Funktion "Mul" mit dem Namen "_Mul" exportiert.

Zusätzlich kann man aber auch das Ordinal, eine Art Indexzahl, exportieren.

EXPORTS
  Mul      @1


Keyword: __declspec(dllexport)


Dies ist ein VisualStudio Keyword das für uns den Export übernimmt, ohne ein .def File anlegen zu müssen.

extern "C" __declspec(dllexport) int Mul(int a, int b) {
  return a * b;
}


Das extern "C" muss bei jedem C++ Export mit angegeben werden.
Der Grund liegt darin, dass der C++ Compiler die Funktionen ^ "mangled" / "dekoriert" um dem Linker erweiterte Informationen zur Verfügung zu stellen. Hierfür gibt es für C++ Compiler keinen definierten Standard, beim VC++ sieht die exportierte Funktion anschliessend so aus: ?Mul@@YAHHH@Z
Da wir aber möchten, dass die Funktion beim Export ebenso: Mul heissen soll, weisen wir den c++ Compiler mit extern "C" an, dass er das dekorieren bitte unterlassen soll.

Nach der Kompilierung wird die dynalib.dll ( + dynalib.lib ) erzeugt, die nun von anderen Programmen verwendet werden kann.

Export Table


Gespeichert werden diese Exporte im PE Format, in der Export table.

DLL Export Table

  • 1 exportierte Funktion mit dem Namen "Mul",

  • Mul erwartet 2 integer Parameter a, b

Dynamische Lib ( DLL ) verwenden


Um Funktionen oder Resourcen einer DLL benutzen zu können, muss diese zuvor in den Speicher geladen werden.

Relokation ( Relocation )


Der Loader lädt die DLL in den virtuellen Adressraum des Programms. Natürlich an eine Stelle, die sich nicht mit dem Programm oder anderen DLLs in diesem Adressraum überschneidet.


Ein Programm muss immer auf interne Adressen zugreifen. Entweder um Daten zu lesen, zu beschreiben oder Sprünge auszuführen. Dabei kann die Adressierung entweder absolut, oder relativ erfolgen.


Relative Adressierung / Relative addressing

Absolute Adressierung


Hierbei wird das Programm auf eine absolute Basis Adresse kompiliert. Bei Programmen wird oft die 0x00400000 verwendet, bei DLLs hingegen oft die 0x10000000. Auch das Programm selbst greift auf absolute Adressen wie z.B.: 0x00400200 zu.
Daher kann das Programm nicht ohne weiteres im Adressraum verschoben werden. Bereits eine zweite DLL würde beim obigen Beispiel zum Problem führen, da sich die beiden DLLs im selben Adressraum beissen würden.

Relative Adressierung ( PIC )


Absolute Adressen versucht man hier zu vermeiden. Adressen werden immer relativ zur Basis angegeben, z.B.: 0x200[BASIS]
Man nennt dies auch "position indepentend adressing ( PIC )". PIC Programme und DLLs können ohne Vorbereitung frei im Speicher verschoben werden.

Code Relokation / code relocation

Wie man in der Grafik sieht, können "Relative" Programme frei im Speicher bewegt werden, was bei den "Absoluten" nicht so einfach möglich ist. Diese müssen erst vorbereitet werden. Man nennt dies: Relokation.

.reloc


Bei der Relokation werden alle Zugriffe auf absolute Adressen auf die gewünschte Ladeadresse umgebogen. Für diesen Zweck enthält das PE File eine ^ .reloc sectionIm Grunde ist die .reloc section nur eine Tabelle die alle Offsets zu Befehlen enthält, die auf absolute Adressen zugreifen.
Der Loader lädt die Reloc Tabelle und addiert die Differenz: Lade Adresse - Basis Adresse zu allen absoluten Adressen die in der Reloc Tabelle angegeben sind.

Bsp:
Loader Adresse: 0x500000, Basis Adresse: 0x400000, Differenz: 0x100000: Wird zu allen absoluten Adresszugriffen addiert

Bei alten Computern ohne Memory Managment Unit ( MMU ) die physische Adressen verwendeten, musste praktisch jedes Programm relokiert werden. Bei aktuellen Prozessoren wird mittels MMU auf virtuelle Adressen zugegriffen, daher entfällt in der Regel die Relokation von Programmen. DLLs hingegen, die absolut adressiert wurden und in den Adressraum eines Programmes geladen werden, müssen oftmals relokiert werden.

Nachteile der Relokation


  • Erhöhte Ladezeiten
  • Müssen immer gepaged werden, da der Loader die Code pages beschreibt

Rebase


Will man die Relokation zur Laufzeit umgehen, so kann man den DLLs unterschiedliche Basis Adressen zuweisen. Dadurch ist es möglich, absolute Programme und ausgewählte absolute DLLs zur selben Zeit im Speicher halten zu können.
Ändert man die Basis Adresse nach der Kompilierung, so nennt man den Vorgang: Rebase

DLL Loader


DLL Ladevorgang / DLL loading


Die DLL wird durch den Loader in den Speicher geladen und initialisiert:


  • Loader prüft, ob die DLL bereits im Speicher liegt, ansonsten wird die DLL in den Speicher geladen.
  • Referenz Counter wird inkrementiert ( +1 )
  • Code und Shared Data Segmente werden an eine "freie" Stelle im Adressraum des Prozesses gemapped, damit dieser darauf zugreifen kann.
  • Die Import Adress Table ( IAT ) wird vom Loader mit den aktuellen Funktions Adressen gepached
    ( DLL Basis + RVA Funktionen )

  • Globale und Statische Daten werden im Adressraum des Prozesses allokiert und von der DLL kopiert.
  • DLL wird initialisiert
    ( Entry Point )


DLL Einbinden


Die Funktionen unserer DLL können wir nun auf zwei unterschiedliche Arten einbinden.

Dynamic Binding ( implizit )


^ Implizites Linken ist die einfachere Weise eine DLL einzubinden, da das System praktisch die ganze Arbeit für uns erledigt.

Beim Programmstart sieht Windows nach, welche Funktionen wir aus welcher DLL benötigen und lädt die entsprechenden DLL's nach. Die DLL muss dabei unbedingt beiliegen, ansonsten bricht Windows mit einer Fehlermeldung ab.

Damit Windows diese Aufgabe erledigen kann, müssen wir zuvor Bescheid geben, welche Funktionen wir aus welcher DLL benötigen.

  • mittels Referenz auf die DLL's geben wir an, aus welchen DLL's wir Funktionen benötigen.
    Dazu wird jeweils die .lib Datei benötigt

  • durch dllimport zeigen wir Windows, dass die aufgerufene Funktion in einer DLL zu finden ist


Der Compiler legt nun einen Funktions Stub an und speichert die benötigte Funktion + Name der DLL im PE in der Import Table ab.


 DLL Import Table


Beim Programmstart lädt der Loader alle DLL's, inklusive deren Abhängigkeiten, die in der Importtabelle vermerkt sind und stellt diese dem Programm zur Verfügung. Kann eine DLL nicht aufgelöst werden, erhält man eine Fehlermeldung und das Programm bricht ab.  


Beispiel:


dll.h

extern "C" __declspec(dllimport) int Mul(int a, int b);

test.cpp

#include "stdafx.h"
#include "dll.h"
#include

using namespace std;

int _tmain(int argc, _TCHAR* argv[]) {
  cout <<
"5 * 7 = " << Mul( 5, 7 );
  return 0;
}


Wie wir bei diesem Beispiel sehen, können wir die Funktionen der DLL wie interne Methoden benutzen.

Sehen wir uns dazu den Assembler Code an: 


Der Kompiler setzt einen "Stub" ein und springt zu einer Adresse die in 0FBA27Ch gespeichert ist.

Zuvor werden die Parameter 5 & 7 zur Übergabe an die Funktion auf dem Stack abgelegt.


00FB14D0 push 7

00FB14D2 push 5
00FB14D4 call dword ptr [__imp__Mul (0FBA27Ch)]

An der Adresse 0FBA27Ch steht folgender 32 bit Wert:

00FBA27C c3 10 77 0f

Adressen werden auf einem x86 im "Little Endian" Format gespeichert, daher müssen wir die Reihenfolge umdrehen.

-> 0f 77 10 c3

Der Call führt uns dann zum effektiven Jump:

0F7710C3 jmp Mul (0F771360h)

An dieser Adresse beginnt nun die "Mul" Funktion unserer DLL:

0F771360 push ebp
...
0F77137E mov eax,dword ptr [a]
0F771381 imul eax,dword ptr [b]

Schlussendlich erledigt die Funktion in unserer DLL erwartungsgemäss die Multiplikation ( imul ).

Runtime Binding ( explizit )


^ Explizites Linken ist zwar etwas aufwendiger, bietet dafür einige zusätzliche Möglichkeiten:


  • liegt die DLL nicht bei, bricht das Programm nicht einfach ab, sondern wir können den Fehler entsprechend behandeln

  • je nachdem, können wir DLL's erst dann laden wenn sie benötigt werden

  • Plugin Systeme lassen sich sehr elegant realisieren


Bevor wir die DLL benutzen können müssen wir folgende Dinge erledigen:

  • DLL laden,

  • Adressen der Funktionen ermitteln.


loader.cpp

#include "stdafx.h"
#include
#include

using namespace std;

typedef int (*Mul)( int a, int b );

int _tmain(int argc, _TCHAR* argv[]) {
  HINSTANCE hLib = LoadLibrary( _T(
"DynaLib.dll"));
  
if ( hLib ) {
     Mul hMul = (Mul)GetProcAddress( hLib,
"Mul" );
     
if ( hMul ) {
        cout <<
"9 * 4 = " << hMul( 9, 4 );
        FreeLibrary( hLib );
        return 0;
     }
     
else {
        cout <<
"Can't find function !";
        
return 1;
     }
  }
  else {
     cout <<
"Can't load dll !";
     
return 1;
  }
}


Die Funktionen der DLL können wir anhand des Namens ...

Mul hMul = (Mul)GetProcAddress( hLib, "Mul" );

... oder aber auch anhand des Ordinals ermitteln ...

Mul hMul = (Mul)GetProcAddress( hLib, (LPCSTR)((WORD)(1)) );

Üblich ist allerdings anhand des Namens.


Der Compiler hat nun folgendes daraus fabriziert:


LoadLibrary:

00A014C0 push offset string L"DynaLib.dll" (0A07870h)

00A014C5 call dword ptr [__imp__LoadLibraryW@4 (0A0A23Ch)] -> "DynaLib.dll" laden

00A014D2 mov dword ptr [hLib],eax
00A014D5 cmp dword ptr [hLib],0
00A014D9 je wmain+0B2h (0A01552h) -> Returnwert in hLib speichern und bei 0 einen Fehler ausgeben
...
GetProcAddress:
00A014DD push offset string "Mul" (0A0786Ch)
00A014E2 mov eax,dword ptr [hLib]
00A014E5 push eax
00A014E6 call dword ptr [__imp__GetProcAddress@8 (0A0A238h)] -> Adresse der Funktion "Mul" ermitteln
00A014F3 mov dword ptr [hMul],eax
00A014F6 cmp dword ptr [hMul],0
00A014FA je wmain+96h (0A01536h) -> Returnwert in hMul speichern, und bei 0 einen Fehler ausgeben
...
Funktions Aufruf:
00A014FE push 4
00A01500 push 9
00A01502 call dword ptr [hMul] -> "Mul" Funktion mit den Parametern 4 & 9 aufrufen


 
Kein Kommentar
 
Letzte Änderung: 03.02.2015
button Canvas not supported button
Zurück zum Seiteninhalt | Zurück zum Hauptmenü