Tekninen artikkeli

PDFlibPas: OMF- ja COFF-objektilinkitys FPC Win32:lla

PDFlibPas kääntyy Free Pascalilla 32-bittiselle Windowsille, ja vaikea osa ei koskaan ollut Pascali vaan objektitiedostot: Delphi-käännöksen linkittämät AES- ja OpenJPEG-objektit ovat OMF-muodossa, Free Pascalin sisäinen linkkeri taas edellyttää COFF-muotoa, ja muunnos näiden välillä tuottaa osionimiä ja osion määrittelysymboleja, jotka saavat linkkerin kaatumaan sisäisiin virheisiin oikean diagnostiikan sijaan

Jokainen, joka on linkittänyt C-objekteja Pascal-kirjastoon, tuntee tämän maaston. Win64 on suhteellisen sivistynyt: yksi objektiformaatti, yksi kutsukonventio eikä nimenkoristelua lainkaan. Win32 säästää jokaisen kerroksen sitä historiaa, jota alusta on ehtinyt kerryttää, ja kolmannen osapuolen C-koodia staattisesti linkittävä kirjasto kohtaa ne kaikki yhtä aikaa

Kääntäjän hakemisto ei kerro kohdealustaa

Aloita käännöksen sisääntulopisteestä, koska tässä tehty virhe haaskaa tunteja ennen kuin mikään objektitiedosto on edes mukana. Free Pascal -asennushakemiston nimi kertoo, missä pääkääntäjä asuu, ei sitä, mitä se tuottaa. 32-bittinen isäntäkääntäjä voi kutsua vieressään istuvaa ristikkäiskääntäjää ja tuottaa 64-bittistä koodia, kun annat oikeat kohdekytkimet, joten kohteen päätteleminen polusta on arvaamista, joka sattuu toimimaan siihen asti, kunnes joku järjestää työkaluketjunsa uusiksi

Luotettava tapa on kysyä kääntäjältä itseltään. Kysy oikea kohdeprosessori ja käyttöjärjestelmä kääntäjän omilla tietokytkimillä ja hyväksy molemmat tavalliset asennusasettelut, litteä binäärihakemisto ja versioihin sisäkkäin aseteltu, koska eri asennusohjelmat ja työkaluketjujen hallintatyökalut tuottavat eri muotoja. Asettelun kovakoodaava buildiskripti toimii täsmälleen yhdellä koneella

Miksi muunnettu objektitiedosto rikkee sisäisen linkkerin?

Koska muunnos säilyttää OMF:n osionimikäytännön ja syntetisoi osion määrittelysymboleja, jotka eivät vastaa sitä, mitä COFF-linkkeri odottaa. OMF-objektien muuntaminen COFF-muotoon on tarpeen mutta ei riitä: syntyneet tiedostot kantavat klassisia _TEXT-, _DATA- ja _BSS-osionimiä sekä niistä johdettuja osion määrittelysymbolien nimiä, ja sen syöttäminen Free Pascalin sisäiselle linkkerille tuottaa kääntäjän sisäisiä virheitä eikä viestiä osionimistä

Sisäinen virhe on pahin vikaantumismuoto käännösongelmalle, koska se ei kerro mitään siitä, mikä syötteessä oli vialla. Korjaus on muunnoksen jälkeinen normalisointikierros COFF-tiedoston yli: kirjoita osionimet odotettuun muotoon ja kirjoita vastaavat osion määrittelysymbolit täsmäämään jättäen symboli-indeksin, kooditavut ja relokaatiot koskemattomiksi. Tuo viimeinen rajoite on koko vaikeus. Uudelleenkirjoitus, joka numerointi symbolit uudelleen tai siirtää siirtymiä, tuottaa objektin, joka linkittää ja sitten kaatuu

Toisella kahdesta objektijoukosta on lisäksi esivaihe. Klassisen 32-bittisen C++-kääntäjän rakentamat OpenJPEG-objektit riippuvat Delphin yksityisistä 64-bittisistä kokolukurutiineista, joita Free Pascal ei tarjoa, joten mikään määrä formaattimuunnoksia ei tee niistä käytettäviä. Ne rakennetaan ensin uudelleen Clangiin perustuvalla kääntäjällä, joka ei emittoi kyseisiä riippuvuuksia, ja muunnetaan vasta sen jälkeen

