...someplace, where there isn't any trouble? Do you suppose there is such a place, Toto?

64 bit Windows - Teil 9

Heute geht es mal um was ganz anderes, nämlich um Managed Code unter x64. Jawoll, ich habe die Stirn, was über Managed Code zu schreiben, obwohl ich bis vor zwei Wochen noch einen weeeiiiten Bogen drumrum gemacht habe. Genauer gesagt will ich was über Managed Code auf x64 schreiben, der P/Invoke macht aus C#-Code, denn dabei kann so einiges schiefgehen. Mit P/Invoke kann man aus Managed Code traditionellen unmanaged Code aufrufen, indem man bei der Deklaration einer Methode angibt, wie die DLL heißt, aus der der unmanaged Code aufgerufen wird, wie die Funktion heißt, die man daraus aufrufen will und wie die Funktionsparameter gemarshalt (gemarshaled?) werden sollen.

Beispielsweise könnte ich schon seit Jahren eine 32-bittige unmanaged DLL namens foo.dll haben, die eine Funktion Bar mit folgendem Prototyp exportiert:

  BOOL __stdcall Bar(DWORD dwLevel, LPVOID pSrc, DWORD dwFlags, 
      LPCWSTR szString, LPCWSTR szMessage);

Ich komme nun auf die Idee, in einem funkelnagelneuen .NET-Assembly diese DLL verwenden zu wollen weil in dieser uralten DLL extrem viel Gehirnschmalz drinsteckt, oder weil das Biest extrem performant und deswegen unmanaged sein muß oder was auch immer.

Dann könnte eine passende P/Invoke-Implementation in C# beispielsweise so aussehen:

namespace MyCompany.Whatever.DoIlooklikeIgivaShit
{
    public class FooDll
    {

       [DllImport("foo.dll", CharSet = CharSet.Unicode, 
                             CallingConvention = 
                             CallingConvention.Winapi, 
                             SetLastError=true,
                             EntryPoint="Bar")]
       public static extern bool FooBar(uint dwLevel, IntPtr pSrc, 
             uint dwFlags, 
             [MarshalAs(UnmanagedType.LPWStr)] string strString,            
             [MarshalAs(UnmanagedType.LPWStr)] string strMsg);
    }
}

Wenn man das nun so macht und mit der Einstellung "Any CPU" für das .NET-Assembly buildet, dann funktioniert das wunderbar auf einer herkömmlichen x86-Maschine. Die Welt ist schön. Wenn man aber nun dieselben Files auf eine x64-Maschine mit dem .NET-Framework 2.0 kopiert und denkt, das funktioniere da genauso, stellt man plötzlich fest, daß der Prozeß nach kürzester Zeit mit folgender Fehlermeldung kollabiert:

