Teknisk artikel

FPC Win32: OMF-till-COFF-länkning i PDFlibPas

PDFlibPas bygger under Free Pascal för 32-bitars Windows, och det svåra var aldrig Pascal. Det var objektfilerna: AES- och OpenJPEG-objekten som Delphi-bygget länkar in är OMF, Free Pascals interna länkare kräver COFF, och konvertering mellan de två formaten ger sektionsnamn och sektionsdefinitionssymboler som får länkaren att falla på interna fel i stället för riktig diagnostik

Vem som helst som har länkat C-objekt in i ett Pascal-bibliotek känner igen terrängen. Win64 är förhållandevis civiliserat, med ett objektformat, en anropskonvention och ingen namndekoration. Win32 bevarar varenda historiskt lager plattformen samlat på sig, och ett bibliotek som statiskt länkar tredjepartskod i C möter alla lagren på en gång

Kompilatorkatalogen säger inte vilket mål du bygger för

Börja med byggets ingångspunkt, för att ha fel här kostar timmar innan någon objektfil ens är inblandad. En Free Pascal-installationskatalog identifierar var huvudkompilatorn bor, inte vad den producerar. En 32-bitars värdkompilator kan anropa en korskompilator bredvid sig och avge 64-bitarskod när du skickar med rätt målswitchar, så att gissa målet från en sökväg är en ren gissning som fungerar tills någon omorganiserar sin verktygskedja

Det tillförlitliga sättet är att fråga kompilatorn. Fråga fram den faktiska målprocessorn och måloperativsystemet via kompilatorns egna informationsswitchar, och acceptera båda vanliga installationslayouterna, den platta binärkatalogen och den versionskapslade, eftersom olika installerare och verktygskedjehanterare producerar olika former. Ett byggskript som hårdkodar någon av layouterna fungerar på exakt en maskin

Varför bryter en konverterad objektfil den interna länkaren?

Därför att konverteringen bevarar OMF:s sektionsnamngivning och syntetiserar sektionsdefinitionssymboler som inte matchar vad COFF-länkaren förväntar sig. Att konvertera OMF-objekten till COFF är nödvändigt men inte tillräckligt: de resulterande filerna bär på de klassiska sektionsnamnen _TEXT, _DATA och _BSS, plus sektionsdefinitionssymbolnamn härledda ur dem, och matar man det till Free Pascals interna länkare får man interna kompilatorfel i stället för ett meddelande om sektionsnamngivning

Ett internt fel är det sämsta felläget för ett byggproblem, eftersom det inte säger något om vad som var fel på indatan. Fixen är ett normaliseringssteg efter konverteringen över COFF-filen: skriv om sektionsnamnen till den förväntade formen och skriv om motsvarande sektionsdefinitionssymboler så att de matchar, samtidigt som symbolindexet, kodbytena och relokeringarna lämnas orörda. Den sista begränsningen är hela svårigheten. En omskrivning som numrerar om symboler eller skiftar offset ger ett objekt som länkar och sedan kraschar

Det finns ett förberedande steg för den ena av de två objektuppsättningarna. OpenJPEG-objekten byggda med den klassiska 32-bitars C++-kompilatorn är beroende av Delphis privata 64-bitars heltalshjälprutiner, som Free Pascal inte tillhandahåller, så ingen mängd formatkonvertering gör dem användbara. De byggs om med Clang-baserade kompilatorn först, som inte avger de beroendena, och konverteras efteråt

