Технічна стаття

Лінкування OMF- і COFF-об'єктів FPC Win32 у PDFlibPas

PDFlibPas збирається під Free Pascal для 32-бітного Windows, і важка частина ніколи не була в Паскалі. Вона в об'єктних файлах: об'єкти AES і OpenJPEG, які лінкує збірка Delphi, мають формат OMF, внутрішній лінкер Free Pascal вимагає COFF, а конвертація між ними породжує імена секцій і символи визначення секцій, через які лінкер падає з внутрішніми помилками замість діагностик

Хто хоч раз лінкував C-об'єкти в бібліотеку на Паскалі, той знає цю територію. Win64 порівняно ввічлива: один формат об'єктів, одна конвенція виклику й жодного прикрашання імен. Win32 зберігає кожен шар історії, який платформа накопичила, і бібліотека, що статично лінкує сторонній C-код, стикається з усіма ними одночасно

За каталогом компілятора не видно цільову платформу

Почніть із точки входу збірки, бо помилка тут коштує години ще до того, як у справі з'явиться хоть один об'єктний файл. Ім'я каталогу встановлення Free Pascal визначає, де живе головний компілятор, але не те, що він продукує. 32-бітний хост-компілятор може викликати сусідній крос-компілятор і випускати 64-бітний код, коли ви передаєте правильні цільові перемикачі, тож виведення цілі зі шляху — це здогадка, яка збігається з реальністю доти, поки хтось не перетасує свій тулчейн

Надійний підхід — спитати сам компілятор. Дізнайтеся фактичний цільовий процесор і операційну систему через власні інформаційні перемикачі компілятора та приймайте обидва поширені макети встановлення — плоский каталог бінарників і вкладений за версіями, бо різні інсталятори й менеджери тулчейнів дають різні форми. Скрипт збірки, який жорстко зашиває один із макетів, працює рівно на одній машині

Чому конвертований об'єктний файл ламає внутрішній лінкер?

Бо конвертація зберігає конвенцію іменування секцій OMF і синтезує символи визначення секцій, що не збігаються з тим, чого очікує COFF-лінкер. Конвертувати OMF-об'єкти в COFF необхідно, але недостатньо: отримані файли несуть класичні імена секцій _TEXT, _DATA і _BSS плюс похідні від них імена символів визначення секцій, і коли згодувати це внутрішньому лінкеру Free Pascal, він видає внутрішні помилки компілятора замість повідомлення про імена секцій

Внутрішня помилка — найгірший режим відмови для проблеми збірки, бо вона не каже нічого про те, що було не так із вхідними даними. Ліки — прохід нормалізації над COFF-файлом після конвертації: переписати імена секцій у очікувану форму й переписати відповідні символи визначення секцій так, щоб вони збігалися, не торкаючись індексу символів, байтів коду та релокацій. Останнє обмеження — і є вся складність. Переписування, яке перенумеровує символи чи зсуває офсети, дає об'єкт, що лінкується, а потім падає

Для одного з двох наборів об'єктів є попередній крок. Об'єкти OpenJPEG, зібрані класичним 32-бітним компілятором C++, залежать від приватних хелперів Delphi для 64-бітової цілочисельної арифметики, яких Free Pascal не дає, тож жодна конвертація формату не зробить їх придатними. Їх спершу перезбирають компілятором на базі Clang, який таких залежностей не продукує, а вже потім конвертують

Конвеєр, що несе статичні C-об'єкти PDFlibPas від Delphi OMF у Lib\thirdparty\Win32 до придатних для FPC COFF у Lib\thirdparty\Win32f на Win32: конвертація OMF у COFF, прохід нормалізації, який переписує імена секцій і символи визначення секцій, не торкаючись індексу символів, байтів коду й релокацій, та перезбірка Clang для об'єктів OpenJPEG, що викликають 64-бітові хелпери Delphi
Конвертація необхідна, але недостатня: згодуйте перейменований, але ненормалізований COFF внутрішньому лінкеру Free Pascal, і він відповість внутрішніми помилками, тож прохід після конвертації лагодить імена й символи, не торкаючись офсетів
// Об'єкти для цілі FPC живуть у власному каталозі. Вони не
// замінюють набір об'єктів Delphi, бо обидва тулчейни збирають
// з того самого дерева сирців і кожному потрібні свої входи для лінкування
//
//   Lib\thirdparty\Win32   OMF-об'єкти Delphi, без змін
//   Lib\thirdparty\Win32f  COFF-об'єкти FPC, конвертовані й нормалізовані
//
// Точки входу збірки:
//   build-Win32-Lib-FPC.cmd
//   build-Win64-Lib-FPC.cmd

Приватні хелпери компілятора непереносимі, як і їхні конвенції

Рантайм Delphi постачає асемблерні трампліни для 64-бітових цілочисельних операцій на 32-бітному x86, і передзібрані C-об'єкти, збудовані під Delphi, завортають саме в них. У Free Pascal ця механіка власна, тож такі посилання треба задовольняти інакше, а не перенаправляти. Деталь, яка робить перенаправлення неможливим, — конвенція виклику: хелпер вимірювання часу, яким користується код обробки зображень, має чотирибайтовий аргумент, який очищає викликана сторона, тоді як хелпер 64-бітового ділення очищає шістнадцять байтів і повертає результат у класичній парі регістрів. Два хелпери, дві конвенції, і трамплін, написаний для одного, мовчки спотворює стек для іншого

