¿Cómo lidiar con las colisiones de símbolos entre bibliotecas vinculadas estáticamente?


83

Una de las reglas y mejores prácticas más importantes al escribir una biblioteca es colocar todos los símbolos de la biblioteca en un espacio de nombres específico de la biblioteca. C ++ facilita esto gracias a la namespacepalabra clave. En C, el enfoque habitual es prefijar los identificadores con algún prefijo específico de la biblioteca.

Reglas de la norma C ponen algunas restricciones en los que (para la compilación de seguridad): compilador de CA puede mirar sólo los primeros 8 caracteres de un identificador, por lo que foobar2k_eggsy foobar2k_spampuede interpretarse como los mismos identificadores válidamente - sin embargo cada compilador moderna permite identificadores largos arbitrarias , por lo que en nuestros tiempos (el siglo XXI) no deberíamos tener que preocuparnos por esto.

Pero, ¿qué sucede si se enfrenta a algunas bibliotecas cuyos nombres / idenfiers de símbolos no puede cambiar? Tal vez solo tenga un binario estático y los encabezados o no quiera, o no se le permita ajustar y recompilar usted mismo.


Respuestas:


142

Al menos en el caso de las bibliotecas estáticas , puede solucionarlo con bastante comodidad.

Considere esos encabezados de bibliotecas foo y bar . Por el bien de este tutorial, también te daré los archivos fuente.

ejemplos / ex01 / foo.h

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

examples / ex01 / foo.c (esto puede ser opaco / no disponible)

int the_spams;
double the_eggs;

int spam()
{
    return the_spams++;
}

double eggs()
{
    return the_eggs--;
}

ejemplo / ex01 / bar.h

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

examples / ex01 / bar.c (esto puede ser opaco / no 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;
}

Queremos usarlos en un programa foobar

ejemplo / 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 problema se hace evidente de inmediato: C no conoce la sobrecarga. Entonces tenemos dos veces dos funciones con el mismo nombre pero de diferente firma. Entonces necesitamos alguna forma de distinguirlos. De todos modos, veamos qué tiene que decir un compilador sobre esto:

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

De acuerdo, esto no fue una sorpresa, solo nos dijo lo que ya sabíamos, o al menos sospechamos.

Entonces, ¿podemos de alguna manera resolver esa colisión de identificadores sin modificar el código fuente o los encabezados de las bibliotecas originales? De hecho podemos.

Primero vamos a resolver los problemas de tiempo de compilación. Para esto, rodeamos el encabezado incluye con un montón de #definedirectivas de preprocesador que prefieren todos los símbolos exportados por la biblioteca. Más tarde, hacemos esto con un bonito y acogedor encabezado de envoltura, pero solo para demostrar lo que está sucediendo lo estamos haciendo textualmente en el archivo fuente foobar.c :

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

Ahora, si compilamos esto ...

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

... primero parece que las cosas empeoraron. Pero mira de cerca: en realidad, la etapa de compilación fue bien. Es solo el enlazador el que ahora se queja de que hay símbolos que chocan y nos dice la ubicación (archivo fuente y línea) donde ocurre esto. Y como podemos ver, esos símbolos no tienen prefijo.

Echemos un vistazo a las tablas de símbolos con la utilidad 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

Así que ahora tenemos el desafío del ejercicio de anteponer esos símbolos en algún binario opaco. Sí, sé que en el transcurso de este ejemplo tenemos las fuentes y podría cambiar esto allí. Pero por ahora, asuma que solo tiene esos archivos .o , o un .a (que en realidad es solo un montón de .o ).

objcopy al rescate

Hay una herramienta especialmente interesante para nosotros: objcopy

objcopy funciona en archivos temporales, por lo que podemos usarlo como si estuviera operando en el lugar. Hay una opción / operación llamada --prefix-symbols y tiene 3 suposiciones de lo que hace.

Así que arrojemos a este amigo a nuestras obstinadas bibliotecas:

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

nm nos muestra que esto parecía funcionar:

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

Intentemos vincular todo esto:

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

Y de hecho, funcionó:

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

Ahora dejo como ejercicio para el lector implementar una herramienta / script que extrae automáticamente los símbolos de una biblioteca usando nm , escribe un archivo de encabezado contenedor de la estructura

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

y aplica el prefijo de símbolo a los archivos de objeto de la biblioteca estática mediante objcopy .

¿Qué pasa con las bibliotecas compartidas?