Pipeline som bär PDFlibPas statiska C-objekt från Delphi-OMF i Lib\thirdparty\Win32 till FPC-länkbar COFF i Lib\thirdparty\Win32f på Win32: OMF-till-COFF-konvertering, ett normaliseringssteg som skriver om sektionsnamn och sektionsdefinitionssymboler medan symbolindex, kodbyte och relokeringar lämnas orörda, samt Clang-ombygget för OpenJPEG-objekt som anropar Delphis 64-bit-hjälpare
Konvertering är nödvändig men inte tillräcklig: mata den omnamnade men inte normaliserade COFF:en till Free Pascals interna länkare och svaret blir interna fel, så ett steg efter konverteringen fixar namn och symboler utan att röra offset
// Objekt för FPC-målet ligger i en egen katalog. De ersätter inte
// Delphi-objektuppsättningen, eftersom båda verktygskedjorna bygger
// från samma källträd och var och en behöver sina egna länkindata
//
//   Lib\thirdparty\Win32   Delphi-OMF-objekt, oförändrade
//   Lib\thirdparty\Win32f  FPC-COFF-objekt, konverterade och normaliserade
//
// Byggningångspunkter:
//   build-Win32-Lib-FPC.cmd
//   build-Win64-Lib-FPC.cmd

Kompilatorprivata hjälprutiner är inte portabla, och deras konventioner är det inte heller

Delphi-runtime:n tillhandahåller assembler-trampoliner för 64-bitars heltalsoperationer på 32-bitars x86, och förkompilerade C-objekt byggda för Delphi anropar dem. Free Pascal har sin egen lösning, så de referenserna måste tillfredsställas på annat sätt i stället för att omdirigeras. Detaljen som gör omdirigering omöjlig är anropskonventionen: timinghjälparen som bildkoden använder låter callee städa upp dess fyra byte långa argument, medan 64-bit divisionshjälparen städar sexton byte och returnerar sitt resultat i det klassiska registerparet. Två hjälpare, två konventioner, och en trampolin skriven för den ena korrumperar tyst stacken för den andra

Namndekorationen utgör den andra halvan av problemet. På Win32 prefixar Free Pascal externa C-importer med understrykningstecken automatiskt medan public name-deklarationer exporteras ordagrant, så importsidan och exportsidan av samma bro följer olika regler. C-runtime-bryggan som OpenJPEG behöver måste därför exportera exakta C-symbolnamn, och de variadiska ingångspunkterna behöver ett 32-bit indirekt hopp i stället för ett direkt. Inget av detta är exotiskt när det väl sagts. Allt misslyckas som ett länkfel som namnger en symbol ingen skrev

Vad fick en Win32-exekverbar att dö före main?

En 64-bit DLL på sökvägen, nådd för att Free Pascal-enheten zlib binder dynamiskt i stället för att länka statiskt. Symptomet var en omedelbar exit med statuskoden för ogiltig avbild, innan någon Pascal-kod i programmet hunnit köra, vilket skickar dig till att granska programmet du just byggt när felet ligger i laddaren som löser en import mot fel arkitektur

Lärdomen handlar om antaganden snarare än om zlib. En enhet uppkallad efter ett komprimeringsbibliotek innehåller inte nödvändigtvis ett sådant; den kan vara en binding som förväntar sig ett delat bibliotek vid körning, och ett dynamiskt beroende du inte avsåg är ett deploymentsansvar även när det råkar lösa sig. Att byta till den rena Pascal-strömimplementeringen ger båda målen en statiskt inkluderad komprimeringsväg helt utan externt beroende, vilket är vad ett bibliotek inbäddat i någons applikation borde ha från början

Samma instinkt gäller den externa JBIG2-encoder-backend:en. På 32-bit-målet länkas den externa encodern inte in, så förfrågningar faller tillbaka på den inbyggda Pascal-encodern, och testet som verifierar detta måste kontrollera registreringstillståndet för det aktuella målet i stället för att behandla en lyckad encoding som bevis för att den externa backend:en finns. En reservväg som fungerar är precis det som gömmer ett saknat beroende, vilket är fellivet undersökt i diagnos av tysta stub-fel. 64-bit-arbetet med statisk länkning tas upp i statisk länkning av jbig2enc under FPC

