Technischer Artikel

FPC Win32: OMF-zu-COFF-Objektlinking in PDFlibPas

PDFlibPas lässt sich unter Free Pascal für 32-Bit-Windows bauen, und der schwierige Teil war nie Pascal selbst. Es waren die Objektdateien: Die AES- und OpenJPEG-Objekte, gegen die der Delphi-Build linkt, sind OMF, der interne Linker von Free Pascal verlangt aber COFF, und die Konvertierung zwischen beiden erzeugt Sektionsnamen und Sektionsdefinitionssymbole, an denen der Linker lieber mit internen Fehlern abbricht, als eine brauchbare Diagnose auszugeben

Wer schon einmal C-Objekte in eine Pascal-Bibliothek gelinkt hat, kennt dieses Terrain. Win64 ist vergleichsweise zivilisiert: ein Objektformat, eine Aufrufkonvention, keine Namensdekoration. Win32 bewahrt jede historische Schicht, die die Plattform angesammelt hat, und eine Bibliothek, die fremden C-Code statisch linkt, trifft auf alle gleichzeitig

Das Compiler-Verzeichnis verrät das Ziel nicht

Beginnen Sie am Build-Einstiegspunkt, denn wer hier falsch liegt, verschenkt Stunden, bevor überhaupt eine Objektdatei im Spiel ist. Der Name eines Free-Pascal-Installationsverzeichnisses sagt, wo der Hauptcompiler wohnt, nicht was er erzeugt. Ein 32-Bit-Host-Compiler kann einen danebenliegenden Cross-Compiler aufrufen und mit den richtigen Target-Switches 64-Bit-Code ausgeben. Wer das Ziel aus einem Pfad ableitet, rät also ins Blaue — solange, bis jemand die Toolchain umsortiert

Zuverlässig ist der Weg, den Compiler selbst zu fragen. Ermitteln Sie den tatsächlichen Zielprozessor und das Ziel-Betriebssystem über die Information-Switches des Compilers, und akzeptieren Sie beide gängigen Installationslayouts, das flache Binärverzeichnis wie das versionsverschachtelte, denn verschiedene Installer und Toolchain-Manager erzeugen unterschiedliche Strukturen. Ein Build-Skript, das eines der beiden Layouts fest verdrahtet, läuft exakt auf einer einzigen Maschine

Warum bricht eine konvertierte Objektdatei den internen Linker?

Weil die Konvertierung die OMF-Sektionsnamenskonvention beibehält und Sektionsdefinitionssymbole synthetisiert, die nicht zum Erwartungsbild des COFF-Linkers passen. Die OMF-Objekte nach COFF zu konvertieren ist notwendig, aber nicht hinreichend: Die entstandenen Dateien tragen die klassischen Sektionsnamen _TEXT, _DATA und _BSS plus die daraus abgeleiteten Namen der Sektionsdefinitionssymbole, und wer das dem internen Linker von Free Pascal vorwirft, erhält interne Compilerfehler statt einer Meldung zur Sektionsbenennung

Ein interner Fehler ist der schlechteste Fehlermodus für ein Build-Problem, denn er verrät nichts darüber, was an der Eingabe falsch war. Die Lösung ist ein Normalisierungslauf über die COFF-Datei nach der Konvertierung: Sektionsnamen in die erwartete Form umschreiben und die zugehörigen Sektionsdefinitionssymbole passend dazu, während Symbolindex, Code-Bytes und Relokationen unangetastet bleiben. Diese letzte Einschränkung ist die gesamte Schwierigkeit. Ein Umschreiben, das Symbole neu nummeriert oder Offsets verschiebt, erzeugt ein Objekt, das linkt und danach abstürzt

Für einen der beiden Objektsätze gibt es einen vorbereitenden Schritt. Die OpenJPEG-Objekte aus dem klassischen 32-Bit-C++-Compiler hängen von privaten 64-Bit-Integer-Helferroutinen von Delphi ab, die Free Pascal nicht mitbringt; keine noch so gute Formatkonvertierung macht sie dadurch brauchbar. Diese Objekte werden zunächst mit dem Clang-basierten Compiler neu gebaut, der diese Abhängigkeiten nicht erzeugt, und danach konvertiert

