¿Por qué el orden en que se vinculan las bibliotecas a veces causa errores en GCC?

Respuestas:

558

(Vea el historial de esta respuesta para obtener el texto más elaborado, pero ahora creo que es más fácil para el lector ver líneas de comando reales).


Archivos comunes compartidos por todos los comandos a continuación

$ cat a.cpp
extern int a;
int main() {
  return a;
}

$ cat b.cpp
extern int b;
int a = b;

$ cat d.cpp
int b;

Vinculación a bibliotecas estáticas

$ g++ -c b.cpp -o b.o
$ ar cr libb.a b.o
$ g++ -c d.cpp -o d.o
$ ar cr libd.a d.o

$ g++ -L. -ld -lb a.cpp # wrong order
$ g++ -L. -lb -ld a.cpp # wrong order
$ g++ a.cpp -L. -ld -lb # wrong order
$ g++ a.cpp -L. -lb -ld # right order

El enlazador busca de izquierda a derecha y observa símbolos sin resolver a medida que avanza. Si una biblioteca resuelve el símbolo, toma los archivos de objetos de esa biblioteca para resolver el símbolo (en este caso, fuera de libb.a).

Las dependencias de las bibliotecas estáticas entre sí funcionan igual: la biblioteca que necesita símbolos debe ser primero, luego la biblioteca que resuelve el símbolo.

Si una biblioteca estática depende de otra biblioteca, pero la otra biblioteca nuevamente depende de la biblioteca anterior, hay un ciclo. Puede resolver esto encerrando las bibliotecas que dependen cíclicamente por -(y -), como -( -la -lb -)(puede que necesite escapar de los parens, como -\(y -\)). Luego, el vinculador busca esas bibliotecas adjuntas varias veces para garantizar que se resuelvan las dependencias del ciclo. Alternativamente, puede especificar las bibliotecas varias veces, de modo que cada una esté una frente a la otra:-la -lb -la .

Vinculación a bibliotecas dinámicas

$ export LD_LIBRARY_PATH=. # not needed if libs go to /usr/lib etc
$ g++ -fpic -shared d.cpp -o libd.so
$ g++ -fpic -shared b.cpp -L. -ld -o libb.so # specifies its dependency!

$ g++ -L. -lb a.cpp # wrong order (works on some distributions)
$ g++ -Wl,--as-needed -L. -lb a.cpp # wrong order
$ g++ -Wl,--as-needed a.cpp -L. -lb # right order

Aquí es lo mismo: las bibliotecas deben seguir los archivos de objetos del programa. La diferencia aquí en comparación con las bibliotecas estáticas es que no necesita preocuparse por las dependencias de las bibliotecas entre sí, porque las bibliotecas dinámicas clasifican sus dependencias ellos mismos .

Aparentemente, algunas distribuciones recientes usan de manera predeterminada el --as-neededindicador de enlazador, lo que exige que los archivos de objetos del programa vengan antes que las bibliotecas dinámicas. Si se pasa ese indicador, el vinculador no se vinculará a bibliotecas que el ejecutable realmente no necesita (y lo detecta de izquierda a derecha). Mi distribución reciente de archlinux no usa este indicador por defecto, por lo que no dio un error por no seguir el orden correcto.

No es correcto omitir la dependencia de b.socontra d.soal crear el primero. Se le pedirá que especifique la biblioteca al vincular en aese momento, pero en arealidad no necesita el entero en bsí mismo, por lo que no debe preocuparse por blas propias dependencias.

Aquí hay un ejemplo de las implicaciones si no especifica las dependencias para libb.so

$ export LD_LIBRARY_PATH=. # not needed if libs go to /usr/lib etc
$ g++ -fpic -shared d.cpp -o libd.so
$ g++ -fpic -shared b.cpp -o libb.so # wrong (but links)

$ g++ -L. -lb a.cpp # wrong, as above
$ g++ -Wl,--as-needed -L. -lb a.cpp # wrong, as above
$ g++ a.cpp -L. -lb # wrong, missing libd.so
$ g++ a.cpp -L. -ld -lb # wrong order (works on some distributions)
$ g++ -Wl,--as-needed a.cpp -L. -ld -lb # wrong order (like static libs)
$ g++ -Wl,--as-needed a.cpp -L. -lb -ld # "right"

