Техническа статия

Свързване на OMF към COFF обекти за FPC Win32 в PDFlibPas

PDFlibPas се компилира под Free Pascal за 32-битов Windows, а трудната част никога не е била Pascalът – обектните файлове бяха тя. Обектите AES и OpenJPEG, които Delphi build-ът свързва, са OMF, вътрешният linker на Free Pascal изисква COFF, а конвертирането между двата формата ражда имена на секции и section-definition символи, които карат linker-ът да се провали с вътрешни грешки вместо с диагностика

Всеки, който някога е свързвал C обекти в Pascal библиотека, познава тази територия. Win64 е сравнително цивилизован – един формат обекти, една calling convention и никакво name decoration. Win32 пази всеки пласт история, който платформата е натрупала, а библиотека, която статично свързва C код на трети страни, се сблъсква с всичко наведнъж

Директорията на компилатора не ви казва целевата платформа

Започнете от входната точка на build-а, защото грешка тук ви гори часове, преди изобщо да се появи обектен файл. Името на инсталационната директория на Free Pascal показва къде живее основният компилатор, а не какво той произвежда. 32-битов host компилатор може да извика до себе си cross-компилатор и да издаде 64-битов код при правилните target ключове, така че отгатването по пътя е чиста случайност, която върви, докато някой не преустрои toolchain-а си

Надеждният подход е да попитате самия компилатор. Разпитайте действителните целеви процесор и операционна система през информационните ключове на компилатора и приемете и двата разпространени вида инсталационни подредби – плоската binary директория и вложената по версии – защото различни инсталатори и toolchain мениджъри раждат различни форми. Build скрипт, залъган на едната подредба, работи точно на една машина

Защо конвертиран обектен файл чупи вътрешния linker?

Защото конвертирането запазва конвенцията за имена на секции от OMF и измисля section-definition символи, които не пасват на очакванията на COFF linker-а. Конвертирането на OMF обектите към COFF е необходимо, но недостатъчно: получените файлове носят класическите имена на секции _TEXT, _DATA и _BSS, плюс производни от тях section-definition имена на символи, а когато подадете това на вътрешния linker на Free Pascal, получавате вътрешни компилаторни грешки вместо съобщение за имената на секциите

Вътрешната грешка е най-лошият режим на провал за build проблем, защото не казва нищо за това какво е било грешното на входа. Поправката е нормализиращ пас върху COFF файла след конверсията: пренаписвате имената на секциите в очакваната форма и пренаписвате съответните section-definition символи, за да пасват, без да пипате symbol index-а, кодовите байтове и релокациите. Последното ограничение е цялата трудност. Пренапис, който преномерира символи или измества офсети, ражда обект, който се свързва и после гърми

За единия от двата набора обекти има и предварителна стъпка. OpenJPEG обектите, сглобени от класическия 32-битов C++ компилатор, зависят от частните на Delphi 64-битови integer helper рутини, които Free Pascal не предоставя, така че никаква конверсия на формата не ги прави годни за употреба. Те първо се компилират наново с Clang-базирания компилатор, който не издава тези зависимости, и чак после се конвертират

Тръбопровод, носещ статичните C обекти на PDFlibPas от Delphi OMF в Lib\thirdparty\Win32 до FPC-свързуем COFF в Lib\thirdparty\Win32f на Win32: конверсия OMF към COFF, нормализиращ пас, който пренаписва имената на секциите и section-definition символите, без да пипа symbol index-а, кодовите байтове и релокациите, и Clang rebuild за OpenJPEG обектите, които викат Delphi 64-битови helper-и
Конверсията е необходима, но недостатъчна: подайте на вътрешния linker на Free Pascal преименувания, но ненормализиран COFF и отговорът са вътрешни грешки, затова пасът след конверсията оправя имена и символи, без да пипа офсетите
// Обектите за FPC целта живеят в собствена директория. Те не
// заменят Delphi обектния набор, защото и двата toolchain-а се
// компилират от едно и също source дърво и всеки има свои link входове
//
//   Lib\thirdparty\Win32   Delphi OMF обекти, непроменени
//   Lib\thirdparty\Win32f  FPC COFF обекти, конвертирани и нормализирани
//
// Build входни точки:
//   build-Win32-Lib-FPC.cmd
//   build-Win64-Lib-FPC.cmd

Компилаторно-частните helper-и не са преносими, и конвенциите им също

Delphi runtime-ът доставя асемблерни trampolines за 64-битови integer операции на 32-битов x86, и предварително компилираните C обекти, строени за Delphi, викат в тях. Free Pascal има собствена уредба, така че тези референции трябва да се задоволят по друг начин, а не да се пренасочат. Детайлът, който прави пренасочването невъзможно, е calling convention-ът: timing helper-ът, ползван от imaging кода, има четирибайтовия си аргумент изчистен от callee-то, докато 64-битовият делещ helper изчиства шестнайсет байта и връща резултата си в класическата регистрова двойка. Два helper-а, две конвенции, и trampoline, писан за едната, безшумно развързва стека за другата

Name decoration добавя втората половина на проблема. На Win32 Free Pascal поставя подчертавка пред външните C импорти автоматично, докато експортира public name декларациите дословно, така че импортната и експортната страна на един и същ мост следват различни правила. C runtime мостът, който OpenJPEG трябва, затова трябва да експортира точните C имена на символи, а variadic входните точки се нуждаят от 32-битов непряк скок вместо директен. Нищо от това не е екзотично, щом бъде изречено. Цялото се проваля като link грешка, назоваваща символ, който никой не е писал