Diagnostikflöde för en Win32-exekverbar som avslutas före main under Free Pascal: laddaren löser importer medan enhetsinitialisering kör, zlib-bindingen hittar en 64-bit DLL på sökvägen och processen dör med ogiltig-avbild-status innan någon Pascal-sats hunnit köra, vilket driver PDFlibPas mot en statiskt inkluderad ren Pascal-komprimeringsväg
Felet var aldrig programmet som just byggts: en enhet uppkallad efter zlib var en runtime-binding löst mot fel arkitektur, och en reservväg som fungerar, som den inbyggda JBIG2-encodern, gömmer ett saknat beroende

32-bit aritmetik på en minnesström

Kod som manipulerar buffertstorlekar med pekarbredd teckenlös aritmetik är korrekt på Win64 och en enda stor bild från överspill på Win32. Minnesströmmen som matar JPEG 2000-codec:en växer genom fördubbling och avancera genom addition, och på ett 32-bit-mål kan båda operationerna wrappa på indata som är stor men helt legitim

Varje skrivning, hopp, seek och initial allokering kontrollerar därför innan den räknar, och taket för kapaciteten är det största teckenlösa pekarbreddsvärdet, valt för att matcha vad blockflyttsrutinen och callback-returvärdena kan uttrycka. Kravet på beteendet när en förfrågan avslås är lätt att få fel: att avslå får inte ändra strömpositionen eller dess längd. En partiell mutation följt av ett fel lämnar strömmen i ett tillstånd anroparen inte kan resonera om, och nästa operation förvärrar det

// Kontrollera innan du räknar. På Win32 wrappar båda dessa på indata
// som en stor JPEG 2000-bild legitimt producerar
if Needed > NativeUInt(High(NativeInt)) - FPosition then
  Exit(False);                    // avslå, rör inte position och storlek

NewCapacity := FCapacity;
while (NewCapacity < FPosition + Needed) do
begin
  if NewCapacity > NativeUInt(High(NativeInt)) shr 1 then
    Exit(False);                  // fördubbling skulle ge överspill
  NewCapacity := NewCapacity shl 1;
end;

Två byggutdata-fällor som överlever porteringen

Att separera test- och sample-exekverbara per målarkitektur i permåls-utdatakataloger är uppenbart rätt och går direkt sönder allt som lokaliserade sin testdata genom att räkna katalognivåer uppåt. Fixen är att söka uppåt efter resurskatalogen i stället för att anta ett fast djup, med en medveten begränsning: signerings-samplen accepterar ett certifikatreservvärde endast från sin egen projektkatalog, aldrig från en godtycklig anfader, eftersom ett samnamnigt certifikat högre upp i trädet är en säkerhetsöverraskning snarare än en bekvämlighet

Den andra fällan överlever varje portering och är värd att bära med sig till varje FPC-projekt. Efter en kompilatoruppgradering är det inte nog att låta kompilatorn avvisa inaktuella PPU-filer, eftersom länkaren fortfarande föredrar kvarlämnade objektfiler i enhetssökvägen även när PPU:en den laddade kom från rätt katalog, och en explicit objektutdatasökväg åsidosätter inte den preferensen. Det enda tillförlitliga svaret är en färsk temporär enhetskatalog per byggerunda. Mindre än så ger en binär länkad från två kompilatorversioner, vilket fallerar på sätt som ser ut som källkodsbuggar

Plattformsvillkoren är den sista biten, och valet av rätt axel betyder mer än det verkar. Den rätta frågan är vanligen om koden är Windows-specifik snarare än om ett visst widgetbibliotek finns, som metafilkonverteringsarbetet i EMF-vektorimport och plattformsvillkor visade: att byta det villkoret från ett kontrollbiblioteksvillkor till ett plattformsvillkor förvandlade en påstådd omskrivning till en ändring av ett enda direktiv. Free Pascal- och Lazarus-stöd för båda Windows-målen medföljer PDFlibPas Delphi PDF library, byggd från samma källor som Delphi- och C++Builder-paketen