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
È difficile produrre un binario Linux portabile perché glibc, la libreria C GNU, garantisce la compatibilità in una sola 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 esegui la compilazione 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, nella maggior parte dei casi, la configurazione è corretta. L'unica eccezione è un'impostazione LD_LIBRARY_PATH che indirizza il loader verso una versione meno recente di libc; il percorso riportato nel messaggio mostra quale libc è stata effettivamente letta. Il loader dinamico legge un tag di versione registrato all'interno del binario, cerca quel tag nella libc, 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?
I dati alla base di questo grafico
[
{
"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 build glibc ordinario restituisce un output di questo tipo:
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 build statico indica statically linked e non specifica alcun interprete. Un build musl specifica un interprete diverso:
ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib/ld-musl-x86_64.so.1, ...L'interprete è il campo più importante. È il dynamic loader che il kernel esegue prima del programma, e quel percorso preciso 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 abbia installato musl.
readelf -d ./mytool elenca le librerie condivise richieste dal binario, indicando i nomi:
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, associato tramite soname. libssl.so.3 è OpenSSL 3, quindi il 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 aggiunge le directory in cui il loader cerca le dipendenze dirette del binario, dopo qualsiasi impostazione LD_LIBRARY_PATH ma prima della cache delle librerie di sistema e dei percorsi predefiniti. $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. Su 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 sia il file effettivamente pubblicato dal progetto, perché verificare un download rispetto al checksum pubblicato è l'unico passaggio che indica a chi appartengono i byte in tuo possesso.
Gli errori che vedrai realmente 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 visibile. Il componente mancante è l'interprete, non il binario. execve restituisce ENOENT quando il percorso del loader nell'intestazione ELF è assente, e la shell visualizza l'unico messaggio disponibile. Esegui file e leggi il percorso dell'interprete. Un binario collegato con musl su un server che dispone soltanto di glibc produce esattamente questo sintomo.
cannot execute binary file: Exec format error di solito indica un'architettura errata: un binario x86-64 su un server arm64, o il contrario. Può anche indicare che il file non è affatto un eseguibile valido, ad esempio perché il download è troncato o danneggiato. La prima riga dell'output di file indica quale architettura stai utilizzando oppure che il file non è un eseguibile ELF utilizzabile.
error while loading shared libraries: libssl.so.3: cannot open shared object file indica che una libreria NEEDED è assente o è presente con un soname diverso. Installa il pacchetto della distribuzione corrispondente. I nomi dei pacchetti cambiano tra le diverse famiglie di distribuzioni, quindi verifica la traduzione prima di copiare una riga apt da un README; gli equivalenti dei comandi dnf e apt mostrano la corrispondenza.
Un semplice Segmentation fault proveniente da una build musl che funziona correttamente con glibc. Di solito indica un problema relativo alla dimensione dello stack dei thread, trattato 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 piccola libreria C progettata per il collegamento statico. Un binario musl statico contiene la propria libc, non specifica alcun interprete ed esegue su qualsiasi kernel Linux dell'architettura corretta. È così che vengono realizzati 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 il target e compilare per quel target:
rustup target add x86_64-unknown-linux-musl
cargo build --release --target x86_64-unknown-linux-muslGo non richiede nulla di tutto questo. Con CGO_ENABLED=0 go build il risultato è già statico e non collega alcuna libc.
Le ricerche NSS cambiano. glibc risolve utenti e nomi host tramite NSS (name service switch), che carica i moduli libnss_* con dlopen mentre il programma è in esecuzione. Un glibc collegato staticamente non può farlo e il linker mostra 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 vedrà, mentre tutti gli altri programmi del sistema continueranno a vederli. Anche il resolver di musl è più recente di quello di glibc: il fallback TCP per le risposte DNS troppo grandi per una risposta UDP è arrivato in musl 1.2.4 nel 2023. Le build basate su versioni precedenti di musl non possono interrogare record DNS di grandi dimensioni e le relative ricerche possono fallire con nameserver ricorsivi che, in caso di risposta UDP troncata, non restituiscono risultati parziali.
dlopen non funziona. In un binario musl statico, dlopen è uno stub che fallisce sempre. Tutto ciò che carica codice durante l'esecuzione è escluso: sistemi di plugin, moduli PAM, driver GPU e i moduli di glibc iconv per i set di caratteri. Se il programma richiede dlopen, il collegamento statico non è praticabile ed è necessario scegliere una delle altre tre opzioni.
Gli aggiornamenti di sicurezza diventano una responsabilità dell'amministratore. 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. Conservare un inventario di ciò che è stato collegato, perché nulla sul sistema di destinazione può informare l'amministratore che il binario autonomo contiene una libreria di due anni prima.
Cambia anche la licenza. glibc è distribuita con licenza LGPL e il collegamento statico attiva l'obbligo di relinking previsto dalla LGPL: è necessario fornire ai destinatari ciò che serve per ricollegare il programma a una glibc diversa. musl è distribuita con licenza MIT e non impone questa condizione. Questo è il motivo principale per cui i progetti che distribuiscono binari autonomi scelgono musl invece di glibc statica.
Due differenze a runtime possono produrre arresti anomali difficili da interpretare. Lo stack predefinito dei thread in musl è di 128 KiB. glibc ricava il valore predefinito dal limite dello stack con cui è stato avviato il programma. In genere il risultato è compreso tra 2 e 10 MB. Quando il limite è illimitato, sulla maggior parte delle architetture glibc usa 2 MB. Di conseguenza, il codice che inserisce un buffer di grandi dimensioni nello stack di un thread può causare un segfault con musl, senza messaggi né righe nei 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 gestire molti thread che eseguono allocazioni simultaneamente. I programmi con molti thread e numerose allocazioni possono quindi risultare sensibilmente più lenti. Eseguire benchmark sul proprio carico di lavoro invece di basarsi sulla 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. Formano una coppia corrispondente. Mescolare il loader del sistema host con una libc inclusa nel bundle causa un crash durante l'avvio invece di un errore leggibile. Includi entrambi oppure nessuno dei due.
AppImage applica questo schema in un formato pacchettizzato. Il contenuto è archiviato in un'immagine squashfs e un piccolo runtime la monta. C'è un aspetto spesso trascurato: la maggior parte degli AppImage usa la glibc dell'host invece di includerla. Di conseguenza, un AppImage compilato in questo modo su una distribuzione recente continua a non funzionare con lo stesso errore di versione su un server meno recente. Le indicazioni sulle best practice di AppImage raccomandano di compilare su una base non più recente del sistema meno recente supportato. In questo caso, AppImage è quindi un formato di distribuzione che si basa sull'opzione 1, non un'alternativa a essa. È possibile includere anche glibc: appimage-builder e go-appimage possono entrambi distribuire glibc all'interno dell'AppImage. Un AppImage compilato in questo modo può essere eseguito su distribuzioni sia meno recenti sia più recenti del sistema di compilazione. Il costo consiste in un file più grande e in librerie come libssl bloccate nel bundle.
Su un server headless, AppImage richiede FUSE (filesystem in userspace) per montare il contenuto. Un'immagine VPS minimale spesso non lo include. Gli AppImage compilati con un runtime meno recente richiedono sul sistema host la libreria FUSE 2, pacchettizzata come libfuse2 su Debian e Ubuntu e rinominata libfuse2t64 in Ubuntu 24.04. Se manca, terminano con AppImages require FUSE to run.. Gli AppImage più recenti integrano il supporto per FUSE 3 e non richiedono più l'installazione di libfuse sull'host. Puoi saltare completamente il mount con ./App.AppImage --appimage-extract-and-run: il contenuto 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 l'errore "version GLIBC_2.38 not found"?
Il binario è stato compilato su una macchina con una versione di glibc più recente di quella disponibile sul server. Il versionamento dei simboli di glibc garantisce la compatibilità in avanti soltanto in una direzione: i binari obsoleti vengono eseguiti su glibc più recente, mentre i binari compilati per una versione più recente non vengono eseguiti su glibc obsoleta, perché il loader richiede il tag di versione esatto registrato nel file e una libc obsoleta non lo contiene. Non esiste un'installazione sicura da eseguire sul server per risolvere il problema. Ricompilare usando una base più obsoleta, distribuire una build statica con musl, includere le librerie con il loader corrispondente oppure distribuire un'immagine del 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 produce alcun output, il binario è statico e non richiede glibc. Confronta il risultato con la versione presente sul server, ottenuta con ldd --version.
Un binario musl è più lento di un binario glibc?
Dipende dal carico di lavoro; la risposta corretta consiste nell'eseguire una misurazione. Un binario statico musl si avvia più rapidamente perché non deve essere eseguito alcun loader e al momento di exec non è necessario effettuare il lavoro di rilocazione. 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 simultaneamente. Inoltre musl include meno routine per stringhe e memoria ottimizzate manualmente rispetto a glibc. Di conseguenza, i programmi multithread con molte 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 sul server Debian o Ubuntu indica /lib/ld-musl-x86_64.so.1, hai un binario collegato a musl su un sistema glibc. Installa il runtime musl della distribuzione, ovvero il pacchetto musl su Debian e Ubuntu, oppure procurati una build glibc o una build statica del programma.
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 tutti i programmi glibc collegati dinamicamente presenti sulla macchina li utilizzano. Sovrascrivere la copia di sistema può quindi impedire l'esecuzione di quasi qualsiasi programma sul server, inclusi gli strumenti necessari per annullare la modifica. Se devi eseguire un singolo binario più recente su un host obsoleto, estrai la versione più recente di glibc in una directory privata e avvia il programma tramite il relativo loader con --library-path. La modifica influenzerà soltanto quel processo. Ricompilare usando la versione obsoleta di glibc resta comunque la soluzione che non crea problemi imprevisti sei mesi dopo.