Vinculando libstdc ++ estaticamente: alguma pegadinha?

93

Eu preciso implantar um aplicativo C ++ construído no Ubuntu 12.10 com libstdc ++ do GCC 4.7 para sistemas que executam o Ubuntu 10.04, que vem com uma versão consideravelmente mais antiga do libstdc ++.

Atualmente, estou compilando com -static-libstdc++ -static-libgcc, como sugerido por esta postagem do blog: Linking libstdc ++ estaticamente . O autor adverte contra o uso de qualquer código C ++ carregado dinamicamente ao compilar libstdc ++ estaticamente, que é algo que ainda não verifiquei. Mesmo assim, tudo parece estar indo bem até agora: posso usar os recursos do C ++ 11 no Ubuntu 10.04, que era o que eu procurava.

Observo que este artigo é de 2005 e talvez muita coisa tenha mudado desde então. Seu conselho ainda é atual? Há algum problema oculto que eu deva estar ciente?

Nick Hutchinson
fonte
Não, vincular estaticamente a libstdc ++ não implica isso. Se isso implicasse que não haveria sentido para a -static-libstdc++opção, você simplesmente usaria-static
Jonathan Wakely
@JonathanWakely -static obterá kernel too olderros em alguns sistemas ubuntu 1404. O glibc.so é como kernel32.dllna janela, faz parte da interface do sistema operacional, não devemos embuti-lo em nosso binário. Você pode usar objdump -T [binary path]para vê-lo carregado dinamicamente libstdc++.soou não. Para programador golang, você pode adicionar #cgo linux LDFLAGS: -static-libstdc++ -static-libgccantes de importar "C"
bronze man
@bronzeman, mas estamos falando -static-libstdc++não -staticde modo libc.sonão será ligado estaticamente.
Jonathan Wakely
1
@NickHutchinson, a postagem do blog com link sumiu. Esta pergunta do SO é uma busca popular para os termos relevantes aqui. Você pode reproduzir as informações críticas daquele post em sua pergunta ou oferecer um novo link se souber para onde ele foi movido?
Brian Cain,
1
@BrianCain O arquivo da Internet tem: web.archive.org/web/20160313071116/http://www.trilithium.com/…
Rob Keniger

Respostas:

136

Essa postagem do blog é muito imprecisa.

Pelo que eu sei, mudanças no C ++ ABI foram introduzidas com cada versão principal do GCC (ou seja, aqueles com componentes de número de primeira ou segunda versão diferentes).

Não é verdade. As únicas alterações do C ++ ABI introduzidas desde o GCC 3.4 são compatíveis com versões anteriores, o que significa que o C ++ ABI está estável há quase nove anos.

Para piorar as coisas, a maioria das principais distribuições do Linux usa instantâneos do GCC e / ou corrige suas versões do GCC, tornando virtualmente impossível saber exatamente com quais versões do GCC você pode estar lidando ao distribuir binários.

As diferenças entre as versões corrigidas das distribuições do GCC são menores e não mudam o ABI, por exemplo, o 4.6.3 20120306 do Fedora (Red Hat 4.6.3-2) é ABI compatível com as versões upstream do FSF 4.6.x e quase certamente com qualquer 4.6. x de qualquer outra distro.

No GNU / Linux, as bibliotecas de tempo de execução do GCC usam a versão de símbolo ELF para que seja fácil verificar as versões de símbolo necessárias para objetos e bibliotecas, e se você tiver um libstdc++.soque fornece esses símbolos ele funcionará, não importa se é uma versão com patch ligeiramente diferente de outra versão da sua distro.

mas nenhum código C ++ (ou qualquer código que use o suporte de tempo de execução C ++) pode ser vinculado dinamicamente se isso funcionar.

Isto também não é verdade.

Dito isso, vincular estaticamente a libstdc++.aé uma opção para você.

O motivo pelo qual pode não funcionar se você carregar dinamicamente uma biblioteca (usando dlopen) é que os símbolos libstdc ++ dos quais ela depende podem não ter sido necessários para seu aplicativo quando você o vinculou (estaticamente), portanto, esses símbolos não estarão presentes em seu executável. Isso pode ser resolvido vinculando dinamicamente a biblioteca compartilhada ao libstdc++.so(o que é a coisa certa a se fazer de qualquer maneira se depender disso). A interposição de símbolo ELF significa que os símbolos que estão presentes em seu executável serão usados ​​pela biblioteca compartilhada, mas outros não presente em seu executável será encontrado em qualquer libstdc++.solink para o qual ele esteja vinculado. Se seu aplicativo não usadlopen você não precisa se preocupar com isso.

