¿Cómo lidiar con colisiones de símbolos entre bibliotecas vinculadas estáticamente?
static libraries (3)
Las reglas del estándar C ponen algunas restricciones en eso (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 válidamente, sin embargo, cada compilador moderno permite arbitrarias identificadores largos, entonces en nuestros tiempos (el siglo 21) no deberíamos tener que preocuparnos por esto.
Esto no es solo una extensión de los compiladores modernos; el estándar actual de C también requiere que el compilador admita nombres externos razonablemente largos. Olvidé la longitud exacta, pero ahora tengo algo así como 31 caracteres si recuerdo bien.
Pero, ¿qué sucede si te enfrentas a algunas bibliotecas de las cuales no puedes cambiar los nombres de los símbolos / idenfiers? Tal vez tengas solo un binario estático y los encabezados o no quieras, o no te permitan ajustar y recompilar tu contenido.
Entonces estás atrapado. Quejarse con el autor de la biblioteca. Una vez me encontré con un error donde los usuarios de mi aplicación no pudieron compilarlo en Debian debido a libSDL
de Debian que enlazaba libsoundfile
, que (al menos en ese momento) contaminó horriblemente el espacio de nombres global con variables como dsp
(¡no me dsp
!). Me quejé con Debian, y arreglaron sus paquetes y enviaron el arreglo en sentido ascendente, donde supongo que se aplicó, ya que nunca volví a saber del problema.
Realmente creo que este es el mejor enfoque, porque resuelve el problema para todos . Cualquier hack local que hagas dejará el problema en la biblioteca para que el siguiente usuario desafortunado se encuentre y pelee de nuevo.
Si realmente necesita una solución rápida, y tiene fuente, podría agregar un montón de -Dfoo=crappylib_foo -Dbar=crappylib_bar
etc. al archivo MAKE para arreglarlo. De lo contrario, use la solución objcopy
que encontró.
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 nombre específico de la biblioteca. C ++ lo hace fácil, debido a la palabra clave del namespace
. En C, el enfoque habitual es prefijar los identificadores con algún prefijo específico de la biblioteca.
Las reglas del estándar C ponen algunas restricciones en eso (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 válidamente, sin embargo, cada compilador moderno permite arbitrarias identificadores largos, entonces en nuestros tiempos (el siglo 21) no deberíamos tener que preocuparnos por esto.
Pero, ¿qué sucede si te enfrentas a algunas bibliotecas de las cuales no puedes cambiar los nombres de los símbolos / idenfiers? Tal vez tengas solo un binario estático y los encabezados o no quieras, o no te permitan ajustar y recompilar tu contenido.
Al menos en el caso de las bibliotecas estáticas , puede solucionarlo de manera bastante conveniente.
Considere los encabezados de las 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 usar esos 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 pone de manifiesto inmediatamente: C no sabe sobrecargar. Entonces tenemos dos veces dos funciones con nombre idéntico pero de firma diferente. Entonces, necesitamos alguna manera de distinguirlos. De todos modos, veamos lo que un compilador tiene que decir al respecto:
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
Bueno, 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 identificador sin modificar el código fuente o los encabezados de las bibliotecas originales? De hecho, podemos.
Primero resolvamos los problemas del tiempo de compilación. Para esto, rodeamos el encabezado con un conjunto de directivas predefinidas #define
que preceden a todos los símbolos exportados por la biblioteca. Más tarde lo hacemos con un encabezado de contenedor acogedor agradable, pero solo para demostrar lo que está sucediendo lo estábamos haciendo textualmente en el archivo fuente de 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 salió bien. Es solo el vinculador el que ahora se queja de que hay símbolos colisionando y nos dice la ubicación (archivo de origen y línea) donde sucede esto. Y como podemos ver, esos símbolos no están prefijados.
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 estamos desafiados con el ejercicio de ponerle un prefijo a esos símbolos en un binario opaco. Sí, sé que en el curso de este ejemplo tenemos las fuentes y podríamos cambiar esto allí. Pero por ahora, supongamos que solo tiene esos archivos .o , o .a (que en realidad es solo un montón de .o ).
objcopy al rescate
Hay una herramienta particularmente interesante para nosotros: objcopy
objcopy funciona en archivos temporales, por lo que podemos usarlo como si estuviera funcionando en el lugar. Hay una opción / operación llamada - prefijo-símbolos y tienes 3 conjeturas de lo que hace.
Así que vamos a lanzar este chico a nuestras bibliotecas obstinadas:
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 unir 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 lo dejo como un ejercicio para el lector para implementar una herramienta / script que extraiga automáticamente los símbolos de una biblioteca usando nm , escriba 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 del símbolo a los archivos de objetos de la biblioteca estática usando objcopy .
¿Qué pasa con las bibliotecas compartidas?
En principio, lo mismo podría hacerse con bibliotecas compartidas. Sin embargo, las bibliotecas compartidas, como su nombre lo dice, se comparten entre varios programas, por lo que jugar con una biblioteca compartida de esta manera no es una buena idea.
No tendrás que escribir una envoltura de trampolín. Lo que es peor, no puede vincularse con la biblioteca compartida en el nivel de archivo de objeto, sino que se lo fuerza a realizar una carga dinámica. Pero esto merece su propio artículo.
Estén atentos, y feliz codificación.
Si está utilizando GCC, el conmutador del enlazador --allow-multiple-definition es una práctica herramienta de depuración. Esto hace que el enlazador use la primera definición (y no lloriquee al respecto). Más sobre esto here .
Esto me ha ayudado durante el desarrollo cuando tengo disponible la fuente de una biblioteca suministrada por el proveedor y necesito rastrearla en una función de biblioteca por algún motivo u otro. El conmutador le permite compilar y vincular en una copia local de un archivo fuente y aún enlazar a la biblioteca de proveedores estáticos no modificados. No olvides quitar el interruptor de los símbolos de la 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 peligros como las colisiones no intencionales de nombres de espacios.