Technický článek

OMF na COFF: linkování objektů pro FPC Win32 v PDFlibPas

PDFlibPas se pod Free Pascalem sestavuje i pro 32bitové Windows, a to těžké v tom nikdy nebyl Pascal. Byly to objektové soubory: objekty AES a OpenJPEG, které linkuje sestavení Delphi, jsou ve formátu OMF, interní linker Free Pascalu vyžaduje COFF, a převod mezi oběma produkuje názvy sekcí a symboly definic sekcí, kvůli nimž linker padá s internal errors místo pořádné diagnostiky

Kdo už někdy linkoval C objekty do Pascalovské knihovny, ten tenhle terén zná. Win64 je v tomhle ohledu poměrně civilizovaný: jeden formát objektů, jedna volací konvence a žádná dekorace jmen. Win32 si naopak nese každou vrstvu historie, kterou platforma nashromáždila, a knihovna, která staticky linkuje C kód třetích stran, se s ní všemi setká najednou

Z adresáře kompilátoru cíl nepoznáte

Začněte u vstupního bodu sestavení, protože chyba tady zahodí hodiny, ještě než se vůbec provede kterýkoli objektový soubor. Název instalačního adresáře Free Pascalu říká, kde bydlí hlavní kompilátor, ne co vyrábí. 32bitový hostitelský kompilátor může vedle sebe vyvolat cross-kompilátor a při správných přepínačích cíle vyslat 64bitový kód, takže odhadovat cíl z cesty je hádání, které funguje právě do chvíle, kdy někdo přeuspořádá svůj toolchain

Spolehlivá cesta je zeptat se kompilátoru. Skutečný cílový procesor a operační systém zjistíte přes informační přepínače kompilátoru, a počítejte s oběma běžnými layouty instalace, s plochým binárním adresářem i s verzí vnořenou do podadresářů, protože různé instalátory a správci toolchainů vyrábějí různé tvary. Build skript, který natvrdo zapeče jeden konkrétní layout, funguje přesně na jediném stroji

Proč zkonvertovaný objektový soubor rozbije interní linker?

Protože převod zachová konvenci pojmenovávání sekcí z OMF a vysyntetizuje symboly definic sekcí, které neodpovídají tomu, co COFF linker čeká. Konverze OMF objektů na COFF je nutná, ale nikoli dostačující: výsledné soubory nesou klasické názvy sekcí _TEXT, _DATA a _BSS plus z nich odvozené názvy symbolů definic sekcí, a když to nakrmíte internímu linkeru Free Pascalu, dostanete interní chyby kompilátoru místo zprávy o pojmenovávání sekcí

Interní chyba je u buildovacího problému nejhorší možný způsob selhání, protože o tom, co bylo špatně na vstupu, neříká vůbec nic. Řešením je normalizační průchod nad COFF souborem po konverzi: přepsat názvy sekcí do očekávané podoby a přepsat odpovídající symboly definic sekcí tak, aby ladily, přičemž symbolový index, bajty kódu i relokace zůstanou nedotčené. Právě ta poslední podmínka je celá obtíž. Přepis, který přečísluje symboly nebo posouvá offsety, vyprodukuje objekt, který se sice linkuje a pak spadne

Pro jednu z obou sad objektů je tu ještě předběžný krok. Objekty OpenJPEG sestavené klasickým 32bitovým C++ kompilátorem závisí na privátních 64bitových celočíselných helper rutinách Delphi, které Free Pascal neposkytuje, takže žádná konverze formátu je neučiní použitelnými. Ty se proto nejdřív znovu sestaví kompilátorem založeným na Clangu, který tyhle závislosti nevysílá, a teprve potom se zkonvertují

