Por que o binário Linux não roda: glibc ou musl?
O erro GLIBC_2.34 not found revela o requisito gravado no binário. Veja como identificar a versão exigida e escolha uma das quatro formas de distribuí-lo.
Por que um binário Linux não é executado num servidor mais antigo
É difícil produzir um binário Linux portátil porque a glibc, a biblioteca GNU C, garante compatibilidade apenas numa direção. Um binário antigo continua a ser executado numa glibc nova. Um binário novo não é executado numa glibc antiga. A máquina onde compila define o requisito mínimo de todas as máquinas onde fará a implementação.
O erro indica a versão exata de que precisava:
./mytool: /lib/x86_64-linux-gnu/libc.so.6: version `GLIBC_2.38' not found (required by ./mytool)Nada está corrompido e, normalmente, nada está configurado incorretamente. A única exceção é uma definição LD_LIBRARY_PATH que encaminha o carregador para uma libc mais antiga. O caminho apresentado na mensagem mostra qual libc foi efetivamente lida. O carregador dinâmico leu uma etiqueta de versão registada no binário, procurou essa etiqueta nessa libc, não a encontrou e recusou iniciar o programa. Transferir novamente o ficheiro e executar chmod +x não altera nada, porque o requisito está gravado no próprio ficheiro. Tem de alterar a forma como o binário é compilado ou alterar o que distribui.
O que o versionamento de símbolos da glibc realmente faz
Cada função exportada pela glibc tem uma etiqueta de versão. printf dentro de libc.so.6 é, na realidade, printf@@GLIBC_2.2.5. Quando a glibc altera o comportamento ou a ABI (interface binária de aplicação) de uma função, não substitui a versão antiga. Mantém o código antigo sob a etiqueta antiga e adiciona o código novo sob uma etiqueta nova. Assim, uma única libc.so.6 contém várias versões do mesmo símbolo ao mesmo tempo. Um binário de 2009 encontra a etiqueta de 2009 ainda disponível. É por isso que a compatibilidade futura funciona tão bem.
A etapa de ligação regista o que foi utilizado. O binário recebe uma secção .gnu.version_r que diz, na prática, "preciso de GLIBC_2.38 de libc.so.6". Uma libc mais antiga nunca teve essa etiqueta. Por isso, o carregador para antes de main ser executada. Não existe fallback, porque isso faria o programa receber silenciosamente uma função diferente daquela contra a qual foi compilado.
O acionador mais comum desde 2021 é a glibc 2.34. Essa versão juntou libpthread e libdl em libc e transferiu __libc_start_main, a função que inicia todos os programas C, para a etiqueta GLIBC_2.34. Por isso, um hello-world compilado numa distribuição com glibc 2.34 ou mais recente passa a exigir GLIBC_2.34, mesmo que o código-fonte não chame nada moderno. É por isso que o problema surgiu subitamente para pessoas cujo próprio código não tinha sido alterado há anos.
Qual versão da glibc a minha distribuição fornece?
Os dados por trás deste gráfico
[
{
"distro": "CentOS 7",
"glibc": 2.17
},
{
"distro": "RHEL 8",
"glibc": 2.28
},
{
"distro": "Ubuntu 20.04",
"glibc": 2.31
},
{
"distro": "Debian 11",
"glibc": 2.31
},
{
"distro": "RHEL 9",
"glibc": 2.34
},
{
"distro": "Ubuntu 22.04",
"glibc": 2.35
},
{
"distro": "Debian 12",
"glibc": 2.36
},
{
"distro": "Ubuntu 24.04",
"glibc": 2.39
},
{
"distro": "Debian 13",
"glibc": 2.41
}
]Estas são as versões que cada distribuição fornece nos seus próprios repositórios, conforme publicadas pelas distribuições e válidas em agosto de 2026. O CentOS 7 já ultrapassou há muito o fim da vida útil, mas continua na lista porque ainda existem servidores herdados que o executam. Verifique qualquer sistema que tenha com ldd --version, que imprime a versão da glibc na primeira linha, ou com getconf GNU_LIBC_VERSION.
Leia estas 9 linhas como uma escala. Compile no Debian 13 com a glibc 2.41 e o resultado não será executado em nada mais antigo que o Debian 13. Compile no Ubuntu 20.04 com a glibc 2.31 e o mesmo código-fonte será executado em todas as linhas acima dessa linha, incluindo o sistema Debian 13. O servidor mais antigo que tem de suportar é o único host de compilação que importa. Por isso, a distribuição que padronizar define o limite inferior da ABI para tudo o que compilar para ela. Vale a pena decidir isso antes de a frota existir, juntamente com os outros compromissos envolvidos em escolher qual SO executar no seu VPS.
Como encontro a versão mínima da glibc de que um binário precisa?
Leia essa informação diretamente do ficheiro. Isto funciona em qualquer binário, incluindo um que um fornecedor lhe entregou sem notas de compilação.
objdump -T ./mytool | grep -o 'GLIBC_[0-9.]*' | sort -Vu | tail -5A última linha indica o requisito mínimo. Se objdump não estiver instalado, instale o pacote binutils. Se não puder instalar nada nesse servidor, strings -a ./mytool | grep -o 'GLIBC_[0-9.]*' | sort -Vu | tail -5 é uma alternativa aproximada, porque as etiquetas estão armazenadas como cadeias de texto simples dentro do ficheiro.
readelf -V ./mytool mostra o mesmo requisito de forma estruturada. Procure o bloco com o cabeçalho Version needs section '.gnu.version_r'. Ele lista uma linha Name: GLIBC_x.y por etiqueta, agrupada sob a biblioteca que tem de a fornecer.
Para descobrir que função definiu o requisito mínimo, procure a etiqueta com grep: objdump -T ./mytool | grep GLIBC_2.38. Muitas vezes, trata-se de um único símbolo que pode, em alguns casos, ser evitado. Se objdump -T não apresentar qualquer resultado, o binário está ligado estaticamente. Nesse caso, não tem uma tabela de símbolos dinâmica nem um requisito de versão para apresentar.
O que file e readelf -d informam sobre um binário que alguém lhe entregou
file mostra a arquitetura e o tipo de ligação numa única linha.
file ./mytoolUma compilação normal com glibc aparece assim:
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 strippedUma compilação estática indica statically linked e não especifica nenhum interpretador. Uma compilação com musl especifica outro:
ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib/ld-musl-x86_64.so.1, ...O interpretador é o campo importante. É o carregador dinâmico que o kernel executa antes do seu programa, e esse caminho exato tem de existir na máquina de destino. /lib/ld-musl-x86_64.so.1 não está presente num servidor Debian ou Ubuntu, a menos que alguém tenha instalado musl nesse servidor.
readelf -d ./mytool lista, por nome, as bibliotecas partilhadas exigidas pelo binário:
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 é um requisito obrigatório, correspondido pelo soname. libssl.so.3 é o OpenSSL 3, portanto esse binário não será carregado num servidor que tenha apenas libssl.so.1.1. O soname é a razão: um soname diferente indica uma ABI deliberadamente incompatível. RUNPATH adiciona diretórios que o carregador pesquisa para encontrar as dependências diretas do binário, depois de qualquer definição de LD_LIBRARY_PATH, mas antes da cache de bibliotecas do sistema e dos caminhos predefinidos. $ORIGIN é expandido para o diretório que contém o binário. É assim que um pacote autónomo encontra as próprias bibliotecas.
Não execute ldd num binário em que não confia. Com glibc, ldd pode resolver dependências executando o programa através do carregador. Assim, um ficheiro hostil pode executar código. readelf e objdump apenas leem bytes. Antes de tudo isto, confirme que a transferência corresponde ao ficheiro realmente publicado pelo projeto, porque verificar uma transferência com o checksum publicado é o único passo que lhe diz quem produziu os bytes que tem em mãos.
Os erros que verá na prática e o significado de cada um
O loader indica uma versão de GLIBC que não consegue encontrar. O binário foi compilado com uma versão de glibc mais recente do que a existente no destino. Nada do que instalar no destino corrige o problema de forma segura. Escolha uma das quatro opções abaixo.
A shell apresenta No such file or directory para um ficheiro que está claramente visível. O que falta é o interpretador, não o binário. execve devolve ENOENT quando o caminho do loader está ausente no cabeçalho ELF, e a shell apresenta a única mensagem que tem disponível. Execute file e leia o caminho do interpretador. Um binário ligado a musl num servidor apenas com glibc produz exatamente este sintoma.
cannot execute binary file: Exec format error normalmente significa que a arquitetura está errada: um binário x86-64 num servidor arm64, ou o inverso. Também pode significar que o ficheiro não é um executável válido, por exemplo, devido a uma transferência truncada ou danificada. A primeira linha da saída de file indica a arquitetura do ficheiro ou informa que o ficheiro não é um executável ELF utilizável.
error while loading shared libraries: libssl.so.3: cannot open shared object file significa que falta uma biblioteca NEEDED ou que ela está presente com um soname diferente. Instale o pacote correspondente da distribuição. Os nomes dos pacotes variam entre famílias, por isso traduza antes de copiar uma linha apt de um README, e os equivalentes dos comandos dnf e apt explicam esse mapeamento.
Um Segmentation fault isolado proveniente de uma compilação com musl que funciona corretamente com glibc. Normalmente está relacionado com o tamanho da stack das threads, abordado na opção 2.
Quatro formas de distribuir um binário Linux portátil
Cada opção tem um custo real. Escolha com base no que o seu programa faz durante a execução, não na opção que parece mais simples.
Opção 1: compilar na distribuição mais antiga que suporta
A resposta menos interessante e, normalmente, a correta. Compile dentro de uma imagem de contentor da distribuição mais antiga que promete suportar. O linker só pode registar tags que essa versão antiga da glibc realmente tenha. Assim, o limite mínimo fica nessa versão, enquanto o binário continua a ser um binário dinâmico normal, com todo o comportamento da 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'A saída é executada na glibc 2.31 e em todas as versões posteriores. Verifique em vez de presumir: execute a linha única objdump -T apresentada anteriormente no resultado e confirme que a tag mais alta é a esperada.
O custo está na toolchain. Uma imagem base antiga também inclui um compilador antigo, o que causa problemas quando o código precisa de um padrão C++ recente. Go e Rust evitam este problema na maioria dos casos, porque as respetivas toolchains são instaladas no contentor de forma independente dos pacotes da distribuição. Para C e C++, pode adicionar um compilador mais recente a partir do canal de toolchains da própria distribuição ou utilizar uma das imagens manylinux, que existem precisamente para combinar uma glibc antiga com um GCC atual. O compilador C incluído no Zig também pode direcionar a compilação para uma glibc escolhida diretamente, como em zig cc -target x86_64-linux-gnu.2.28. Isso fornece o mesmo limite mínimo sem manter uma imagem antiga.
Opção 2: ligação estática com musl
musl é uma pequena biblioteca C concebida para a ligação estática. Um binário musl estático contém a sua própria libc, não referencia nenhum interpretador e é executado em qualquer kernel Linux com a arquitetura correta. É assim que são compilados muitos downloads de ferramentas de ficheiro único.
sudo apt install -y musl-tools
musl-gcc -static -O2 hello.c -o hello
file ./hellofile deve agora indicar statically linked, sem campo de interpretador. Para Rust, adicione o target e compile para ele:
rustup target add x86_64-unknown-linux-musl
cargo build --release --target x86_64-unknown-linux-muslGo não precisa de nada disto. Com CGO_ENABLED=0 go build, o resultado já é estático e não faz ligação com nenhuma libc.
As consultas NSS mudam. glibc resolve utilizadores e nomes de hosts através de NSS (name service switch), que carrega módulos libnss_* com dlopen durante a execução do programa. Uma glibc ligada estaticamente não consegue fazer isso, e o linker apresenta este aviso:
warning: Using 'getaddrinfo' in statically linked applications requires at runtime the shared libraries from the glibc version used for linkingmusl evita o aviso porque não implementa NSS. Tem o seu próprio resolver e lê /etc/resolv.conf e /etc/hosts diretamente. Num VPS normal, isso é suficiente e mais simples. Num host onde as contas ou os nomes vêm de LDAP ou SSSD, o seu binário não os verá, embora todos os outros programas do sistema os vejam. O resolver de musl também é mais recente do que o de glibc: o fallback para TCP quando as respostas DNS são demasiado grandes para uma resposta UDP chegou ao musl 1.2.4 em 2023. Compilações feitas com versões mais antigas de musl não conseguem consultar registos DNS grandes, e as consultas podem falhar contra nameservers recursivos que não devolvem resultados parciais numa resposta UDP truncada.
dlopen não funciona. Num binário musl estático, dlopen é um stub que falha sempre. Tudo o que carregue código durante a execução fica excluído: sistemas de plugins, módulos PAM, drivers de GPU e os próprios módulos iconv de conjuntos de caracteres do glibc. Se o seu programa precisar de dlopen, a ligação estática está fora de questão e precisa de uma das outras três opções.
As atualizações de segurança passam a ser da sua responsabilidade. Um binário ligado dinamicamente recebe uma correção da libc assim que o servidor instala a atualização do pacote. Um binário estático nunca recebe essa correção. Quando surge uma entrada de CVE (common vulnerabilities and exposures) contra a sua libc ou contra um OpenSSL estático incluído por si, tem de recompilar e redistribuir o binário. Todas as cópias já implementadas continuam vulneráveis até alguém substituir o ficheiro. Mantenha um registo do que foi incluído na ligação, porque nada no destino consegue informar um administrador de que o seu binário de ficheiro único contém uma biblioteca de há dois anos.
A licença também muda. glibc está sob LGPL, e a ligação estática ativa a obrigação de relink da LGPL: tem de fornecer aos destinatários o necessário para fazerem o relink do seu programa contra uma glibc diferente. musl está sob MIT e não impõe essa condição. Essa é a principal razão por que os projetos que distribuem binários de ficheiro único escolhem musl em vez de glibc estático.
Duas diferenças de execução produzem falhas difíceis de interpretar. A stack de threads predefinida do musl tem 128 KiB. glibc obtém o valor predefinido do limite de stack com que o programa foi iniciado. Normalmente, esse valor fica entre 2 e 10 MB. Quando o limite é ilimitado, a maioria das arquiteturas usa 2 MB. Por isso, código que coloca um buffer grande na stack de uma thread pode terminar com um segfault em musl, sem mensagem nem linha no log. Defina explicitamente o tamanho com pthread_attr_setstacksize ou mova o buffer para a heap. O allocator do musl também foi concebido para tamanhos pequenos e comportamento previsível, e não para muitas threads fazerem alocações em simultâneo. Por isso, programas com muitas alocações em várias threads podem ser significativamente mais lentos. Faça benchmark da sua própria carga de trabalho, em vez de confiar na reputação de qualquer uma das bibliotecas.
Opção 3: incluir o loader e as bibliotecas
Inclua as bibliotecas junto do binário, juntamente com o loader correspondente, e inicie o programa através desse loader.
./ld-linux-x86-64.so.2 --library-path ./lib ./mytoolPara tornar a alteração permanente, escreva os caminhos no ficheiro com patchelf:
patchelf --set-interpreter "$PWD/lib/ld-linux-x86-64.so.2" --set-rpath '$ORIGIN/lib' ./mytoolUma regra determina se isto funciona: o loader e libc.so.6 têm de provir da mesma compilação do glibc. Formam um par compatível. Misturar o loader do sistema anfitrião com uma libc incluída no pacote provoca uma falha durante o arranque, em vez de apresentar um erro legível. Inclua ambos ou não inclua nenhum.
AppImage é este padrão empacotado, com o conteúdo numa imagem squashfs e um pequeno runtime que a monta. Há uma propriedade que muitas pessoas ignoram: a maioria dos AppImages usa o glibc do sistema anfitrião em vez de o incluir. Por isso, um AppImage criado dessa forma numa distribuição atual continua a falhar com o mesmo erro de versão num servidor antigo. A orientação de boas práticas do AppImage recomenda criar o pacote numa base que não seja mais recente do que o sistema mais antigo suportado. Assim, esse AppImage funciona como um formato de distribuição assente na opção 1, e não como substituto dela. Também é possível incluir o glibc: appimage-builder e go-appimage podem incluir o glibc dentro do AppImage. Um AppImage criado dessa forma pode ser executado em distribuições mais antigas ou mais recentes do que o sistema de compilação. O custo é um ficheiro maior e bibliotecas como libssl ficam congeladas no pacote.
Num servidor sem interface gráfica, o AppImage precisa de FUSE (filesystem in userspace) para montar o conteúdo. Uma imagem VPS mínima muitas vezes não o inclui. Os AppImages criados com um runtime mais antigo precisam da biblioteca FUSE 2 no sistema anfitrião. Em Debian e Ubuntu, essa biblioteca é fornecida como libfuse2. No Ubuntu 24.04, foi renomeada para libfuse2t64. Quando está em falta, o AppImage termina com AppImages require FUSE to run.. Os AppImages mais recentes têm suporte para FUSE 3 integrado e já não precisam de libfuse instalada no sistema anfitrião. Pode ignorar completamente a montagem com ./App.AppImage --appimage-extract-and-run. Nesse modo, o conteúdo é extraído para um diretório temporário e o programa é executado a partir daí.
Opção 4: distribuir uma imagem de contentor
Transporte todo o userland juntamente com o programa. A imagem inclui a sua própria libc, por isso a glibc do host deixa de ser relevante. Apenas o kernel e a arquitetura têm de ser compatíveis. Esta é a opção menos sofisticada e elimina toda esta classe de problemas. Por isso, muito software de servidor é distribuído desta forma. Executar Docker numa VPS é a forma habitual de o utilizar.
O kernel do host continua a impor um limite. Versões mais recentes da glibc usam chamadas de sistema mais recentes, e um host de contentores antigo pode bloqueá-las: a glibc 2.34 e posteriores usam clone3, que os perfis seccomp predefinidos mais antigos rejeitam. O sintoma é uma falha imediata com Operation not permitted, sem qualquer referência à glibc. Atualizar o runtime de contentores no host corrige o problema, porque o bloqueio está no filtro de chamadas de sistema do runtime, não no kernel.
Os custos são normais. O destino precisa de um runtime de contentores e de permissões para o utilizar. O download aumenta de um ficheiro para dezenas ou centenas de megabytes. Passa a ser responsável por uma imagem base e pelo respetivo calendário de atualizações, pelo que o trabalho de segurança evitado na opção 2 regressa sob a forma de reconstruções da imagem. Para um serviço de longa duração, é uma troca razoável. Para uma ferramenta de linha de comandos que alguém executa uma vez, não é.
Que opção deve escolher?
- Uma ferramenta interna para servidores que controla e que usam todos a mesma distribuição: compile dinamicamente nessa distribuição e termine aí.
- Um único ficheiro que pessoas desconhecidas descarregam e executam: use musl estático, se o programa não precisar de
dlopennem de pesquisas baseadas em NSS. - Um programa com plugins ou acesso à GPU: mantenha-o dinâmico, compile numa base antiga e use a opção 3 para o distribuir.
- Um serviço de longa duração numa máquina que já tem um runtime: distribua a imagem.
Seja qual for a escolha, registe-a e verifique-a. A glibc do host de compilação passa a fazer parte do processo de lançamento, e uma máquina de compilação atualizada de uma versão LTS para a seguinte aumenta silenciosamente o requisito mínimo e interrompe o funcionamento para utilizadores que ainda funcionavam no mês passado. Fixe a imagem de compilação por tag e valide a tag GLIBC_ mais alta na saída como uma etapa da compilação, para que a verificação falhe no seu pipeline e não no terminal de outra pessoa.
FAQ
Por que recebo "version GLIBC_2.38 not found" no meu servidor?
O binário foi compilado numa máquina com uma versão do glibc mais recente do que a disponível no seu servidor. O versionamento de símbolos do glibc fornece apenas compatibilidade para a frente: binários antigos funcionam com versões novas do glibc, mas binários novos não funcionam com versões antigas, porque o carregador precisa da etiqueta de versão exata registada no ficheiro e uma versão antiga do libc nunca teve essa etiqueta. Nada que instale no servidor corrige o problema de forma segura. Recompile numa base mais antiga, disponibilize uma compilação estática com musl, inclua as bibliotecas com o carregador correspondente ou disponibilize uma imagem de contentor.
Como descubro de que versão do glibc um binário precisa?
Leia as etiquetas de versão do ficheiro com objdump -T ./mytool | grep -o 'GLIBC_[0-9.]*' | sort -Vu | tail -5. A última linha indica a versão mínima do glibc que pode carregá-lo. readelf -V ./mytool mostra os mesmos requisitos na secção .gnu.version_r, agrupados pela biblioteca que os deve fornecer. Se objdump -T não apresentar nada, o binário é estático e não tem qualquer requisito de glibc. Compare o resultado com a versão existente no próprio servidor, obtida com ldd --version.
Um binário musl é mais lento do que um binário glibc?
Depende da carga de trabalho. A resposta correta é medir. Um binário musl estático inicia mais depressa, porque não há carregador para executar nem trabalho de relocação no momento do exec. Por outro lado, o alocador do musl foi concebido para ocupar pouco espaço e ter um comportamento previsível, e não para muitas threads fazerem alocações ao mesmo tempo. O musl também tem menos rotinas de strings e memória otimizadas manualmente do que o glibc. Por isso, programas com muitas alocações ou operações sobre strings em várias threads podem ser claramente mais lentos. Faça um benchmark do seu próprio programa no seu próprio servidor antes de aceitar qualquer uma dessas afirmações.
Por que o bash diz "No such file or directory" para um ficheiro que existe?
O ficheiro em falta é o carregador dinâmico, não o seu binário. O kernel lê o caminho do interpretador a partir do cabeçalho ELF e devolve ENOENT quando esse caminho não existe. A shell apresenta essa mensagem porque é a única que tem disponível. Execute file ./mytool e leia o campo interpreter. Se indicar /lib/ld-musl-x86_64.so.1 num servidor Debian ou Ubuntu, tem um binário ligado ao musl num sistema glibc. Instale o runtime musl da distribuição, o pacote musl no Debian e no Ubuntu, ou obtenha uma compilação glibc ou estática do programa.
Posso copiar libc.so.6 de um servidor mais recente para corrigir isto?
Não. libc.so.6 e ld-linux-x86-64.so.2 formam um par correspondente da mesma compilação do glibc. Todos os programas glibc ligados dinamicamente na máquina utilizam-nos. Substituir a cópia do sistema pode impedir o servidor de executar quase tudo, incluindo as ferramentas necessárias para desfazer a alteração. Se tiver de executar um binário mais recente num host antigo, extraia o glibc mais recente para um diretório privado e inicie o programa através do próprio carregador com --library-path. Isto afeta apenas esse processo. Recompilar com a versão antiga do glibc continua a ser a solução que não lhe causará surpresas seis meses mais tarde.