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

Lesezeit: 10 Minuten

Benutzeravatar von datenwolf
Datenwolf

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

Die Regeln des C-Standards setzen einige Einschränkungen für diese (für eine sichere Kompilierung): AC-Compiler dürfen nur die ersten 8 Zeichen eines Bezeichners betrachten, so foobar2k_eggs und foobar2k_spam können gültig als dieselben Bezeichner interpretiert werden – jedoch erlaubt jeder moderne Compiler beliebig lange Bezeichner, sodass wir uns in unserer Zeit (dem 21. Jahrhundert) nicht darum kümmern müssen.

Aber was ist, wenn Sie mit einigen Bibliotheken konfrontiert sind, deren Symbolnamen / Bezeichner Sie nicht ändern können? Vielleicht hast du nur ein statisches Binary und die Header bekommen oder willst oder darfst es nicht selbst anpassen und neu kompilieren.

  • Siehe auch: stackoverflow.com/questions/6538501/…

    – ninjalj

    4. August 2011 um 15:04 Uhr

Benutzeravatar von datenwolf
Datenwolf

Zumindest im Fall von statisch Bibliotheken können Sie ganz bequem umgehen.

Betrachten Sie diese Header von Bibliotheken foo und Bar. Für dieses Tutorial gebe ich Ihnen auch die Quelldateien

Beispiele/ex01/foo.h

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

example/ex01/foo.c (dies kann undurchsichtig/nicht verfügbar sein)

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

example/ex01/bar.c (dies kann undurchsichtig/nicht verfügbar sein)

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 einem Programm foobar verwenden

example/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 kein Überladen. Wir haben also zwei mal zwei Funktionen mit identischem Namen, aber unterschiedlicher Signatur. Wir brauchen also eine Möglichkeit, diese zu unterscheiden. 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 sagte uns nur, was wir bereits wussten oder zumindest vermuteten.

Können wir diese Identifer-Kollision also irgendwie lösen, ohne den Quellcode oder die Header der ursprünglichen Bibliotheken zu ändern? Tatsächlich können wir.

Lassen Sie uns zuerst die Kompilierzeitprobleme lösen. Dazu umgeben wir die Header-Includes mit einer Reihe von Präprozessoren #define Direktiven, die allen von der Bibliothek exportierten Symbolen ein Präfix voranstellen. Später machen wir das mit einem netten, gemütlichen Wrapper-Header, aber nur um zu demonstrieren, was vor sich geht, haben wir es wörtlich in der gemacht foobar.c Quelldatei:

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

Wenn wir das jetzt 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 ob es schlimmer geworden wäre. Aber schaut genau hin: Eigentlich lief die Compilation-Phase ganz gut. Es ist nur der Linker, der sich jetzt darüber beschwert, dass Symbole kollidieren, und uns die Stelle (Quelldatei und Zeile) mitteilt, an der dies passiert. Und wie wir sehen können, haben diese Symbole kein Präfix.

Werfen wir einen Blick auf die Symboltabellen mit dem nm Dienstprogramm:

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 stehen wir also vor der Herausforderung, diesen Symbolen in einer undurchsichtigen Binärdatei ein Präfix voranzustellen. Ja, ich weiß, im Zuge dieses Beispiels haben wir die Quellen und könnten dies dort ändern. Aber nehmen Sie jetzt einfach an, dass Sie nur diese haben Dateien oder eine .a (was eigentlich nur ein Haufen ist ).

Objektkopie zur Rettung

Ein Tool ist für uns besonders interessant: Objektkopie

objcopy arbeitet mit temporären Dateien, sodass wir es so verwenden können, als ob es vor Ort ausgeführt würde. Es gibt eine Option/Operation namens –Präfix-Symbole und Sie haben 3 Vermutungen, was es tut.

Also werfen wir diesen Kerl auf unsere sturen 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, das Ganze zu verlinken:

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

Und tatsächlich, es hat 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

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