Si ahora observa qué dependencias tiene el binario, observa que el binario en sí mismo también depende libd, no solo libbcomo debería. Será necesario volver a vincular el binario si libbmás tarde depende de otra biblioteca, si lo hace de esta manera. Y si alguien más carga libbusando dlopenen tiempo de ejecución (piense en cargar complementos dinámicamente), la llamada también fallará. Entonces, el "right"realmente debería ser wrongtambién.

Johannes Schaub - litb
fuente
10
Repita hasta que se resuelvan todos los símbolos, eh: pensaría que podrían manejar un tipo topológico. LLVM tiene 78 bibliotecas estáticas por sí mismo, con dependencias de quién sabe qué. Es cierto que también tiene un script para descubrir las opciones de compilación / enlace, pero no puede usarlo en todas las circunstancias.
Steve314
66
@ Steve es lo que hacen los programas lorder+ tsort. Pero a veces no hay orden, si tiene referencias cíclicas. Luego solo tiene que recorrer la lista de bibliotecas hasta que todo esté resuelto.
Johannes Schaub - litb
10
@Johannes: determine los componentes máximos fuertemente conectados (por ejemplo, el algoritmo de Tarjans) y luego clasifique topológicamente el dígrafo (inherentemente no cíclico) de los componentes. Cada componente se puede tratar como una biblioteca: si se necesita una biblioteca del componente, los ciclos de dependencia harán que se necesiten todas las bibliotecas de ese componente. Entonces, no, realmente no hay necesidad de recorrer todas las bibliotecas para resolver todo, y no hay necesidad de opciones de línea de comandos incómodas: un método que usa dos algoritmos bien conocidos puede manejar todos los casos correctamente.
Steve314
44
Me gustaría agregar un detalle importante a esta excelente respuesta: Usar "- (archivos -)" o "--archivos de inicio de grupo --end-group" es la única forma segura de resolver dependencias circulares , ya que cada vez el vinculador visita un archivo, extrae (y registra los símbolos no resueltos de) solo los archivos de objetos que resuelven los símbolos no resueltos actualmente . Debido a esto, el algoritmo de CMake de repetir componentes conectados en el gráfico de dependencia puede ocasionalmente fallar. (Consulte también la excelente publicación de blog de Ian Lance Taylor sobre enlaces para obtener más detalles.)
jorgen
3
Su respuesta me ayudó a resolver mis errores de vinculación y usted ha explicado muy claramente CÓMO evitar meterse en problemas, pero ¿tiene alguna idea de POR QUÉ fue diseñado para funcionar de esta manera?
Anton Daneyko
102

El enlazador GNU ld es un llamado enlazador inteligente. Realizará un seguimiento de las funciones utilizadas por las bibliotecas estáticas anteriores, eliminando permanentemente las funciones que no se utilizan en sus tablas de búsqueda. El resultado es que si vincula una biblioteca estática demasiado pronto, las funciones de esa biblioteca ya no estarán disponibles para las bibliotecas estáticas más adelante en la línea de enlace.

El enlazador típico de UNIX funciona de izquierda a derecha, por lo tanto, coloque todas sus bibliotecas dependientes a la izquierda y las que satisfagan esas dependencias a la derecha de la línea de enlace. Es posible que algunas bibliotecas dependan de otras mientras que al mismo tiempo otras bibliotecas dependen de ellas. Aquí es donde se complica. ¡Cuando se trata de referencias circulares, arregle su código!

casualcoder
fuente
2
¿Es esto algo con solo gnu ld / gcc? ¿O es algo común con los enlazadores?
Mike
2
Aparentemente, más compiladores de Unix tienen problemas similares. MSVC no está completamente libre de estos problemas, pero no parecen ser tan malos.
MSalters
44
Las herramientas de desarrollo de MS no tienden a mostrar estos problemas tanto porque si usas una cadena de herramientas de todo MS termina configurando el orden del enlazador correctamente, y nunca notas el problema.
Michael Kohne
16
El enlazador MSVC es menos sensible a este problema porque buscará en todas las bibliotecas un símbolo sin referencia. El orden de la biblioteca aún puede afectar qué símbolo se resuelve si más de una biblioteca tiene el símbolo. Desde MSDN: "Las bibliotecas también se buscan en el orden de la línea de comandos, con la siguiente advertencia: los símbolos que no se resuelven al traer un archivo de objeto de una biblioteca se buscan primero en esa biblioteca, y luego las siguientes bibliotecas de la línea de comandos y / DEFAULTLIB (Especificar biblioteca predeterminada) y luego a cualquier biblioteca al comienzo de la línea de comando "
Michael Burr
44
"... enlace inteligente ..." - Creo que está clasificado como un enlace "de un solo paso", no como un "enlace inteligente".
JWW
54

