Teknisk artikel

FPC Win32 OMF til COFF-object-linking i PDFlibPas

PDFlibPas bygges under Free Pascal til 32-bit Windows, og den svære del var aldrig Pascal. Det var objektfilerne: AES- og OpenJPEG-objekterne, som Delphi-builden linker, er OMF, Free Pascals interne linker kræver COFF, og konvertering mellem de to producerer sektionsnavne og sektionsdefinitionssymboler, der får linkeren til at fejle med interne fejl i stedet for ordentlige fejlmeldinger

Alle, der har linket C-objekter ind i et Pascal-bibliotek, kender dette territorium. Win64 er forholdsvis civiliseret med ét objektformat, én calling convention og ingen name decoration. Win32 bevarer alle de historielag, platformen har akkumuleret, og et bibliotek, der statisk linker tredjeparts-C-kode, møder dem alle på én gang

Compiler-mappen fortæller dig ikke, hvad target er

Start med build-indgangspunktet, for at tage fejl her koster timer, før nogen objektfil er involveret. Navnet på en Free Pascal-installationsmappe identificerer, hvor selve compileren bor, ikke hvad den producerer. En 32-bit host-compiler kan kalde en cross-compiler, der ligger ved siden af, og generere 64-bit kode, når du giver de rigtige target-switches, så at udlede target'en fra en sti er gætværk, der tilfældigvis virker, indtil nogen omorganiserer deres toolchain

Den pålidelige tilgang er at spørge compileren. Forespørg den faktiske target-processor og det faktiske styresystem gennem compilerens egne informationsswitches, og acceptér begge gængse installationslayouts, den flade binærmappe og den versionsindlejrede, for forskellige installers og toolchain-managere producerer forskellige former. Et build-script, der hardkoder ét af layoutsne, virker på præcis én maskine

Hvorfor bryder en konverteret objektfil den interne linker?

Fordi konverteringen bevarer OMF-konventionen for sektionsnavne og syntetiserer sektionsdefinitionssymboler, der ikke matcher, hvad COFF-linkeren forventer. At konvertere OMF-objekterne til COFF er nødvendigt, men ikke tilstrækkeligt: de resulterende filer bærer de klassiske _TEXT-, _DATA- og _BSS-sektionsnavne plus sektionsdefinitionssymbolnavne afledt af dem, og giver man det til Free Pascals interne linker, får man interne compilerfejl frem for en melding om sektionsnavngivning

En intern fejl er den værste fejlmåde for et build-problem, for den siger intet om, hvad der var galt med inputtet. Rettelsen er en normaliseringsomgang over COFF-filen efter konverteringen: omskriv sektionsnavnene til den forventede form, og omskriv de tilsvarende sektionsdefinitionssymboler, så de matcher, mens symbolindekset, kodebytene og relokeringerne holdes urørte. Sidstnævnte begrænsning er hele sværhedsgraden. En omskrivning, der omnummererer symboler eller flytter offsets, producerer et objekt, der linker og derefter crasher

Der er et forberedende trin for det ene af de to objectsæt. OpenJPEG-objekterne bygget med den klassiske 32-bit C++-compiler afhænger af Delphis private 64-bit integer-hjælperutiner, som Free Pascal ikke leverer, så ingen mængde formatkonvertering gør dem brugbare. De genbygges først med Clang-compileren, som ikke genererer de afhængigheder, og konverteres bagefter

