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

Линковка объектов OMF в COFF для FPC Win32 в PDFlibPas

PDFlibPas собирается под Free Pascal для 32-битной Windows, и трудной частью никогда не был сам Pascal. Трудность была в объектных файлах: объекты AES и OpenJPEG, которые линкует Delphi-сборка, — это OMF, внутренний линковщик Free Pascal требует COFF, а конвертация между форматами даёт имена секций и символы определений секций, от которых линковщик падает с внутренними ошибками вместо внятной диагностики

Кто хоть раз линковал C-объекты в Pascal-библиотеку, тот эту территорию знает. 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 от OMF Delphi в Lib\thirdparty\Win32 к линкуемому FPC COFF в Lib\thirdparty\Win32f на Win32: конвертация OMF в COFF, проход нормализации, который переписывает имена секций и символы определений секций, не трогая индекс символов, байты кода и релокации, и пересборка Clang для объектов OpenJPEG, вызывающих 64-битные хелперы Delphi
Конвертация необходима, но недостаточна: скормите внутреннему линковщику Free Pascal переименованный, но не нормализованный COFF, и он ответит внутренними ошибками, поэтому проход после конвертации чинит имена и символы, не трогая смещения
// Объекты для FPC-таргета лежат в отдельном каталоге. Они не заменяют
// набор объектов Delphi: оба тулчейна собираются из одного дерева
// исходников, и каждому нужны свои входные файлы для линковки
//
//   Lib\thirdparty\Win32   OMF-объекты Delphi, без изменений
//   Lib\thirdparty\Win32f  COFF-объекты FPC, сконвертированы и нормализованы
//
// Точки входа сборки:
//   build-Win32-Lib-FPC.cmd
//   build-Win64-Lib-FPC.cmd

Приватные хелперы компилятора непереносимы, и их соглашения тоже

Runtime Delphi поставляет ассемблерные трамплины для 64-битных целочисленных операций на 32-битном x86, и прекомпилированные C-объекты под Delphi вызывают их. У Free Pascal своё устройство, так что эти ссылки приходится удовлетворять иначе, а не перенаправлять. Деталь, делающая перенаправление невозможным, — соглашение о вызовах: таймерный хелпер, который использует imaging-код, очищает свой четырёхбайтовый аргумент на стороне вызываемого, а хелпер 64-битного деления очищает шестнадцать байт и возвращает результат в классической регистровой паре. Два хелпера, два соглашения, и трамплин, написанный под одно, молча портит стек для другого

Декорирование имён добавляет вторую половину проблемы. На Win32 Free Pascal автоматически добавляет подчёркивание к внешним C-импортам, а объявления public name экспортирует как есть, так что импортная и экспортная стороны одного моста живут по разным правилам. Поэтому C-мост для runtime, который нужен OpenJPEG, обязан экспортировать точные имена C-символов, а вариадик-точкам входа нужен 32-битный косвенный переход вместо прямого. Ничего экзотического, если сформулировать. И всё это падает ошибкой линковки, называющей символ, который никто не писал

Почему исполняемый файл Win32 умирал до main?

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

Урок здесь про предположения, а не про zlib. Юнит, названный в честь библиотеки сжатия, не обязательно её содержит: это может быть биндинг, ожидающий разделяемую библиотеку в рантайме, и нежданная динамическая зависимость — минус к деплою, даже если она случайно разрешается. Переключение на чисто Pascal-реализацию потоков даёт обоим таргетам статически включённый путь сжатия без внешних зависимостей вообще — именно то, что библиотеке, встраиваемой в чужое приложение, и следовало иметь с самого начала

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

Диагностический поток для исполняемого файла Win32, который завершается до main под Free Pascal: загрузчик разрешает импорты, пока идёт инициализация юнитов, биндинг zlib находит 64-битную DLL на путях поиска, и процесс умирает со статусом неверного образа до первой Pascal-инструкции, что подталкивает PDFlibPas к статически включённому чисто Pascal-пути сжатия
Вины только что собранной программы не было: юнит с именем zlib оказался биндингом к runtime, разрешённым против чужой архитектуры, а работающий фолбэк вроде встроенного кодировщика 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-векторики и платформенных условиях: смена этой проверки с условия на библиотеку контролов на условие по платформе превратила мнимую переписывание в изменение одной директивы. Поддержка Free Pascal и Lazarus обоих Windows-таргетов поставляется с Delphi PDF-библиотекой PDFlibPas, собираемой из тех же исходников, что и пакеты Delphi и C++Builder