Pipeline nesoucí statické C objekty PDFlibPas z Delphi OMF v Lib\thirdparty\Win32 do FPC-linkovatelného COFF v Lib\thirdparty\Win32f na Win32: konverze OMF na COFF, normalizační průchod přepisující názvy sekcí a symboly definic sekcí, aniž by sáhl na symbolový index, bajty kódu a relokace, a Clang rebuild pro OpenJPEG objekty volající 64bitové helpery Delphi
Konverze je nutná, ale nestačí: nenormalizovaný, jen přejmenovaný COFF dá internímu linkeru Free Pascalu k zakousnutí a odpoví si internal errors, takže post-konverzní průchod opraví názvy a symboly, aniž by sahl na offsety
// Objekty pro cíl FPC žijí ve vlastním adresáři. Nenahrazují sadu
// objektů Delphi, protože oba toolchainy sestavují ze stejného
// zdrojového stromu a každý potřebuje vlastní vstupy linkování
//
//   Lib\thirdparty\Win32   OMF objekty Delphi, beze změn
//   Lib\thirdparty\Win32f  COFF objekty pro FPC, zkonvertované a normalizované
//
// Vstupní body sestavení:
//   build-Win32-Lib-FPC.cmd
//   build-Win64-Lib-FPC.cmd

Privátní helpery kompilátoru nejsou přenositelné, a ani jejich konvence

Delphi runtime dodává assembly trampolíny pro 64bitové celočíselné operace na 32bitovém x86 a předkompilované C objekty stavěné pro Delphi se do nich odkazují. Free Pascal má vlastní uspořádání, takže tyhle reference se musí uspokojit jinak, nikoli přesměrovat. Detail, který přesměrování znemožňuje, je volací konvence: timing helper používaný imagingovým kódem má svůj čtyřbajtový argument uklizený volaným, zatímco helper 64bitového dělení uklízí šestnáct bajtů a výsledek vrací v klasickém páru registrů. Dva helpery, dvě konvence, a trampolína napsaná pro ten jeden tiše rozhází zásobník tomu druhému

Druhou polovinu problému přidává dekorace jmen. Na Win32 Free Pascal automaticky prefixuje externí C importy podtržítkem, zatímco deklarace public name exportuje doslova, takže importní a exportní strana téhož mostu se řídí různými pravidly. C runtime most, který OpenJPEG potřebuje, proto musí exportovat přesné symbolové názvy z C a variadické vstupní body potřebují 32bitový nepřímý skok místo přímého. Jakmile je to takhle vysvětlené, není na ničem z toho nic exotického. Celé to ale selhává jako linkovací chyba uvádějící symbol, který nikdo nenapsal

Co donutilo Win32 spustitelný soubor zemřít před main?

64bitová DLL na search path, na kterou se program dostal proto, že jednotka zlib Free Pascalu se váže dynamicky místo statického linkování. Symptomem bylo okamžité ukončení se statusovým kódem invalid image, ještě před spuštěním jakéhokoli Pascalovského kódu, což člověka pošle zkoumat program, který právě sestavil, zatímco zrada je v loaderu párujícím import se špatnou architekturou

Poučení se týká předpokladů, ne zlib. Jednotka pojmenovaná po kompresní knihovně nemusí nutně nějakou obsahovat; může to být binding čekající za běhu na sdílenou knihovnu a dynamická závislost, kterou jste nezamýšleli, je deployní zátěž i v okamžiku, kdy se náhodou zrovna vyřeší. Přechod na čistě Pascalovskou implementaci streamů dává oběma cílům staticky zahrnutou kompresní cestu bez jediné externí závislosti, a přesně to by měla mít knihovna zabudovaná v cizí aplikaci od začátku

Týž instinkt platí pro externí backend JBIG2 encoderu. Na 32bitovém cíli se externí encoder nelinkuje, takže požadavky spadnou na vestavěný Pascalovský encoder, a test, který tohle ověřuje, musí zkontrolovat registrační stav aktuálního cíle místo toho, aby bral úspěšné kódování jako důkaz přítomnosti externího backendu. Fallback, který funguje, je přesně ta věc, která schovanou chybějící závislost ukrývá, a to je selhávácí vzor rozebraný v článku diagnostika tichých selhání stubů. Práci se statickým linkováním na 64 bitů pokrývá statické linkování jbig2enc pod FPC

