Por qué un binario Linux no se ejecuta: glibc y musl
El error GLIBC_2.34 not found revela el mínimo fijado al compilar. Aprende a leer lo que exige el binario y elige 1 de 4 formas de distribuirlo.
Por qué un binario de Linux no se ejecuta en un servidor antiguo
Es difícil producir un binario de Linux portable porque glibc, la biblioteca C de GNU, sólo garantiza la compatibilidad en una dirección. Un binario antiguo sigue ejecutándose con una glibc nueva. Un binario nuevo no se ejecuta con una glibc antigua. La máquina donde compila establece el mínimo requerido por todas las máquinas en las que implementa el binario.
El error indica la versión exacta que necesitaba:
./mytool: /lib/x86_64-linux-gnu/libc.so.6: version `GLIBC_2.38' not found (required by ./mytool)No hay corrupción y, normalmente, tampoco hay una configuración incorrecta. La única excepción es un ajuste de LD_LIBRARY_PATH que dirige el cargador a una libc antigua. La ruta del mensaje muestra qué libc leyó realmente. El cargador dinámico leyó una etiqueta de versión registrada dentro del binario, buscó esa etiqueta en esa libc, no la encontró y rechazó iniciar el programa. Volver a descargar el archivo y ejecutar chmod +x no cambia nada, porque el requisito está escrito en el propio archivo. Debe cambiar cómo se compila el binario o cambiar lo que distribuye.
Qué hace realmente el versionado de símbolos de glibc
Cada función que exporta glibc incluye una etiqueta de versión. printf dentro de libc.so.6 es realmente printf@@GLIBC_2.2.5. Cuando glibc cambia el comportamiento o la ABI (interfaz binaria de aplicaciones) de una función, no sustituye la versión anterior. Mantiene el código antiguo con la etiqueta anterior y añade el código nuevo con una etiqueta nueva, de modo que un solo libc.so.6 contiene varias versiones del mismo símbolo. Un binario de 2009 encuentra la etiqueta de 2009, que sigue presente. Por eso la compatibilidad hacia delante funciona tan bien.
La fase de enlazado registra lo que utilizó. El binario obtiene una sección .gnu.version_r que indica, en esencia, «necesito GLIBC_2.38 de libc.so.6». Una libc antigua nunca tuvo esa etiqueta, por lo que el cargador se detiene antes de que se ejecute main. No existe ningún mecanismo alternativo, porque eso implicaría entregar silenciosamente al programa una función distinta de aquella contra la que se compiló.
El desencadenante más habitual desde 2021 es glibc 2.34. Esa versión integró libpthread y libdl en libc y trasladó __libc_start_main, la función que inicia todos los programas en C, a la etiqueta GLIBC_2.34. Por tanto, un programa hello-world compilado en cualquier distribución con glibc 2.34 o posterior exige GLIBC_2.34, aunque el código fuente no llame a nada moderno. Por eso el problema apareció de repente en sistemas cuyo código propio no había cambiado durante años.
¿Qué versión de glibc incluye mi distribución?
Los datos de este gráfico
[
{
"distro": "CentOS 7",
"glibc": 2.17
},
{
"distro": "RHEL 8",
"glibc": 2.28
},
{
"distro": "Ubuntu 20.04",
"glibc": 2.31
},
{
"distro": "Debian 11",
"glibc": 2.31
},
{
"distro": "RHEL 9",
"glibc": 2.34
},
{
"distro": "Ubuntu 22.04",
"glibc": 2.35
},
{
"distro": "Debian 12",
"glibc": 2.36
},
{
"distro": "Ubuntu 24.04",
"glibc": 2.39
},
{
"distro": "Debian 13",
"glibc": 2.41
}
]Estas son las versiones que cada distribución incluye en sus propios repositorios, según lo publicado por las distribuciones y vigente en agosto de 2026. CentOS 7 está mucho más allá del fin de su ciclo de vida, pero permanece en la lista porque todavía se ejecuta en servidores heredados. Compruebe cualquier sistema que tenga con ldd --version, que muestra la versión de glibc en su primera línea, o con getconf GNU_LIBC_VERSION.
Lea esas 9 filas como una escala. Compile en Debian 13 con glibc 2.41 y el resultado no se ejecutará en nada anterior a Debian 13. Compile en Ubuntu 20.04 con glibc 2.31 y el mismo código fuente se ejecutará en todas las filas situadas por encima de esa línea, incluido el sistema Debian 13. El servidor más antiguo que debe admitir es el único host de compilación que importa, por lo que la distribución que estandarice establece el límite inferior de ABI para todo lo que compile para ella. Conviene decidirlo antes de que exista la flota, junto con las demás consideraciones al elegir qué sistema operativo ejecutar en su VPS.
¿Cómo puedo encontrar la versión mínima de glibc que necesita un binario?
Léala del archivo. Este método funciona con cualquier binario, incluido uno que le haya entregado un proveedor sin notas de compilación.
objdump -T ./mytool | grep -o 'GLIBC_[0-9.]*' | sort -Vu | tail -5La última línea indica el mínimo requerido. Si falta objdump, instale el paquete binutils. Si no puede instalar nada en ese equipo, strings -a ./mytool | grep -o 'GLIBC_[0-9.]*' | sort -Vu | tail -5 ofrece un resultado aproximado, porque las etiquetas se almacenan como cadenas de texto simples dentro del archivo.
readelf -V ./mytool muestra el mismo requisito con su estructura. Busque el bloque encabezado por Version needs section '.gnu.version_r'. Contiene una línea Name: GLIBC_x.y por cada etiqueta, agrupada bajo la biblioteca que debe proporcionarla.
Para saber qué conjunto de funciones establece el mínimo, busque la etiqueta con grep: objdump -T ./mytool | grep GLIBC_2.38. A menudo corresponde a un solo símbolo, y en algunos casos puede evitarse. Si objdump -T no muestra ninguna salida, el binario está enlazado estáticamente, por lo que no tiene tabla de símbolos dinámica ni requisitos de versión que mostrar.
Qué te indican file y readelf -d sobre un binario que te han entregado
file muestra la arquitectura y el tipo de enlace en una sola línea.
file ./mytoolUna compilación normal con glibc se muestra así:
ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=..., for GNU/Linux 3.2.0, not strippedUna compilación estática indica statically linked y no nombra ningún intérprete. Una compilación con musl nombra otro:
ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib/ld-musl-x86_64.so.1, ...El intérprete es el campo importante. Es el cargador dinámico que el kernel ejecuta antes que el programa, y esa ruta exacta debe existir en la máquina de destino. /lib/ld-musl-x86_64.so.1 no está presente en un servidor Debian o Ubuntu a menos que alguien haya instalado musl allí.
readelf -d ./mytool muestra las bibliotecas compartidas que el binario necesita, por nombre:
Dynamic section at offset 0x2d58 contains 27 entries:
Tag Type Name/Value
0x0000000000000001 (NEEDED) Shared library: [libssl.so.3]
0x0000000000000001 (NEEDED) Shared library: [libc.so.6]
0x000000000000001d (RUNPATH) Library runpath: [$ORIGIN/../lib]Cada entrada NEEDED es un requisito obligatorio que se comprueba mediante el soname. libssl.so.3 corresponde a OpenSSL 3, por lo que ese binario no se cargará en un servidor que sólo tenga libssl.so.1.1. El soname explica el motivo: un soname diferente indica una ABI deliberadamente incompatible. RUNPATH añade directorios a la búsqueda que realiza el cargador para encontrar las dependencias directas del binario, después de cualquier configuración LD_LIBRARY_PATH, pero antes de la caché de bibliotecas del sistema y de las rutas predeterminadas. $ORIGIN se expande al directorio que contiene el binario. Así es como un paquete autónomo encuentra sus propias bibliotecas.
No ejecutes ldd en un binario en el que no confíes. En glibc, ldd puede resolver las dependencias ejecutando el programa bajo el cargador, por lo que un archivo malicioso puede ejecutar código. readelf y objdump sólo leen bytes. Antes de todo esto, confirma que la descarga es el archivo que el proyecto publicó realmente, porque verificar una descarga con la suma de comprobación publicada es el único paso que te indica de quién son los bytes que tienes.
Los errores que verá realmente y lo que significa cada uno
El cargador indica una versión de GLIBC que no puede encontrar. El binario se compiló con una versión de glibc más reciente que la disponible en el sistema de destino. No hay ninguna instalación en el sistema de destino que lo solucione de forma segura. Elija una de las cuatro opciones siguientes.
El shell muestra No such file or directory para un archivo que puede ver claramente. Lo que falta es el intérprete, no el binario. execve devuelve ENOENT cuando falta la ruta del cargador en la cabecera ELF, y el shell muestra el único mensaje que tiene disponible. Ejecute file y lea la ruta del intérprete. Un binario enlazado con musl en un servidor que sólo tiene glibc produce exactamente este síntoma.
cannot execute binary file: Exec format error normalmente significa que la arquitectura es incorrecta: un binario x86-64 en un servidor arm64, o al contrario. También puede significar que el archivo no es un ejecutable válido, por ejemplo, porque la descarga está truncada o dañada. La primera línea de la salida de file indica qué arquitectura tiene el archivo o si no es un ejecutable ELF utilizable.
error while loading shared libraries: libssl.so.3: cannot open shared object file significa que falta una biblioteca NEEDED o que está disponible con otro soname. Instale el paquete correspondiente de la distribución. Los nombres de los paquetes varían entre familias, así que traduzca la línea de apt antes de copiarla desde un README, y los equivalentes de los comandos dnf y apt explican esa correspondencia.
Un Segmentation fault aislado procedente de una compilación con musl que funciona correctamente con glibc. Normalmente se debe al tamaño de la pila de los hilos, que se trata en la opción 2.
Cuatro formas de distribuir un binario portátil de Linux
Cada opción tiene un coste real. Elija según lo que haga su programa durante la ejecución, no según cuál parezca más limpia.
Opción 1: compilar sobre la distribución más antigua compatible
Es la respuesta menos llamativa y normalmente la correcta. Compile dentro de una imagen de contenedor de la distribución más antigua que se haya comprometido a admitir. El enlazador sólo puede registrar etiquetas que esa versión antigua de glibc realmente tenga, por lo que el límite inferior se establece en esa versión, mientras el binario sigue siendo un binario dinámico normal con todo el comportamiento de glibc intacto.
docker run --rm -v "$PWD:/src" -w /src ubuntu:20.04 sh -c 'apt-get update && apt-get install -y build-essential && make'La salida se ejecuta en glibc 2.31 y en todas las versiones posteriores. Verifique en lugar de darlo por supuesto: ejecute contra el resultado el comando de una línea objdump -T indicado anteriormente y compruebe que la etiqueta más alta sea la esperada.
El coste es la cadena de herramientas. Una imagen base antigua también incluye un compilador antiguo, lo que puede ser un problema si el código necesita un estándar reciente de C++. Go y Rust evitan este problema en gran medida porque sus cadenas de herramientas se instalan dentro del contenedor de forma independiente de los paquetes de la distribución. Para C y C++ puede añadir un compilador más reciente desde el canal de herramientas de la propia distribución o usar las imágenes manylinux, que existen precisamente para combinar una versión antigua de glibc con un GCC actual. El compilador de C incluido con Zig también puede apuntar directamente a una versión de glibc elegida, como en zig cc -target x86_64-linux-gnu.2.28, y proporciona el mismo límite inferior sin tener que conservar una imagen antigua.
Opción 2: enlazar estáticamente con musl
musl es una biblioteca C pequeña diseñada para el enlace estático. Un binario estático de musl contiene su propia libc, no especifica ningún intérprete y se ejecuta en cualquier kernel de Linux con la arquitectura adecuada. Así se compilan la mayoría de las descargas de herramientas distribuidas como un único archivo.
sudo apt install -y musl-tools
musl-gcc -static -O2 hello.c -o hello
file ./hellofile ahora debería indicar statically linked, sin campo de intérprete. En Rust, añada el destino y compile para él:
rustup target add x86_64-unknown-linux-musl
cargo build --release --target x86_64-unknown-linux-muslGo no necesita nada de esto. Con CGO_ENABLED=0 go build, el resultado ya es estático y no enlaza ninguna libc.
Las búsquedas NSS cambian. glibc resuelve usuarios y nombres de host mediante NSS (name service switch), que carga módulos libnss_* con dlopen mientras el programa se ejecuta. glibc enlazado estáticamente no puede hacerlo, y el enlazador muestra una advertencia con estas palabras:
warning: Using 'getaddrinfo' in statically linked applications requires at runtime the shared libraries from the glibc version used for linkingmusl evita esta advertencia porque no implementa NSS. Tiene su propio resolvedor y lee /etc/resolv.conf y /etc/hosts directamente. En un VPS convencional, esto funciona bien y es más sencillo. En un host donde las cuentas o los nombres proceden de LDAP o SSSD, el binario no los verá, aunque los demás programas del sistema sí. El resolvedor de musl también es más reciente que el de glibc: la conmutación a TCP para respuestas DNS demasiado grandes para UDP llegó a musl 1.2.4 en 2023. Las compilaciones con versiones anteriores de musl no pueden consultar registros DNS grandes, y sus búsquedas pueden fallar contra servidores de nombres recursivos que no devuelven resultados parciales en una respuesta UDP truncada.
dlopen no funciona. En un binario estático de musl, dlopen es un stub que siempre falla. Todo lo que cargue código durante la ejecución queda descartado: sistemas de plugins, módulos PAM, controladores de GPU y los propios módulos de conjuntos de caracteres iconv de glibc. Si el programa necesita dlopen, el enlace estático no es una opción y deberá usar una de las otras tres respuestas.
Las actualizaciones de seguridad pasan a ser responsabilidad suya. Un binario enlazado dinámicamente recibe una corrección de libc en cuanto el servidor ejecuta la actualización de paquetes. Un binario estático no. Cuando aparece una entrada CVE (common vulnerabilities and exposures) relacionada con su libc o con una versión estática de OpenSSL que haya incluido, debe recompilar y redistribuir el binario. Cada copia ya desplegada seguirá siendo vulnerable hasta que alguien sustituya el archivo. Mantenga un registro de lo que enlazó, porque nada en el sistema de destino puede indicar al administrador que su binario de un solo archivo contiene una biblioteca de hace dos años.
La licencia cambia. glibc usa LGPL, y el enlace estático activa la obligación de permitir el reenlace establecida por LGPL: debe proporcionar a los destinatarios lo necesario para reenlazar el programa con otra versión de glibc. musl usa MIT y no impone esa condición. Esta es la razón principal por la que los proyectos que distribuyen binarios de un solo archivo eligen musl en lugar de glibc estático.
Dos diferencias en tiempo de ejecución producen fallos difíciles de diagnosticar. La pila predeterminada de los hilos en musl es de 128 KiB. glibc obtiene su valor predeterminado del límite de pila con el que se inició el programa, que normalmente queda entre 2 y 10 MB, y usa 2 MB en la mayoría de las arquitecturas cuando ese límite es ilimitado. Por tanto, el código que coloca un búfer grande en la pila de un hilo puede producir un error de segmentación con musl sin mensaje ni entrada de registro. Establezca el tamaño explícitamente con pthread_attr_setstacksize o mueva el búfer al heap. El asignador de musl también está diseñado para tamaños pequeños y un comportamiento predecible, no para muchos hilos que asignan memoria al mismo tiempo. Por eso, los programas con muchos hilos y asignaciones pueden ejecutarse de forma apreciablemente más lenta. Compare el rendimiento con su propia carga de trabajo en lugar de confiar en la reputación de cualquiera de las dos bibliotecas.
Opción 3: incluir el loader y las bibliotecas
Distribuya las bibliotecas junto al binario, junto con el loader correspondiente, y después inicie el programa mediante ese loader.
./ld-linux-x86-64.so.2 --library-path ./lib ./mytoolPara hacerlo permanente, escriba las rutas en el archivo mediante patchelf:
patchelf --set-interpreter "$PWD/lib/ld-linux-x86-64.so.2" --set-rpath '$ORIGIN/lib' ./mytoolUna regla determina si esto funciona: el loader y libc.so.6 deben proceder de la misma compilación de glibc. Forman un par compatible. Mezclar el loader del sistema con una libc incluida en el paquete provoca un error durante el arranque, en lugar de mostrar un mensaje legible. Incluya ambos o no incluya ninguno.
AppImage aplica este patrón en un paquete. El contenido se almacena en una imagen squashfs y un runtime pequeño la monta. Hay una característica que suele pasarse por alto: la mayoría de los AppImage usan la glibc del sistema en lugar de incluirla. Por tanto, un AppImage creado de esa forma en una distribución actual sigue fallando con el mismo error de versión en un servidor antiguo. Las recomendaciones de buenas prácticas de AppImage indican compilar en una base que no sea más reciente que el sistema más antiguo compatible. Así, ese AppImage es un formato de distribución basado en la opción 1, no un sustituto de ella. También es posible incluir glibc: appimage-builder y go-appimage pueden distribuir glibc dentro del AppImage. Un AppImage creado de esa forma puede ejecutarse en distribuciones más antiguas y más nuevas que el sistema de compilación. El coste es un archivo más grande y bibliotecas como libssl congeladas dentro del paquete.
En un servidor sin interfaz gráfica, AppImage necesita FUSE (filesystem in userspace) para montar su contenido, y una imagen mínima de VPS a menudo no lo incluye. Los AppImage creados con un runtime antiguo necesitan la biblioteca FUSE 2 en el sistema, proporcionada como libfuse2 en Debian y Ubuntu y renombrada como libfuse2t64 en Ubuntu 24.04. Si falta, se detienen con AppImages require FUSE to run.. Los AppImage más recientes incorporan compatibilidad con FUSE 3 y ya no necesitan que libfuse esté instalado en el sistema. Puede omitir el montaje mediante ./App.AppImage --appimage-extract-and-run. Esta opción extrae el contenido en un directorio temporal y lo ejecuta desde allí.
Opción 4: distribuir una imagen de contenedor
Traslade todo el espacio de usuario junto con el programa. La imagen incluye su propia libc, por lo que glibc del host deja de ser relevante. Sólo deben coincidir el kernel y la arquitectura. Es la opción menos ingeniosa y elimina toda esta clase de problemas. Por eso gran parte del software de servidor se distribuye de esta forma. Ejecutar Docker en un VPS es la forma habitual de utilizarla.
El kernel del host sigue imponiendo un límite. Las versiones nuevas de glibc usan llamadas al sistema más recientes, y un host de contenedores antiguo puede bloquearlas: glibc 2.34 y posteriores usan clone3, que los perfiles seccomp predeterminados antiguos rechazan. El síntoma es un fallo inmediato con Operation not permitted, sin ninguna mención de glibc. Actualizar el runtime de contenedores en el host lo corrige, porque el bloqueo está en el filtro de llamadas al sistema del runtime, no en el kernel.
Los costes son los habituales. El destino necesita un runtime de contenedores y permisos para utilizarlo. La descarga pasa de un archivo a decenas o cientos de megabytes. Ahora debe mantener una imagen base y su calendario de parches, por lo que el trabajo de seguridad que evitó en la opción 2 vuelve en forma de reconstrucciones de la imagen. Para un servicio de larga duración, es un intercambio razonable. Para una herramienta de línea de comandos que alguien ejecuta una sola vez, no lo es.
¿Qué opción debe elegir?
- Una herramienta interna para servidores que administra y que ejecutan todos la misma distribución: compílela dinámicamente en esa distribución y no haga nada más.
- Un único archivo que otras personas descargan y ejecutan: use musl estático si el programa no necesita
dlopenni búsquedas respaldadas por NSS. - Un programa con plugins o acceso a GPU: manténgalo dinámico, compílelo en una base antigua y use la opción 3 para distribuirlo.
- Un servicio de larga duración en una máquina que ya tiene un runtime: distribuya la imagen.
Elija lo que elija, documéntelo y compruébelo. La glibc del host de compilación ahora forma parte de su proceso de publicación. Una máquina de compilación actualizada de una versión LTS a la siguiente eleva silenciosamente el requisito mínimo y rompe el funcionamiento para usuarios a los que les funcionaba el mes pasado. Fije la imagen de compilación mediante una etiqueta y compruebe como paso de compilación la etiqueta GLIBC_ más alta presente en la salida. Así, la comprobación falla en su pipeline y no en el terminal de otra persona.
FAQ
¿Por qué aparece «version GLIBC_2.38 not found» en mi servidor?
El binario se compiló en una máquina con una versión de glibc más reciente que la disponible en el servidor. El control de versiones de símbolos de glibc sólo proporciona compatibilidad hacia delante: los binarios antiguos se ejecutan con una glibc nueva, pero los binarios nuevos no se ejecutan con una glibc antigua, porque el cargador necesita la etiqueta de versión exacta registrada en el archivo y una libc antigua nunca tuvo esa etiqueta. No hay nada que pueda instalar de forma segura en el servidor para corregirlo. Vuelva a compilar con una imagen base más antigua, publique una compilación estática con musl, incluya las bibliotecas junto con su cargador correspondiente o publique una imagen de contenedor.
¿Cómo averiguo qué versión de glibc necesita un binario?
Lea las etiquetas de versión del archivo con objdump -T ./mytool | grep -o 'GLIBC_[0-9.]*' | sort -Vu | tail -5. La última línea indica la versión mínima de glibc que puede cargarlo. readelf -V ./mytool muestra los mismos requisitos en la sección .gnu.version_r, agrupados por la biblioteca que debe proporcionarlos. Si objdump -T no muestra nada, el binario es estático y no requiere glibc. Compare el resultado con la versión del propio servidor, que puede obtener con ldd --version.
¿Un binario de musl es más lento que un binario de glibc?
Depende de la carga de trabajo, y la respuesta correcta es medirlo. Un binario estático de musl se inicia más rápido porque no hay que ejecutar un cargador ni realizar reubicaciones durante exec. En cambio, el asignador de musl está diseñado para ocupar poco espacio y ofrecer un comportamiento predecible, no para atender muchas asignaciones simultáneas de varios hilos. Además, musl incluye menos rutinas de cadenas y memoria optimizadas manualmente que glibc, por lo que los programas con muchos hilos y con muchas asignaciones o procesamiento de cadenas pueden ser notablemente más lentos. Compare su propio programa en su propio servidor antes de aceptar cualquiera de esas afirmaciones.
¿Por qué bash muestra «No such file or directory» para un archivo que existe?
El archivo que falta es el cargador dinámico, no el binario. El kernel lee la ruta del intérprete desde la cabecera ELF y devuelve ENOENT cuando esa ruta no existe; el shell muestra ese resultado porque es el único mensaje del que dispone. Ejecute file ./mytool y lea el campo interpreter. Si indica /lib/ld-musl-x86_64.so.1 en un servidor Debian o Ubuntu, tiene un binario enlazado con musl en un sistema con glibc. Instale el runtime de musl de la distribución (el paquete musl en Debian y Ubuntu), o consiga una compilación con glibc o una compilación estática del programa.
¿Puedo copiar libc.so.6 desde un servidor más reciente para corregirlo?
No. libc.so.6 y ld-linux-x86-64.so.2 forman un par asociado de una misma compilación de glibc, y todos los programas enlazados dinámicamente con glibc de la máquina los utilizan. Por tanto, sobrescribir la copia del sistema puede impedir que el servidor ejecute casi cualquier programa, incluidas las herramientas que necesitaría para revertir el cambio. Si debe ejecutar un binario más reciente en un host antiguo, desempaquete la glibc nueva en un directorio privado e inicie ese programa mediante su propio cargador con --library-path. Esto sólo afecta a ese proceso. Volver a compilar con la glibc antigua sigue siendo la opción que no le dará sorpresas seis meses después.