Waarom een Linux-binary niet draait op oudere servers
De foutmelding GLIBC_2.34 not found ontstaat door versiebeheer in de C-library. Leer hoe u de minimale glibc-vereisten controleert en ontdek vier methoden voor portable builds.
Waarom een Linux-binary niet draait op een oudere server
Het is lastig om een draagbare Linux-binary te maken omdat glibc, de GNU C-library, alleen achterwaartse compatibiliteit garandeert. Een oude binary blijft werken op een nieuwe glibc. Een nieuwe binary werkt echter niet op een oude glibc. De machine waarop u compileert, bepaalt de minimale vereisten voor elke machine waarop u de software uitrolt.
De foutmelding vermeldt de exacte versie die vereist is:
./mytool: /lib/x86_64-linux-gnu/libc.so.6: version `GLIBC_2.38' not found (required by ./mytool)Er is geen sprake van corruptie of een onjuiste configuratie. De dynamic loader heeft een versie-tag gelezen die in de binary is vastgelegd, zocht naar die tag in de libc van het systeem, vond deze niet en weigerde het programma te starten. Het opnieuw downloaden van het bestand en het uitvoeren van chmod +x verandert niets, omdat de vereiste in het bestand zelf is ingebakken. U moet ofwel de manier waarop de binary wordt gebouwd aanpassen, ofwel de versie die u distribueert wijzigen.
Wat glibc-symboolversiebeheer daadwerkelijk doet
Elke functie die glibc exporteert, bevat een versietag. printf binnen libc.so.6 is in feite printf@@GLIBC_2.2.5. Wanneer glibc het gedrag of de ABI (application binary interface) van een functie wijzigt, wordt de oude versie niet vervangen. De oude code blijft behouden onder de oude tag en de nieuwe code wordt toegevoegd onder een nieuwe tag, waardoor één libc.so.6 meerdere versies van hetzelfde symbool tegelijkertijd bevat. Een binary uit 2009 vindt de tag uit 2009 nog steeds terug, wat de reden is waarom voorwaartse compatibiliteit zo goed werkt.
De link-stap legt vast wat er is gebruikt. Uw binary krijgt een .gnu.version_r-sectie die in feite zegt: "Ik heb GLIBC_2.38 uit libc.so.6 nodig". Een oudere libc bevatte die tag nooit, dus de loader stopt voordat main wordt uitgevoerd. Er is geen fallback, omdat een fallback zou betekenen dat het programma stilletjes een andere functie krijgt dan degene waar het tegenaan is gecompileerd.
De meest voorkomende aanleiding sinds 2021 is glibc 2.34. Die release voegde libpthread en libdl samen in libc en verplaatste __libc_start_main, de functie die elk C-programma start, naar de GLIBC_2.34-tag. Een hello-world-programma dat is gecompileerd op een distributie met glibc 2.34 of nieuwer, vereist daarom GLIBC_2.34, ook al roept de broncode niets moderns aan. Dit is de reden waarom het probleem plotseling verscheen voor mensen van wie de eigen code al jaren niet was gewijzigd.
Welke glibc-versie wordt door mijn distributie geleverd?
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
}
]Dit zijn de versies die elke distributie in de eigen repositories levert, zoals gepubliceerd door de distributies en actueel in augustus 2026. CentOS 7 is al lang "end of life", maar blijft op de lijst staan omdat overgenomen servers er nog steeds op draaien. Controleer elke server die u beheert met ldd --version, die de glibc-release op de eerste regel afdrukt, of met getconf GNU_LIBC_VERSION.
Lees deze 9 rijen als een ladder. Bouw op Debian 13 met glibc 2.41 en het resultaat draait op niets ouder dan Debian 13. Bouw op Ubuntu 20.04 met glibc 2.31 en dezelfde broncode draait op elke rij boven die lijn, inclusief de Debian 13-server. De oudste server die u moet ondersteunen is de enige build-host die ertoe doet; de distributie waarop u standaardiseert, bepaalt dus de ABI-ondergrens voor alles wat u daarvoor compileert. Het is de moeite waard om dit te beslissen voordat het serverpark bestaat, naast de andere afwegingen bij het kiezen van het besturingssysteem voor uw VPS.
Hoe bepaal ik de minimale glibc-versie die een binary vereist?
Lees dit uit het bestand. Dit werkt op elk bestand, inclusief een binary die door een leverancier is geleverd zonder build-notities.
objdump -T ./mytool | grep -o 'GLIBC_[0-9.]*' | sort -Vu | tail -5De laatste regel is de ondergrens. Als objdump ontbreekt, installeer dan het binutils-pakket. Als u op die machine niets kunt installeren, komt strings -a ./mytool | grep -o 'GLIBC_[0-9.]*' | sort -Vu | tail -5 in de buurt, omdat de tags als platte tekst in het bestand zijn opgeslagen.
readelf -V ./mytool toont dezelfde vereiste met structuur. Zoek naar het blok met de kop Version needs section '.gnu.version_r'. Dit bevat één Name: GLIBC_x.y-regel per tag, gegroepeerd onder de bibliotheek die deze moet leveren.
Om te achterhalen welke functie de ondergrens bepaalt, gebruikt u grep op de tag: objdump -T ./mytool | grep GLIBC_2.38. Dit is vaak een enkel symbool, en soms een symbool dat u kunt vermijden. Als objdump -T helemaal niets weergeeft, is de binary statisch gelinkt; deze heeft dan geen dynamische symbolentabel en geen versievereiste om weer te geven.
Wat file en readelf -d u vertellen over een binary die u heeft ontvangen
file geeft u de architectuur en het linktype in één regel.
file ./mytoolEen standaard glibc-build ziet er als volgt uit:
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 strippedEen statische build vermeldt statically linked en noemt geen interpreter. Een musl-build noemt een andere:
ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib/ld-musl-x86_64.so.1, ...De interpreter is het veld dat ertoe doet. Dit is de dynamic loader die de kernel uitvoert vóór uw programma, en dat exacte pad moet bestaan op de doelmachine. /lib/ld-musl-x86_64.so.1 is niet aanwezig op een Debian- of Ubuntu-server, tenzij iemand daar musl heeft geïnstalleerd.
readelf -d ./mytool somt de gedeelde bibliotheken op die de binary vereist, op naam:
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]Elk NEEDED-item is een harde vereiste, gekoppeld op basis van soname. libssl.so.3 is OpenSSL 3, dus die binary zal niet laden op een server die alleen libssl.so.1.1 heeft, en de soname is de reden: een andere soname betekent een bewust incompatibele ABI. RUNPATH vertelt de loader waar deze als eerste moet zoeken, en $ORIGIN expandeert naar de map die de binary bevat; dit is hoe een zelfstandig pakket zijn eigen bibliotheken vindt.
Voer ldd niet uit op een binary die u niet vertrouwt. Op glibc kan ldd afhankelijkheden oplossen door het programma onder de loader uit te voeren, waardoor een kwaadaardig bestand code kan uitvoeren. readelf en objdump lezen alleen bytes. Bevestig vóór dit alles dat de download het bestand is dat het project daadwerkelijk heeft gepubliceerd, want het verifiëren van een download aan de hand van de gepubliceerde checksum is de enige stap die u vertelt wiens bytes u in handen heeft.
De foutmeldingen die u daadwerkelijk zult zien, en wat ze betekenen
De loader noemt een GLIBC-versie die hij niet kan vinden. Het binaire bestand is gecompileerd tegen een nieuwere glibc dan op het doelsysteem aanwezig is. Niets dat op het doelsysteem wordt geïnstalleerd, lost dit veilig op. Kies een van de vier onderstaande opties.
De shell rapporteert No such file or directory voor een bestand dat u duidelijk kunt zien. Het ontbrekende onderdeel is de interpreter, niet het binaire bestand. execve geeft ENOENT terug wanneer het loader-pad in de ELF-header ontbreekt, en de shell geeft de enige melding die hij heeft. Voer file uit en lees het interpreter-pad. Een met musl gelinkt binair bestand op een server die alleen glibc ondersteunt, geeft precies dit symptoom.
cannot execute binary file: Exec format error betekent dat de architectuur onjuist is: een x86-64 binair bestand op een arm64-server, of andersom. De eerste regel van de file-uitvoer vertelt u met welke architectuur u te maken heeft.
error while loading shared libraries: libssl.so.3: cannot open shared object file betekent dat een NEEDED-bibliotheek afwezig is of aanwezig is onder een andere soname. Installeer het bijbehorende distributiepakket. Pakketnamen verschillen per familie, dus vertaal deze voordat u een apt-regel uit een README kopieert; de equivalenten voor dnf- en apt-commando's dekken die mapping.
Een kale Segmentation fault van een musl-build die prima draait onder glibc. Meestal betreft dit de stackgrootte voor threads, wat wordt behandeld onder optie 2.
Vier manieren om een portable Linux binary uit te brengen
Elke methode brengt specifieke kosten met zich mee. Maak uw keuze op basis van wat uw programma tijdens runtime doet, niet op basis van welke methode het meest elegant oogt.
Optie 1: bouwen op de oudste distributie die u ondersteunt
Dit is het minst spannende antwoord en meestal het juiste. Compileer binnen een container-image van de oudste distributie die u belooft te ondersteunen. De linker kan alleen tags vastleggen die de oude glibc daadwerkelijk bevat, waardoor de ondergrens daalt naar die release, terwijl het binaire bestand een normaal dynamisch binaire bestand blijft met al het gedrag van glibc intact.
docker run --rm -v "$PWD:/src" -w /src ubuntu:20.04 sh -c 'apt-get update && apt-get install -y build-essential && make'De output draait op glibc 2.31 en alles daarboven. Controleer dit in plaats van er vanuit te gaan: voer de objdump -T one-liner van eerder uit op het resultaat en controleer of de hoogste tag de verwachte tag is.
De kosten zitten in de toolchain. Een oude basis-image bevat ook een oude compiler, wat problemen geeft wanneer uw code een recente C++ standaard vereist. Go en Rust ontwijken dit grotendeels, omdat hun toolchains onafhankelijk van de distributiepakketten in de container worden geïnstalleerd. Voor C en C++ kunt u een nieuwere compiler toevoegen vanuit het eigen toolchain-kanaal van de distributie, of gebruikmaken van de manylinux-images. Deze bestaan specifiek om een verouderde glibc te combineren met een actuele GCC. De meegeleverde C-compiler van Zig kan ook direct een gekozen glibc als doel instellen, zoals in zig cc -target x86_64-linux-gnu.2.28, wat dezelfde ondergrens oplevert zonder dat u een oude image hoeft te behouden.
Optie 2: statisch linken tegen musl
musl is een compacte C-bibliotheek die is ontworpen met statische linking als uitgangspunt. Een statisch musl-binary bevat zijn eigen libc, benoemt geen interpreter en draait op elke Linux-kernel met de juiste architectuur. Dit is de methode waarop de meeste downloads van losse bestanden worden gebouwd.
sudo apt install -y musl-tools
musl-gcc -static -O2 hello.c -o hello
file ./hellofile zou nu statically linked moeten aangeven, zonder interpreter-veld. Voeg voor Rust de target toe en bouw hiertegen:
rustup target add x86_64-unknown-linux-musl
cargo build --release --target x86_64-unknown-linux-muslGo heeft dit niet nodig. Met CGO_ENABLED=0 go build is het resultaat al statisch en wordt er geen libc gelinkt.
NSS-lookups veranderen. glibc lost gebruikers en hostnamen op via NSS (name service switch), dat tijdens het draaien van het programma libnss_*-modules laadt met dlopen. Een statisch gelinkte glibc kan dit niet, en de linker waarschuwt u met de volgende woorden:
warning: Using 'getaddrinfo' in statically linked applications requires at runtime the shared libraries from the glibc version used for linkingmusl omzeilt deze waarschuwing door NSS simpelweg niet te implementeren. Het heeft een eigen resolver en leest /etc/resolv.conf en /etc/hosts direct uit. Op een standaard VPS is dit prima en eenvoudiger. Op een host waar accounts of namen afkomstig zijn van LDAP of SSSD, zal uw binary deze niet zien, terwijl elk ander programma op de server dat wel doet. De resolver van musl is bovendien jonger dan die van glibc: TCP-fallback voor DNS-antwoorden groter dan 512 bytes werd in 2023 toegevoegd in musl 1.2.4; builds tegen oudere musl-versies kappen grote antwoorden af.
dlopen werkt niet. In een statisch musl-binary is dlopen een stub die altijd faalt. Alles wat tijdens runtime code laadt, is uitgesloten: pluginsystemen, PAM-modules, GPU-drivers en de eigen iconv-tekensetmodules van glibc. Als uw programma dlopen nodig heeft, is statisch linken geen optie en moet u een van de andere drie oplossingen kiezen.
Beveiligingsupdates worden uw verantwoordelijkheid. Een dynamisch gelinkt binary ontvangt een libc-reparatie zodra de server een pakket-upgrade uitvoert. Een statisch binary doet dat nooit. Wanneer er een CVE (common vulnerabilities and exposures) verschijnt voor uw libc of een statisch gebundelde OpenSSL, moet u opnieuw bouwen en distribueren. Elke reeds geïmplementeerde kopie blijft kwetsbaar totdat iemand het bestand vervangt. Houd bij wat u heeft gelinkt, want op het doelsysteem is voor een beheerder niet te zien dat uw binary een bibliotheek van twee jaar oud bevat.
De licentie verandert. glibc is LGPL, en statisch linken activeert de verplichting tot relinking onder de LGPL: u moet ontvangers voorzien van wat zij nodig hebben om uw programma opnieuw te linken tegen een andere glibc. musl is MIT en kent een dergelijke voorwaarde niet. Dat is de voornaamste reden waarom projecten die binaries als losse bestanden distribueren, kiezen voor musl in plaats van statische glibc.
Twee runtime-verschillen veroorzaken verwarrende crashes. De standaard thread-stack van musl is 128 KiB, vergeleken met 8 MiB bij glibc. Code die een grote buffer op een thread-stack plaatst, veroorzaakt daarom een segfault zonder melding of logregel. Stel de grootte expliciet in met pthread_attr_setstacksize of verplaats de buffer naar de heap. De allocator van musl is bovendien geschreven voor een kleine omvang en voorspelbaar gedrag, in plaats van voor veel threads die tegelijkertijd geheugen toewijzen. Hierdoor kunnen programma's met veel threads en intensief geheugengebruik meetbaar trager zijn. Benchmark uw eigen workload in plaats van te vertrouwen op de reputatie van een van beide bibliotheken.
Optie 3: de loader en de libraries bundelen
Lever de libraries samen met de bijbehorende loader naast het binaire bestand en start het programma vervolgens via die loader.
./ld-linux-x86-64.so.2 --library-path ./lib ./mytoolOm dit permanent te maken, schrijft u de paden naar het bestand met patchelf:
patchelf --set-interpreter "$PWD/lib/ld-linux-x86-64.so.2" --set-rpath '$ORIGIN/lib' ./mytoolEén regel bepaalt of dit werkt: de loader en libc.so.6 moeten afkomstig zijn van dezelfde glibc-build. Het is een bij elkaar horend paar; het combineren van de loader van de host met een gebundelde libc leidt tot een crash tijdens het opstarten in plaats van een leesbare foutmelding. Bundel ze beide of bundel ze geen van beide.
AppImage is dit patroon in een pakketvorm, waarbij de payload in een squashfs-image zit en een kleine runtime deze mount. Het heeft één eigenschap die mensen vaak over het hoofd zien: een AppImage bundelt glibc niet. Daarom zal een AppImage die op een huidige distributie is gebouwd, op een oude server nog steeds falen met dezelfde versie-foutmelding. De richtlijn van AppImage zelf is om te bouwen op de oudste basis die u ondersteunt. Hierdoor is dit een distributieformaat dat bovenop optie 1 wordt toegepast, in plaats van een vervanging daarvan.
Op een headless server heeft AppImage FUSE (filesystem in userspace) nodig om de payload te mounten, en een minimale VPS-image bevat dit vaak niet. De foutmelding noemt libfuse.so.2. U kunt het mounten volledig overslaan met ./App.AppImage --appimage-extract-and-run, wat het pakket uitpakt naar een tijdelijke map en het vanaf daar uitvoert.
Optie 4: een container image distribueren
Verplaats de volledige userland samen met het programma. De image bevat zijn eigen libc, waardoor de glibc van de host niet langer relevant is en alleen de kernel en de architectuur overeen moeten komen. Dit is de minst complexe optie en het elimineert de gehele categorie problemen, wat de reden is dat veel serversoftware op deze manier wordt gedistribueerd. Docker draaien op een VPS is de gebruikelijke manier om dit te gebruiken.
De host-kernel stelt nog steeds een limiet. Nieuwere glibc-versies gebruiken nieuwere systeem-aanroepen, en een oude container-host kan deze blokkeren: glibc 2.34 en later gebruiken clone3, wat oudere standaard seccomp-profielen afwijzen. Het symptoom is een onmiddellijke foutmelding met Operation not permitted, zonder enige vermelding van glibc. Het upgraden van de container-runtime op de host lost dit op, omdat de blokkade zich in het syscall-filter van de runtime bevindt en niet in de kernel zelf.
De kosten zijn gebruikelijk. De doelomgeving heeft een container-runtime nodig en toestemming om deze te gebruiken. Uw download groeit van één bestand naar tientallen of honderden megabytes. U bent nu verantwoordelijk voor een base image en het bijbehorende patchschema, waardoor het beveiligingswerk dat u in optie 2 vermeed, terugkeert in de vorm van het opnieuw bouwen van images. Voor een langlopende service is dat een redelijke afweging. Voor een command-line tool die iemand eenmalig uitvoert, is dat niet het geval.
Welke optie moet u kiezen?
- Een interne tool voor servers die u beheert en die allemaal dezelfde distributie draaien: bouw dynamisch op die distributie en stop daar.
- Een enkel bestand dat vreemden downloaden en uitvoeren: gebruik statische musl, mits het programma geen
dlopenen geen NSS-gebaseerde opzoekingen nodig heeft. - Een programma met plugins of GPU-toegang: blijf dynamisch, bouw op een oude basis en gebruik optie 3 voor de levering.
- Een langlopende service op een machine die al over een runtime beschikt: lever de image.
Wat u ook kiest, leg het vast en controleer het. De glibc van de build-host maakt nu deel uit van uw releaseproces, en een build-machine die wordt geüpgraded van de ene LTS-release naar de volgende, verhoogt stilletjes de ondergrens en breekt gebruikers die vorige maand nog probleemloos werkten. Pin de build-image op tag en dwing de hoogste GLIBC_-tag in de output af als build-stap, zodat de controle faalt in uw pipeline in plaats van in de terminal van iemand anders.
FAQ
Waarom krijg ik de melding "version GLIBC_2.38 not found" op mijn server?
Het binaire bestand is gecompileerd op een machine met een nieuwere glibc dan op uw server aanwezig is. De symboolversiebeheer van glibc biedt alleen voorwaartse compatibiliteit: oude binaire bestanden draaien op een nieuwe glibc, maar nieuwe binaire bestanden draaien niet op een oude glibc. De loader vereist de exacte versietag die in het bestand is vastgelegd, en een oudere libc bevat die tag niet. Niets wat u op de server installeert, lost dit op een veilige manier op. Compileer opnieuw op een oudere basis, lever een statische musl-build, bundel de bibliotheken met de bijbehorende loader, of lever een container-image.
Hoe achterhaal ik welke glibc-versie een binair bestand vereist?
Lees de versietags uit het bestand met objdump -T ./mytool | grep -o 'GLIBC_[0-9.]*' | sort -Vu | tail -5. De laatste regel toont de laagste glibc-versie die het bestand kan laden. readelf -V ./mytool toont dezelfde vereisten in de sectie .gnu.version_r, gegroepeerd per bibliotheek die deze moet leveren. Als objdump -T niets uitvoert, is het binaire bestand statisch en heeft het geen glibc-vereisten. Vergelijk het resultaat met de versie van uw eigen server via ldd --version.
Is een musl-binair bestand trager dan een glibc-binair bestand?
Dit hangt af van de werklast; het eerlijke antwoord is: meten is weten. Een statisch musl-binair bestand start sneller op, omdat er geen loader uitgevoerd hoeft te worden en er geen relocatiewerkzaamheden nodig zijn bij het uitvoeren. Daartegenover staat dat de allocator van musl is gebouwd voor een kleine omvang en voorspelbaar gedrag, in plaats van voor situaties waarin veel threads tegelijkertijd geheugen toewijzen. Bovendien bevat musl minder handgeoptimaliseerde string- en geheugenroutines dan glibc, waardoor programma's die intensief gebruikmaken van geheugentoewijzing of string-operaties merkbaar trager kunnen zijn. Benchmark uw eigen programma op uw eigen server voordat u een van beide claims aanneemt.
Waarom geeft bash "No such file or directory" voor een bestand dat wel bestaat?
Het ontbrekende bestand is de dynamische loader, niet uw binaire bestand. De kernel leest het pad van de interpreter uit de ELF-header en geeft ENOENT terug wanneer dat pad ontbreekt; de shell rapporteert dit met de enige foutmelding die beschikbaar is. Voer file ./mytool uit en lees het veld interpreter. Als hier /lib/ld-musl-x86_64.so.1 staat op een Debian- of Ubuntu-server, heeft u een met musl gelinkt binair bestand op een glibc-systeem. U heeft dan de musl-compatibele download of een statische build nodig.
Kan ik libc.so.6 kopiëren van een nieuwere server om dit op te lossen?
Nee. libc.so.6 en ld-linux-x86-64.so.2 vormen een bij elkaar horend paar uit één glibc-build. Elk proces op de machine maakt hier gebruik van. Het overschrijven van de systeemkopie kan ertoe leiden dat de server niets meer kan uitvoeren, inclusief de tools die u nodig heeft om de wijziging ongedaan te maken. Als u per se één nieuwer binair bestand op een oude host moet draaien, pak dan de nieuwere glibc uit in een eigen map en start dat programma via de bijbehorende loader met --library-path; dit heeft alleen invloed op dat specifieke proces. Opnieuw compileren tegen de oudere glibc blijft de oplossing die u over zes maanden niet voor verrassingen stelt.