Vincular libstdc ++ estáticamente: ¿algún problema?

90

Necesito implementar una aplicación C ++ construida en Ubuntu 12.10 con libstdc ++ de GCC 4.7 en sistemas que ejecutan Ubuntu 10.04, que viene con una versión considerablemente más antigua de libstdc ++.

Actualmente, estoy compilando con -static-libstdc++ -static-libgcc, como sugiere esta publicación de blog: Vinculando libstdc ++ estáticamente . El autor advierte contra el uso de cualquier código C ++ cargado dinámicamente al compilar libstdc ++ estáticamente, que es algo que aún no he comprobado. Aún así, todo parece ir bien hasta ahora: puedo hacer uso de las funciones de C ++ 11 en Ubuntu 10.04, que es lo que buscaba.

Observo que este artículo es de 2005 y quizás mucho ha cambiado desde entonces. ¿Sigue vigente su consejo? ¿Hay algún problema oculto que deba tener en cuenta?

Nick Hutchinson
fuente
No, vincular estáticamente a libstdc ++ no implica eso. Si implicara que la -static-libstdc++opción no tendría sentido, simplemente usaría-static
Jonathan Wakely
@JonathanWakely -static obtendrá un kernel too olderror en algún sistema ubuntu 1404. El glibc.so es como kernel32.dllen la ventana, es parte de la interfaz del sistema operativo, no debemos incrustarlo en nuestro binario. Puede utilizar objdump -T [binary path]para verlo cargado dinámicamente libstdc++.soo no. Para el programador de golang, puede agregar #cgo linux LDFLAGS: -static-libstdc++ -static-libgccantes de importar "C"
hombre de bronce
@bronzeman, pero estamos hablando de que -static-libstdc++no, -staticpor libc.solo que no estará vinculado estáticamente.
Jonathan Wakely
1
@NickHutchinson, la publicación del blog vinculada ha desaparecido. Esta pregunta SO es un éxito de búsqueda popular para los términos relevantes aquí. ¿Puede reproducir la información crítica de esa publicación de blog en su pregunta u ofrecer un nuevo enlace si sabe a dónde se movió?
Brian Cain
1
@BrianCain El archivo de Internet lo tiene: web.archive.org/web/20160313071116/http://www.trilithium.com/…
Rob Keniger

Respuestas:

134

Esa publicación de blog es bastante inexacta.

Hasta donde yo sé, se han introducido cambios en C ++ ABI con cada lanzamiento importante de GCC (es decir, aquellos con componentes de números de primera o segunda versión diferentes).

No es verdad. Los únicos cambios de C ++ ABI introducidos desde GCC 3.4 han sido compatibles con versiones anteriores, lo que significa que C ++ ABI se ha mantenido estable durante casi nueve años.

Para empeorar las cosas, la mayoría de las principales distribuciones de Linux utilizan instantáneas de GCC y / o parchean sus versiones de GCC, lo que hace que sea prácticamente imposible saber exactamente con qué versiones de GCC puede estar tratando cuando distribuye binarios.

Las diferencias entre las versiones parcheadas de GCC de las distribuciones son menores y no cambian la ABI, por ejemplo, la 4.6.3 20120306 de Fedora (Red Hat 4.6.3-2) es compatible con ABI con las versiones anteriores de FSF 4.6.xy casi con certeza con cualquier 4.6. x de cualquier otra distro.

En GNU / Linux, las bibliotecas en tiempo de ejecución de GCC usan el control de versiones de símbolos ELF, por lo que es fácil verificar las versiones de símbolos que necesitan los objetos y las bibliotecas, y si tiene una libstdc++.soque proporciona esos símbolos, funcionará, no importa si es una versión parcheada ligeramente diferente. de otra versión de tu distribución.

pero no se puede vincular dinámicamente ningún código C ++ (o cualquier código que utilice el soporte de tiempo de ejecución de C ++) para que esto funcione.

Esto tampoco es cierto.

Dicho esto, vincular estáticamente a libstdc++.aes una opción para ti.

La razón por la que podría no funcionar si carga dinámicamente una biblioteca (usando dlopen) es que los símbolos libstdc ++ de los que depende podrían no haber sido necesarios para su aplicación cuando la vinculó (estáticamente), por lo que esos símbolos no estarán presentes en su ejecutable. Eso se puede resolver vinculando dinámicamente la biblioteca compartida a libstdc++.so(que es lo correcto de todos modos si depende de ello). La interposición de símbolos ELF significa que los símbolos que están presentes en su ejecutable serán utilizados por la biblioteca compartida, pero otros no presente en su ejecutable se encontrará en el que libstdc++.sose vincule. Si su aplicación no se usa dlopen, no necesita preocuparse por eso.