Putki, joka vie PDFlibPasin staattiset C-objektit Delphin OMF:stä hakemistosta Lib\thirdparty\Win32 FPC:llä linkitettäviin COFF-objekteihin hakemistossa Lib\thirdparty\Win32f Win32:lla: OMF:stä COFF-muotoon -muunnos, normalisointikierros, joka kirjoittaa osionimet ja osion määrittelysymbolit uudelleen jättäen symboli-indeksin, kooditavut ja relokaatiot koskemattomiksi, sekä Clang-uudelleenrakennus niille OpenJPEG-objekteille, jotka kutsuvat Delphin 64-bittisiä apurutiineja
Muunnos on tarpeen mutta ei riittävä: uudelleennimetty mutta normalisoimaton COFF saa Free Pascalin sisäisen linkkerin vastaamaan sisäisillä virheillä, joten muunnoksen jälkeinen kierros korjaa nimet ja symbolit koskematta siirtymiin
// FPC-kohteen objektit asuvat omassa hakemistossaan eivätkä korvaa
// Delphin objektijoukkoa: molemmat työkaluketjut rakentuvat samasta
// lähdekoodipuusta ja kummalla on omat linkityssyötteensä
//
//   Lib\thirdparty\Win32   Delphin OMF-objektit, muuttumattomia
//   Lib\thirdparty\Win32f  FPC:n COFF-objektit, muunnetut ja normalisoidut
//
// Käännösten sisääntulopisteet:
//   build-Win32-Lib-FPC.cmd
//   build-Win64-Lib-FPC.cmd

Kääntäjän yksityiset apurutiinit eivät ole siirrettäviä, eivätkä niiden konventiotkaan

Delphin ajonaikainen ympäristö tarjoaa Assembly-trampoliinit 64-bittisiin kokolukuoperaatioihin 32-bittisellä x86:lla, ja Delphille rakennetut esikäännetyt C-objektit kutsuvat niihin. Free Pascalilla on oma järjestelynsä, joten nuo viittaukset on tyydytettävä eri tavalla, ei uudelleenohjattava. Yksityiskohta, joka tekee uudelleenohjauksesta mahdotonta, on kutsukonventio: kuvankäsittelykoodin käyttämän ajoitusapurin nelitavuinen argumentti siivotaan kutsuttavan puolelta, kun taas 64-bittisen jakolaskun apuri siivoaa kuusitoista tavua ja palauttaa tuloksensa klassisessa rekisteriparissa. Kaksi apuria, kaksi konventiota, ja toiselle kirjoitettu trampoliini korruptoi hiljaisesti pinon toiselle

Nimenkoristelu lisää ongelman toisen puoliskon. Win32:lla Free Pascal etuliittää ulkoiset C-tuonnit alaviivalla automaattisesti ja vie public name-määrittelyt sellaisenaan, joten saman sillan tuontipuoli ja vientipuoli seuraavat eri sääntöjä. Se C-ajonaikainen silta, jonka OpenJPEG tarvitsee, on siksi vietävä tarkoilla C-symbolinimillä, ja variadiset sisääntulopisteet tarvitsevat 32-bittisen epäsuoran hypyn suoran sijaan. Mikään tästä ei ole eksoottista kun sen kerran sanoo ääneen. Kaikki epäonnistuu linkitysvirheenä, joka nimeää symbolin, jota kukaan ei kirjoittanut

Mikä sai Win32-ohjelman kuolemaan ennen mainia?

64-bittinen DLL hakupolulla, johon jouduttiin, koska Free Pascalin zlib-yksikkö sitoutuu dynaamisesti staattisen linkittämisen sijaan. Oire oli välitön poistuminen virheellisen imagen tilakoodilla ennen kuin mikään ohjelman Pascal-koodi ehti ajaa, mikä lähettää sinut tuijottamaan juuri rakentamaasi ohjelmaa, vaikka vika on lataajassa, joka ratkaisee tuonnin väärälle arkkitehtuurille

Oppitunti koskee oletuksia, ei zlibiä. Pakkauskirjaston mukaan nimetty yksikkö ei välttämättä sisällä sellaista; se voi olla sidonta, joka odottaa jaettua kirjastoa ajonaikana, ja tahaton dynaaminen riippuvuus on jakeluriski silloinkin, kun se sattuu ratkeamaan. Siirtyminen puhtaaseen Pascal-virtatoteutukseen antaa molemmille kohteille staattisesti mukana tulevan pakkauspolun ilman mitään ulkoista riippuvuutta, eli juuri sen, mitä toisen sovellukseen upotettavan kirjaston pitäisi alun pitäen olla

Sama vaisto pätee ulkoiseen JBIG2-enkooderin taustajärjestelmään. 32-bittisellä kohteella ulkoinen enkooderi ei ole linkitettynä, joten pyynnöt palaavat sisäänrakennettuun Pascal-enkooderiin, ja tämän varmistavan testin on tarkistettava nykyisen kohteen rekisteröintitila sen sijaan, että se pitäisi onnistunutta pakkausta todisteena ulkoisen taustan läsnäolosta. Toimiva varapolku on juuri se asia, joka kätkee puuttuvan riippuvuuden, ja kyseessä on vikakaava, jota tarkastellaan artikkelissa hiljaisten stub-vikojen diagnostiikka. 64-bittisen staattisen linkityksen työ on käsitelty artikkelissa jbig2encin staattinen linkitys FPC:n alla

