Műszaki cikk

OMF–COFF objektumlinkelés FPC Win32 alatt a PDFlibPas-ban

A PDFlibPas 32 bites Windowson Free Pascal alatt is fordul, és a nehéz rész sosem a Pascal volt. Hanem az objektumfájlok: az AES és az OpenJPEG objektumok, amelyeket a Delphi build linkel, OMF formátumúak, a Free Pascal belső linkere COFF-ot kíván, és a kettő közötti átalakítás olyan szekcióneveket és szekciódefiníciós szimbólumokat ad, amelyekre a linker diagnosztika helyett belső hibákkal reagál

Aki valaha linkelt C objektumokat Pascal könyvtárba, az ismeri ezt a terepet. A Win64 viszonylag civilizált: egy objektumformátum, egy hívási konvenció, névfestés nélkül. A Win32 viszont megőrzi a platform által felhalmozott történelem minden rétegét, és egy harmadik féltől származó C kódot statikusan linkelő könyvtár mindegyikkel egyszerre találkozik

A fordító könyvtárneve nem árulja el a célt

Kezdje a build belépési pontjánál, mert ha itt rontja el, az objektumfájlok érintése előtt órákat veszít. A Free Pascal telepítési könyvtárának neve azt mondja meg, hol lakik a fő fordító, nem azt, mit állít elő. Egy 32 bites gazdafordító meghívhatja a mellette ülő cross-compilert, és a megfelelő célkapcsolókkal 64 bites kódot ad ki, tehát a cél kikövetkeztetése az útvonalból találgatás, ami éppen működik, amíg valaki át nem rendezi a toolchaint

A megbízható út az, ha megkérdezi a fordítót. Kérdezze le a tényleges célprocesszort és operációs rendszert a fordító saját információs kapcsolóin keresztül, és fogadja el mindkét szokásos telepítési elrendezést, a lapos bináris könyvtárat és a verzió szerint egymásba ágyazottat, mert a különböző telepítők és toolchain-kezelők különböző formákat hagynak maguk után. A valamelyik elrendezést beégető build szkript pontosan egy gépen működik

Miért töri meg az átalakított objektumfájl a belső linkert?

Mert az átalakítás megőrzi az OMF szekcióelnevezési konvenciót, és olyan szekciódefiníciós szimbólumokat gyárt, amelyek nem egyeznek azzal, amit a COFF linker vár. Az OMF objektumok COFF-ra váltása szükséges, de nem elegendő: a keletkező fájlok a klasszikus _TEXT, _DATA és _BSS szekcióneveket viselik, hozzájuk a belőlük származtatott szekciódefiníciós szimbólumneveket, és ha ezt eteti a Free Pascal belső linkerébe, a szekcióelnevezésre utaló üzenet helyett belső fordítóhibákat kap

A belső hiba a legrosszabb hibamód egy build problémára, mert semmit sem mond arról, mi volt baj a bemenettel. A javítás egy átalakítás utáni normalizáló menet a COFF fájlon: írja át a szekcióneveket a várt formára, és a hozzájuk illő szekciódefiníciós szimbólumokat is, miközben a szimbólumindex, a kódbájtok és a relokációk érintetlenül maradnak. Az utolsó megkötés az egész nehézség. Az olyan átírás, amely újszámozza a szimbólumokat vagy eltolja az offseteket, olyan objektumot ad, amely linkel, aztán elszáll

A két objektumkészlet egyikéhez egy előzetes lépés is kell. A klasszikus 32 bites C++ fordítóval épített OpenJPEG objektumok Delphi privát 64 bites egészsegéd-rutinoktól függenek, amelyeket a Free Pascal nem biztosít, tehát egyetlen formátumátalakítás sem teszi őket használhatóvá. Ezeket először a Clang alapú fordítóval építik újra, amely ezeket a függőségeket nem emitálja, és csak utána alakítják át