Pipeline, der bærer PDFlibPas' statiske C-objekter fra Delphi-OMF i Lib\thirdparty\Win32 til FPC-linkbar COFF i Lib\thirdparty\Win32f på Win32: OMF-til-COFF-konvertering, en normaliseringsomgang, der omskriver sektionsnavne og sektionsdefinitionssymboler, mens symbolindeks, kodebyte og relokeringer holdes urørte, og Clang-genbygningen af OpenJPEG-objekter, der kalder Delphi-64-bit-hjælpere
Konvertering er nødvendig, men ikke tilstrækkelig: giv den omdøbte men ikke-normaliserede COFF til Free Pascals interne linker, og den svarer med interne fejl, så en omgang efter konverteringen retter navne og symboler uden at røre offsets
// Objekterne til FPC-target ligger i deres egen mappe. De erstatter ikke
// Delphi-objectsættet, for begge toolchains bygger fra det samme
// source tree, og hver af dem har brug for sine egne link-inputs
//
//   Lib\thirdparty\Win32   Delphi-OMF-objekter, uændrede
//   Lib\thirdparty\Win32f  FPC-COFF-objekter, konverterede og normaliserede
//
// Build-indgangspunkter:
//   build-Win32-Lib-FPC.cmd
//   build-Win64-Lib-FPC.cmd

Compiler-private hjælpere er ikke portable, og deres konventioner er det heller ikke

Delphi-runtime'en leverer assembly-trampoliner til 64-bit integer-operationer på 32-bit x86, og forudbyggede C-objekter bygget til Delphi kalder dem. Free Pascal har sin egen ordning, så de referencer skal tilfredsstilles anderledes i stedet for at blive viderestillet. Detaljen, der gør viderestilling umulig, er calling convention: timing-hjælperen, som imaging-koden bruger, får sit fire-byte-argument ryddet af callee, mens 64-bit divisionshjælperen rydder seksten byte og returnerer sit resultat i det klassiske registerpar. To hjælpere, to konventioner, og en trampoline skrevet til den ene korrumperer lydløst stakken for den anden

Name decoration tilføjer den anden halvdel af problemet. På Win32 sætter Free Pascal automatisk understreg foran eksterne C-importer, mens public name-deklarationer eksporteres ordret, så importsiden og eksportsiden af samme bro følger forskellige regler. Den C-runtime-bro, som OpenJPEG har brug for, skal derfor eksportere de præcise C-symbolnavne, og de variadiske entry points skal bruge et 32-bit indirekte jump frem for et direkte. Intet af det er eksotisk, når det først er sagt. Det hele fejler som en link-fejl, der nævner et symbol, ingen har skrevet

Hvad fik en Win32-executable til at dø, før main?

En 64-bit DLL på search path, nået fordi Free Pascal-uniten zlib binder dynamisk i stedet for at linke statisk. Symptomet var en øjeblikkelig exit med invalid-image-statuskoden, før nogen Pascal-kode i programmet var kørt, hvilket sender dig hen til at kigge på programmet, du lige har bygget, mens fejlen ligger i loaderen, der resolver en import mod den forkerte arkitektur

Lektionen handler om antagelser snarere end om zlib. En unit opkaldt efter et komprimeringsbibliotek indeholder ikke nødvendigvis et; det kan være en binding, der forventer et shared library ved runtime, og en dynamisk afhængighed, du ikke havde tænkt dig, er en deployment-liability, selv når den tilfældigvis resolver. Skifter man til den rene Pascal-streamimplementering, får begge targets en statisk inkluderet komprimeringssti helt uden eksterne afhængigheder, og det er, hvad et bibliotek, der indlejres i en andens applikation, burde have i første omgang

Samme instinkt gælder den eksterne JBIG2-encoder-backend. På 32-bit-target er den eksterne encoder ikke linket, så forespørgsler falder tilbage på den indbyggede Pascal-encoder, og testen, der verificerer det, er nødt til at tjekke registreringstilstanden for det aktuelle target i stedet for at tage en vellykket encode som bevis på, at den eksterne backend er til stede. En fallback, der virker, er præcis det, der skjuler en manglende afhængighed, hvilket er fejlmønsteret undersøgt i diagnostik af tavse stub-fejl. Arbejdet med 64-bit statisk linking er dækket i statisk linking af jbig2enc under FPC