Pipeline, die die statischen C-Objekte von PDFlibPas unter Win32 vom Delphi-OMF in Lib\thirdparty\Win32 zu FPC-linkbarem COFF in Lib\thirdparty\Win32f führt: OMF-zu-COFF-Konvertierung, ein Normalisierungslauf, der Sektionsnamen und Sektionsdefinitionssymbole umschreibt, während Symbolindex, Code-Bytes und Relokationen unangetastet bleiben, und der Clang-Neubau für OpenJPEG-Objekte, die Delphi-64-Bit-Helfer aufrufen
Konvertierung ist notwendig, aber nicht hinreichend: Gibt man das umbenannte, aber nicht normalisierte COFF an den internen Linker von Free Pascal, antwortet er mit internen Fehlern — ein Lauf nach der Konvertierung korrigiert daher Namen und Symbole, ohne Offsets anzufassen
// Die Objekte für das FPC-Ziel liegen in einem eigenen Verzeichnis. Sie
// ersetzen nicht den Delphi-Objektsatz, denn beide Toolchains bauen aus
// demselben Quellbaum und jeder braucht seine eigenen Link-Eingaben
//
//   Lib\thirdparty\Win32   Delphi-OMF-Objekte, unverändert
//   Lib\thirdparty\Win32f  FPC-COFF-Objekte, konvertiert und normalisiert
//
// Build-Einstiegspunkte:
//   build-Win32-Lib-FPC.cmd
//   build-Win64-Lib-FPC.cmd

Compiler-private Helfer sind nicht portabel, ihre Konventionen ebenso wenig

Die Delphi-Runtime stellt Assembler-Trampolines für 64-Bit-Integer-Operationen auf 32-Bit-x86 bereit, und vorkompilierte C-Objekte, die für Delphi gebaut wurden, springen diese an. Free Pascal hat eine eigene Lösung, daher müssen diese Referenzen anders befriedigt werden, statt sie umzuleiten. Das Detail, das eine Umleitung unmöglich macht, ist die Aufrufkonvention: Beim Timing-Helfer, den der Imaging-Code nutzt, räumt der Callee das vier Byte große Argument ab, während der 64-Bit-Divisions-Helfer sechzehn Byte abrechnet und sein Ergebnis im klassischen Registerpaar zurückgibt. Zwei Helfer, zwei Konventionen — und ein für den einen geschriebenes Trampoline korrumpiert den Stack für den anderen lautlos

Die Namensdekoration liefert die zweite Hälfte des Problems. Unter Win32 stellt Free Pascal externen C-Imports automatisch einen Unterstrich voran, exportiert aber public name-Deklarationen wörtlich — Import- und Exportseite derselben Brücke folgen also unterschiedlichen Regeln. Die C-Runtime-Brücke, die OpenJPEG braucht, muss daher exakt die C-Symbolnamen exportieren, und die variadischen Einsprungpunkte brauchen einen indirekten 32-Bit-Sprung statt eines direkten. Keines davon ist exotisch, sobald man es ausspricht. Und alles davon scheitert als Link-Fehler, der ein Symbol nennt, das niemand geschrieben hat

Warum starb eine Win32-Executable, bevor main lief?

Eine 64-Bit-DLL auf dem Suchpfad, erreicht, weil die Free-Pascal-Unit zlib dynamisch bindet statt statisch zu linken. Das Symptom war ein sofortiger Exit mit dem Invalid-Image-Statuscode, bevor irgendein Pascal-Code im Programm lief — man untersucht also das gerade gebaute Programm, während der Fehler darin besteht, dass der Loader einen Import gegen die falsche Architektur auflöst

Die Lektion betrifft Annahmen, nicht zlib. Eine Unit, die nach einer Kompressionsbibliothek benannt ist, enthält nicht zwangsläufig eine; sie kann ein Binding sein, das zur Laufzeit eine Shared Library erwartet, und eine unbeabsichtigte dynamische Abhängigkeit ist ein Deployment-Risiko, selbst wenn sie zufällig gerade auflöst. Der Wechsel zur reinen Pascal-Stream-Implementierung gibt beiden Zielen einen statisch eingebundenen Kompressionspfad ganz ohne externe Abhängigkeit — was eine Bibliothek, die in die Anwendung eines anderen eingebettet wird, ohnehin von Anfang an haben sollte

Dasselbe Prinzip gilt für das externe JBIG2-Encoder-Backend. Im 32-Bit-Ziel wird der externe Encoder nicht gelinkt, daher fallen Anfragen auf den eingebauten Pascal-Encoder zurück, und der Test, der das verifiziert, muss den Registrierungszustand des aktuellen Ziels prüfen, statt einen erfolgreichen Encode als Beweis dafür zu nehmen, dass das externe Backend vorhanden ist. Ein funktionierender Fallback ist genau das, was eine fehlende Abhängigkeit versteckt — das Fehlermuster, das in der Diagnose stiller Stub-Fehler untersucht wird. Die 64-Bit-Arbeit am statischen Linken ist in Statischem Linken von jbig2enc unter FPC beschrieben

