Article technique

PDFlibPas : objets statiques OMF et COFF sous FPC Win32

PDFlibPas se construit sous Free Pascal pour Windows 32 bits, et la partie difficile n'a jamais été le Pascal. C'étaient les fichiers objets : les objets AES et OpenJPEG que l'édition de liens Delphi consomme sont en OMF, l'éditeur de liens interne de Free Pascal exige du COFF, et la conversion entre les deux produit des noms de sections et des symboles de définition de section qui font échouer l'éditeur de liens avec des erreurs internes plutôt qu'avec de vrais diagnostics

Quiconque a déjà édité les liens de fichiers objets C dans une bibliothèque Pascal connaît ce territoire. Win64 est comparativement civilisé, avec un seul format d'objet, une seule convention d'appel et aucune décoration de noms. Win32 préserve toutes les couches d'histoire accumulées par la plateforme, et une bibliothèque qui édite statiquement les liens de code C tiers les rencontre toutes en même temps

Le répertoire du compilateur ne vous dit pas la cible

Commencez par le point d'entrée de construction, car se tromper ici vous fait perdre des heures avant même qu'un fichier objet ne soit impliqué. Le nom d'un répertoire d'installation de Free Pascal indique où vit le compilateur principal, pas ce qu'il produit. Un compilateur hôte 32 bits peut invoquer un compilateur croisé installé à côté de lui et émettre du code 64 bits quand vous passez les bons commutateurs de cible, donc déduire la cible d'un chemin relève de la devinette qui fonctionne par hasard jusqu'à ce que quelqu'un réorganise sa chaîne d'outils

L'approche fiable consiste à demander au compilateur. Interrogez le processeur cible et le système d'exploitation effectifs via les commutateurs d'information du compilateur lui-même, et acceptez les deux dispositions d'installation courantes, le répertoire binaire à plat et celui imbriqué par version, car différents installeurs et gestionnaires de chaîne d'outils produisent des formes différentes. Un script de construction qui code en dur l'une ou l'autre disposition ne fonctionne qu'en exactement une machine

Pourquoi un fichier objet converti casse-t-il l'éditeur de liens interne ?

Parce que la conversion préserve la convention de nommage des sections OMF et synthétise des symboles de définition de section qui ne correspondent pas à ce que l'éditeur de liens COFF attend. Convertir les objets OMF en COFF est nécessaire mais pas suffisant : les fichiers obtenus portent les noms de sections classiques _TEXT, _DATA et _BSS, plus des noms de symboles de définition de section qui en dérivent, et nourrir l'éditeur de liens interne de Free Pascal avec cela produit des erreurs internes du compilateur plutôt qu'un message parlant de nommage de sections

Une erreur interne est le pire mode d'échec pour un problème de construction, car elle ne dit rien de ce qui n'allait pas dans l'entrée. La correction est une passe de normalisation post-conversion sur le fichier COFF : réécrire les noms de sections sous la forme attendue et réécrire les symboles de définition de section correspondants pour qu'ils concordent, en laissant intacts l'index des symboles, les octets de code et les relocalisations. Cette dernière contrainte constitue toute la difficulté. Une réécriture qui renumérote les symboles ou décale les offsets produit un objet qui édite ses liens puis plante

Il y a une étape préliminaire pour l'un des deux jeux d'objets. Les objets OpenJPEG construits par le compilateur C++ 32 bits classique dépendent de routines privées Delphi pour les entiers 64 bits, que Free Pascal ne fournit pas, donc aucune conversion de format ne les rend utilisables. On les reconstruit d'abord avec le compilateur basé sur Clang, qui n'émet pas ces dépendances, puis on les convertit