Outra opção (e a que eu prefiro) é implantar o mais recente libstdc++.sojunto com seu aplicativo e garantir que ele seja encontrado antes do sistema padrão libstdc++.so, o que pode ser feito forçando o vinculador dinâmico a procurar no lugar certo, usando $LD_LIBRARY_PATHa variável de ambiente em run- tempo, ou definindo um RPATHno executável no momento do link. Prefiro usar RPATH, pois não depende do ambiente estar configurado corretamente para o aplicativo funcionar. Se você vincular seu aplicativo com '-Wl,-rpath,$ORIGIN'(observe as aspas simples para evitar que o shell tente se expandir $ORIGIN), o executável terá um RPATHdos $ORIGINque informa ao vinculador dinâmico para procurar bibliotecas compartilhadas no mesmo diretório que o próprio executável. Se você colocar o mais recentelibstdc++.sono mesmo diretório do executável ele será encontrado em tempo de execução, problema resolvido. (Outra opção é colocar o executável em /some/path/bin/e o libstdc ++ mais recente. Assim, em/some/path/lib/e vincular com '-Wl,-rpath,$ORIGIN/../lib'ou qualquer outro local fixo em relação ao executável e definir o RPATH em relação a $ORIGIN)

Jonathan Wakely
fonte
8
Esta explicação, especialmente sobre RPATH, é gloriosa.
nilweed 01 de
3
Enviar libstdc ++ com seu aplicativo no Linux é um conselho ruim. Procure no Google por "steam libstdc ++" para ver todo o drama que isso traz. Resumindo, se o seu exe carrega libs externas (como opengl) que querem abrir libstdc ++ novamente (como drivers radeon), essas libs estarão usando sua libstdc ++ porque já está carregada, em vez da sua própria, que é o que eles precisam e Espero. Então você está de volta à estaca zero.
7
@cap, o OP está perguntando especificamente sobre a implantação em uma distro onde o sistema libstdc ++ é mais antigo. O problema do Steam é que eles empacotaram um libstdc ++. Então era mais antigo que o do sistema (presumivelmente era mais novo na época em que o empacotaram, mas as distros mudaram para outros ainda mais novos). Isso pode ser resolvido fazendo com que o RPATH aponte para um diretório contendo um libstdc++.so.6link simbólico que é definido no momento da instalação para apontar para a lib empacotada ou para o sistema, se for mais recente. Existem modelos de ligação mista mais complicados, como os usados ​​pelo Red Hat DTS, mas eles são difíceis de fazer você mesmo.
Jonathan Wakely
5
Ei cara, me desculpe se não quero que meu modelo de envio de binários backwards-compat inclua "confiar que outras pessoas manterão o libstdc ++ ABI compat" ou "vincular condicionalmente libstdc ++ em tempo de execução" ... se isso incomoda algumas pessoas aqui e aí, o que posso fazer, quero dizer sem desrespeito. E se você se lembra do drama memcpy@GLIBC_2.14, você não pode realmente me culpar por ter problemas de confiança com isso :)
6
Tive que usar '-Wl, -rpath, $ ORIGIN' (observe o '-' na frente de rpath). Não consigo editar a resposta porque as edições devem ter pelo menos 6 caracteres ....
user368507
11

Uma adição à excelente resposta de Jonathan Wakely, por que dlopen () é problemático:

Devido ao novo pool de tratamento de exceções no GCC 5 (consulte PR 64535 e PR 65434 ), se você abrir e fechar uma biblioteca que está estaticamente vinculada a libstdc ++, você obterá um vazamento de memória (do objeto pool) a cada vez. Portanto, se houver alguma chance de você usar dlopen, parece uma péssima ideia vincular estaticamente libstdc ++. Observe que este é um vazamento real, em oposição ao benigno mencionado no PR 65434 .

Emil Styrke
fonte
1
A função __gnu_cxx::__freeres()parece fornecer pelo menos alguma ajuda com esse problema, uma vez que libera o buffer interno do objeto pool. Mas para mim não está claro qual implicação uma chamada para essa função tem com respeito às exceções acidentalmente lançadas posteriormente.
phlipsy
3

Complemento à resposta de Jonathan Wakely sobre o RPATH:

RPATH só funcionará se o RPATH em questão for o RPATH do aplicativo em execução . Se você tiver uma biblioteca que se vincula dinamicamente a qualquer biblioteca por meio de seu próprio RPATH, o RPATH da biblioteca será sobrescrito pelo RPATH do aplicativo que o carrega. Este é um problema quando você não pode garantir que o RPATH do aplicativo é o mesmo que o de sua biblioteca, por exemplo, se você espera que suas dependências estejam em um diretório específico, mas esse diretório não faz parte do RPATH do aplicativo.

Por exemplo, digamos que você tenha um aplicativo App.exe que possui uma dependência vinculada dinamicamente em libstdc ++. So.x para GCC 4.9. O App.exe tem essa dependência resolvida por meio do RPATH, ou seja,

App.exe (RPATH=.:./gcc4_9/libstdc++.so.x)