Прикрашання імен додає другу половину проблеми. На Win32 Free Pascal автоматично додає підкреслення до зовнішніх C-імпортів, тоді як оголошення public name експортує дослівно, тож імпортна та експортна сторони одного мосту живуть за різними правилами. Тому C-міст рантайму, потрібний OpenJPEG, мусить експортувати точні імена C-символів, а варіадичні точки входу потребують 32-бітний непрямий стрибок замість прямого. Нічого екзотичного, коли це сформульовано. Але все це падає як помилка лінкування з іменем символу, який ніхто не писав

Через що Win32-виконуваний файл умирав до main?

64-бітна DLL на шляху пошуку, до якої справа дійшла тому, що юніт zlib у Free Pascal прив'язується динамічно, а не лінкується статично. Симптом — негайний вихід зі статус-кодом невалідного образу ще до того, як виконається хоть один паскалівський оператор програми, і це посилає вас розглядати програму, яку ви щойно зібрали, тоді як вина в завантажувачі, що розв'язує імпорт проти чужої архітектури

Урок тут про припущення, а не про zlib. Юніт, названий на честь бібліотеки стиснення, не обов'язково її містить; він може бути прив'язкою, яка очікує спільну бібліотеку в рантаймі, а ненавмисна динамічна залежність — це тягар деплою навіть тоді, коли їй щасливо розв'язатися. Перехід на чисто паскалівську реалізацію потоків дає обом цілям статично вбудований шлях стиснення без жодної зовнішньої залежності — а саме це бібліотека, що живе всередині чужого застосунку, й мала б мати з самого початку

Той самий інстинкт стосується зовнішнього бекенду кодувальника JBIG2. На 32-бітній цілі зовнішній кодувальник не лінкується, тож запити відкатуються на вбудований паскалівський кодувальник, і тест, який це перевіряє, мусить дивитися на стан реєстрації поточної цілі, а не вважати успішне кодування доказом присутності зовнішнього бекенду. Робочий fallback — рівно те, що ховає відсутню залежність, і саме цей патерн відмови розібрано в діагностиці німих збоїв заглушок. Роботі зі статичним лінкуванням на 64 бітах присвячено статичне лінкування jbig2enc під FPC

Діагностичний потік для Win32-виконуваного файлу, який виходить до main під Free Pascal: завантажувач розв'язує імпорти, поки йде ініціалізація юнітів, прив'язка zlib знаходить 64-бітну DLL на шляху пошуку, і процес умирає зі статусом невалідного образу до будь-якого паскалівського оператора, штовхаючи PDFlibPas до статично вбудованого чисто паскалівського шляху стиснення
Вина ніколи не була в щойно зібраній програмі: юніт на ім'я zlib виявився рантайм-прив'язкою, розв'язаною проти чужої архітектури, а робочий fallback на кшталт вбудованого кодувальника JBIG2 ховає відсутню залежність

32-бітна арифметика у потоці в пам'яті

Код, який маніпулює розмірами буферів беззнаковою арифметикою шириною в покажчик, коректний на Win64 і знаходиться на відстані одного великого зображення від переповнення на Win32. Потік у пам'яті, який годує кодек JPEG 2000, росте подвоєнням і просувається додаванням, і на 32-бітній цілі обидві операції можуть завернутися на входах великих, але цілком легітимних

Тому кожне записування, пропуск, seek і початкове виділення спершу перевіряє, а потім рахує, а стеля ємності — це максимальне знакове значення шириною в покажчик, обране так, щоб узгоджуватися з тим, що можуть висловити процедура блокового переміщення та значення, які повертають колбеки. Поведінкову вимогу на випадок відмови легко зіпсувати: відмова не повинна змінювати ані позицію потоку, ані його довжину. Часткова мутація, за якою слідує помилка, лишає потік у стані, про який викликаючий не може нічого судити, а наступна операція це тільки поглиблює

// Перевіряй, перш ніж рахувати. На 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;

Дві пастки виходу збірки, які переживають сам порт

Розділити тестові й зразкові виконувані файли за цільовою архітектурою по каталогах виводу — очевидно правильно, і це миттєво ламає все, що знаходило тестові дані, рахуючи рівні каталогів угору. Ліки — шукати каталог ресурсів угору, а не припускати фіксовану глибину, з одним навмисним обмеженням: зразок підписування приймає запасний сертифікат лише з власного каталогу проєкту, аніж із довільного предка, бо сертифікат з тим самим ім'ям, знайдений вище по дереву, — це сюрприз для безпеки, а не зручність

Друга пастка переживає кожен порт і варта того, щоб носити її в будь-який FPC-проєкт. Після оновлення компілятора змусити його відхилити застарілі PPU-файли — недостатньо, бо лінкер досі віддає перевагу залишковим об'єктним файлам у шляху пошуку юнітів, навіть коли завантажений PPU прийшов із правильного каталогу, і явний шлях виводу об'єктів цю перевагу не перекриває. Єдиний надійний ответ — свіжий тимчасовий каталог юнітів на кожен раунд збірки. Усе, що менше, дає бінарник, зілінкований із двох версій компілятора, який відмовляється так, ніби це баги в сирцях

Платформені умовні директиви — останній шматок, і вибір правильної осі важить більше, ніж здається. Правильне питання зазвичай не в тому, чи присутня певна бібліотека віджетів, а в тому, чи код специфічний для Windows, як показала робота над метафайлами в імпорті EMF-векторів і платформених умовних: зміна того guard'а з умови на бібліотеку контролів на умову платформи перетворила нібито переписування на правку однієї директиви. Підтримка Free Pascal і Lazarus для обох цілей Windows постачається разом із бібліотекою PDFlibPas для Delphi, зібраною з тих самих сирців, що й пакети Delphi та C++Builder