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 GNU C, sólo garantiza la compatibilidad en una dirección. Un binario antiguo sigue ejecutándose con una versión nueva de glibc. Un binario nuevo no se ejecuta con una versión antigua de glibc. La máquina donde se compila establece el requisito mínimo para todas las máquinas donde se despliega.
El error indica la versión exacta que necesita:
./mytool: /lib/x86_64-linux-gnu/libc.so.6: version `GLIBC_2.38' not found (required by ./mytool)No hay corrupción ni una configuración incorrecta. El cargador dinámico leyó una etiqueta de versión registrada dentro del binario, buscó esa etiqueta en la libc del sistema, 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 la forma de compilar 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?
The data behind this chart
[
{
"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 dicen 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 especifica ningún intérprete. Una compilación con musl especifica 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 tu 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 necesita el binario, 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 es la razón: un soname diferente indica una ABI deliberadamente incompatible. RUNPATH indica al cargador dónde buscar primero, y $ORIGIN se expande al directorio que contiene el binario. Así es como un paquete autocontenido encuentra sus propias bibliotecas.
No ejecutes ldd sobre un binario en el que no confíes. En glibc, ldd puede resolver 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 su 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 qué 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 destino. Nada de lo que instale en el destino lo solucionará 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 significa que la arquitectura es incorrecta: un binario x86-64 en un servidor arm64, o al contrario. La primera línea de la salida de file indica cuál de los dos tiene.
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 de la distribución que corresponda. Los nombres de los paquetes varían entre familias, por lo que debe traducirlos antes de copiar una línea de apt de un README, y los equivalentes de los comandos dnf y apt muestran esa correspondencia.
Un Segmentation fault aislado 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 explica 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 de la arquitectura adecuada. Así se compilan la mayoría de las descargas de herramientas contenidas en un solo archivo.
sudo apt install -y musl-tools
musl-gcc -static -O2 hello.c -o hello
file ./hellofile ahora debería mostrar 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 se ejecuta el programa. Una glibc enlazada 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 normal, esto es suficiente y 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 de más de 512 bytes llegó a musl 1.2.4 en 2023, por lo que las compilaciones contra versiones anteriores de musl truncan las respuestas grandes.
dlopen no funciona. En un binario estático de musl, dlopen es un stub que siempre falla. Todo lo que cargue código en tiempo de ejecución queda descartado: sistemas de plugins, módulos PAM, controladores de GPU y los propios módulos de conjuntos de caracteres de iconv. Si el programa necesita dlopen, el enlace estático no es viable y debe usar una de las otras tres opciones.
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 la recibe. Cuando aparece una entrada CVE (common vulnerabilities and exposures) contra su libc o contra una versión estática de OpenSSL que haya incluido, debe recompilar y redistribuir el binario. Además, 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 informar al administrador de que el 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 relinkado de LGPL: debe proporcionar a los destinatarios lo necesario para volver a enlazar el programa contra una glibc diferente. musl usa MIT y no impone esta 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ática.
Dos diferencias de ejecución producen fallos confusos. La pila de subproceso predeterminada de musl es de 128 KiB, frente a 8 MiB en glibc. Por eso, el código que coloca un búfer grande en la pila de un subproceso puede terminar con un error de segmentación sin mensaje ni línea en el registro. Establezca el tamaño explícitamente con pthread_attr_setstacksize o mueva el búfer al heap. El asignador de memoria de musl también está diseñado para tamaños pequeños y un comportamiento predecible, no para muchos subprocesos asignando memoria al mismo tiempo. Por ello, los programas con muchos subprocesos y muchas asignaciones pueden ejecutarse de forma apreciablemente más lenta. Compare el rendimiento de 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 y añada también el loader correspondiente. Después, inicie el programa a través de 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 anfitrión con una libc incluida provoca un bloqueo durante el arranque en lugar de mostrar un error legible. Incluya ambos o no incluya ninguno.
AppImage es este patrón empaquetado. Incluye la carga útil en una imagen squashfs y un pequeño runtime que la monta. Tiene una característica que se suele pasar por alto: AppImage no incluye glibc. Por tanto, un AppImage compilado en una distribución actual sigue fallando con el mismo error de versión en un servidor antiguo. Las propias indicaciones de AppImage recomiendan compilar en la base más antigua compatible. Por eso, este formato de distribución se apoya en la opción 1 en lugar de sustituirla.
En un servidor sin interfaz gráfica, AppImage necesita FUSE (filesystem in userspace) para montar su carga útil. Una imagen mínima de VPS a menudo no lo incluye. El fallo menciona libfuse.so.2. Puede omitir completamente el montaje con ./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 un equipo con una versión de glibc más reciente que la del 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 base más antigua, distribuya una compilación estática con musl, incluya las bibliotecas junto con su cargador correspondiente o distribuya una imagen de contenedor.
¿Cómo puedo saber 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 necesita glibc. Compare el resultado con la versión del propio servidor, obtenida mediante ldd --version.
¿Un binario con musl es más lento que uno con glibc?
Depende de la carga de trabajo, y la respuesta correcta es medirlo. Un binario estático con musl se inicia más rápido porque no tiene que ejecutar un cargador ni realizar reubicaciones durante exec. Por otro lado, 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 uso intensivo de asignaciones o cadenas pueden ser claramente más lentos. Haga pruebas con 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 lo muestra con el único mensaje del que dispone. Ejecute file ./mytool y lea el campo interpreter. Si muestra /lib/ld-musl-x86_64.so.1 en un servidor Debian o Ubuntu, tiene un binario enlazado con musl en un sistema con glibc. Necesita la descarga compatible con musl o una compilación estática.
¿Puedo copiar libc.so.6 desde un servidor más nuevo para corregirlo?
No. libc.so.6 y ld-linux-x86-64.so.2 forman un par compatible de una misma compilación de glibc, y todos los procesos del equipo los utilizan. Sobrescribir la copia del sistema puede impedir que el servidor ejecute cualquier programa, incluidas las herramientas necesarias para deshacer el cambio. Si debe ejecutar un binario más nuevo en un host antiguo, desempaquete la glibc más reciente 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 causará problemas inesperados seis meses después.