Útvonal, amelyen a PDFlibPas statikus C objektumjai a Lib\thirdparty\Win32 könyvtárban lévő Delphi OMF formából az FPC számára linkelhető COFF formába jutnak a Lib\thirdparty\Win32f könyvtárba Win32-n: OMF–COFF átalakítás, egy normalizáló menet, amely a szekcióneveket és a szekciódefiníciós szimbólumokat írja át a szimbólumindex, a kódbájtok és a relokációk érintetlenül hagyásával, valamint Clang újraépítés a Delphi 64 bites segédjeket hívó OpenJPEG objektumokhoz
Az átalakítás szükséges, de nem elegendő: az átnevezett, de nem normalizált COFF-ot a Free Pascal belső linkerébe adva belső hibákkal válaszol, ezért az átalakítás utáni menet a neveket és a szimbólumokat javítja, az offsetekhez nem nyúl
// Az FPC célhoz tartozó objektumok saját könyvtárban élnek. Nem
// váltják fel a Delphi objektumkészletet, mert mindkét toolchain
// ugyanabból a forrásfából épít, és mindegyiknek saját linkbemenet kell
//
//   Lib\thirdparty\Win32   Delphi OMF objektumok, változatlanul
//   Lib\thirdparty\Win32f  FPC COFF objektumok, átalakítva és normalizálva
//
// Build belépési pontok:
//   build-Win32-Lib-FPC.cmd
//   build-Win64-Lib-FPC.cmd

A fordító privát segédjei nem hordozhatók, és a konvenciójuk sem

A Delphi futtatókörnyezete assembly trampoline-okat szolgáltat a 64 bites egészműveletekhez 32 bites x86-on, és a Delphi számára előre lefordított C objektumok ezekbe hívnak bele. A Free Pascalnak megvan a maga megoldása, tehát ezekre a hivatkozásokra másképp kell választ adni, nem átirányítással. Az a részlet, amely az átirányítást lehetetlenné teszi, a hívási konvenció: a képkezelő kód által használt időzítő segéd négybájtos argumentumát a hívott fél takarítja le, míg a 64 bites osztó segéd tizenhat bájtot, és az eredményét a klasszikus regiszterpárban adja vissza. Két segéd, két konvenció, és az egyikre írt trampoline csendben megrongálja a vermet a másiknál

A névfestés adja a probléma második felét. Win32-n a Free Pascal a külső C importok elé automatikusan aláhúzást tesz, miközben a public name deklarációkat szóról szóra exportálja, tehát ugyanannak a hídnak az import oldala és export oldala különböző szabályok szerint él. Ezért annak a C futásidejű hídnak, amelyre az OpenJPEG-nek szüksége van, pontosan a C szimbólumneveket kell exportálnia, a variadikus belépési pontoknak pedig 32 bites közvetett ugrásra van szükségük, nem közvetlenre. Ebből semmi sem egzotikus, ha már kimondták. Mindegyik linkhibaként bukik meg, amely egy senki által nem írt szimbólumot nevez meg

Mitől halt meg egy Win32 futtatható fájl a main előtt?

Egy 64 bites DLL a keresési útvonalon, ahová azért jutott el a program, mert a Free Pascal zlib unitja dinamikusan köt, nem statikusan linkel. A tünet azonnali kilépés volt érvénytelen-kép státuszkóddal, még a programban lévő bármilyen Pascal kód lefutása előtt, ami arra küldi az embert, hogy az imént épített programját bámulja, miközben a hiba abban van, hogy a betöltő egy importot rossz architektúrához old fel

A tanulság a feltevésekről szól, nem a zlibről. Egy tömörítő könyvtárról elnevezett unit nem feltétlenül tartalmaz ilyet; lehet olyan kötés is, amely futásidőben osztott könyvtárat vár, és egy nem szándékolt dinamikus függőség telepítési teher akkor is, ha éppen feloldódik. A tiszta Pascal stream implementációra váltás mindkét célhoz statikusan befordított tömörítési utat ad külső függőség nélkül, és pontosan ez kellene annak a könyvtárnak, amelyet valaki más alkalmazásába ágyaznak be

Ugyanez az ösztön érvényes a külső JBIG2 encoder backendre is. A 32 bites célon a külső encoder nincs belinkelve, tehát a kérések a beépített Pascal encoderhez esnek vissza, és az ezt ellenőrző tesztnek az aktuális cél regisztrációs állapotát kell megnéznie, nem pedig a sikeres kódolást kell bizonyítéknak vennie arra, hogy a külső backend jelen van. Egy működő fallback pontosan az, ami elrejt egy hiányzó függőséget, és ez a hibaminta, amelyet a néma stub hibák diagnosztizálása tárgyal. A 64 bites statikus linkelés munkáját a jbig2enc FPC alatti statikus linkelése fedi le

