Binario Linux non si avvia: glibc o musl?
L'errore GLIBC_2.34 not found indica il requisito minimo fissato dall'ambiente di build. Scopri cosa richiede il binario e 4 modi per distribuirlo.
Perché un binario Linux non viene eseguito su un server meno recente
Produrre un binario Linux portabile è difficile perché glibc, la libreria GNU C, garantisce la compatibilità soltanto in una direzione. Un binario meno recente continua a funzionare con una versione più recente di glibc. Un binario più recente non funziona con una versione meno recente di glibc. Il sistema su cui compili stabilisce il requisito minimo per ogni sistema su cui distribuisci il binario.
L'errore indica la versione esatta richiesta:
./mytool: /lib/x86_64-linux-gnu/libc.so.6: version `GLIBC_2.38' not found (required by ./mytool)Non c'è alcuna corruzione e la configurazione non è errata. Il dynamic loader legge un tag di versione registrato nel binario, cerca quel tag nella libc del sistema, non lo trova e rifiuta di avviare il programma. Scaricare di nuovo il file ed eseguire chmod +x non cambia nulla, perché il requisito è scritto nel file stesso. Devi modificare il modo in cui viene compilato il binario oppure modificare ciò che distribuisci.
Che cosa fa realmente il versionamento dei simboli glibc
Ogni funzione esportata da glibc ha un tag di versione. printf all'interno di libc.so.6 è in realtà printf@@GLIBC_2.2.5. Quando glibc modifica il comportamento o l'ABI (application binary interface) di una funzione, non sostituisce quella esistente. Mantiene il codice precedente con il vecchio tag e aggiunge il nuovo codice con un nuovo tag, quindi un singolo libc.so.6 contiene contemporaneamente più versioni dello stesso simbolo. Un binario del 2009 trova ancora il tag del 2009, ed è per questo che la compatibilità in avanti funziona così bene.
La fase di linking registra ciò che ha utilizzato. Il binario riceve una sezione .gnu.version_r che, in sostanza, indica: "mi serve GLIBC_2.38 da libc.so.6". Una libc meno recente non ha mai avuto quel tag, quindi il loader si interrompe prima che venga eseguito main. Non esiste un fallback, perché ciò significherebbe fornire silenziosamente al programma una funzione diversa da quella rispetto alla quale è stato compilato.
Il trigger più comune dal 2021 è glibc 2.34. Questa release ha unito libpthread e libdl in libc e ha spostato __libc_start_main, la funzione che avvia ogni programma C, nel tag GLIBC_2.34. Di conseguenza, un programma hello-world compilato su qualsiasi distribuzione con glibc 2.34 o successiva richiede GLIBC_2.34, anche se il codice sorgente non chiama nulla di moderno. Per questo il problema è comparso improvvisamente per persone il cui codice non era cambiato da anni.
Quale versione di glibc distribuisce la mia distribuzione?
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
}
]Queste sono le versioni che ogni distribuzione distribuisce nei propri repository, come pubblicato dalle distribuzioni e aggiornato ad agosto 2026. CentOS 7 ha terminato da tempo il proprio ciclo di vita, ma resta nell’elenco perché esistono ancora server ereditati che lo eseguono. Controlla qualsiasi sistema disponibile con ldd --version, che stampa il rilascio di glibc nella prima riga, oppure con getconf GNU_LIBC_VERSION.
Leggi queste 9 righe come una scala. Compila su Debian 13 con glibc 2.41 e il risultato non funzionerà su sistemi più vecchi di Debian 13. Compila su Ubuntu 20.04 con glibc 2.31 e lo stesso sorgente funzionerà su ogni riga sopra quella soglia, incluso il sistema Debian 13. Il server più vecchio che devi supportare è l’unico host di compilazione rilevante; di conseguenza, la distribuzione che standardizzi determina il livello ABI minimo per tutto ciò che compili per quella distribuzione. È una scelta da definire prima che il parco sistemi esista, insieme agli altri compromessi descritti in scegliere quale sistema operativo eseguire sul proprio VPS.
Come individuare la versione minima di glibc richiesta da un binario?
Leggila direttamente dal file. Questo metodo funziona con qualsiasi binario, compresi quelli forniti da un vendor senza note di compilazione.
objdump -T ./mytool | grep -o 'GLIBC_[0-9.]*' | sort -Vu | tail -5L'ultima riga indica il requisito minimo. Se objdump non è disponibile, installa il pacchetto binutils. Se su quel sistema non puoi installare nulla, strings -a ./mytool | grep -o 'GLIBC_[0-9.]*' | sort -Vu | tail -5 è una buona alternativa, perché i tag sono memorizzati come stringhe semplici all'interno del file.
readelf -V ./mytool mostra lo stesso requisito in forma strutturata. Cerca il blocco con intestazione Version needs section '.gnu.version_r'. Contiene una riga Name: GLIBC_x.y per ogni tag, raggruppata sotto la libreria che deve fornirlo.
Per individuare il gruppo di funzioni che determina il requisito minimo, cerca il tag con grep: objdump -T ./mytool | grep GLIBC_2.38. Spesso corrisponde a un singolo simbolo, che in alcuni casi puoi evitare. Se objdump -T non restituisce alcun output, il binario è collegato staticamente: non dispone quindi di una tabella dei simboli dinamici e non ha requisiti di versione da visualizzare.
Cosa ti dicono file e readelf -d su un binario che ti è stato consegnato
file mostra in una riga l'architettura e il tipo di linking.
file ./mytoolUn normale binario compilato con glibc appare così:
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 strippedUn binario statico indica statically linked e non specifica alcun interprete. Un binario compilato con musl ne indica uno diverso:
ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib/ld-musl-x86_64.so.1, ...Il campo dell'interprete è quello importante. È il loader dinamico che il kernel esegue prima del programma, e quel percorso esatto deve esistere sulla macchina di destinazione. /lib/ld-musl-x86_64.so.1 non è presente su un server Debian o Ubuntu, a meno che qualcuno non vi abbia installato musl.
readelf -d ./mytool elenca, per nome, le librerie condivise richieste dal binario:
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]Ogni voce NEEDED è un requisito obbligatorio, verificato tramite soname. libssl.so.3 è OpenSSL 3, quindi quel binario non verrà caricato su un server che dispone soltanto di libssl.so.1.1; il motivo è il soname: un soname diverso indica un'ABI deliberatamente incompatibile. RUNPATH indica al loader dove cercare per prima cosa, mentre $ORIGIN si espande nella directory che contiene il binario. È così che un bundle autonomo trova le proprie librerie.
Non eseguire ldd su un binario di cui non ti fidi. Con glibc, ldd può risolvere le dipendenze eseguendo il programma tramite il loader, quindi un file ostile può eseguire codice. readelf e objdump leggono soltanto i byte. Prima di tutto, verifica che il download corrisponda al file effettivamente pubblicato dal progetto, perché verificare un download rispetto al checksum pubblicato è l'unico passaggio che ti permette di sapere a chi appartengono i byte che hai ricevuto.
Gli errori che vedrai effettivamente e il significato di ciascuno
Il loader indica una versione di GLIBC che non riesce a trovare. Il binario è stato compilato con una versione di glibc più recente di quella disponibile sul sistema di destinazione. Installare altri componenti sul sistema di destinazione non risolve il problema in modo sicuro. Scegli una delle quattro opzioni riportate di seguito.
La shell restituisce No such file or directory per un file che è chiaramente presente. L'elemento mancante è l'interprete, non il binario. execve restituisce ENOENT quando nell'header ELF manca il percorso del loader, e la shell visualizza l'unico messaggio disponibile. Esegui file e leggi il percorso dell'interprete. Un binario collegato a musl su un server che dispone solo di glibc produce esattamente questo sintomo.
cannot execute binary file: Exec format error indica che l'architettura non è corretta: un binario x86-64 su un server arm64 o il contrario. La prima riga dell'output di file indica quale dei due casi si applica.
error while loading shared libraries: libssl.so.3: cannot open shared object file indica che manca una libreria NEEDED oppure che è presente con un soname diverso. Installa il pacchetto della distribuzione corrispondente. I nomi dei pacchetti variano tra le diverse famiglie, quindi verifica la traduzione prima di copiare una riga apt da un README; gli equivalenti dei comandi dnf e apt mostrano questa corrispondenza.
Un semplice Segmentation fault proveniente da una build musl che funziona correttamente con glibc. Di solito la causa è la dimensione dello stack dei thread, descritta nell'opzione 2.
Quattro modi per distribuire un binario Linux portabile
Ogni soluzione comporta un costo concreto. Scegli in base al comportamento del programma durante l’esecuzione, non in base a quella che sembra più ordinata.
Opzione 1: compilare sulla distribuzione più vecchia supportata
La risposta meno interessante e, di solito, quella corretta. Compila all'interno di un'immagine container della distribuzione più vecchia che dichiari di supportare. Il linker può registrare soltanto i tag effettivamente presenti in quella versione obsoleta di glibc. In questo modo il requisito minimo scende a quella release, mentre il binario resta un normale binario dinamico e mantiene intatti tutti i comportamenti di glibc.
docker run --rm -v "$PWD:/src" -w /src ubuntu:20.04 sh -c 'apt-get update && apt-get install -y build-essential && make'L'output viene eseguito su glibc 2.31 e su tutte le versioni successive. Verifica invece di dare per scontato: esegui sul risultato il comando objdump -T su una sola riga riportato in precedenza e controlla che il tag più alto sia quello previsto.
Il costo riguarda la toolchain. Un'immagine di base obsoleta include anche un compilatore obsoleto, il che crea problemi quando il codice richiede uno standard C++ recente. Go e Rust evitano in gran parte questo problema, perché le rispettive toolchain vengono installate nel container indipendentemente dai pacchetti della distribuzione. Per C e C++ puoi aggiungere un compilatore più recente dal canale toolchain della distribuzione oppure usare una delle immagini manylinux, create proprio per combinare una versione antica di glibc con un GCC attuale. Anche il compilatore C incluso in Zig può puntare direttamente a una versione scelta di glibc, come in zig cc -target x86_64-linux-gnu.2.28, ottenendo lo stesso requisito minimo senza dover conservare un'immagine obsoleta.
Opzione 2: collegamento statico con musl
musl è una libreria C di piccole dimensioni, progettata per il collegamento statico. Un binario musl statico contiene la propria libc, non specifica alcun interprete ed esegue su qualsiasi kernel Linux con l'architettura corretta. È così che vengono compilati la maggior parte dei download di strumenti costituiti da un singolo file.
sudo apt install -y musl-tools
musl-gcc -static -O2 hello.c -o hello
file ./hellofile dovrebbe ora indicare statically linked, senza il campo dell'interprete. Per Rust, aggiungere la destinazione e compilare usandola:
rustup target add x86_64-unknown-linux-musl
cargo build --release --target x86_64-unknown-linux-muslPer Go non serve nulla di tutto questo. Con CGO_ENABLED=0 go build il risultato è già statico e non si collega ad alcuna libc.
Le ricerche NSS cambiano. glibc risolve utenti e nomi host tramite NSS (name service switch), che durante l'esecuzione del programma carica i moduli libnss_* con dlopen. Un glibc collegato staticamente non può farlo e il linker visualizza questo avviso:
warning: Using 'getaddrinfo' in statically linked applications requires at runtime the shared libraries from the glibc version used for linkingmusl evita il problema perché non implementa affatto NSS. Usa il proprio resolver e legge direttamente /etc/resolv.conf e /etc/hosts. Su un VPS ordinario questo comportamento è sufficiente e più semplice. Su un host in cui gli account o i nomi provengono da LDAP o SSSD, il binario non li rileverà, mentre tutti gli altri programmi del sistema sì. Anche il resolver di musl è più recente di quello di glibc: il fallback TCP per le risposte DNS superiori a 512 byte è arrivato in musl 1.2.4 nel 2023, quindi le compilazioni basate su versioni precedenti di musl troncano le risposte di grandi dimensioni.
dlopen non funziona. In un binario musl statico, dlopen è uno stub che restituisce sempre un errore. Tutto ciò che carica codice durante l'esecuzione non è utilizzabile: sistemi di plugin, moduli PAM, driver GPU e gli stessi moduli iconv per i set di caratteri di glibc. Se il programma richiede dlopen, il collegamento statico non è possibile e occorre scegliere una delle altre tre opzioni.
Gli aggiornamenti di sicurezza diventano una responsabilità dell'utente. Un binario collegato dinamicamente riceve una correzione di libc non appena il server esegue l'aggiornamento dei pacchetti. Un binario statico non lo fa mai. Quando viene pubblicata una voce CVE (common vulnerabilities and exposures) relativa alla libc o a una versione statica di OpenSSL inclusa nel binario, è necessario ricompilare e ridistribuire il programma. Ogni copia già distribuita rimane vulnerabile finché qualcuno non sostituisce il file. Tenere traccia delle librerie collegate, perché nulla sul sistema di destinazione può informare un amministratore che il binario costituito da un singolo file contiene una libreria vecchia di due anni.
La licenza cambia. glibc è distribuita con licenza LGPL e il collegamento statico attiva l'obbligo di consentire il ricollegamento previsto dalla LGPL: è necessario fornire ai destinatari ciò che serve per ricollegare il programma a una versione diversa di glibc. musl usa la licenza MIT e non impone questa condizione. Questo è il motivo principale per cui i progetti che distribuiscono binari costituiti da un singolo file scelgono musl invece di glibc statica.
Due differenze a runtime producono arresti anomali difficili da interpretare. Lo stack predefinito dei thread di musl è di 128 KiB, contro gli 8 MiB di glibc. Di conseguenza, il codice che inserisce un buffer di grandi dimensioni nello stack di un thread può causare un errore di segmentazione senza messaggi e senza righe nel log. Impostare esplicitamente la dimensione con pthread_attr_setstacksize oppure spostare il buffer nell'heap. Anche l'allocator di musl è progettato per dimensioni ridotte e comportamento prevedibile, non per molti thread che eseguono allocazioni simultaneamente. I programmi multithread con molte allocazioni possono quindi essere sensibilmente più lenti. Eseguire benchmark sul proprio carico di lavoro invece di affidarsi alla reputazione dell'una o dell'altra libreria.
Opzione 3: includere il loader e le librerie
Distribuisci le librerie accanto al binario insieme al loader corrispondente, quindi avvia il programma tramite quel loader.
./ld-linux-x86-64.so.2 --library-path ./lib ./mytoolPer renderlo permanente, scrivi i percorsi nel file con patchelf:
patchelf --set-interpreter "$PWD/lib/ld-linux-x86-64.so.2" --set-rpath '$ORIGIN/lib' ./mytoolUna sola regola determina se questa soluzione funziona: il loader e libc.so.6 devono provenire dalla stessa build di glibc. Sono una coppia corrispondente. Usare il loader dell'host con una libc inclusa nel pacchetto provoca un crash durante l'avvio, invece di un errore leggibile. Includi entrambi oppure nessuno dei due.
AppImage è questa soluzione in un formato pacchettizzato. Il payload si trova in un'immagine squashfs e un piccolo runtime la monta. C'è un aspetto che spesso viene trascurato: un AppImage non include glibc. Di conseguenza, un AppImage creato su una distribuzione recente continua a non funzionare con lo stesso errore di versione su un server datato. Le indicazioni di AppImage raccomandano di eseguire la build sulla base più vecchia supportata. Questo rende AppImage un formato di distribuzione basato sull'opzione 1, non un suo sostituto.
Su un server headless, AppImage richiede FUSE (filesystem in userspace) per montare il payload, ma un'immagine VPS minimale spesso non lo include. L'errore indica libfuse.so.2. Puoi evitare completamente il montaggio con ./App.AppImage --appimage-extract-and-run: il payload viene estratto in una directory temporanea e il programma viene eseguito da lì.
Opzione 4: distribuire un’immagine container
Spostare l’intero userland insieme al programma. L’immagine include la propria libc, quindi la glibc dell’host non è più rilevante: devono essere compatibili soltanto il kernel e l’architettura. È l’opzione meno sofisticata e rimuove alla radice l’intera classe di problemi. Per questo gran parte del software per server viene distribuita in questo modo. Eseguire Docker su un VPS è il metodo abituale per utilizzarla.
Il kernel dell’host continua a imporre un limite. Le versioni più recenti di glibc usano system call più recenti, che un host container obsoleto può bloccare: glibc 2.34 e successive usano clone3, rifiutata dai profili seccomp predefiniti meno recenti. Il sintomo è un errore immediato con Operation not permitted, senza alcun riferimento a glibc. Aggiornare il runtime dei container sull’host risolve il problema, perché il blocco si trova nel filtro delle system call del runtime e non nel kernel.
I costi sono ordinari. Il sistema di destinazione deve disporre di un runtime per container e delle autorizzazioni necessarie per utilizzarlo. Il download passa da un singolo file a decine o centinaia di megabyte. Ora devi gestire un’immagine di base e il relativo ciclo di patching, quindi il lavoro di sicurezza evitato nell’opzione 2 ritorna sotto forma di rebuild dell’immagine. Per un servizio a esecuzione prolungata è un compromesso ragionevole. Per uno strumento da riga di comando eseguito una sola volta, invece, non lo è.
Quale opzione scegliere?
- Uno strumento interno per server che controlli direttamente e che eseguono tutti la stessa distribuzione: esegui la build dinamicamente su quella distribuzione e fermati qui.
- Un singolo file che utenti sconosciuti scaricano ed eseguono: usa musl statico, se il programma non richiede
dlopenné ricerche supportate da NSS. - Un programma con plugin o accesso alla GPU: mantienilo dinamico, esegui la build su una base obsoleta e usa l'opzione 3 per distribuirlo.
- Un servizio a esecuzione prolungata su una macchina che dispone già di un runtime: distribuisci l'immagine.
Qualunque opzione tu scelga, documentala e verifica la scelta. La versione di glibc dell'host di build entra ora a far parte del processo di rilascio. Se aggiorni una macchina di build da una release LTS a quella successiva, il requisito minimo aumenta senza avvisi e gli utenti che fino al mese scorso non avevano problemi possono smettere di eseguire il programma. Blocca l'immagine di build tramite tag e verifica nel risultato il tag GLIBC_ più alto come fase della build. In questo modo il controllo fallisce nella tua pipeline, anziché nel terminale di qualcun altro.
FAQ
Perché sul server viene visualizzato "version GLIBC_2.38 not found"?
Il binario è stato compilato su una macchina con una versione di glibc più recente di quella presente sul server. Il versionamento dei simboli di glibc garantisce la compatibilità in avanti soltanto: i binari meno recenti funzionano con una glibc più nuova, mentre i binari più nuovi non funzionano con una glibc meno recente, perché il loader richiede il tag di versione esatto registrato nel file e una libc meno recente non ha mai incluso quel tag. Installare componenti aggiuntivi sul server non risolve il problema in modo sicuro. Ricompila usando una base più vecchia, distribuisci una build statica con musl, includi le librerie insieme al loader corrispondente oppure distribuisci un'immagine container.
Come posso sapere quale versione di glibc richiede un binario?
Leggi i tag di versione dal file con objdump -T ./mytool | grep -o 'GLIBC_[0-9.]*' | sort -Vu | tail -5. L'ultima riga indica la versione minima di glibc in grado di caricarlo. readelf -V ./mytool mostra gli stessi requisiti nella sezione .gnu.version_r, raggruppati in base alla libreria che deve fornirli. Se objdump -T non stampa nulla, il binario è statico e non richiede affatto glibc. Confronta il risultato con la versione presente sul server, ottenuta tramite ldd --version.
Un binario musl è più lento di un binario glibc?
Dipende dal carico di lavoro, quindi la risposta corretta è misurare. Un binario statico con musl si avvia più rapidamente, perché non deve eseguire un loader e non deve effettuare rilocazioni al momento di exec. D'altra parte, l'allocator di musl è progettato per occupare poco spazio e offrire un comportamento prevedibile, non per gestire molti thread che allocano memoria nello stesso momento. Inoltre, musl include meno routine di stringa e memoria ottimizzate manualmente rispetto a glibc. Di conseguenza, i programmi con molti thread e un uso intensivo di allocazioni o operazioni sulle stringhe possono essere sensibilmente più lenti. Esegui il benchmark del tuo programma sul tuo server prima di accettare una delle due affermazioni.
Perché bash segnala "No such file or directory" per un file che esiste?
Il file mancante è il loader dinamico, non il binario. Il kernel legge il percorso dell'interprete dall'header ELF e restituisce ENOENT quando quel percorso non esiste; la shell lo segnala usando l'unico messaggio disponibile. Esegui file ./mytool e leggi il campo interpreter. Se indica /lib/ld-musl-x86_64.so.1 su un server Debian o Ubuntu, hai un binario collegato con musl su un sistema glibc. Devi quindi usare il download compatibile con musl oppure una build statica.
Posso copiare libc.so.6 da un server più recente per risolvere il problema?
No. libc.so.6 e ld-linux-x86-64.so.2 sono una coppia abbinata proveniente dalla stessa build di glibc, e ogni processo sulla macchina li utilizza. Sovrascrivere la copia di sistema può quindi impedire al server di eseguire qualsiasi programma, compresi gli strumenti necessari per annullare la modifica. Se devi eseguire un singolo binario più recente su un host vecchio, estrai la glibc più recente in una directory privata e avvia il programma tramite il relativo loader con --library-path. La modifica riguarda soltanto quel processo. Ricompilare usando la glibc più vecchia resta la soluzione più prevedibile anche a distanza di sei mesi.