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 faz a implementação.
O erro indica a versão exata necessária:
./mytool: /lib/x86_64-linux-gnu/libc.so.6: version `GLIBC_2.38' not found (required by ./mytool)Nada está corrompido e nada está mal configurado. O carregador dinâmico leu uma etiqueta de versão registada no binário, procurou essa etiqueta na libc do sistema, 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?
The data behind this chart
[
{
"distro": "CentOS 7",
"glibc": 2.17
},
{
"distro": "RHEL 8",
"glibc": 2.28
},
{
"distro": "Ubuntu 20.04",
"glibc": 2.31
},
{
"distro": "Debian 11",
"glibc": 2.31
},
{
"distro": "RHEL 9",
"glibc": 2.34
},
{
"distro": "Ubuntu 22.04",
"glibc": 2.35
},
{
"distro": "Debian 12",
"glibc": 2.36
},
{
"distro": "Ubuntu 24.04",
"glibc": 2.39
},
{
"distro": "Debian 13",
"glibc": 2.41
}
]Estas 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 relevante. É o carregador dinâmico que o kernel executa antes do 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 sistema.
readelf -d ./mytool lista, pelo 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 é o motivo: um soname diferente indica uma ABI deliberadamente incompatível. RUNPATH informa o carregador sobre onde procurar primeiro, e $ORIGIN é expandido para o diretório que contém o binário. É assim que um pacote autónomo encontra as suas 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, permitindo que um ficheiro malicioso execute código. readelf e objdump apenas leem bytes. Antes de tudo isto, confirme que a transferência corresponde ao ficheiro efetivamente publicado pelo projeto, porque verificar uma transferência com o checksum publicado é o único passo que informa de quem são os bytes que tem em mãos.
Os erros que você verá na prática e o que cada um significa
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 disponível no sistema de destino. Nada instalado no destino corrige isto com segurança. Escolha uma das quatro opções abaixo.
A shell mostra 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 não existe no cabeçalho ELF, e a shell mostra a única mensagem que tem disponível. Execute file e leia o caminho do interpretador. Um binário ligado a musl num servidor que só tem glibc apresenta exatamente este sintoma.
cannot execute binary file: Exec format error significa que a arquitetura está errada: um binário x86-64 num servidor arm64, ou o contrário. A primeira linha da saída de file indica qual das duas situações se aplica.
error while loading shared libraries: libssl.so.3: cannot open shared object file significa que falta uma biblioteca NEEDED ou que ela está disponível com outro soname. Instale o pacote correspondente da distribuição. Os nomes dos pacotes diferem entre famílias, por isso faça a tradução antes de copiar uma linha apt de um README, e os equivalentes dos comandos dnf e apt mostram esse mapeamento.
Um Segmentation fault isolado num build 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: fazer a ligação estática com musl
musl é uma biblioteca C pequena, desenvolvida a pensar na ligação estática. Um binário musl estático contém a sua própria libc, não especifica nenhum interpretador e é executado em qualquer kernel Linux da arquitetura correta. É assim que são compilados muitos downloads de ferramentas num único ficheiro.
sudo apt install -y musl-tools
musl-gcc -static -O2 hello.c -o hello
file ./hellofile deve agora mostrar statically linked, sem campo de interpretador. Em Rust, adicione o destino 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 enquanto o programa é executado. Uma glibc ligada estaticamente não consegue fazer isso, e o linker avisa com estas palavras:
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 comum, isto é 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 da musl também é mais recente que o da glibc: o fallback para TCP em respostas DNS com mais de 512 bytes chegou à musl 1.2.4 em 2023, pelo que compilações com versões mais antigas da musl truncam respostas grandes.
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, controladores de GPU e os próprios módulos iconv de conjuntos de caracteres da glibc. Se o seu programa precisar de dlopen, a ligação estática está fora de questão e terá de usar 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 executa a atualização dos pacotes. Um binário estático nunca a recebe. Quando surge uma entrada CVE (common vulnerabilities and exposures) contra a sua libc ou contra uma versão estática do OpenSSL que tenha incluído, terá de recompilar e redistribuir o binário. Cada cópia já instalada continuará vulnerável 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 um único ficheiro contém uma biblioteca com dois anos.
A licença também muda. glibc está sob a 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 ligarem novamente o seu programa a uma glibc diferente. musl está sob a licença MIT e não impõe essa condição. Essa é a principal razão por que os projetos que distribuem binários de um único ficheiro escolhem musl em vez de glibc estática.
Duas diferenças de execução produzem falhas difíceis de diagnosticar. A stack de threads predefinida da musl tem 128 KiB, contra 8 MiB na glibc. Por isso, o código que coloca um buffer grande na stack de uma thread termina com um segfault sem mensagem nem linha no log. Defina explicitamente o tamanho com pthread_attr_setstacksize ou mova o buffer para a heap. O allocator da musl também foi desenvolvido para tamanhos pequenos e comportamento previsível, e não para muitas threads fazerem alocações simultaneamente. Por isso, programas com muitas alocações em threads podem ser visivelmente mais lentos. Faça um 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
Coloque as bibliotecas junto ao 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 isto 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 de glibc. Formam um par compatível, e misturar o loader do host com uma libc incluída no pacote provoca um crash 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 payload numa imagem squashfs e um pequeno runtime que a monta. Há uma propriedade que muitas pessoas ignoram: um AppImage não inclui glibc. Por isso, um AppImage compilado numa distribuição atual continua a falhar com o mesmo erro de versão num servidor antigo. As próprias orientações do AppImage recomendam compilar na base mais antiga que pretende suportar. Isto torna o AppImage um formato de distribuição construído sobre a opção 1, e não um substituto para ela.
Num servidor headless, o AppImage precisa de FUSE (filesystem in userspace) para montar o payload, e uma imagem VPS mínima muitas vezes não o inclui. A falha identifica libfuse.so.2. Pode ignorar completamente a montagem com ./App.AppImage --appimage-extract-and-run. Essa opção extrai o conteúdo para um diretório temporário e executa-o 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 de glibc mais recente do que a disponível no seu servidor. O versionamento de símbolos da glibc fornece compatibilidade apenas para versões anteriores: binários antigos funcionam com uma glibc nova, mas binários novos não funcionam com uma glibc antiga, porque o loader precisa da tag de versão exata registada no ficheiro e a libc antiga nunca teve essa tag. Nada que instale no servidor corrige este problema com segurança. Recompile com uma base mais antiga, distribua uma compilação estática com musl, inclua as bibliotecas com o loader correspondente ou distribua uma imagem de contentor.
Como descubro de que versão da glibc um binário precisa?
Leia as tags 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 da glibc que pode carregá-lo. readelf -V ./mytool mostra os mesmos requisitos na secção .gnu.version_r, agrupados pela biblioteca que deve fornecê-los. 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 do próprio servidor, obtida com ldd --version.
Um binário com musl é mais lento do que um binário com glibc?
Depende da carga de trabalho, e a resposta correta é medir. Um binário estático com musl inicia mais depressa porque não há loader para executar nem trabalho de relocação no momento da execução. Em contrapartida, o allocator da musl foi concebido para ocupar pouco espaço e ter um comportamento previsível, não para muitos threads alocarem memória ao mesmo tempo. A musl também inclui menos rotinas de strings e memória otimizadas manualmente do que a glibc. Por isso, programas com muitos threads e muitas alocações ou operações sobre strings podem ser visivelmente mais lentos. Faça benchmark do seu próprio programa no seu próprio servidor antes de aceitar qualquer uma das afirmações.
Por que o bash diz "No such file or directory" para um ficheiro que existe?
O ficheiro em falta é o loader dinâmico, não o seu binário. O kernel lê o caminho do interpretador no cabeçalho ELF e devolve ENOENT quando esse caminho não existe. O shell apresenta-o com a única mensagem que tem disponível. Execute file ./mytool e leia o campo interpreter. Se apresentar /lib/ld-musl-x86_64.so.1 num servidor Debian ou Ubuntu, tem um binário ligado à musl num sistema com glibc. Precisa da versão compatível com musl ou de uma compilação estática.
Posso copiar libc.so.6 de um servidor mais recente para corrigir este problema?
Não. libc.so.6 e ld-linux-x86-64.so.2 formam um par correspondente da mesma compilação da glibc, e todos os processos da máquina utilizam-nos. Substituir a cópia do sistema pode impedir o servidor de executar qualquer programa, incluindo as ferramentas necessárias para desfazer a alteração. Se tiver de executar um binário mais recente num host antigo, extraia a glibc mais recente para um diretório privado e inicie esse programa através do próprio loader com --library-path. Isto afeta apenas esse processo. Recompilar com a glibc antiga continua a ser a opção que não lhe causará surpresas seis meses mais tarde.