Diagnosefluss für eine Win32-Executable, die unter Free Pascal vor main endet: Der Loader löst Imports auf, während die Unit-Initialisierung läuft, das zlib-Binding findet eine 64-Bit-DLL auf dem Suchpfad, und der Prozess stirbt mit einem Invalid-Image-Status, bevor irgendeine Pascal-Anweisung läuft — PDFlibPas weicht dadurch auf einen statisch eingebundenen, reinen Pascal-Kompressionspfad aus
Der Fehler war nie das gerade gebaute Programm: Eine Unit namens zlib war ein Laufzeit-Binding, das gegen die falsche Architektur aufgelöst wurde, und ein funktionierender Fallback wie der eingebaute JBIG2-Encoder versteckt eine fehlende Abhängigkeit

32-Bit-Arithmetik auf einem Memory-Stream

Code, der Puffergrößen mit vorzeichenloser Arithmetik in Zeigerbreite manipuliert, ist auf Win64 korrekt und auf Win32 ein großes Bild vom Overflow entfernt. Der In-Memory-Stream, der den JPEG-2000-Codec speist, wächst durch Verdoppeln und rückt durch Addition vor, und auf einem 32-Bit-Ziel können beide Operationen bei Eingaben überlaufen, die groß, aber völlig legitim sind

Jeder Schreib-, Skip- und Seek-Vorgang sowie die anfängliche Allokation prüfen daher, bevor sie rechnen, und die Kapazitätsobergrenze ist der größte vorzeichenbehaftete Wert in Zeigerbreite, gewählt passend zu dem, was die Blockverschieberoutine und die Callback-Rückgabewerte ausdrücken können. Die Verhaltensanforderung, wenn eine Anfrage abgelehnt wird, ist leicht falsch umzusetzen: Eine Ablehnung darf weder die Stream-Position noch deren Länge verändern. Eine teilweise Mutation, auf die ein Fehler folgt, lässt den Stream in einem Zustand zurück, mit dem der Aufrufer nicht mehr rechnen kann, und die nächste Operation macht es schlimmer

// Erst prüfen, dann rechnen. Auf Win32 laufen beide Stellen bei Eingaben
// über, die ein großes JPEG-2000-Bild ganz legitim erzeugt
if Needed > NativeUInt(High(NativeInt)) - FPosition then
  Exit(False);                    // ablehnen, Position und Größe unangetastet lassen

NewCapacity := FCapacity;
while (NewCapacity < FPosition + Needed) do
begin
  if NewCapacity > NativeUInt(High(NativeInt)) shr 1 then
    Exit(False);                  // Verdoppeln würde überlaufen
  NewCapacity := NewCapacity shl 1;
end;

Zwei Build-Ausgaben-Fallen, die das Portieren überdauern

Test- und Beispiel-Executables nach Zielarchitektur in eigene Ausgabeverzeichnisse zu trennen, ist offensichtlich richtig und bricht sofort alles, was seine Testdaten durch Hochzählen von Verzeichnisebenen gefunden hat. Die Lösung ist, das Asset-Verzeichnis aufwärts zu suchen, statt eine feste Tiefe anzunehmen — mit einer bewussten Einschränkung: Das Signing-Beispiel akzeptiert einen Zertifikat-Fallback nur aus seinem eigenen Projektverzeichnis, nie aus einem beliebigen Vorfahren, denn ein gleichnamiges Zertifikat weiter oben im Baum ist eine Sicherheitsüberraschung, keine Annehmlichkeit

Die zweite Falle überlebt jedes Portieren und gehört in jedes FPC-Projekt mitgenommen. Nach einem Compiler-Upgrade reicht es nicht, dass der Compiler veraltete PPU-Dateien zurückweist, denn der Linker bevorzugt weiterhin liegengebliebene Objektdateien im Unit-Suchpfad, selbst wenn die geladene PPU aus dem richtigen Verzeichnis kam, und ein expliziter Objektausgabepfad ändert an dieser Bevorzugung nichts. Die einzige verlässliche Antwort ist ein frisches temporäres Unit-Verzeichnis pro Build-Runde. Alles andere erzeugt eine Binary, die aus zwei Compilerversionen gelinkt wurde und auf Arten scheitert, die nach Quellcode-Bugs aussehen

Plattform-Conditionals sind der letzte Baustein, und die richtige Achse zu wählen, matter mehr, als es scheint. Die richtige Frage lautet meist, ob der Code Windows-spezifisch ist, nicht ob eine bestimmte Widget-Bibliothek vorhanden ist, wie die Metafile-Konvertierungsarbeit in EMF-Vektorimport und Plattform-Conditionals zeigte: Diese Guard von einer Control-Library-Bedingung auf eine Plattformbedingung umzustellen, machte aus einer angeblichen Neuschreibung eine Änderung an einer einzigen Direktive. Free-Pascal- und Lazarus-Unterstützung für beide Windows-Ziele liegt der PDFlibPas Delphi PDF library bei, gebaut aus denselben Quellen wie die Delphi- und C++Builder-Pakete