Chaîne de traitement emmenant les objets C statiques de PDFlibPas depuis l'OMF Delphi dans Lib\thirdparty\Win32 vers du COFF éditable par FPC dans Lib\thirdparty\Win32f sur Win32 : conversion OMF vers COFF, une passe de normalisation qui réécrit les noms de sections et les symboles de définition de section sans toucher à l'index des symboles, aux octets de code ni aux relocalisations, et la reconstruction Clang pour les objets OpenJPEG qui appellent des helpers Delphi 64 bits
La conversion est nécessaire mais pas suffisante : donnez le COFF renommé mais non normalisé à l'éditeur de liens interne de Free Pascal et il répond par des erreurs internes, donc une passe post-conversion corrige noms et symboles sans toucher aux offsets
// Les objets pour la cible FPC vivent dans leur propre répertoire. Ils ne
// remplacent pas le jeu d'objets Delphi, car les deux chaînes d'outils
// construisent depuis le même arbre source et chacune a besoin de ses propres entrées d'édition de liens
//
//   Lib\thirdparty\Win32   objets OMF Delphi, inchangés
//   Lib\thirdparty\Win32f  objets COFF FPC, convertis et normalisés
//
// Points d'entrée de construction :
//   build-Win32-Lib-FPC.cmd
//   build-Win64-Lib-FPC.cmd

Les helpers privés du compilateur ne sont pas portables, et leurs conventions non plus

Le runtime Delphi fournit des trampolines en assembleur pour les opérations sur entiers 64 bits en x86 32 bits, et les objets C précompilés construits pour Delphi les appellent. Free Pascal a sa propre organisation, donc ces références doivent être satisfaites différemment plutôt que redirigées. Le détail qui rend la redirection impossible est la convention d'appel : le helper de minutage utilisé par le code d'imagerie fait nettoyer son argument de quatre octets par l'appelé, tandis que le helper de division 64 bits nettoie seize octets et rend son résultat dans la paire de registres classique. Deux helpers, deux conventions, et un trampoline écrit pour l'un corrompt silencieusement la pile pour l'autre

La décoration de noms ajoute la seconde moitié du problème. Sur Win32, Free Pascal préfixe automatiquement d'un underscore les imports C externes tout en exportant verbatim les déclarations public name, si bien que le côté import et le côté export du même pont suivent des règles différentes. Le pont vers le runtime C dont OpenJPEG a besoin doit donc exporter les noms de symboles C exacts, et les points d'entrée variadiques ont besoin d'un saut indirect 32 bits plutôt que d'un saut direct. Rien de tout cela n'est exotique une fois énoncé. Tout échoue sous forme d'erreur d'édition de liens nommant un symbole que personne n'a écrit

Qu'est-ce qui faisait mourir un exécutable Win32 avant le main ?

Une DLL 64 bits sur le chemin de recherche, atteinte parce que l'unité zlib de Free Pascal se lie dynamiquement au lieu d'être liée statiquement. Le symptôme était une sortie immédiate avec le code de statut d'image invalide, avant que le moindre code Pascal du programme ne s'exécute, ce qui vous envoie inspecter le programme que vous venez de construire alors que le défaut est dans le chargeur qui résout un import contre la mauvaise architecture

La leçon porte sur les hypothèses plutôt que sur zlib. Une unité nommée d'après une bibliothèque de compression n'en contient pas nécessairement une ; ce peut être une liaison qui attend une bibliothèque partagée à l'exécution, et une dépendance dynamique non voulue est un fardeau de déploiement même quand elle se résout par chance. Passer à l'implémentation de flux en Pascal pur donne aux deux cibles un chemin de compression statiquement inclus sans aucune dépendance externe, ce que devrait avoir dès le départ une bibliothèque embarquée dans l'application de quelqu'un d'autre

Le même instinct s'applique au backend externe d'encodage JBIG2. Sur la cible 32 bits, l'encodeur externe n'est pas lié, donc les requêtes retombent sur l'encodeur Pascal intégré, et le test qui vérifie cela doit contrôler l'état d'enregistrement de la cible courante plutôt que de traiter un encodage réussi comme la preuve de la présence du backend externe. Un repli qui fonctionne est précisément ce qui cache une dépendance manquante, et c'est le schéma d'échec examiné dans le diagnostic des échecs silencieux de stubs. Le travail d'édition de liens statique 64 bits est couvert dans l'édition de liens statique de jbig2enc sous FPC