Unhandled Exception: System.BadImageFormatException: 
An attempt was made to load a program with an incorrect format. 
(Exception from HRESULT: ....

Das Problem hierbei ist, daß das .NET-Assembly mit "Any CPU" übersetzt worden ist. Daher wird versucht, den Prozeß als nativen Prozeß (für die CPU) auszuführen, also als x64-Prozeß. Wenn aber leider die DLL, die aus diesem 64-bittigen Prozeß aufgerufen werden soll, eine 32-bittige ist, wie in unserem Fall die Datei foo.dll, dann geht das nicht. Tyischer Fall von "Isso".

Man hat nun zwei Optionen:

  • Das .NET-Assembly nur für x86 übersetzen, wobei die schöne Prozessorunabhängigkeit von .NET verloren geht (sounds like Java, eh?)
  • Die Datei foo.dll native auf x64 portieren

Wer mit Option 1 zufrieden ist, ist fein raus, wird aber niemals die Goodies von x64 in Anspruch nehmen können und kann jetzt mit weiterlesen aufhören.

Im folgenden nehme ich an, daß wir aber unser tolles .NET-Assembly unter x86 als native x86-Prozeß laufen lassen wollen und unter x64 als native x64-Prozeß. Wir lassen also die Einstellung für das Assembly auf "Any CPU" und fangen an, die Datei foo.dll nach x64 zu portieren, schließlich haben wir auch ein paar native x64 Binaries aus unmanaged Code, die einen x64-Port von foo.dll auch ganz gut benutzen könnten.

Haben wir jetzt ein reinrassiges x86-Softwareprodukt und ein reinrassiges x64-Softwareprodukt, wo es keine Übereinstimmungen im Auslieferungsumfang zwischen beiden Varianten geben soll, dann haben wir jetzt kein Problem mehr. Besteht unser Softwareprodukt jedoch aus gemischten Komponenten, also teils aus x86-Binaries und x64-Binaries und installieren wir alles in ein und dasselbe Installationsverzeichnis, dann haben wir jetzt ein Problem: Wie soll denn die native x64-Version von foo.dll heissen? foo.dll? Geht nicht, so heisst schon die x86-Version. foox64.dll? Geht nicht, so kann unser tolles .NET-Assembly sie nicht laden, weil es im DllImport-Attribut was von "foo.dll" stehen hat und nicht "foox64.dll". Sollen wir dann zwei Versionen von unserem Assembly machen, die sich jeweils nur im Dll-Namen im DllImport-Attribut unterscheiden? Can you say maintenance nightmare?

Was wir brauchen, ist eigentlich ein "Mediator", der automatisch die Aufrufe aus unserem tollen .NET-Assembly unter x86 in Aufrufe in die x86-Variante foo.dll übersetzt und unter x64 in die x64-Variante foox64.dll. Und an der Stelle kommt uns eine nette Eigenschaft von native DLLs entgegen, nämlich die Möglichkeit, Aufrufe in andere DLLs zu forwarden. Wir erzeugen also eine neue DLL namens foofwd.dll, die keinerlei Code enthält, sondern alle Exporte von foo per def-file identisch exportiert. Diese foofwd.dll builden wir einmal für x86 und einmal für x64 und nennen sie beidesmal gleich, nämlich foofwd.dll. Die x86-Version wird alle ihre Exporte nach foo.dll forwarden und die x64-Version alle ihre Exporte nach foox64.dll. Mit zwei getrennten def files, die man evtl auch noch als Teil des Builds skriptgesteuert erzeugen läßt, ist das absolut Kinderfasching.

Was ist aber nun mit unserem tollen .NET-Assembly? Das .NET-Assembly schreiben wir nun, da wir um den "Mediator" in Form von foofwd.dll wissen, in vorauseilendem Gehorsam so, daß er ausschließlich die Datei "foofwd.dll" in seinen DllImport-Attributen referenziert, also nicht foo.dll oder foox64.dll.

Und was liefern wir nun auf welcher Plattform wie aus?

Auf einem 32-bit Windows XP macht es gar keinen Sinn, die x64-Binaries mitauszuliefern. Unser Installationsprozeß wird also die x64-Binaries auf diesem Zielsystem komplett übergehen. Die Datei foofwd.dll landet hier zusammen mit foo.dll in unserem Installationsverzeichnis und alles ist gut, denn die .NET-basierten Prozesse können hier nur als 32-bit-Prozesse laufen, finden zur Laufzeit die 32-bittige foofwd.dll im Installationsverzeichnis, welche beim Laden automatisch die 32-bittige foo.dll nachzieht.

Auf einem 64-bittigen Windows XP machen wir erstmal die Installation exakt wie unter dem 32-bittigen XP, kopieren also die 32-bittige foofwd.dll und die 32-bittige foo.dll ins Installationsverzeichnis. Dann kopieren wir die 64-bittige foox64.dll ins Installationsverzichnis. Damit aber ein .NET-basierter Prozess nun seine 64-bittige forwarder DLL findet, müsssen wir diese an einen Ort kopieren, wo ein beliebiger 32-bittiger Prozeß (also beispielweise auch ein .NET-Assembly das nicht mit "Any CPU" übersetzt wurde sondern mit "x86") sie auf keinen Fall aus Versehen laden kann (was nämlich fehlschlagen würde) aber ein 64-bittiger Prozeß sie auf jeden Fall findet.

Genau dieses Problem hat MS ja auch, denn es gibt unter Windows XP x64 Edition beispielsweise einen 32-bittigen regedit.exe und einen 64-bittigen regedit.exe. Oder einen 32-bittigen cmd.exe und einen 64-bittigen cmd.exe. Beide linken dynamisch gegen kernel32.dll aber der 32-bittige regedit gegen den 32-bittigen kernel32.dll und der 64-bittige regedit gegen den 64-bittigen kernel32.dll, und beide Versionen von kernel32.dll befinden sich in verschiedenen Unterverzeichnissen, die durch file system redirection voneinander abgeschottet sind. So ist die 32-bittige Version von kernel32.dll unter %systemroot%\syswow64 zu finden und die 64-bittige Version von kernel32.dll unter %systemroot%\system32. Einem 32-bittigen Prozess wird, so er irgendetwas aus %systemroot%\system32 laden will, automatisch immer in Wahrheit %systemroot%\syswow64 durch diesen Mechanismus namens "file system redirection" untergeschoben.
Und diesen Mechanismus können wir uns bei unsrem Problem ganz einfach zunutze machen, und zwar indem wir die Datei foofwd.dll in ihrer 64-bittigen Variante bei der Installation auf einem x64-Betriebssystem schlicht und einfach nach %systemroot%\system32 kopieren. Dann finden alle "Any-CPU"-.NET-basierten Prozesse die foofwd.dll über %systemroot%\system32 (also über die Umgebungsvariable %PATH%) und alle "x86"-.NET-basierten Prozesse über das lokale Installationsverzeichnis.

Wieviel .NET steckt in Vista Build 5308?

So, ähem, jetzt schauen wir mal auf die February CTP, den Build 5308. Hier findet sich dieses ominöse Verzeichnis c:\build, wie etwa beim Build 5270, nimmer. Von 7492 EXE und DLL-Dateien benutzen hier genau 430 das .NET-Framework, also 5,74%, was einen leichten Anstieg gegenüber der December CTP Build 5270 bedeutet. Da diese February CTP "feature complete" sein soll, ist wohl eine großartige Änderung des Anteils an Managed Code im Auslieferungszustand nicht mehr zu erwarten. Dennoch werde ich meine Augen offenhalten und mit jeder CTP, RC und dem finalen Produkt über deren Anteil an Managed Code hier berichten.

Wieviel .NET steckt in Vista Build 5270?

Noch immer ganz geschockt von einer Woche Vista Ascend Training bei MS in U'schleißheim als offenbar einziger native Coder unter einer Horde .NET-Zealots, wollte ich doch jetzt mal wissen, wie sehr MS wirklich an .NET glaubt und selbst Teile seiner Betriebssystemkomponenten als Managed Code ausliefert. Also schnell ein häßliches Tool geschrieben, das den dumpbin bemüht, um Managed Code Binaries zu finden. Man findet es hier und benutzt es wie folgt:

dotnetsearch.exe c:

Damit sucht es rekursiv die komplette Partition c: nach allen EXE und DLL files ab und ruft für jedes Binary den dumpbin auf. Findet dumpbin eine Abhängigkeit von mscoree.dll, dann ist das Binary aus Managed Code erstellt worden.

Und das Ergebnis: Die December CTP (Build 5270) hat ziemlich genau 5% Managed Code, jedenfalls zumindest was die Anzahl der Dateien angeht. Wenn man nämlich obige Kommandozeile auf ein frisch installiertes Vista Build 5270 anwendet, findet das Tool bei mir ca. 10800 EXE- und DLL-Dateien, von denen 520 gegen die mscoree.dll linken, also etwa 4,81%. Auffällig ist, daß sich ein Großteil der Dateien unter c:\build befinden und daß Dateien, die anderswo liegen, auch nochmal unter c:\build liegen, was den Verdacht nahelegt, daß c:\build so eine Art Repository ist für die Nachinstallation optionaler Komponenten. Tante Gugel wußte jedenfalls nichts genaues über Sinn und Zweck von c:\build. Trotzdem aber noch mal das Tool angeworfen, diesmal mit folgender Kommandozeile:

dotnetsearch.exe c:\build

Das Ergebnis: Von 3500 Dateien linken 122 gegen die .NET-runtime. Rechnet man also den Anteil unter c:\build aus dem Gesamtergebnis raus, erhält man: (520-122)/(10800-3500) = 0,0545. Auch diese Rechnerei ändert also an dem aus meiner Sicht mageren .NET-Anteil von etwa 5% nicht viel.

Das nächste Mal schreibe ich, wieviel .NET denn in der aktuellen February CTP Build 5308 drinnesteckt.

Wo man Logfiles hinschreiben sollte

Eine immer wieder aufkeimende Diskussion unter denjenigen Kollegen bei επτ€σ, die sich Gedanken um Sicherheitsaspekte machen, ist die Frage, wo man Logfiles anlegen sollte. Und zwar nicht irgendwelche Logfiles von irgendeinem User sondern Logfiles, die von unterschiedlichen Benutzerkontexten erzeugt werden und alle in ein- und demselben Verzeichnis landen sollen. Welches ist also das richtige Verzeichnis für diesen Zweck, oder wo sollte dieses Verzeichnis liegen?

Man muß wissen, daß gegenwärtig die Logfiles der Produkte von επτ€σ defaultmäßig im Produktunterverzeichnis unter dem "Program Files"-Verzeichnis liegen und das ist ja nun security-wise nicht so das Gelbe vom Ei. Denn viele große Firmen legen fest, daß unprivilegierte User in alles unter dem "Program Files"-Verzeichnis nicht schreiben dürfen, also auch ihre Logfiles dort nicht geschrieben werden dürfen.

Meine Meinung orientiert sich an einer Beobachtung: Es gibt ein Verzeichnis auf jeder XP-Maschine, in das a jeda schreiben darf. Und zwar ist dies das Verzeichnis, in das auch MS Logfiles jeglicher Herkunft schreibt: Das Verzeichnis für die Logfiles von Dr. Watson. Diese stehen normalerweise in folgendem Verzeichnis, auf das jeder User Schreibrechte hat:

c:\Documents and Settings\All Users\Application Data
  \Microsoft\Dr Watson

Damit meine ich nicht, daß auch επτ€σ seine Logfiles in genau dieses Verzeichnis schreiben sollte, sondern eher daß επτ€σ-Binaries ihre Logfiles in ein Verzeichnis

c:\Documents and Settings\All Users\Application Data
\επτ€σ\logfiles

speichern sollten, wobei ein privilegierter User während einer Installation dafür sorgen sollte, daß die Rechte auf dieses Verzeichnis entsprechend aufgebohrt sind.

Und niemand unter den Kunden kann behaupten, daß in diesem Verzeichnis aus Sicherheitsgründen die Rechte nicht aufgebohrt werden dürfen, denn MS macht's ja mit Dr. Watson vor, wie man's machen sollte.

Wie kommt man an dieses Verzeichnis? SHGetFolderPath mit CSIDL_COMMON_APPDATA. Geht ab Windows 2000.

Wie RPC Context Handles nicht funktionieren

Bei επτ€σ schreibt gerade Hinz und Kunz irgendwelches RPC-Gedöns und so mancher beginnt zu begreifen, was für eine Power hinter dieser eigentlich uralten Technologie steckt, insbesondere hinter Context Handles. Dummerweise scheint nur jeder, inklusive meiner bescheidenen Wenigkeit, am Anfang immer den gleichen Denkfehler dabei zu begehen, weshalb ich hier 'mal drüber schreibe, nicht zuletzt weil ich heute im Code eines prominenten Kollegen genau diesen Denkfehler, den kein geringerer als LarryO bei mir hier vor gut einem Jahr beseitigt hat, wiedergefunden habe. (Wenn im folgenden von "einem RPC" die Rede ist, dann meine ich damit "einen Remote Procedure Call", nicht RPC als Technologie.)

Was'n das überhaupt, so'n Context Handle?

Ein Context Handle repräsentiert State, der in einem RPC-Server angelegt wird und über mehrere Aufrufe des Clients hinweg verwendet wird um letztendlich im Server dann wieder abgebaut zu werden. Typischerweise ruft zu dem Zweck der Client einen RPC auf, der ihm ein opakes HANDLE zurückgibt, das er sich dann merkt und in nachfolgenden Aufrufen verwendet, bis er genug davon hat und einen RPC aufruft, der den State explizit wieder freigibt. Dieses Pattern findet man im Windows API überall: Um eine Datei zu öffnen, rufe ich CreateFile auf und bekomme ein File Handle zurück, das kann ich dann irgendwelchen anderen APIs zufüttern (WriteFile, ReadFile, etc...) um es schließlich mit CloseHandle zu schließen. Genauso erhalte ich mit RegOpenKeyEx/RegCreateKeyEx ein Handle auf einen Registry Key das ich verwenden kann um Values auszulesen (RegQueryValueEx), Subkeys anzulegen (RegCreateKeyEx), etc... und wenn ich fertig bin, mache ich einen RegCloseKey.

Was dabei unter der Haube im RPC Server passiert, wenn ein Client solch ein Context Handle anfordert, ist dem Server überlassen. Zumeist legt er sich aber irgendeine Datenstruktur auf seinem Heap an, deren Adresse er dann dem serverseitigen Context Handle zuweist und die der serverseitige Funktionsaufruf dann als [out]-Parameter an die RPC-runtime zurückgibt. Auf Clientseite erhält dann der Client sein clientseitiges Context Handle von dem RPC zurück, das er in nachfolgenden Aufrufen dann verwenden kann. Wann immer nun ein Client mit diesem clientseitigen Context Handle einen RPC aufruft, wird im Server der entsprechende RPC mit diesem Handle Value, den der Server zuvor an die RPC-runtime zurückgegeben hatte, aufgerufen. Der Server castet dann einfach diesen Wert in einen Pointer auf die zuvor angelegte Datenstruktur und kann dann mit dem State weiterarbeiten. Das schöne ist aber, daß die RPC-runtime verhindert, daß ein böswilliger Client einfach Garbage als Context Handle schicken kann um somit den Server zu crashen und weitherhin, daß verlorengegangene Clients zu einem rundown des serverseitigen Context Handles führen.

Was'n ein rundown?

Der rundown verhindert, daß Clients, die ja über das Context Handle zumeist Speicher in einem Server anlegen, ein Speicherleck im Server verursachen, wenn sie vergessen, dieses Context Handle mit dem dafür vorgesehenen RPC wieder zu schließen. Auch dadurch daß ein Client einfach crasht oder das Netzwerk zwischen dem Client und dem Server runterfällt, bevor er zum Schließen des Context Handles kommt, könnte es zu dem Speicherleck kommen, das der rundown verhindert.

Wenn man also ein RPC-Interface schreibt, das Context Handles an den Client ausgibt, muß man zwangsweise eine Rundown-Methode für die Context Handles implementieren, die in aller Regel nichts anderes tut, als das, was die Methode zum regulären Schließen des Context Handles durch den Client auch tun würde. Diese Rundown-Methode ruft dann die RPC-runtime immer dann auf, wenn ein Client-Prozeß beendet wurde (oder nicht mehr erreichbar ist), der noch austehende Context Handles hat, und zwar genau einmal pro ausstehendem Context Handle.

Und um welchen Denkfehler geht es jetzt?

Der Denkfehler bei alldem besteht nun darin, anzunehmen, daß exakt der Wert, den der Server seinem serverseitigen Context Handle bei der Erzeugung zuweist, dann auch beim Client auftaucht. So hat der liebe Kollege in seinem Server für den Fehlerfall dem Context Handle den Wert INVALID_HANDLE_VALUE (das ist (HANDLE)-1 oder (void *)-1 oder auf 32-bit Systemen der Wert 0xFFFFFFFF) zugewiesen und ist steif und fest davon ausgegangen, daß exakt dieser Wert auch dem clientseitigen Context Handle von der RPC runtime zugewiesen wird, so daß der Client anhand von INVALID_HANDLE_VALUE erkennen kann, daß im Server ein Fehler aufgetreten ist. Was aber stattdessen in diesem Fall tatsächlich im Client ankommt, ist ein gültiger Wert, der weder NULL ist, noch INVALID_HANDLE_VALUE, denn der einzige Wert, der einem Context Handle im Server zugewiesen werden kann, der dann auch identisch im Client erscheint, ist NULL. Und aus dem Grund hat auch ein gültiges Context Handle auf Clientseite einen Non-NULL-Wert und keinen Non-INVALID_HANDLE_VALUE-Wert. Das bedeutet, daß die RPC runtime ein serverseitiges Context Handle das nicht den Wert NULL (also beispielsweise den Wert INVALID_HANDLE_VALUE) hat, immer als gültiges Context Handle betrachtet und für dieses im Bedarfsfall auch den rundown aufruft. Im Umkehrschluß bedeutet daß, daß man immer nur mit dem Wert NULL einen Fehler anzeigen sollte, nicht aber mit dem Wert INVALID_HANDLE_VALUE. Und was für eine Koinzidenz: Weist man im Server einem Context Handle den Wert NULL zu, wird dafür im Bedarfsfall auch kein rundown aufgerufen.

Mein Denkfehler in dem Zusammenhang vor einem Jahr war ein wenig anders: Ich konnte damals nicht begreifen, daß ein 64-bittiger RPC-Server ein 64-bittiges Context Handle an den Client ausgeben kann, und der dann über ein 32-bittiges Context Handle (das ja dann nach meiner Logik den truncateden Wert des 64-bittigen Context Handles beinhalten mußte) korrekt den State im Server bei nachfolgenden RPCs ansprechen kann. Aber das war eben ein Denkfehler, denn der Wert den der Client als Context Handle sieht, hat außer bei NULL überhaupt nichts zu tun mit dem Wert, den der Server ausgegeben hat - außer für die RPC runtime selber, die die Zuordnung zwischen beiden auf magische Weise herstellt.

<< 1 ... 31 32 33 34 35 36 37 38 39 40 41 ... 47 >>