Aquí hay un ejemplo para dejar en claro cómo funcionan las cosas con GCC cuando están involucradas las bibliotecas estáticas . Así que supongamos que tenemos el siguiente escenario:

  • myprog.o- que contiene la main()función, dependiente delibmysqlclient
  • libmysqlclient- static, por el bien del ejemplo (preferiría la biblioteca compartida, por supuesto, ya que libmysqlclientes enorme); en /usr/local/lib; y depende de cosas delibz
  • libz (dinámica)

¿Cómo vinculamos esto? (Nota: ejemplos de compilación en Cygwin usando gcc 4.3.4)

gcc -L/usr/local/lib -lmysqlclient myprog.o
# undefined reference to `_mysql_init'
# myprog depends on libmysqlclient
# so myprog has to come earlier on the command line

gcc myprog.o -L/usr/local/lib -lmysqlclient
# undefined reference to `_uncompress'
# we have to link with libz, too

gcc myprog.o -lz -L/usr/local/lib -lmysqlclient
# undefined reference to `_uncompress'
# libz is needed by libmysqlclient
# so it has to appear *after* it on the command line

gcc myprog.o -L/usr/local/lib -lmysqlclient -lz
# this works
Lumi
fuente
31

Si agregas -Wl,--start-group a las banderas del vinculador, no le importa en qué orden están o si hay dependencias circulares.

En Qt esto significa agregar:

QMAKE_LFLAGS += -Wl,--start-group

Ahorra mucho tiempo jugando y no parece ralentizar mucho la vinculación (que de todos modos lleva mucho menos tiempo que la compilación).

SvaLopLop
fuente
8

Otra alternativa sería especificar la lista de bibliotecas dos veces:

gcc prog.o libA.a libB.a libA.a libB.a -o prog.x

Al hacer esto, no tiene que molestarse con la secuencia correcta ya que la referencia se resolverá en el segundo bloque.

Eckes
fuente
5

Puede usar la opción -Xlinker.

g++ -o foobar  -Xlinker -start-group  -Xlinker libA.a -Xlinker libB.a -Xlinker libC.a  -Xlinker -end-group 

es casi igual a

g++ -o foobar  -Xlinker -start-group  -Xlinker libC.a -Xlinker libB.a -Xlinker libA.a  -Xlinker -end-group 

Cuidado !

  1. ¡El orden dentro de un grupo es importante! Aquí hay un ejemplo: una biblioteca de depuración tiene una rutina de depuración, pero la biblioteca sin depuración tiene una versión débil de la misma. Debe colocar la biblioteca de depuración PRIMERO en el grupo o resolverá la versión sin depuración.
  2. Debe preceder cada biblioteca en la lista de grupos con -Xlinker
yundorri
fuente
5

Un consejo rápido que me hizo tropezar: si está invocando el enlazador como "gcc" o "g ++", el uso de "--start-group" y "--end-group" no pasará esas opciones al enlazador - ni marcará un error. Simplemente fallará el enlace con símbolos indefinidos si el orden de la biblioteca fue incorrecto.

Debe escribirlos como "-Wl, - start-group", etc. para indicarle a GCC que pase el argumento al enlazador.

MM
fuente
2

El orden de los enlaces ciertamente importa, al menos en algunas plataformas. He visto bloqueos para aplicaciones vinculadas con bibliotecas en orden incorrecto (donde incorrecto significa A vinculado antes de B pero B depende de A).

David Cournapeau
fuente
2

Lo he visto mucho, algunos de nuestros módulos enlazan más de 100 bibliotecas de nuestro código más sistema y librerías de terceros.

Dependiendo de los diferentes enlazadores HP / Intel / GCC / SUN / SGI / IBM / etc., puede obtener funciones / variables no resueltas, etc., en algunas plataformas debe enumerar las bibliotecas dos veces.

En su mayor parte, usamos una jerarquía estructurada de bibliotecas, núcleo, plataforma, diferentes capas de abstracción, pero para algunos sistemas todavía tiene que jugar con el orden en el comando de enlace.

Una vez que encuentre un documento de solución, el próximo desarrollador no tendrá que resolverlo nuevamente.

Mi antiguo profesor solía decir, " alta cohesión y bajo acoplamiento ", todavía es cierto hoy.

titanae
fuente