Какво накара Win32 изпълним файл да умре преди main?

64-битова DLL по search пътя, достигната защото zlib unit-ът на Free Pascal се обвързва динамично вместо статично. Симптомът беше незабавен изход с invalid-image статус код, преди да се е изпълнил какъвто и да е Pascal код в програмата – това те праща да гледаш програмата, която току-що си build-нал, докато повредата е в loader-а, разрешаващ импорт срещу грешната архитектура

Урокът е за допущенията, не за zlib. Unit, кръстен на компресионна библиотека, не задължително съдържа една; може да е binding, който очаква споделена библиотека по време на изпълнение, а динамична зависимост, която не сте искали, е deployment задължение дори когато случайно се разреши. Превключването към чистата Pascal имплементация на stream дава на двете цели статично включен компресионен път без никаква външна зависимост – точно това трябва да има библиотека, вградена в чуждо приложение

Същият инстинкт важи и за външния JBIG2 encoder backend. На 32-битовата цел външният encoder не се свързва, така че заявките падат обратно на вградения Pascal encoder, а тестът, който това проверява, трябва да гледа регистрационното състояние на текущата цел, вместо да брои успешен encode за доказателство, че външният backend е наличен. Работещият fallback е точно нещото, което скрива липсваща зависимост – повредовият модел, разгледан в диагностика на безшумни stub провали. Работата по 64-битовото статично свързване е обхваната в статично свързване на jbig2enc под FPC

Диагностичен поток за Win32 изпълним файл, който излиза преди main под Free Pascal: loader-ът разрешава импорти, докато unit инициализацията върви, zlib binding-ът намира 64-битова DLL по search пътя, и процесът умира с invalid-image статус преди всяко Pascal изречение, бутайки PDFlibPas към статично включен чист Pascal компресионен път
Повредата никога не беше току-що build-натата програма: unit на име zlib беше runtime binding, разрешен срещу грешната архитектура, а работещ fallback като вградения JBIG2 encoder скрива липсваща зависимост

32-битова аритметика върху memory stream

Код, който манипулира размери на буфери с unsigned аритметика в широчина на указателя, е правилен на Win64 и на едра картинка разстояние от препълване на Win32. In-memory stream-ът, който храни JPEG 2000 кодека, расте чрез удвояване и напредва чрез събиране, и на 32-битова цел двете операции могат да се превъртят при входове, големи, но съвсем легитимни

Затова всяко записване, прескачане, seek и първоначална алокация проверява, преди да сметне, а таванът на капацитета е максималната signed стойност в широчина на указателя, избрана да съвпада с това, което рутината за блоково местене и return стойностите на callback-а могат да изразят. Поведенческото изискване при отказана заявка лесно се обърква: отказът не бива да мени позицията на stream-а или дължината му. Частична мутация, следвана от грешка, оставя stream-а в състояние, за което викащият не може да разсъждава, а следващата операция утежнява положението

// Провери, преди да смяташ. На Win32 и двете се превъртат при входове,
// които голямо JPEG 2000 изображение произвежда напълно легитимно
if Needed > NativeUInt(High(NativeInt)) - FPosition then
  Exit(False);                    // отказ, не пипай позиция и размер

NewCapacity := FCapacity;
while (NewCapacity < FPosition + Needed) do
begin
  if NewCapacity > NativeUInt(High(NativeInt)) shr 1 then
    Exit(False);                  // удвояването би препълнило
  NewCapacity := NewCapacity shl 1;
end;

Два build-output капана, които надживяват порта

Разделянето на тестовите и sample изпълнимите файлове по целева архитектура в per-target output директории е очевидно правилно и мигновено чупи всичко, което е намирало тестовите си данни чрез броене на директорийни нива нагоре. Поправката е търсене нагоре за asset директорията вместо допущане за фиксирана дълбочина, с едно умишлено ограничение: signing sample-ът приема certificate fallback само от собствения си project directory, никога от произволен прародител, защото едноименен сертификат, намерен по-нагоре по дървото, е security изненада, а не удобство

Вторият капан надживява всяка порция и си струва да го носите във всеки FPC проект. След ъпгрейд на компилатора недостатъчно е компилаторът да отхвърли остарелите PPU файлове, защото linker-ът все още предпочита останали обектни файлове в unit search пътя дори когато зареденият PPU идва от правилната директория, а добавянето на изричен object output път не отменя това предпочитание. Единственият надежден отговор е свежа временна unit директория за всеки build рунд. По-малко от това ражда binary, свързан от две версии на компилатора, който се проваля по начини, изглеждащи като source бъгове

Платформените conditionals са последното парче, а изборът на правилната ос значи повече, отколкото изглежда. Правилният въпрос обикновено е дали кодът е Windows-специфичен, а не дали присъства конкретна widget библиотека, както показа работата по metafile конверсията в EMF векторен импорт и платформени conditionals: смяната на този guard от условие за control-библиотека на платформено условие превърна предполагаемо пренаписване в смяна на една директива. Free Pascal и Lazarus поддръжката и за двете Windows цели идва с PDFlibPas Delphi PDF библиотеката, build-ната от същите източници като Delphi и C++Builder пакетите