Visual Studio 2005 Runtimes - Part 2: Wie man sich um den winSxS-Folder herumbescheißt...
Vor ein paar Wochen hat Andre Stille, MVP, in der Newsgroup microsoft.public.de.vc skizziert, wie man erreichen kann, daß man auch unter XP eine applikationsprivate CRT und MFC bekommen kann, und zwar unter vollständiger Mißachtung des winsxs-Folders. Das soll heißen: Selbst wenn die offiziellen MSI-Pakete installiert sind, wird trotzdem die eigene CRT und MFC der Applikation verwendet (diese Problematik habe ich ja hier schon einmal versucht, zu erläutern). Der Ruf nach einer Anleitung wurde schnell laut, aber Andre scheint der Sache noch nicht nachgekommen zu sein. Drum springe ich hier jetzt kurzerhand in die Bresche. Wenn vom nachfolgenden Text dem Leser beim testweisen Nachvollziehen irgendetwas erfolgreich gelingt, dann ist dies ausschließlich Andres Verdienst, etwaige fehlerhafte Dokumentation und daraus resultierende verweigerte Applikationsstarts sind allein meine Schuld:
Wir starten, indem wir eine MFC-Applikation namens testapp (wie sinnig) erzeugen und zwar in ein Unterverzeichnis unter c:\. Das Verzeichnis c:\ hat ja bestimmt a jeda und so sieht das dann bei mir aus (ein Klick auf ein Bild öffnet es in einem neuen Browserfenster in der Originalgröße):
Wir klicken auf OK und die zweite Wizardseite erscheint:
Dort ändern wir nichts, wir klicken nur auf "Finish". Dann wird das Projekt für uns generiert und wir könnten es jetzt einmal builden. Tun wir aber nicht. Stattdessen machen wir einen rechten Mausklick auf das Projekt im "Solution Explorer" und wählen den untersten Eintrag, "Properties", an. Es öffnet sich nun der moduslose Dialog mit den Projekteigenschaften. Dort auf der linken Seite wählen wir als Konfiguration erstmal den Release-Build und versuchen dann im Baum den Punkt "Configuration Properties"-"Manifest Tool"-"input and Output" zu orten und klicken drauf. In der rechten Hälfte des Dialogs können wir nun festlegen, ob ein Manifest in das Binary hineinkompiliert werden soll, oder als separate Datei erzeugt werden soll. Wir wollen letzteres und wählen wie im folgenden Screenshot gezeigt, "No" für die Einstellung "Embed Manifest":
So, und jetzt builden wir das Projekt in all seiner Schönheit und Pracht, und zwar im Releasebuild (das muß ich jetzt nicht weiter erklären, oder?). Als Ergebnis dieses Builds erhalten wir im Unterverzeichnis C:\testapp\release nun die testapp.exe selber sowie ihr Manifest, die Datei testapp.exe.manifest. Spaßeshalber können wir testapp.exe jetzt auch 'mal starten und uns beispielsweise mit dem "Process Explorer" (procexp.exe) davon überzeugen, daß der DLL-Lader die runtime (msvcr80.dll) sowie die MFC (mfc80u.dll) aus irgendeinem Verzeichnis mit einem Namen, der total funky ist, unterhalb von %systemroot%\winsxs lädt.
Als nächstes erzeugen wir zwei Unterverzeichnisse unterhalb von C:\testapp\release, nämlich Microsoft.VC80.CRT und Microsoft.VC80.MFC. In das Verzeichnis Microsoft.VC80.CRT kopieren wir nun die drei runtime-Dateien msvcm80.dll, msvcp80.dll und msvcr80.dll. Wir finden sie durch eine Suche unterhalb des %systemroot%\winsxs folders. Bei mir sind sie beispielsweise im Verzeichnis
c:\WINDOWS\WinSxS\x86_Microsoft.VC80.CRT_1fc8b3b9a1e18e3b_8.0.50727.42_x-ww_0de06acd
Ein Blick auf deren Versionsinfo-Ressource zeigt, daß alle drei Dateien die Version 8.0.50727.42 haben, das wird später wichtig. In unseren Lieblingstexteditor verfrachten wir jetzt nur noch folgendes:
<?xml version="1.0" encoding="UTF-8" standalone="yes"?> <assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0"> <assemblyIdentity type="win32" name="Microsoft.VC80.CRT" version="8.0.50727.42" processorArchitecture="x86"> </assemblyIdentity> <file name="msvcr80.dll"></file> <file name="msvcp80.dll"></file> <file name="msvcm80.dll"></file> </assembly>
(hach, wie ich das hasse, spitze Klammern in HTML zu schreiben...)
und erzeugen uns mit diesem Inhalt eine Datei namens Microsoft.VC80.CRT.Manifest im zuvor erzeugten Unterverzeichnis c:\testapp\release\Microsoft.VC80.CRT. Etwas ähnliches machen wir mit dem Verzeichnis C:\testapp\release\Microsoft.VC80.MFC. Da drin erzeugen wir eine Datei mit dem Namen Microsoft.VC80.MFC.Manifest, die folgenden Inhalt hat:
<?xml version="1.0" encoding="UTF-8" standalone="yes"?> <assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0"> <assemblyIdentity type="win32" name="Microsoft.VC80.MFC" version="8.0.50727.42" processorArchitecture="x86"> </assemblyIdentity> <file name="mfc80.dll"></file> <file name="mfc80u.dll"></file> <file name="mfcm80.dll"></file> <file name="mfcm80u.dll"></file> </assembly>
Und natürlich kopieren hier hin die ganzen MFC-Redistributables, nämlich mfc80.dll, mfc80u.dll, mfcm80.dll und mfcm80u.dll. Wir finden sie auch wieder durch eine Suche unterhalb des %systemroot%\winsxs folders. Bei mir waren sie zu finden unter
c:\WINDOWS\WinSxS\x86_Microsoft.VC80.MFC_1fc8b3b9a1e18e3b_8.0.50727.42_x-ww_dec6ddd2
Und auch für diese Dateien stellen wir fest, daß sie die Version 8.0.50727.42 haben.
Starten wir nun testapp.exe erneut, stellen wir fest, daß bisher alle Bemühungen umsonst waren, die runtime und MFC werden nachwie vor aus dem winsxs-Folder geladen. Ist aber auch ganz logisch, schließlich haben wir ja das Manifest von testapp.exe selber, die Datei testapp.exe.manifest aus dem Ordner c:\testapp\release, noch unberührt gelassen. Das ändert sich jetzt und wir laden diese Datei in unseren Lieblingseditor. Sie hat folgenden Inhalt:
<?xml version="1.0" encoding="UTF-8" standalone="yes"?> <assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0"> <dependency> <dependentAssembly> <assemblyIdentity type="win32" name="Microsoft.VC80.CRT" version="8.0.50608.0" processorArchitecture="x86" publicKeyToken="1fc8b3b9a1e18e3b"> </assemblyIdentity> </dependentAssembly> </dependency> <dependency> <dependentAssembly> <assemblyIdentity type="win32" name="Microsoft.VC80.MFC" version="8.0.50608.0" processorArchitecture="x86" publicKeyToken="1fc8b3b9a1e18e3b"> </assemblyIdentity> </dependentAssembly> </dependency> <dependency> <dependentAssembly> <assemblyIdentity type="win32" name="Microsoft.Windows.Common-Controls" version="6.0.0.0" processorArchitecture="x86" publicKeyToken="6595b64144ccf1df" language="*"></assemblyIdentity> </dependentAssembly> </dependency> </assembly>
Anhand des Manifestes ist deutlich erkennbar daß drei "fusionized" Assemblies referenziert werden, die CRT, die MFC und die Common Controls. Anders als in Andre Stilles Usenet Posting beschrieben, würde ich an den Common-Controls-Manifest-informationen nichts drehen, denn die Common Controls DLL, die hier referenziert wird, ist nicht redistributable. Anders die runtime (CRT) und die MFC: Für deren Einträge im Applikationsmanifest (das haben wir noch im Editor, richtig?) ändern wir jetzt beidesmal die Versionsnummer von 8.0.50608.0 auf 8.0.50727.42 ab, schließlich ist das die Version, die wir in den beiden Unterverzeichnissen haben. Dann werfen wir die beiden Einträge publicKeyToken="1fc8b3b9a1e18e3b" kurzerhand raus und speichern die Datei sicherheitshalber einmal, denn eigentlich sind wir jetzt schon fertig. Die Datei sollte jetzt so ausschauen:
<?xml version="1.0" encoding="UTF-8" standalone="yes"?> <assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0"> <dependency> <dependentAssembly> <assemblyIdentity type="win32" name="Microsoft.VC80.CRT" version="8.0.50727.42" processorArchitecture="x86" ></assemblyIdentity> </dependentAssembly> </dependency> <dependency> <dependentAssembly> <assemblyIdentity type="win32" name="Microsoft.VC80.MFC" version="8.0.50727.42" processorArchitecture="x86" ></assemblyIdentity> </dependentAssembly> </dependency> <dependency> <dependentAssembly> <assemblyIdentity type="win32" name="Microsoft.Windows.Common-Controls" version="6.0.0.0" processorArchitecture="x86" publicKeyToken="6595b64144ccf1df" language="*"></assemblyIdentity> </dependentAssembly> </dependency> </assembly>
Aber halt: Wenn wir jetzt erneut builden, wird unser kunstvoll manipuliertes Aplikationsmanifest wieder mit einem Defaultmanifest überschrieben. Ich kopiere es deswegen ins übergeordnete Verzeichnis (c:\testapp) und ergänze einen "Post-Build Event" mit dem Kommando
copy c:\testapp\testapp.exe.manifest c:\testapp\release
wie folgender Screenshot zeigt:
Damit wird dann nach jedem Build das gerade generierte Applikationsmanifest durch unser eigenes ersetzt. Falls irgendjemand hierfür eine elegantere Variante kennt, oder weiß, wie man um die hartkodierten Pfade im obigen Beispiel drumrumkommt, bin ich natürlich für jeden sachdienlichen Hinweis dankbar.
Fazit: Mit dem hier beschriebenen Verfahren kann man auch mit den "fusionized" Assemblies aus Visual Studio 2005 unter XP ein eigenes side-by-side von Applikation und runtimes erreichen. Das Ganze wurde von mir getestet unter Windows XP RTM und im Vista Build 5381 und jedesmal wurden die runtimes aus dem Applikationsverzeichnis geladen, egal ob die runtime mit dem offiziellen MSI-installer vorher installiert war oder nicht. Die von Andre in seinem Posting angesprochene Problematik mit der lokalisierten MFC Ressourcen DLL scheint nach meinen Beobachtungen überhaupt nicht zu existieren, bei Nichtvorhandensein der DLL für MFC8 wird einfach eine vorhandene einer früheren Version geladen. Anyway, the kudos goes to Andre, credits, where credits are due.
(Mein Testprojekt kann man sich übrigens hier herunterladen.)
Visual Studio 2005 Runtimes - Part 1: Licht am Ende des Tunnels
So, ich bin seit heute krankgeschrieben bis zum Rest der Woche. Das hält mich aber keineswegs vom Bloggen ab, schließlich tue ich das ja aus dem Bett mit dem Laptop auf dem Schoß und dem Fieberthermometer im Anus. Hoffen wir, daß mir da draus keiner einen Strick dreht...
Zum Problem: Windows XP (Jahrgang 2001) bringt ein äußerst esoterisches Feature mit, die SxS-Execution von DLLs. Jahrelang hat dieses Feature des DLL-Laders ein Mauerblümchendasein gefristet und hat sich allenfalls in Form einer gänzlich neuen comctl32.dll manifestiert, die genau dann geladen wurde, wenn das Binary ein Manifest in Form einer entsprechenden XML-Datei entweder beiligend, oder als Ressource hineinkompiliert hatte. Damit kann man dann den tollen Klickibunti-Luna-Look für sein Binary erzeugen. Und diese comctl32.dll wird, anders als früher, nicht aus dem system32-Verzeichnis geladen,sondern aus diesem komischen winsxs Folder.
Beginnend mit den Runtimes für Visual Studio 2005 (Jahrgang 2005) macht Microsoft aber so richtig ernst mit den Manifesten. So weigert sich etwa ein Binary überhaupt erst, gestartet zu werden, wenn es ohne Manifest erzeugt wurde (zumindest wenn man die Runtime dynamisch linkt). Und wenn man ein Manifest einfügt, hat man erstmal überhaupt keine Kontrolle mehr, von wo die Runtime oder die MFC Dlls geladen werden. Denn wenn erstmal diese Binaries der Runtime über den offiziellen MSI-Installer installiert sind, dann stehen sie im WinSxS-Folder und fortan werden sie immer von dort geladen und nicht mehr vom Installationsverzeichnis. Das Ganze hat eindeutige Vorteile, aber auch handfeste Nachteile:
- Vorteil: Bei einem Bug in der Runtime/MFC können die Binaries auf einen Schlag für alle Applikationen per Windows Update augetauscht werden
- Nachteil: Weiß bei Windows Update Microsoft selbst mal wieder nicht, was es tut, tun auf einen Schlag alle Applikationen nicht mehr
- Nachteil: Eine echte side-by-side execution ist nicht mehr möglich
- Vorteil: Ein Drama wie bei gdiplus.dll, das jeder mit dem JPEG-buffer overrun side-by-side zu seiner Applikation installieren konnte, wird es auch nicht mehr geben.
- Nachteil: Das offizielle Redistributable ist ein MSI Paket und erwartet MSI Version 3.1. Dazu muß man XP SP2 haben oder W2K3Server SP1, oder aber MSI-Redistributables für XP RTM, W2k3Server RTM, XP SP1 oder W2K SP3. In Summe kommt da ein hübscher Brocken an Patches und Service Packs zusammen, um das Ganze auf einem x-beliebigen Rechner zum Fliegen zu bringen.
Der letzte Punkt scheint ein massiver Showstopper für viele ISVs gewesen zu sein. Daher wird allem Anschein nach Visual Studio 2005 SP1 (Jahrgang 2006?) mit einem upgedateten MSI-Installer ausgeliefert, der nur MSI 2.0 voraussetzt. Ich habe den zugehörigen Hotfix von MS Support angefordert und ausprobiert. Es funktioniert ohne Probleme auf Windows XP RTM und Windows 2000 SP4 (müßte auch mit SP3 gehen, habe ich aber keine VMWare dafür).
Das nächste Mal schreibe ich dann, wenn ich nicht gerade im Fieberwahn deliriere, wie man sich kunstvoll um den winsxs-Folder herumbescheißt. Stay tuned!
Urban Myths: ASSERT und VERIFY
Aus aktuellem Anlaß schreibe ich jetzt was über ASSERT und VERIFY. Zwei Ereignisse in den vergangenen beiden Tagen in meinem näheren Dunstkreis bei επτ€σ veranlassen mich hierzu. Das erste war gestern, als ein Kollege zu mir kommt und erzählt, ein anderer Kollege hätte zum wiederholten Mal so etwas eingecheckt:
ASSERT(DoSomethingReallyImportant());
und die Tester hätten das Feature "SomethingReallyImportant" nicht abnehmen können, weil es schlicht nicht vorhanden war. Dann mußte ich heute früh stutzen, weil ein dritter Kollege bei einem von mir geforderten Bugfix genau dasselbe Pattern eingecheckt hatte. Als er dann in Mission Control einlief, verkündete er dann auch noch im Brustton der Überzeugung: "Ich baue an dieser Stelle keinen VERIFY statt dem ASSERT ein, weil ich auf keinen Fall will, daß dann diese MessageBox bei unseren Kunden auftaucht, wenn der Code versagt!".
Gak!
Diese Beispiele zeigen mir, daß offenbar unter einem Teil der Kollegen leichte Verwirrung darüber herrscht, was ASSERT und VERIFY tun, und bevor dadurch noch mehr Schaden entsteht (und weil ich weiß daß dieser Blog zumindest von einem Teil meiner Entwicklerkollegen aufmerksam verfolgt wird), hier ein paar hard facts:
- ASSERT und VERIFY zeigen niemals in einem Releasebuild (also beim Kunden) eine MessageBox an.
- Der Inhalt von VERIFY wird im Releasebuild immer ausgeführt, nur der Boolsche Ausdruck den das Argument von VERIFY darstellt, wird nicht evaluiert.
- Der Code der Teil des boolschen Ausdrucks ist, den das Argument von ASSERT darstellt, wird im Releasebuild eliminiert. Man sollte da also niemals lebenswichtige Funktionalität oder Funktionalität mit Seiteneffekten drin haben.
- ASSERT und VERIFY gehören zu den schärfsten Waffen, die wir als Entwickler haben, wer sie nicht einsetzt, sollte seine Ignoranz in dieser Hinsicht schleunigst und grundlegend überdenken.
Dem DLL-Lader auf die Sprünge helfen...Teil 2
So, jetzt wo wir wissen, wie man fehlendes oder falsches Rebasing erkennen kann, die Antort auf die Frage "Wozu das Ganze, geht doch auch so!?!".
Um das zu erklären muß man wissen, was mit dem Inhalt einer DLL passiert, wenn sie vom Programmlader in den Adreßraum einer DLL geladen wird. Was für jedermann einleuchtend passieren muss, ist, daß die Instruktionen und die Daten, die in einer DLL drinstecken, irgendwie in den Adreßraum des sie verwendenden Prozesses kommen müssen. Klar ist sicherlich auch, daß ein Funktionsaufruf, wie er in einer Sprache wie C geschieht, in aller Regel zu einer Assembler-Instruktion führt, die den Instruction Pointer (IP) auf eine andere Adresse setzt, beispielsweise eine JMP oder CALL Instruktion. Für diese Assemblerinstruktionen braucht man aber innerhalb des virtuellen Adreßraums eineindeutige Adressen, wo dann die Ausführung hinspringen kann um den Assemblercode an dieser Stelle zur Ausführung zu bringen. Haben wir aber nun ein derartiges Konzept mit nachladbarem Code, wie unter Windows es mit DLLs nun einfach mal und gottseidank existiert, so kann eine einzelne DLL niemals wissen ob sie wirklich an diejenige Stelle im virtuellen Adressraum des Prozesses geladen wird, wo sie all ihre Sprungadressen oder die Adressen von globalen Daten nun mal hat. Denn der Prozess könnte ja eine völlig wildfremde unbekannte DLL an genau diese Stelle schon Jahre vorher geladen haben. Daher müssen DLLs eigentlich immer damit rechnen, daß sie ohne Vorwarnung an eine vogelwilde Adresse im Adreßraum des sie verwendenden Prozesses geladen werden.
Wie aber nun werden die Zieladressen für Sprungziele oder die Adressen von globalen Variablen der DLL zur Laufzeit ermittelt? Ganz einfach, all die Stellen, wo diese Adaption an die tatsächliche Ladeadresse erforderlich ist, werden in einer Tabelle erfasst, der sogenannten Relocation Table, die Teil des PE File Formats ist und die vom Programmlader während des Ladens der DLL ausgelesen wird. Selbiger geht dann nämlich während des Ladevorgangs alle Tabelleneinträge durch und ändert alle relativen virtuellen Adressen (RVA) in der zu ladenden Datei auf die tatsächlichen Adressen im Adreßraum des fraglichen Prozesses ab.
Soweit so gut, dann geht doch alles auch gut, ohne daß man die DLL rebaset, oder? Ja, schon, aber es gibt drei Riesenprobleme damit:
- Dieser Vorgang des Fixups der Einträge in der Relocation Table ist extrem teuer. Die Performance leidet darunter erheblich. Bei einer sehr prominenten Applikation von επτ€σ konnte ich durch korrektes Rebasing die Startzeit von 10s auf 5s drücken. Das heißt für den täglichen Benutzer der Software schon einiges.
- Wird ein- und dieselbe DLL in zwei unterschiedlichen Prozessen an unterschiedliche Ladeadressen geladen, kann der Code und konstante Daten (also alle read-only Pages in virual memory parlance) beider DLL-Instanzen nicht geshared werden, der Memory Manager muß für jede DLL-Instanz Memory committen. Das verbraucht nicht nur unnötig Speicher sondern ist auch schlecht für die Performance, aus verschiedensten Gründen.
- Ein Dr.Watson Log in Verbindung mit einem Map-File und Symbolen ist komplett unbrauchbar, wenn man daraus nicht die Codezeile ermitteln kann, wo ein Crash beim Kunden passiert ist.
Beim nächsten Mal schreibe ich dann darüber, welche Möglichkeiten des Rebaing es gibt und wie man mit Hilfe von Tools erkennen kann, ob ein Binary sauber gerebaset wurde.
Dem DLL-Lader auf die Sprünge helfen...
Derzeit entstehen bei επτ€σ neue DLLs wie nix Gutes. Das ist einerseits etwas Vorteilhaftes, weil dadurch weniger dieser Riesenklötze von Monolithen wie in der Vergangenheit entstehen, andererseits geht schnell aber 'mal der Überblick verloren. So beispielsweise bei der Frage, ob eine DLL auch wirklich an der virtuellen Adresse geladen werden kann, an der sie geladen werden soll. Weil kein Mensch das unmöglich von Hand überprüfen kann, habe ich am vergangenen Wochenende hierzu ein Tool geschrieben, das man einfach in den Buildprozess integrieren kann und das dem Buildmaster nicht nur sagen kann, ob jemand einfach nur geschlampt hat und sein Rebasing vergessen hat, sondern auch, ob es trotz aller guten Absichten bei eigentlich sauber ge-rebaseten (wie schreibt man das eigentlich?) DLLs irgendwelche Überlappungen in ihrem virtuellen Adreßraum gibt. Das Tool kann man hier runterladen und es wird so angewendet:
prefload dir <-e=excludefile> <-v> wildcards
also beispielsweise so:
prefload d:\devstudio\mysoftwareproduct\release *.dll *.ocx
Im Beispiel durchsucht es alle Dateien mit den Endungen dll und ocx unterhalb des Verzeichnisses d:\devstudio\mysoftwareproduct\release und stellt dabei potentielle Ladekonflikte fest. Wenn ein derartiger Ladekonflikt gefunden wird, wird er nach stdout geschrieben:
hornet.dll (10000000-10013000) has a conflict with ant.dll(10000000-10008000)
Und wenn irgendein solcher Ladekonflikt auftritt, dann kehrt das Tool auch mit einem Exitcode ungleich 0 zum Betriebssystem zurueck, was natürlich die Verwendung in einem batchbasierten System wie dem daily build besonders geschmeidig macht.
Der Clou ist natürlich, daß man mit dem Tool, für das ich mir gar nicht erst die Mühe eines UNICODE- oder x64-Builds gemacht habe, auch x64-Binaries ohne Probleme untersuchen kann. Möglich macht's der simple Aufbau von Windows PE-files. Man öffnet einfach ein PE-File mit plain-vanilla CreateFile und bildet es in ein memory mapped file ab. Ab da hangelt man sich anhand der Strukturen aus winnt.h an der Datei entlang um an die entscheidenden Informationen zu gelangen. Piece o' cake.
Was ich aber in keinster Weise weiß: Muß man das für DLLs, die aus managed code bestehen, eigentlich auch tun? Eigentlich schon, oder? DEI to the rescue!
Und das nächste Mal: Wozu überhaupt dieses gestörte Rebasing, tut doch schließlich auch so, oder!?!