Flux de diagnostic pour un exécutable Win32 qui quitte avant le main sous Free Pascal : le chargeur résout les imports pendant que les initialisations d'unités s'exécutent, la liaison zlib trouve une DLL 64 bits sur le chemin de recherche, et le processus meurt avec un statut d'image invalide avant toute instruction Pascal, poussant PDFlibPas vers un chemin de compression en Pascal pur statiquement inclus
Le défaut n'a jamais été le programme qu'on vient de construire : une unité nommée zlib était une liaison d'exécution résolue contre la mauvaise architecture, et un repli qui fonctionne, comme l'encodeur JBIG2 intégré, cache une dépendance manquante

Arithmétique 32 bits sur un flux mémoire

Du code qui manipule des tailles de tampons avec de l'arithmétique non signée de la largeur d'un pointeur est correct sur Win64 et à une grande image du débordement sur Win32. Le flux en mémoire qui alimente le codec JPEG 2000 croît par doublement et avance par addition, et sur une cible 32 bits les deux opérations peuvent boucler sur des entrées volumineuses mais parfaitement légitimes

Chaque écriture, saut, positionnement et allocation initiale vérifie donc avant de calculer, et le plafond de capacité est la plus grande valeur signée de la largeur du pointeur, choisie pour correspondre à ce que la routine de déplacement de blocs et les valeurs de retour des callbacks peuvent exprimer. L'exigence comportementale quand une requête est refusée est facile à rater : refuser ne doit ni changer la position du flux ni sa longueur. Une mutation partielle suivie d'une erreur laisse le flux dans un état que l'appelant ne peut pas raisonner, et l'opération suivante aggrave le cas

// Vérifiez avant de calculer. Sur Win32 les deux tests bouclent sur des
// entrées qu'une grande image JPEG 2000 produit légitimement
if Needed > NativeUInt(High(NativeInt)) - FPosition then
  Exit(False);                    // refuser, laisser position et taille intactes

NewCapacity := FCapacity;
while (NewCapacity < FPosition + Needed) do
begin
  if NewCapacity > NativeUInt(High(NativeInt)) shr 1 then
    Exit(False);                  // le doublement déborderait
  NewCapacity := NewCapacity shl 1;
end;

Deux pièges de sortie de construction qui survivent au portage

Séparer les exécutables de test et d'exemple par architecture cible dans des répertoires de sortie par cible est évidemment juste et casse immédiatement tout ce qui localisait ses données de test en comptant les niveaux de répertoires vers le haut. La correction est de chercher vers le haut le répertoire de ressources plutôt que de supposer une profondeur fixe, avec une restriction délibérée : l'exemple de signature n'accepte un certificat de repli que depuis son propre répertoire de projet, jamais depuis un ancêtre arbitraire, car un certificat de même nom trouvé plus haut dans l'arbre est une surprise de sécurité plutôt qu'une commodité

Le second piège survit à tous les portages et mérite d'être emporté dans tout projet FPC. Après une montée de version du compilateur, faire rejeter par le compilateur les fichiers PPU périmés ne suffit pas, car l'éditeur de liens préfère encore les fichiers objets résiduels dans le chemin de recherche des unités même quand le PPU qu'il a chargé venait du bon répertoire, et ajouter un chemin de sortie d'objets explicite ne contourne pas cette préférence. La seule réponse fiable est un répertoire d'unités temporaire frais à chaque tour de construction. Moins que cela produit un binaire édité depuis deux versions du compilateur, qui échoue de façons qui ressemblent à des bugs de source

Les conditionnelles de plateforme sont la dernière pièce, et choisir le bon axe compte plus qu'il n'y paraît. La bonne question est généralement de savoir si le code est spécifique à Windows plutôt que si une bibliothèque de composants particulière est présente, comme l'a montré le travail de conversion de métafichiers dans l'import vectoriel EMF et les conditionnelles de plateforme : basculer cette garde d'une condition sur la bibliothèque de contrôles vers une condition de plateforme a transformé une réécriture supposée en changement d'une directive. Le support de Free Pascal et Lazarus pour les deux cibles Windows est livré avec la bibliothèque PDF Delphi PDFlibPas, construite depuis les mêmes sources que les paquets Delphi et C++Builder