Diagnosztikai folyamat egy Free Pascal alatt a main előtt kilépő Win32 futtatható fájlhoz: a betöltő az importokat oldja fel, miközben a unit inicializáció fut, a zlib kötés 64 bites DLL-t talál a keresési útvonalon, és a folyamat érvénytelen-kép státusszal hal meg az első Pascal utasítás előtt, ami a PDFlibPast a statikusan befordított, tiszta Pascal tömörítési út felé tereli
A hiba sosem az imént épített program volt: a zlib nevű unit futásidejű kötés volt, amelyet rossz architektúrához oldott fel a betöltő, és a működő fallback, például a beépített JBIG2 encoder, épp azt rejti el, hogy egy függőség hiányzik

32 bites aritmetika a memóriastreamen

A pufferméreteket pointer-szélességű, előjel nélküli aritmetikával kezelő kód Win64-en helyes, és Win32-en egyetlen nagy kéttől van átvitelig. A JPEG 2000 kodeket ellátó memóriabeli stream duplázással nő és összeadással halad, és egy 32 bites célon mindkét művelet átcsoroghat olyan bemeneteken, amelyek nagyok, de teljesen legálisak

Minden írás, átugrás, seek és kezdeti foglalás ezért számolás előtt ellenőriz, és a kapacitásplafon a maximális előjeles pointer-szélességű érték, amelyet ahhoz választottak, amit a blokkmozgató rutin és a callback visszatérési értékei ki tudnak fejezni. A viselkedési követelmény arra az esetre, amikor egy kérést elutasítanak, könnyen félremegy: az elutasítás nem változtathatja meg a stream pozícióját sem, sem a hosszát. Egy részleges módosítás, amelyet hiba követ, olyan állapotban hagyja a streamet, amiről a hívó nem tud következtetni, és a következő művelet ráhalmoz

// Ellenőrizzen, mielőtt számol. Win32-n mindkettő átcsorog olyan
// bemeneteken, amelyeket egy nagy JPEG 2000 kép legálisan előállít
if Needed > NativeUInt(High(NativeInt)) - FPosition then
  Exit(False);                    // elutasítás, pozíció és méret érintetlen

NewCapacity := FCapacity;
while (NewCapacity < FPosition + Needed) do
begin
  if NewCapacity > NativeUInt(High(NativeInt)) shr 1 then
    Exit(False);                  // a duplázás átvinné a tartományt
  NewCapacity := NewCapacity shl 1;
end;

Két buildkimeneti csapda, amelyek túlélik a portolást

A teszt- és minta-executablek szétválasztása célarchitektúránként saját kimeneti könyvtárakba nyilvánvalóan helyes, és azonnal megtörik bármi, ami a tesztadatait felfelé számolt könyvtárszintek alapján találta meg. A javítás az, hogy felfelé keresve találja meg az eszközkönyvtárat, rögzített mélység feltételezése helyett, egy szándékos megszorítással: a signing minta csak a saját projektkönyvtárából fogad el tartalék tanúsítványt, soha nem tetszőleges ősökönyvtárból, mert egy azonos nevű tanúsítvány, amelyet messzebb a fában találunk, biztonsági meglepetés, nem kényelem

A második csapda minden portot túléli, és érdemes magával vinni bármely FPC projektbe. Fordítófrissítés után nem elég, ha a fordító elutasítja az elavult PPU fájlokat, mert a linker továbbra is a maradvány objektumfájlokat részesíti előnyben a unit keresési útvonalon, akkor is, ha a betöltött PPU helyes könyvtárból származott, és egy explicit objektumkimeneti útvonal megadása ezt a preferenciát nem írja felül. Az egyetlen megbízható válasz friss, ideiglenes unit könyvtár build körönként. Bármi kevesebb olyan binárist ad, amelyet két fordítóverzióból linkeltek, és az olyan módokon bukik meg, amelyek forráshibának látszanak

A platformfeltételek az utolsó darab, és a helyes tengely megválasztása fontosabb, mint amennyinek látszik. A helyes kérdés általában az, hogy a kód Windows-specifikus-e, és nem az, hogy egy adott widget könyvtár jelen van-e, ahogy a EMF vektorimport és platformfeltételek cikk metafájl-átalakítási munkája megmutatta: az őrző átállítása vezérlőkönyvtár-feltételről platformfeltételre az állítólagos újraírást egyetlen direktíva megváltoztatására csökkentette. A Free Pascal és a Lazarus támogatása mindkét Windows célhoz a PDFlibPas Delphi PDF könyvtárral együtt jár, amelyet a Delphi és a C++Builder csomagokkal azonos forrásokból építenek