En principio, se podría hacer lo mismo con las bibliotecas compartidas. Sin embargo, las bibliotecas compartidas, el nombre lo indica, se comparten entre varios programas, por lo que jugar con una biblioteca compartida de esta manera no es una buena idea.

No podrás escribir un envoltorio de trampolín. Peor aún, no puede vincular con la biblioteca compartida a nivel de archivo de objeto, pero se ve obligado a realizar una carga dinámica. Pero esto merece su propio artículo.

Estén atentos y feliz codificación.


4
¡Impresionante! No esperaba que fuera tan fácil objcopy.
Kos

12
¿Acabas de ... respondiste tu propia pregunta dentro de 1 minuto de haberla hecho?
Alex B

18
@Alex B: Este es un artículo tutorial y seguí un camino que se me sugirió en meta.stackoverflow.com cómo se podrían colocar tutoriales sobre preguntas (¿interesantes?) Y sus soluciones. Surgió la pregunta sobre las bibliotecas en conflicto y pensé "hm, sé cómo manejar este tipo de solución", escribí un artículo y lo publiqué aquí en forma de preguntas y respuestas. meta.stackexchange.com/questions/97240/…
datenwolf

4
@datenwolf alguna idea sobre cómo resolver este problema para las bibliotecas de iOS. Como descubrí, objcopy no es compatible con las bibliotecas de iOS: /
Ege Akpinar

6
Bla, bla, bla objcopy --prefix-symbols ... +1!
Ben Jackson

7

Las reglas del estándar C imponen algunas restricciones (para una compilación segura): el compilador de CA puede ver solo los primeros 8 caracteres de un identificador, por lo que foobar2k_eggs y foobar2k_spam pueden interpretarse como los mismos identificadores de manera válida; sin embargo, cada compilador moderno permite arbitrarios identificadores largos, por lo que en nuestros tiempos (el siglo XXI) no deberíamos tener que preocuparnos por esto.

Esto no es solo una extensión de los compiladores modernos; el estándar C actual también requiere que el compilador admita nombres externos razonablemente largos. Olvidé la longitud exacta, pero ahora son algo así como 31 caracteres si no recuerdo mal.

Pero, ¿qué sucede si se enfrenta a algunas bibliotecas de las que no puede cambiar los nombres / identificadores de símbolos? Tal vez solo tenga un binario estático y los encabezados o no quiera, o no se le permita ajustar y recompilar usted mismo.

Entonces estás atascado. Quejarse con el autor de la biblioteca. Una vez encontré un error de este tipo en el que los usuarios de mi aplicación no podían compilarla en Debian debido a la libSDLvinculación de Debian libsoundfile, que (al menos en ese momento) contaminó el espacio de nombres global horriblemente con variables como dsp(¡No bromeo!). Me quejé con Debian y ellos arreglaron sus paquetes y enviaron la corrección en sentido ascendente, donde supongo que se aplicó, ya que nunca más escuché del problema.

Realmente creo que este es el mejor enfoque, porque resuelve el problema para todos. . Cualquier truco local que hagas dejará el problema en la biblioteca para que el próximo usuario desafortunado lo encuentre y luche de nuevo.

Si realmente necesita una solución rápida y tiene la fuente, puede agregar un montón de -Dfoo=crappylib_foo -Dbar=crappylib_baretc. al archivo MAKE para solucionarlo. Si no es así, use la objcopysolución que encontró.


Tienes razón, por supuesto, sin embargo, a veces necesitas un truco sucio, como el que mostré arriba. Por ejemplo, si está atrapado con alguna biblioteca heredada donde el proveedor cerró o similar. Escribí esto específicamente para bibliotecas estáticas .
datenwolf

3

Si está utilizando GCC, el modificador de enlazador --allow-multiple-definition es una útil herramienta de depuración. Esto obliga al enlazador a usar la primera definición (y no a quejarse). Más sobre esto aquí .

Esto me ha ayudado durante el desarrollo cuando tengo la fuente de una biblioteca proporcionada por el proveedor disponible y necesito rastrear una función de biblioteca por alguna razón u otra. El conmutador le permite compilar y vincular en una copia local de un archivo de origen y aún vincular a la biblioteca del proveedor estático no modificado. No olvide retirar el interruptor de los símbolos de marca una vez que se complete el viaje de descubrimiento. El código de liberación de envío con colisiones intencionales de espacios de nombres es propenso a errores, incluidas las colisiones involuntarias de espacios de nombres.

Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.