Otra opción (y la que prefiero) es implementar la más nueva libstdc++.sojunto con su aplicación y asegurarse de que se encuentre antes que el sistema predeterminado libstdc++.so, lo que se puede hacer obligando al vinculador dinámico a buscar en el lugar correcto, ya sea utilizando $LD_LIBRARY_PATHla variable de entorno en la ejecución. time, o estableciendo un RPATHen el ejecutable en link-time. Prefiero usarlo RPATHporque no depende de que el entorno esté configurado correctamente para que la aplicación funcione. Si vincula su aplicación con '-Wl,-rpath,$ORIGIN'(tenga en cuenta las comillas simples para evitar que el shell intente expandirse $ORIGIN), el ejecutable tendrá una RPATHde las $ORIGINcuales le indica al vinculador dinámico que busque bibliotecas compartidas en el mismo directorio que el ejecutable. Si pones lo nuevolibstdc++.soen el mismo directorio que el ejecutable se encontrará en tiempo de ejecución, problema resuelto. (Otra opción es poner el ejecutable /some/path/bin/y el libstdc ++ más nuevo. Así, /some/path/lib/y vincularlo con '-Wl,-rpath,$ORIGIN/../lib'o cualquier otra ubicación fija relativa al ejecutable, y establecer el RPATH relativo a $ORIGIN)

Jonathan Wakely
fuente
8
Esta explicación, especialmente sobre RPATH, es gloriosa.
nilweed
3
Enviar libstdc ++ con su aplicación en Linux es un mal consejo. Busque en Google "steam libstdc ++" para ver todo el drama que esto trae. En resumen, si su exe carga bibliotecas externas (como, opengl) que quieren abrir libstdc ++ nuevamente (como, controladores radeon), esas bibliotecas usarán su libstdc ++ porque ya está cargada, en lugar de la suya propia, que es lo que necesitan y esperar. Así que has vuelto al punto de partida.
7
@cap, el OP está preguntando específicamente sobre la implementación en una distribución donde el sistema libstdc ++ es más antiguo. El problema de Steam es que empaquetaron un libstdc ++. Por lo que era más antiguo que el del sistema (presumiblemente era más nuevo en el momento en que lo empaquetaron, pero las distribuciones pasaron a otros más nuevos). Eso se puede resolver haciendo que el RPATH apunte a un directorio que contenga un libstdc++.so.6enlace simbólico que se establece en el momento de la instalación para apuntar a la biblioteca incluida o al sistema si es más nuevo. Hay modelos de vínculos mixtos más complicados, como los que usa Red Hat DTS, pero son difíciles de hacer por sí mismo.
Jonathan Wakely
5
oye, lo siento si no quiero que mi modelo para enviar binarios compatibles con versiones anteriores incluya "confiar en que otras personas mantengan libstdc ++ ABI compat" o "vincular condicionalmente libstdc ++ en tiempo de ejecución" ... si eso altera algunas plumas aquí y ahí, qué puedo hacer, no quiero faltarle el respeto. Y si recuerdas el drama memcpy@GLIBC_2.14, no puedes culparme por tener problemas de confianza con esto :)
6
Tuve que usar '-Wl, -rpath, $ ORIGIN' (tenga en cuenta el '-' delante de rpath). No puedo editar la respuesta porque las ediciones deben tener al menos 6 caracteres ....
user368507
11

Una adición a la excelente respuesta de Jonathan Wakely, por qué dlopen () es problemático:

Debido al nuevo grupo de manejo de excepciones en GCC 5 (consulte PR 64535 y PR 65434 ), si abre y cierra una biblioteca que está vinculada estáticamente a libstdc ++, obtendrá una pérdida de memoria (del objeto del grupo) cada vez. Entonces, si hay alguna posibilidad de que alguna vez uses dlopen, parece una muy mala idea vincular estáticamente libstdc ++. Tenga en cuenta que se trata de una fuga real a diferencia de la benigna mencionada en PR 65434 .

Emil Styrke
fuente
1
La función __gnu_cxx::__freeres()parece proporcionar al menos algo de ayuda con este problema, ya que libera el búfer interno del objeto del grupo. Pero para mí no está claro qué implicación tiene una llamada a esta función con respecto a las excepciones lanzadas accidentalmente después.
phlipsy
3

Complemento a la respuesta de Jonathan Wakely con respecto al RPATH:

RPATH solo funcionará si el RPATH en cuestión es el RPATH de la aplicación en ejecución . Si tiene una biblioteca que se vincula dinámicamente a cualquier biblioteca a través de su propio RPATH, el RPATH de la biblioteca será sobrescrito por el RPATH de la aplicación que lo carga. Esto es un problema cuando no puede garantizar que el RPATH de la aplicación sea el mismo que el de su biblioteca, por ejemplo, si espera que sus dependencias estén en un directorio en particular, pero ese directorio no es parte del RPATH de la aplicación.

Por ejemplo, supongamos que tiene una aplicación App.exe que tiene una dependencia vinculada dinámicamente en libstdc ++. So.x para GCC 4.9. El App.exe tiene esta dependencia resuelta a través del RPATH, es decir

