Wie gehe ich mit Symbolkollisionen zwischen statisch verknüpften Bibliotheken um?

82

Eine der wichtigsten Regeln und Best Practices beim Schreiben einer Bibliothek besteht darin, alle Symbole der Bibliothek in einen bibliotheksspezifischen Namespace zu stellen. C ++ macht dies aufgrund des namespaceSchlüsselworts einfach . In C besteht der übliche Ansatz darin, den Bezeichnern ein bibliotheksspezifisches Präfix voranzustellen.

Die Regeln des C-Standards legen einige Einschränkungen für diese fest (für eine sichere Kompilierung): Der AC-Compiler betrachtet möglicherweise nur die ersten 8 Zeichen eines Bezeichners foobar2k_eggsund foobar2k_spamkann daher als dieselben Bezeichner gültig interpretiert werden. Jeder moderne Compiler lässt jedoch beliebig lange Bezeichner zu In unserer Zeit (dem 21. Jahrhundert) sollten wir uns also nicht darum kümmern müssen.

Aber was ist, wenn Sie mit Bibliotheken konfrontiert sind, deren Symbolnamen / Identifikatoren Sie nicht ändern können? Vielleicht haben Sie nur eine statische Binärdatei und die Header oder möchten nicht oder dürfen sich nicht anpassen und neu kompilieren.

Datenwolf
quelle

Antworten:

138

Zumindest bei statischen Bibliotheken können Sie dies ganz bequem umgehen.

Betrachten Sie die Überschriften der Bibliotheken foo und bar . Für dieses Tutorial gebe ich Ihnen auch die Quelldateien

Beispiele / ex01 / foo.h.

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

examples / ex01 / foo.c (dies ist möglicherweise undurchsichtig / nicht verfügbar)

int the_spams;
double the_eggs;

int spam()
{
    return the_spams++;
}

double eggs()
{
    return the_eggs--;
}

Beispiel / ex01 / bar.h.

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

Beispiele / ex01 / bar.c (dies ist möglicherweise undurchsichtig / nicht verfügbar)

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;
}

Wir wollen diese in einer Programm-Foobar verwenden

Beispiel / 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;
}

Ein Problem wird sofort deutlich: C kennt keine Überladung. Wir haben also zwei mal zwei Funktionen mit identischem Namen, aber unterschiedlicher Signatur. Wir brauchen also einen Weg, um diese zu unterscheiden. Wie auch immer, mal sehen, was ein Compiler dazu zu sagen hat:

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

Okay, das war keine Überraschung, es hat uns nur gesagt, was wir bereits wussten oder zumindest vermuteten.

Können wir diese Identifikatorkollision also irgendwie beheben, ohne den Quellcode oder die Header der Originalbibliotheken zu ändern? In der Tat können wir.

Lassen Sie uns zunächst die Probleme mit der Kompilierungszeit beheben. Zu diesem Zweck umgeben wir die Header-Includes mit einer Reihe von Präprozessor- #defineDirektiven, die allen von der Bibliothek exportierten Symbolen vorangestellt sind. Später machen wir das mit einem netten, gemütlichen Wrapper-Header, aber nur um zu demonstrieren, was los ist, haben wir es wörtlich in der foobar.c- Quelldatei gemacht:

Beispiel / 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;
}

Nun, wenn wir dies kompilieren ...

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

... es sieht zunächst so aus, als wäre es schlimmer geworden. Aber schauen Sie genau hin: Eigentlich lief die Kompilierungsphase ganz gut. Es ist nur der Linker, der sich jetzt darüber beschwert, dass Symbole kollidieren, und der uns den Ort (Quelldatei und Zeile) mitteilt, an dem dies geschieht. Und wie wir sehen können, sind diese Symbole nicht fixiert.

Schauen wir uns die Symboltabellen mit dem Dienstprogramm nm an :

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

Jetzt müssen wir diese Symbole in einer undurchsichtigen Binärdatei voranstellen. Ja, ich weiß im Verlauf dieses Beispiels, dass wir die Quellen haben und dies dort ändern könnten. Aber für den Moment nehmen Sie einfach an, dass Sie nur diese .o- Dateien oder eine .a-Datei haben (was eigentlich nur ein Haufen von .o ist ).

Objekt zur Rettung

Es gibt ein Werkzeug, das für uns besonders interessant ist: die Objektkopie

objcopy funktioniert mit temporären Dateien, sodass wir es so verwenden können, als ob es direkt funktioniert. Es gibt eine Option / Operation namens --prefix-Symbole und Sie haben 3 Vermutungen, was es tut.

Also werfen wir diesen Kerl auf unsere hartnäckigen Bibliotheken:

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

nm zeigt uns, dass dies zu funktionieren schien:

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

Versuchen wir, diese ganze Sache zu verknüpfen:

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

Und tatsächlich hat es funktioniert:

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

Jetzt überlasse ich es dem Leser als Übung, ein Tool / Skript zu implementieren, das die Symbole einer Bibliothek automatisch mit nm extrahiert und eine Wrapper-Header-Datei der Struktur schreibt

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

und wendet das Symbolpräfix mithilfe von objcopy auf die Objektdateien der statischen Bibliothek an .

Was ist mit gemeinsam genutzten Bibliotheken?

Im Prinzip könnte das gleiche mit gemeinsam genutzten Bibliotheken gemacht werden. Wie der Name schon sagt, werden gemeinsam genutzte Bibliotheken von mehreren Programmen gemeinsam genutzt. Daher ist es keine gute Idee, auf diese Weise mit einer gemeinsam genutzten Bibliothek herumzuspielen.