Diagnostický tok pro Win32 spustitelný soubor končící před main pod Free Pascalem: loader vyřizuje importy, zatímco běží inicializace unit, binding zlib najde na search path 64bitovou DLL a proces umírá se statusem invalid image ještě před jakýmkoli Pascalovským příkazem, což PDFlibPas postrčí k staticky zahrnuté čistě Pascalovské kompresní cestě
Vina nikdy nebyla v čerstvě sestaveném programu: jednotka pojmenovaná zlib byla runtime binding párovaný se špatnou architekturou a fungující fallback, jako vestavěný JBIG2 encoder, ukrývá chybějící závislost

32bitová aritmetika na memory streamu

Kód, který manipuluje s velikostmi bufferů pomocí unsigned aritmetiky šířky ukazatele, je na Win64 správně a na Win32 je od přetečení vzdálen jeden velký obrázek. In-memory stream, který krmí kodek JPEG 2000, roste zdvojnásobováním a posunuje se sčítáním, a na 32bitovém cíli mohou obě operace přetéct na vstupech velkých, ale naprosto legitimních

Každý zápis, skip, seek i počáteční alokace se proto nejdřív zkontroluje a teprve pak počítá, a strop kapacity je maximální signed hodnota šířky ukazatele, zvolená tak, aby odpovídala tomu, co dokážou vyjádřit rutina blokového přesunu i návratové hodnoty callbacku. Behaviorální požadavek při odmítnutí se snadno pokazí: odmítnutí nesmí změnit pozici streamu ani jeho délku. Částečná mutace následovaná chybou nechá stream ve stavu, nad nímž volající nedokáže uvažovat, a další operace to ještě znásobí

// Nejdřív zkontroluj, pak počítej. Na Win32 obě věci přetečou na vstupech,
// které velký obrázek JPEG 2000 produkuje zcela legitimně
if Needed > NativeUInt(High(NativeInt)) - FPosition then
  Exit(False);                    // odmítni, pozici a velikost nech být

NewCapacity := FCapacity;
while (NewCapacity < FPosition + Needed) do
begin
  if NewCapacity > NativeUInt(High(NativeInt)) shr 1 then
    Exit(False);                  // zdvojnásobení by přeteklo
  NewCapacity := NewCapacity shl 1;
end;

Dvě pasti build výstupů, které port přežijí

Oddělit testovací a ukázkové spustitelné soubory podle cílové architektury do výstupních adresářů per cíl je jasně správně a okamžitě rozbije cokoli, co si svoje testovací data hledalo počítáním adresářových úrovní nahoru. Řešením je hledat adresář s assety směrem nahoru místo předpokládat pevnou hloubku, s jedním záměrným omezením: signing sample přijme certifikátový fallback jen ze svého vlastního projektového adresáře, nikdy z libovolného předka, protože stejně pojmenovaný certifikát nalezený výš ve stromu je bezpečnostní překvapení, ne pohodlí

Druhá past přežije každý port a stojí za to ji vzít do jakéhokoli FPC projektu. Po upgradu kompilátoru není dost, že kompilátor odmítne zastaralé PPU soubory, protože linker dál dává přednost zbytkovým objektovým souborům v unit search path, i když PPU, které načetl, pochází ze správného adresáře, a přidání explicitní cesty k objektovému výstupu tu preferenci nepřebije. Jediná spolehlivá odpověď je čerstvý dočasný adresář unit na každé kolo sestavení. Cokoli méně vyprodukuje binárku linkovanou ze dvou verzí kompilátoru, která selhává způsoby vypadajícími jako chyby ve zdrojácích

Platformové podmíněné kompilace jsou poslední dílek a volba správné osy záleží víc, než se zdá. Správná otázka obvykle zní, zda je kód specifický pro Windows, ne zda je přítomna ta či ona widget knihovna, jak ukázala práce na metafile konverzi v článku import EMF vektorů a platformové podmínky: přepnutí té hlídky z podmínky na control knihovnu na podmínku platformy změnilo domnělé přepsání na změnu jediné direktivy. Podporu Free Pascalu a Lazarusu pro oba Windows cíle přináší PDFlibPas Delphi PDF knihovna, sestavovaná ze stejných zdrojáků jako balíčky pro Delphi a C++Builder