Agora, digamos que haja outra biblioteca Dependency.so, que possui uma dependência vinculada dinamicamente em libstdc ++. So.y para GCC 5.5. A dependência aqui é resolvida através do RPATH da biblioteca, ou seja

Dependency.so (RPATH=.:./gcc5_5/libstdc++.so.y)

Quando App.exe carrega Dependency.so, ele não acrescenta nem acrescenta o RPATH da biblioteca . Não o consulta de forma alguma. O único RPATH considerado será o do aplicativo em execução ou App.exe neste exemplo. Isso significa que se a biblioteca depender de símbolos que estão em gcc5_5 / libstdc ++. So.y, mas não em gcc4_9 / libstdc ++. So.x, a biblioteca falhará ao carregar.

Esta é apenas uma palavra de advertência, uma vez que já me deparei com esses problemas no passado. RPATH é uma ferramenta muito útil, mas sua implementação ainda tem alguns problemas.

Jonathan McDevitt
fonte
então RPATH para bibliotecas compartilhadas é meio inútil! E eu esperava que eles melhorassem um pouco o Linux nesse aspecto nas últimas 2 décadas ...
Frank Puck
2

Você também pode precisar ter certeza de que não depende da glibc dinâmica. Execute lddem seu executável resultante e observe todas as dependências dinâmicas (libc / libm / libpthread são normalmente suspeitas).

O exercício adicional seria construir um monte de exemplos C ++ 11 envolvidos usando esta metodologia e realmente tentar os binários resultantes em um sistema 10.04 real. Na maioria dos casos, a menos que você faça algo estranho com o carregamento dinâmico, você saberá imediatamente se o programa funciona ou trava.

Alexander L. Belikoff
fonte
1
Qual é o problema de depender da glibc dinâmica?
Nick Hutchinson
Acredito que pelo menos há algum tempo libstdc ++ implicava dependência da glibc. Não tenho certeza de como as coisas estão hoje.
Alexander L. Belikoff
9
libstdc ++ depende da glibc (por exemplo, iostreams são implementados em termos de printf), mas desde que a glibc no Ubuntu 10.04 forneça todos os recursos necessários para a libstdc ++ mais recente, não há problema em depender da glibc dinâmica, na verdade, é altamente recomendado nunca vincular estaticamente para glibc
Jonathan Wakely
1

Eu gostaria de acrescentar o seguinte à resposta de Jonathan Wakely.

Jogando -static-libstdc++no Linux, eu enfrentei o problema com dlclose(). Suponha que tenhamos uma aplicação 'A' vinculada estaticamente libstdc++e carregue vinculada dinamicamente ao libstdc++plugin 'P' em tempo de execução. Isso é bom. Mas quando 'A' descarrega 'P', ocorre falha de segmentação. Minha suposição é que após o descarregamento libstdc++.so, 'A' não pode mais usar símbolos relacionados a libstdc++. Observe que se 'A' e 'P' estiverem estaticamente vinculados a libstdc++, ou se 'A' estiver vinculado dinamicamente e 'P' estaticamente, o problema não ocorre.

Resumo: se seu aplicativo carrega / descarrega plug-ins que podem libstdc++ser vinculados dinamicamente, o aplicativo também deve ser vinculado a ele dinamicamente. Esta é apenas uma observação minha e gostaria de receber seus comentários.

Fedorov7890
fonte
1
Isso provavelmente é semelhante a misturar implementações libc (digamos, vincular dinamicamente a um plugin que por sua vez vincula glibc dinamicamente, enquanto o próprio aplicativo está estaticamente vinculado a musl-libc). Rich Felker, autor de musl-libc, afirma que o problema em tal cenário é que o gerenciamento de memória glibc (usando sbrk) faz certas suposições e espera estar sozinho dentro de um processo ... não tenho certeza se isso é limitado a um versão glibc particular ou qualquer outra coisa.
0xC0000022L
e as pessoas ainda não veem as vantagens da interface de heap do Windows, que é capaz de lidar com várias cópias independentes de libc ++ / libc dentro de um único processo. Essas pessoas não deveriam projetar software.
Frank Puck
@FrankPuck tendo uma quantidade razoável de experiência com Windows e Linux, posso dizer que a forma como o "Windows" o faz não vai ajudá-lo quando o MSVC for a parte que decide qual alocador será usado e como. A principal vantagem que vejo nos heaps no Windows é que você pode distribuir bits e peças e, em seguida, liberá-los de uma só vez. Mas com o MSVC você ainda enfrentará o problema descrito acima, por exemplo, ao passar os ponteiros alocados por outro tempo de execução do VC (versão vs. depuração ou estaticamente vs. vinculados dinamicamente). Portanto, o "Windows" não está imune. Cuidado deve ser tomado em ambos os sistemas.
0xC0000022L