App.exe (RPATH=.:./gcc4_9/libstdc++.so.x)

Ahora digamos que hay otra biblioteca Dependency.so, que tiene una dependencia vinculada dinámicamente en libstdc ++. So.y para GCC 5.5. La dependencia aquí se resuelve a través del RPATH de la biblioteca, es decir

Dependency.so (RPATH=.:./gcc5_5/libstdc++.so.y)

Cuando App.exe carga Dependency.so, no agrega ni antepone el RPATH de la biblioteca . No lo consulta en absoluto. El único RPATH que se considerará será el de la aplicación en ejecución, o App.exe en este ejemplo. Eso significa que si la biblioteca se basa en símbolos que están en gcc5_5 / libstdc ++. So.y pero no en gcc4_9 / libstdc ++. So.x, la biblioteca no se cargará.

Esto es solo una advertencia, ya que me he encontrado con estos problemas en el pasado. RPATH es una herramienta muy útil pero su implementación aún tiene algunos inconvenientes.

Jonathan McDevitt
fuente
¡así que RPATH para bibliotecas compartidas no tiene sentido! Y esperaba que mejoraran un poco Linux a este respecto en las últimas 2 décadas ...
Frank Puck
2

También es posible que deba asegurarse de no depender del glibc dinámico. Ejecute lddel ejecutable resultante y observe las dependencias dinámicas (libc / libm / libpthread son sospechosos habituales).

Un ejercicio adicional sería construir un montón de ejemplos de C ++ 11 involucrados usando esta metodología y realmente probar los binarios resultantes en un sistema 10.04 real. En la mayoría de los casos, a menos que haga algo extraño con la carga dinámica, sabrá de inmediato si el programa funciona o falla.

Alexander L. Belikoff
fuente
1
¿Cuál es el problema de depender de la glibc dinámica?
Nick Hutchinson
Creo que al menos hace algún tiempo libstdc ++ implicaba dependencia en glibc. No estoy seguro de dónde están las cosas hoy.
Alexander L. Belikoff
9
libstdc ++ depende de glibc (por ejemplo, iostreams se implementan en términos de printf) pero siempre que glibc en Ubuntu 10.04 proporcione todas las funciones que necesita la nueva libstdc ++, no hay problema con la dependencia de la glibc dinámica, de hecho, se recomienda encarecidamente no vincular nunca estáticamente a glibc
Jonathan Wakely
1

Me gustaría agregar a la respuesta de Jonathan Wakely lo siguiente.

Jugando -static-libstdc++con Linux, me he enfrentado al problema con dlclose(). Supongamos que tenemos una aplicación 'A' vinculada estáticamente libstdc++y se carga dinámicamente vinculada al libstdc++complemento 'P' en tiempo de ejecución. Esta bien. Pero cuando 'A' descarga 'P', se produce una falla de segmentación. Mi suposición es que después de descargar libstdc++.so, 'A' ya no puede usar símbolos relacionados con libstdc++. Tenga en cuenta que si tanto 'A' como 'P' están vinculados estáticamente libstdc++, o si 'A' está vinculado dinámicamente y 'P' estáticamente, el problema no ocurre.

Resumen: si su aplicación carga / descarga complementos que pueden vincularse dinámicamente libstdc++, la aplicación también debe estar vinculada a ella de forma dinámica. Esta es solo mi observación y me gustaría recibir sus comentarios.

Fedorov7890
fuente
1
Esto probablemente sea similar a mezclar implementaciones de libc (por ejemplo, vincular dinámicamente a un complemento que a su vez vincula dinámicamente glibc, mientras que la aplicación en sí está vinculada estáticamente a musl-libc). Rich Felker, autor de musl-libc, afirma que el problema en tal escenario es que la administración de memoria glibc (uso sbrk) hace ciertas suposiciones y espera estar solo dentro de un proceso ... no estoy seguro si esto se limita a un versión glibc particular o lo que sea.
0xC0000022L
y la gente aún no ve las ventajas de la interfaz del montón de Windows, que puede manejar múltiples copias independientes de libc ++ / libc dentro de un solo proceso. Estas personas no deberían diseñar software.
Frank Puck
@FrankPuck tiene una cantidad decente de experiencia tanto en Windows como en Linux. Puedo decirte que la forma en que "Windows" lo hace no te ayudará cuando MSVC es la parte que decide qué asignador se usa y cómo. La principal ventaja que veo con los montones en Windows es que puedes repartir fragmentos y luego liberarlos de una sola vez. Pero con MSVC todavía se encontrará con prácticamente el problema descrito anteriormente, por ejemplo, al pasar los punteros asignados por otro tiempo de ejecución de VC (liberación frente a depuración o enlazado estático frente a dinámicamente). Entonces "Windows" no es inmune. Se debe tener cuidado en ambos sistemas.
0xC0000022L