web-dev-qa-db-fra.com

Comment gérer les collisions de symboles entre des bibliothèques liées statiquement?

L'une des règles et des meilleures pratiques les plus importantes lors de l'écriture d'une bibliothèque consiste à placer tous les symboles de la bibliothèque dans un espace de noms spécifique à la bibliothèque. C++ facilite cela, grâce au mot clé namespace. En C, l'approche habituelle consiste à préfixer les identificateurs avec un préfixe spécifique à la bibliothèque.

Les règles du standard C imposent certaines contraintes (pour une compilation sûre): Un compilateur C ne peut regarder que les 8 premiers caractères d'un identifiant, donc foobar2k_eggs et foobar2k_spam peut être interprété valablement comme les mêmes identifiants - cependant, chaque compilateur moderne autorise des identifiants longs arbitraires, donc à notre époque (21e siècle), nous ne devrions pas nous en préoccuper.

Mais que faire si vous faites face à des bibliothèques dont vous ne pouvez pas changer les noms/idenfiers des symboles? Peut-être que vous n'avez qu'un binaire statique et les en-têtes ou que vous ne le souhaitez pas, ou que vous n'êtes pas autorisé à vous ajuster et à vous recompiler.

77
datenwolf

Au moins dans le cas des bibliothèques statiques, vous pouvez les contourner très facilement.

Considérez les en-têtes des bibliothèques foo et bar . Pour les besoins de ce tutoriel, je vous donnerai également les fichiers source

exemples/ex01/foo.h

int spam(void);
double eggs(void);

examples/ex01/foo.c (cela peut être opaque/non disponible)

int the_spams;
double the_eggs;

int spam()
{
    return the_spams++;
}

double eggs()
{
    return the_eggs--;
}

exemple/ex01/bar.h

int spam(int new_spams);
double eggs(double new_eggs);

exemples/ex01/bar.c (cela peut être opaque/non disponible)

int the_spams;
double the_eggs;

int spam(int new_spams)
{
    int old_spams = the_spams;
    the_spams = new_spams;
    return old_spams;
}

double eggs(double new_eggs)
{
    double old_eggs = the_eggs;
    the_eggs = new_eggs;
    return old_eggs;
}

Nous voulons les utiliser dans un programme foobar

exemple/ex01/foobar.c

#include <stdio.h>

#include "foo.h"
#include "bar.h"

int main()
{
    const int    new_bar_spam = 3;
    const double new_bar_eggs = 5.0f;

    printf("foo: spam = %d, eggs = %f\n", spam(), eggs() );
    printf("bar: old spam = %d, new spam = %d ; old eggs = %f, new eggs = %f\n", 
            spam(new_bar_spam), new_bar_spam, 
            eggs(new_bar_eggs), new_bar_eggs );

    return 0;
}

Un problème apparaît immédiatement: C ne connaît pas la surcharge. Nous avons donc deux fois deux fonctions de nom identique mais de signature différente. Il nous faut donc un moyen de les distinguer. Quoi qu'il en soit, voyons ce qu'un compilateur a à dire à ce sujet:

example/ex01/ $ make
cc    -c -o foobar.o foobar.c
In file included from foobar.c:4:
bar.h:1: error: conflicting types for ‘spam’
foo.h:1: note: previous declaration of ‘spam’ was here
bar.h:2: error: conflicting types for ‘eggs’
foo.h:2: note: previous declaration of ‘eggs’ was here
foobar.c: In function ‘main’:
foobar.c:11: error: too few arguments to function ‘spam’
foobar.c:11: error: too few arguments to function ‘eggs’
make: *** [foobar.o] Error 1

D'accord, ce n'était pas une surprise, cela nous a juste dit ce que nous savions déjà, ou du moins soupçonnions.

Alors, pouvons-nous en quelque sorte résoudre cette collision d'identifiants sans modifier le code source ou les en-têtes des bibliothèques d'origine? En fait, nous le pouvons.

Permet d'abord de résoudre les problèmes de temps de compilation. Pour cela, nous entourons l'en-tête comprend un tas de préprocesseur #define directives qui préfixent tous les symboles exportés par la bibliothèque. Plus tard, nous le faisons avec un joli en-tête enveloppant, mais juste pour démontrer ce qui se passait, nous le faisions textuellement dans la source foobar.c fichier:

exemple/ex02/foobar.c

#include <stdio.h>

#define spam foo_spam
#define eggs foo_eggs
#  include "foo.h"
#undef spam
#undef eggs

#define spam bar_spam
#define eggs bar_eggs
#  include "bar.h"
#undef spam
#undef eggs

int main()
{
    const int    new_bar_spam = 3;
    const double new_bar_eggs = 5.0f;

    printf("foo: spam = %d, eggs = %f\n", foo_spam(), foo_eggs() );
    printf("bar: old spam = %d, new spam = %d ; old eggs = %f, new eggs = %f\n", 
           bar_spam(new_bar_spam), new_bar_spam, 
           bar_eggs(new_bar_eggs), new_bar_eggs );

    return 0;
}

Maintenant, si nous compilons cela ...

example/ex02/ $ make
cc    -c -o foobar.o foobar.c
cc   foobar.o foo.o bar.o   -o foobar
bar.o: In function `spam':
bar.c:(.text+0x0): multiple definition of `spam'
foo.o:foo.c:(.text+0x0): first defined here
bar.o: In function `eggs':
bar.c:(.text+0x1e): multiple definition of `eggs'
foo.o:foo.c:(.text+0x19): first defined here
foobar.o: In function `main':
foobar.c:(.text+0x1e): undefined reference to `foo_eggs'
foobar.c:(.text+0x28): undefined reference to `foo_spam'
foobar.c:(.text+0x4d): undefined reference to `bar_eggs'
foobar.c:(.text+0x5c): undefined reference to `bar_spam'
collect2: ld returned 1 exit status
make: *** [foobar] Error 1

... il semble d'abord que les choses ont empiré. Mais regardez bien: en fait, la phase de compilation s'est très bien passée. C'est juste l'éditeur de liens qui se plaint maintenant de la collision de symboles et nous indique l'emplacement (fichier source et ligne) où cela se produit. Et comme nous pouvons le voir, ces symboles ne sont pas préfixés.

Jetons un coup d'œil aux tables de symboles avec l'utilitaire nm:

example/ex02/ $ nm foo.o
0000000000000019 T eggs
0000000000000000 T spam
0000000000000008 C the_eggs
0000000000000004 C the_spams

example/ex02/ $ nm bar.o
0000000000000019 T eggs
0000000000000000 T spam
0000000000000008 C the_eggs
0000000000000004 C the_spams

Alors maintenant, nous sommes mis au défi avec l'exercice de préfixer ces symboles dans un binaire opaque. Oui, je sais qu'au cours de cet exemple, nous avons les sources et que nous pourrions changer cela là. Mais pour l'instant, supposez que vous n'avez que ces fichiers . O , ou a . A (qui est en fait juste un tas de . o ).

objcopy à la rescousse

Il existe un outil particulièrement intéressant pour nous: objcopy

objcopy fonctionne sur des fichiers temporaires, nous pouvons donc l'utiliser comme s'il fonctionnait sur place. Il y a une option/opération appelée - préfixe-symboles et vous avez 3 suppositions ce qu'il fait.

Jetons donc ce gars sur nos bibliothèques tenaces:

example/ex03/ $ objcopy --prefix-symbols=foo_ foo.o
example/ex03/ $ objcopy --prefix-symbols=bar_ bar.o

nm nous montre que cela semblait fonctionner:

example/ex03/ $ nm foo.o
0000000000000019 T foo_eggs
0000000000000000 T foo_spam
0000000000000008 C foo_the_eggs
0000000000000004 C foo_the_spams

example/ex03/ $ nm bar.o
000000000000001e T bar_eggs
0000000000000000 T bar_spam
0000000000000008 C bar_the_eggs
0000000000000004 C bar_the_spams

Essayons de lier tout cela:

example/ex03/ $ make
cc   foobar.o foo.o bar.o   -o foobar

Et en effet, cela a fonctionné:

example/ex03/ $ ./foobar 
foo: spam = 0, eggs = 0.000000
bar: old spam = 0, new spam = 3 ; old eggs = 0.000000, new eggs = 5.000000

Maintenant, je laisse au lecteur le soin d'implémenter un outil/script qui extrait automatiquement les symboles d'une bibliothèque en utilisant nm , écrit un fichier d'en-tête wrapper de la structure

/* wrapper header wrapper_foo.h for foo.h */
#define spam foo_spam
#define eggs foo_eggs
/* ... */
#include <foo.h>
#undef spam
#undef eggs
/* ... */

et applique le préfixe de symbole aux fichiers objets de la bibliothèque statique en utilisant objcopy .

Et les bibliothèques partagées?

En principe, la même chose pourrait être faite avec les bibliothèques partagées. Cependant, les bibliothèques partagées, comme son nom l'indique, sont partagées entre plusieurs programmes, donc jouer avec une bibliothèque partagée de cette manière n'est pas une si bonne idée.

Vous ne pourrez pas contourner l'écriture d'un emballage de trampoline. Pire encore, vous ne pouvez pas créer de lien avec la bibliothèque partagée au niveau du fichier objet, mais vous êtes obligé d'effectuer un chargement dynamique. Mais cela mérite son propre article.

Restez à l'écoute et bon codage.

125
datenwolf

Les règles de la norme C imposent certaines contraintes (pour une compilation sûre): le compilateur AC ne peut regarder que les 8 premiers caractères d'un identifiant, donc foobar2k_eggs et foobar2k_spam peuvent être interprétés valablement comme les mêmes identificateurs - cependant, chaque compilateur moderne autorise l'arbitraire longs identifiants, donc à notre époque (le 21e siècle), nous ne devrions pas nous en préoccuper.

Ce n'est pas seulement une extension des compilateurs modernes; la norme C actuelle aussi nécessite le compilateur pour prendre en charge les noms externes raisonnablement longs. J'oublie la longueur exacte mais c'est quelque chose comme 31 caractères maintenant si je me souviens bien.

Mais que faire si vous faites face à des bibliothèques dont vous ne pouvez pas changer les noms/idenfiers des symboles? Peut-être que vous n'avez qu'un binaire statique et les en-têtes ou que vous ne le souhaitez pas, ou que vous n'êtes pas autorisé à vous ajuster et à vous recompiler.

Alors tu es coincé. Plainte à l'auteur de la bibliothèque. J'ai rencontré un jour un tel bogue où les utilisateurs de mon application n'étaient pas en mesure de le construire sur Debian en raison de la liaison libSDLlibsoundfile de Debian, qui (au moins à l'époque) polluait horriblement l'espace de noms global avec des variables comme dsp (je ne vous moque pas!). Je me suis plaint à Debian, et ils ont corrigé leurs paquets et envoyé le correctif en amont, où je suppose qu'il a été appliqué, car je n'ai plus jamais entendu parler du problème.

Je pense vraiment que c'est la meilleure approche, car elle résout le problème pour tout le monde. Tout piratage local que vous ferez laissera le problème dans la bibliothèque pour le prochain utilisateur malheureux à rencontrer et à combattre avec à nouveau.

Si vous avez vraiment besoin d'une solution rapide et que vous avez une source, vous pouvez ajouter un tas de -Dfoo=crappylib_foo -Dbar=crappylib_bar etc. au makefile pour le corriger. Sinon, utilisez la solution objcopy que vous avez trouvée.

7
R..

Si vous utilisez GCC, le commutateur de l'éditeur de liens --allow-multiple-definition est un outil de débogage pratique. Cela oblige l'éditeur de liens à utiliser la première définition (et à ne pas se plaindre). En savoir plus ici .

Cela m'a aidé pendant le développement lorsque j'ai la source d'une bibliothèque fournie par le fournisseur et que je dois tracer une fonction de bibliothèque pour une raison ou une autre. Le commutateur vous permet de compiler et de lier dans une copie locale d'un fichier source et de toujours lier à la bibliothèque de fournisseur statique non modifiée. N'oubliez pas de retirer l'interrupteur des symboles de marque une fois le voyage de découverte terminé. L'expédition du code de version avec des collisions intentionnelles dans l'espace de noms est sujette à des écueils, y compris involontaire collisions dans l'espace de noms.

3
JJJSchmidt