Diagnostisk flow for en Win32-executable, der exiter før main under Free Pascal: loaderen resolver imports, mens unit-initialiseringen kører, zlib-bindingen finder en 64-bit DLL på search path, og processen dør med invalid-image-status før nogen Pascal-sætning, hvilket skubber PDFlibPas mod en statisk inkluderet ren Pascal-komprimeringssti
Fejlen var aldrig programmet, der lige var bygget: en unit ved navn zlib var en runtime-binding resolver mod den forkerte arkitektur, og en fallback, der virker, som den indbyggede JBIG2-encoder, skjuler en manglende afhængighed

32-bit aritmetik på en memory stream

Kode, der manipulerer bufferstørrelser med pointer-bredde unsigned-aritmetik, er korrekt på Win64 og ét stort billede fra overflow på Win32. In-memory-streamen, der fodrer JPEG 2000-codec'et, vokser ved fordobling og avancerer ved addition, og på et 32-bit target kan begge operationer wrappe på input, der er store, men helt legitime

Enhver write, skip, seek og initial allokering tjekker derfor, før den regner, og loftet for kapacitet er den maksimale signede pointer-bredde-værdi, valgt til at matche, hvad block move-rutinen og callback-returværdierne kan udtrykke. Kravet om adfærd, når en forespørgsel afvises, er let at tage fejl af: en afvisning må ikke ændre stream-positionen eller dens længde. En delvis mutation efterfulgt af en fejl efterlader streamen i en tilstand, som kalderen ikke kan ræsonnere om, og den næste operation forstærker det

// Tjek før du regner. På Win32 wrapper begge dele på input, som et
// stort JPEG 2000-billede legitimit producerer
if Needed > NativeUInt(High(NativeInt)) - FPosition then
  Exit(False);                    // afvis, lad position og størrelse være

NewCapacity := FCapacity;
while (NewCapacity < FPosition + Needed) do
begin
  if NewCapacity > NativeUInt(High(NativeInt)) shr 1 then
    Exit(False);                  // fordobling ville give overflow
  NewCapacity := NewCapacity shl 1;
end;

To build-output-fælder, der overlever porteringen

Adskillelse af test- og sample-executables efter target-arkitektur i output-mapper pr. target er åbenlyst rigtigt og bryder øjeblikkeligt alt, hvad der fandt sine testdata ved at tælle mappeleveller opad. Rettelsen er at søge opad efter asset-mappen i stedet for at antage en fast dybde, med én bevidst begrænsning: signing-samplet accepterer et certifikat som fallback kun fra sin egen projektmappe, aldrig fra en vilkårlig forfader, for et enslydende certifikat fundet længere oppe i træet er en sikkerhedsmæssig overraskelse snarere end en bekvemmelighed

Den anden fælde overlever enhver portering og er værd at tage med til ethvert FPC-projekt. Efter en compiler-opgradering er det ikke nok at få compileren til at afvise forældede PPU-filer, for linkeren foretrækker stadig efterladte objektfiler i unit search path, selv når den PPU, den indlæste, kom fra den korrekte mappe, og at tilføje en eksplicit object output-sti tilsidesætter ikke den præference. Det eneste pålidelige svar er en frisk midlertidig unit-mappe pr. build-runde. Mindre end det giver en binary linket fra to compilerversioner, som fejler på måder, der ligner source-bugs

Platform conditionals er den sidste brik, og valget af den rigtige akse betyder mere, end det ser ud. Det rigtige spørgsmål er som regel, om koden er Windows-specifik, snarere end om et bestemt widget-bibliotek er til stede, som metafile-konverteringsarbejdet i EMF-vektorimport og platform conditionals viste: at skifte den guard fra en control-library-betingelse til en platformsbetingelse forvandlede en formodet rewrite til en ændring af ét enkelt direktiv. Free Pascal- og Lazarus-understøttelse af begge Windows-targets følger med PDFlibPas Delphi PDF-biblioteket, bygget fra de samme sources som Delphi- og C++Builder-pakkerne