/* 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 auf die Objektdateien der statischen Bibliothek an Objektkopie.

Was ist mit gemeinsam genutzten Bibliotheken?

Im Prinzip könnte man dasselbe mit Shared Libraries machen. Shared Libraries werden jedoch, wie der Name schon sagt, von mehreren Programmen gemeinsam genutzt, daher ist es keine so gute Idee, auf diese Weise mit einer Shared Library herumzuspielen.

Sie werden nicht darum herumkommen, eine Trampolinfolie zu schreiben. Noch schlimmer ist, dass Sie auf Objektdateiebene nicht mit der gemeinsam genutzten Bibliothek verknüpfen können, sondern gezwungen sind, dynamisch zu laden. Aber das verdient einen eigenen Artikel.

Bleiben Sie dran und viel Spaß beim Programmieren.

  • Beeindruckend! Hätte nicht erwartet, dass es so einfach ist mit objcopy.

    – Kos

    4. August 2011 um 11:57 Uhr

  • Haben Sie gerade… Ihre eigene Frage innerhalb von 1 Minute beantwortet, nachdem Sie sie gestellt haben?

    – Alex B

    4. August 2011 um 12:14 Uhr

  • @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 könnte. Die Frage nach kollidierenden Bibliotheken kam 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 Q&A. meta.stackexchange.com/questions/97240/…

    – Datenwolf

    4. August 2011 um 12:23 Uhr


  • @datenwolf eine Idee zur Lösung dieses Problems für iOS-Bibliotheken. Wie ich herausgefunden habe, unterstützt objcopy keine iOS-Bibliotheken :/

    – Ege Akpinar

    11. März 2013 um 17:00 Uhr

  • Bla bla bla objcopy --prefix-symbols … +1!

    – Ben Jackson

    18. April 2015 um 19:56 Uhr

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

Dies ist nicht nur eine Erweiterung moderner Compiler; der aktuelle C-Standard auch erfordert den Compiler, um einigermaßen lange externe Namen zu unterstützen. Ich habe die genaue Länge vergessen, aber es sind jetzt ungefähr 31 Zeichen, wenn ich mich recht erinnere.

Aber was ist, wenn Sie mit einigen Bibliotheken konfrontiert sind, deren Symbolnamen / Bezeichner Sie nicht ändern können? Vielleicht hast du nur ein statisches Binary und die Header bekommen oder willst oder darfst es nicht selbst 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 sie aufgrund von Debian nicht auf Debian erstellen konnten libSDL verlinken libsoundfiledie (zumindest damals) den globalen Namensraum fürchterlich mit Variablen wie verschmutzte dsp (Das ist kein Scherz!). Ich habe mich bei Debian beschwert, und sie haben ihre Pakete repariert und den Fix nach oben geschickt, wo ich annehme, dass er angewendet wurde, da ich nie wieder von dem Problem gehört habe.

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

Wenn Sie wirklich eine schnelle Lösung benötigen und eine Quelle haben, können Sie eine Reihe von hinzufügen -Dfoo=crappylib_foo -Dbar=crappylib_bar usw. in das Makefile, um es zu beheben. Wenn nicht, verwenden Sie die objcopy Lösung, die Sie gefunden haben.

  • Sie haben natürlich Recht, aber manchmal brauchen Sie einen schmutzigen Hack, wie den, den ich oben gezeigt habe. Zum Beispiel, wenn Sie mit einer Legacy-Bibliothek festsitzen, deren Anbieter sein Geschäft eingestellt hat, oder ähnliches. Ich habe das speziell für geschrieben statisch Bibliotheken.

    – Datenwolf

    4. August 2011 um 15:15 Uhr

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

Dies hat mir während der Entwicklung geholfen, wenn ich die Quelle einer vom Anbieter bereitgestellten Bibliothek zur Verfügung habe und aus irgendeinem Grund eine Bibliotheksfunktion nachverfolgen muss. Der Schalter ermöglicht es Ihnen, eine lokale Kopie einer Quelldatei zu kompilieren und zu verknüpfen und dennoch mit der unveränderten statischen Anbieterbibliothek zu verknüpfen. Vergessen Sie nicht, den Schalter wieder aus den Make-Symbolen herauszuziehen, sobald die Entdeckungsreise abgeschlossen ist. Der Versandfreigabecode mit absichtlichen Namensraumkollisionen ist anfällig für Fallstricke, einschließlich unbeabsichtigt Namespace-Kollisionen.

1420410cookie-checkWie gehe ich mit Symbolkollisionen zwischen statisch verknüpften Bibliotheken um?

This website is using cookies to improve the user-friendliness. You agree by using the website further.

Privacy policy