Diagnostiikkavirta Win32-suoritettavalle, joka poistuu ennen mainia Free Pascalilla: lataaja ratkaisee tuonnit yksikön alustuksen aikana, zlib-sidonta löytää 64-bittisen DLL:n hakupolulta, ja prosessi kuolee virheellisen imagen tilakoodiin ennen mitään Pascal-lausetta, mikä työntää PDFlibPasin kohti staattisesti mukana olevaa puhdasta Pascal-pakkauspolkua
Vika ei koskaan ollut juuri rakennettu ohjelma: zlib-niminen yksikkö oli ajonaikainen sidonta, joka ratkesi väärälle arkkitehtuurille, ja toimiva varapolku, kuten sisäänrakennettu JBIG2-enkooderi, kätkee puuttuvan riippuvuuden

Muistivirran 32-bittinen aritmetiikka

Puskurikokoja osoitinleveällä etumerkittömällä aritmetiikalla käsittelevä koodi on oikein Win64:lla ja yhden suuren kuvan päässä ylivuodosta Win32:lla. JPEG 2000 -koodekkia ruokkiva muistissa oleva virta kasvaa kaksinkertaistamalla ja etenee lisäämällä, ja 32-bittisellä kohteellä molemmat operaatiot voivat kiertyä syötteillä, jotka ovat suuria mutta täysin laillisia

Jokainen kirjoitus, ohitus, seek ja alkuvaraus tarkistaa siis ennen kuin laskee, ja kapasiteettikatto on suurin etumerkkinen osoitinleveä arvo, valittu vastaamaan sitä, mitä lohkosiirtorutiini ja callbackien paluuarvot pystyvät ilmaisemaan. Vaatimus käytöksestä silloin, kun pyyntö hylätään, on helppo saada väärin: hylkääminen ei saa muuttaa virran sijaintia eikä sen pituutta. Osittainen muutos, jota seuraa virhe, jättää virran tilaan, josta kutsuja ei pysty päättelemään mitään, ja seuraava operaatio pahentaa sitä

// Tarkista ennen kuin lasket. Win32:lla molemmat kiertyvät syötteillä,
// joita suuri JPEG 2000 -kuva tuottaa täysin laillisesti
if Needed > NativeUInt(High(NativeInt)) - FPosition then
  Exit(False);                    // hylkää, jätä sijainti ja koko rauhaan

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

Kaksi käännöstulosten ansaa, jotka elävät porttauksen jälkeen

Testi- ja esimerkkiohjelmien erottelu kohdearkkitehtuurin mukaan omiin tulostushakemistoihinsa on ilmeisen oikein ja rikkoo välittömästi kaiken, joka paikallisti testidatansa laskemalla hakemistotasoa ylöspäin. Korjaus on etsiä resurssihakemistoa ylöspäin kiinteän syvyyden olettamisen sijaan, yhdellä tahallisella rajoitteella: allekirjoitusesimerkki hyväksyy varmenteen varapolun vain omasta projektihakemistostaan, ei koskaan mielivaltaisesta esivanhemmasta, koska puusta ylempää löytyvä samanniminen varmenne on turvallisuusyllätys eikä mukavuus

Toinen ansa selviytyy jokaisesta porttauksesta ja kannattaa viedä mukanaan mihin tahansa FPC-projektiin. Kääntäjän päivityksen jälkeen ei riitä, että kääntäjä hylkää vanhentuneet PPU-tiedostot, koska linkkeri suosii yhä yksikköhakupolussa lojuneita objektitiedostoja silloinkin, kun sen lataama PPU tuli oikeasta hakemistosta, eikä eksplisiittisen objektitulostuspolun lisääminen kumoa tätä mieltymystä. Ainoa luotettava vastaus on tuore väliaikainen yksikköhakemisto jokaista käännöskierrosta varten. Mistä tahansa vähemmästä syntyy binääri, joka on linkitetty kahdesta kääntäjäversiosta ja joka epäonnistuu tavoilla, jotka näyttävät lähdekoodin bugeilta

Alustaehdollistukset ovat viimeinen palanen, ja oikean akselin valitseminen merkitsee enemmän kuin näyttää. Oikea kysymys on yleensä se, onko koodi Windows-kohtainen, ei se, onko jokin tietty widget-kirjasto läsnä, kuten metafailien muunnostyö artikkelissa EMF-vektorituonti ja alustaehdollistukset osoitti: kyseisen vahdin vaihtaminen kontrollikirjastoehdosta alustaehdoksi muutti oletetun uudelleenkirjoituksen yhden direktiivin muutokseksi. Free Pascal- ja Lazarus-tuki molemmille Windows-kohteille toimituu PDFlibPas Delphi PDF -kirjaston mukana, rakennettuna samoista lähteistä kuin Delphi- ja C++Builder-paketit