Sie werden nicht darum herumkommen, eine Trampolinverpackung zu schreiben. Schlimmer noch, Sie können keine Verknüpfung mit der gemeinsam genutzten Bibliothek auf der Ebene der Objektdatei herstellen, müssen jedoch dynamisch laden. Dies verdient jedoch einen eigenen Artikel.

Bleiben Sie dran und viel Spaß beim Codieren.

Datenwolf
quelle
4
Beeindruckend! Ich hatte nicht erwartet, dass es so einfach sein würde objcopy.
Kos
12
Haben Sie gerade ... Ihre eigene Frage innerhalb von 1 Minute nach der Beantwortung beantwortet?
Alex B
18
@Alex B: Dies ist ein Tutorial-Artikel, und ich bin einem Pfad gefolgt, der mir auf meta.stackoverflow.com vorgeschlagen wurde, wie man Tutorials zu (interessanten?) Fragen und deren Lösungen platzieren kann. Die Frage nach zusammenstoßenden Bibliotheken tauchte auf und ich dachte "hm, ich weiß, wie man mit dieser Art von Lösung umgeht", schrieb einen Artikel und postete ihn hier in Form von Fragen und Antworten. meta.stackexchange.com/questions/97240/…
datenwolf
4
@datenwolf jede Idee zur Lösung dieses Problems für iOS-Bibliotheken. Wie ich herausgefunden habe, unterstützt objcopy keine iOS-Bibliotheken: /
Ege Akpinar
6
Bla bla bla objcopy --prefix-symbols ... +1!
Ben Jackson
7

Die Regeln des C-Standards legen einige Einschränkungen für diese fest (für eine sichere Kompilierung): Der AC-Compiler betrachtet möglicherweise nur die ersten 8 Zeichen eines Bezeichners, sodass foobar2k_eggs und foobar2k_spam gültig als dieselben Bezeichner interpretiert werden können - jeder moderne Compiler lässt jedoch beliebige Zeichen zu lange Identifikatoren, daher sollten wir uns in unserer Zeit (dem 21. Jahrhundert) nicht darum kümmern müssen.

Dies ist nicht nur eine Erweiterung moderner Compiler. Der aktuelle C-Standard verlangt auch, dass der Compiler relativ lange externe Namen unterstützt. Ich vergesse die genaue Länge, aber wenn ich mich recht erinnere, sind es jetzt ungefähr 31 Zeichen.

Aber was ist, wenn Sie mit Bibliotheken konfrontiert sind, deren Symbolnamen / Identifikatoren Sie nicht ändern können? Vielleicht haben Sie nur eine statische Binärdatei und die Header oder möchten nicht oder dürfen sich nicht anpassen und neu kompilieren.

Dann steckst du fest. Beschweren Sie sich beim Autor der Bibliothek. Ich bin einmal auf einen solchen Fehler gestoßen, bei dem Benutzer meiner Anwendung aufgrund der libSDLVerknüpfung von Debian libsoundfile, die (zumindest zu der Zeit) den globalen Namespace mit Variablen wie dsp(ich mache dir nichts vor!) Schrecklich verschmutzte, nicht in der Lage war, ihn auf Debian aufzubauen . Ich habe mich bei Debian beschwert, und sie haben ihre Pakete repariert und das Update stromaufwärts gesendet, wo ich davon ausgehe, dass es angewendet wurde, da ich nie wieder von dem Problem gehört habe.

Ich denke wirklich, dass dies der beste Ansatz ist, weil er das Problem für alle löst . Jeder lokale Hack, den Sie ausführen, lässt das Problem in der Bibliothek, damit der nächste unglückliche Benutzer erneut auf ihn stoßen und mit ihm kämpfen kann.

Wenn Sie wirklich eine schnelle Lösung benötigen und über eine Quelle verfügen, können Sie -Dfoo=crappylib_foo -Dbar=crappylib_bardem Makefile eine Reihe von usw. hinzufügen , um diese zu beheben. Wenn nicht, verwenden objcopySie die gefundene Lösung.

R .. GitHub HÖREN SIE AUF, EIS ZU HELFEN
quelle
Sie haben natürlich Recht, aber manchmal brauchen Sie einen schmutzigen Hack, wie den, den ich oben gezeigt habe. Zum Beispiel, wenn Sie in einer Legacy-Bibliothek stecken bleiben, in der der Anbieter sein Geschäft eingestellt hat oder ähnliches. Ich habe dies speziell für statische Bibliotheken geschrieben.
Datenwolf
3

Wenn Sie GCC verwenden, ist der Linker-Schalter --allow-multiple-definition ein praktisches Debugging-Tool. Dies bringt den Linker dazu, die erste Definition zu verwenden (und nicht darüber zu jammern). Mehr dazu hier .

Dies hat mir bei der Entwicklung geholfen, wenn ich die Quelle für eine vom Hersteller bereitgestellte Bibliothek zur Verfügung habe und aus irgendeinem Grund auf eine Bibliotheksfunktion zurückgreifen muss. Mit dem Schalter können Sie eine lokale Kopie einer Quelldatei kompilieren und verknüpfen und dennoch eine Verknüpfung mit der unveränderten statischen Herstellerbibliothek herstellen. Vergessen Sie nicht, den Schalter wieder aus den Make-Symbolen zu ziehen, sobald die Entdeckungsreise abgeschlossen ist. Versandversandcode mit absichtlichen Namensraumkollisionen ist anfällig für Fallstricke, einschließlich unbeabsichtigter Namensraumkollisionen.

JJJSchmidt
quelle