Escolar Documentos
Profissional Documentos
Cultura Documentos
html
Compilação
G e C ++
Um guia de engenharia para compilar, Knkng e
bibliotecas usando C e C ++.
Milan Stevanovic
Apress
Para sua conveniência, a Apress colocou parte do material anterior após o índice. Use os links de Favoritos e
Conteúdo para acessá-los.
Apress
amigos de
Resumo do conteúdo
Sobre o autor............................................... .................................................. ............... xv
Introdução
Levei algum tempo para me dar conta da incrível analogia que existe entre a arte culinária e a arte da programação de computadores.
Provavelmente, a comparação mais óbvia que vem à mente é que tanto o especialista em culinária quanto o programador têm objetivos finais semelhantes:
alimentar-se. Para um chef, é o ser humano, para o qual muitas matérias-primas são utilizadas para fornecer nutrientes comestíveis e também prazer
gastronômico, enquanto para o programador é o microprocessador, para o qual uma série de procedimentos diferentes são usados para fornecer o código isso não
só precisa produzir algumas ações significativas, mas também precisa ser executado da forma ideal.
Por mais que este ponto de comparação introdutório possa parecer um pouco rebuscado ou até infantil, os pontos de comparação subsequentes são algo que
considero muito mais aplicável e convincente.
As receitas e instruções para preparar pratos de todos os tipos são abundantes e onipresentes. Quase todas as revistas populares têm uma seção de culinária
dedicada a todos os tipos de alimentos e todos os tipos de cenários de preparação de alimentos, desde receitas rápidas e fáceis / de última hora até receitas
realmente elaboradas, desde aquelas com foco em tabelas nutricionais de ingredientes para aqueles que se concentram na interação delicada entre ingredientes
extraordinários e difíceis de encontrar.
No entanto, no próximo nível de especialização na arte culinária, a disponibilidade de recursos cai exponencialmente. As receitas e instruções para o
funcionamento do negócio alimentar (produção por volume, funcionamento do restaurante ou negócio de catering), planejamento das quantidades e ritmo de
entrega para o processo de preparação de alimentos, técnicas e estratégias para otimizar a eficiência da entrega de alimentos, técnicas para escolher o certo
ingredientes, minimizando a deterioração dos ingredientes armazenados - esse tipo de informação é substancialmente mais difícil de encontrar. Com razão, já que
esses tipos de tópicos delineiam a diferença entre a culinária amadora e o negócio de alimentação profissional.
As informações sobre uma grande variedade de linguagens de programação estão prontamente disponíveis, por meio de milhares de livros, revistas, artigos,
fóruns da web e blogs, desde o nível de iniciante absoluto até as dicas de "preparação para a entrevista de programação do Google".
Esses tipos de tópicos, entretanto, cobrem apenas cerca de metade das habilidades exigidas pelo profissional de software. Logo após a gratificação imediata de ver
o programa que criamos realmente executando (e fazendo certo) vem o próximo nível de questões importantes: como arquitetar o código para permitir
modificações posteriores fáceis, como extrair partes reutilizáveis da funcionalidade para uso futuro , como permitir um ajuste suave para diferentes ambientes
(começando com diferentes idiomas e alfabetos humanos, até a execução em diferentes sistemas operacionais).
Em comparação com os outros tópicos de programação, esses tipos de tópicos raramente são discutidos e até hoje pertencem à forma de "arte negra" reservada
para alguns raros espécimes de profissionais de ciência da computação (principalmente arquitetos de software e engenheiros de construção) também quanto ao
domínio das classes de nível universitário relacionadas ao projeto do compilador / vinculador.
Um fator particular - a ascensão do Linux e a proliferação de suas práticas de programação em uma infinidade de ambientes de design - trouxe um grande ímpeto
para um programador prestar atenção a esses tópicos. Ao contrário dos colegas que escrevem software para plataformas "bem protegidas" (Windows e Mac, em
que a plataforma, IDEs e SDKs dispensam o programador de pensar sobre certos aspectos de programação), a rotina diária de um programador Linux é combinar
o código proveniente de variedade de fontes, práticas de codificação e em formas que requerem compreensão imediata do funcionamento interno do compilador,
do vinculador, do mecanismo de carregamento do programa e, portanto, dos detalhes do projeto e uso dos vários tipos de bibliotecas.
O primeiro grupo é formado por programadores C / C ++ vindos de várias áreas de engenharia (EE, mecânica, robótica e controle de sistemas, aeroespacial, física,
química, etc.) que lidam com programação diariamente. A falta de educação formal e mais focada em ciência da computação, bem como a falta de literatura não
teórica sobre o assunto, tornam este livro um recurso precioso para este grupo específico.
O segundo grupo é formado por programadores de nível júnior com formação em ciência da computação. Este livro pode ajudar a concretizar o corpo de seu
conhecimento existente adquirido em cursos básicos e focalizá-lo no nível operacional. Manter os resumos rápidos dos Capítulos 12-14 em algum lugar acessível
pode valer a pena, mesmo para os perfis mais experientes desse grupo em particular.
O terceiro grupo é formado por pessoas com interesse no domínio da integração e personalização do sistema operacional. Compreender o mundo dos binários e
os detalhes de seu funcionamento interno pode ajudar a "limpar o ar" tremendamente.
Sobre o livro
Originalmente, eu não tinha planos de escrever este livro em particular. Nem mesmo um livro no domínio da ciência da computação. (Processamento de sinais?
Arte de programar? Talvez ... mas um livro de ciência da computação? Naaah ...)
A única razão pela qual este livro surgiu é o fato de que, ao longo de minha carreira profissional, tive que lidar com certas questões, que na época achei que outra
pessoa deveria cuidar.
Era uma vez, eu fiz a escolha de seguir o caminho profissional de um tipo de assassino de alta tecnologia, o cara que é chamado pelos cidadãos de comunidades de
alta tecnologia calmas e decentes para livrá-los do terror da desagradável multimídia que se aproxima. problemas de design relacionados, causando estragos,
juntamente com uma gangue de bugs horríveis. Essa escolha de carreira praticamente não deixava espaço para exclusividade nas preferências pessoais
tipicamente encontradas pelas crianças que comeriam frango, mas não ervilhas. O sinistro "ou então" está sempre lá. Embora FFTs, wavelets, transformadas Z,
filtros FIR e IIR, oitavas, semitons, interpolações e decimações sejam naturalmente minha escolha preferida de tarefas (junto com uma quantidade decente de
programação C / C ++), eu tive que lidar com problemas que iriam não tem sido minha preferência pessoal. Alguém tinha que fazer isso.
Surpreendentemente, ao procurar respostas diretas para perguntas muito simples e pontuais, tudo que pude encontrar foi uma variedade dispersa de artigos da
web, principalmente sobre os detalhes de alto nível. Eu estava pacientemente coletando as "peças do quebra-cabeça" e consegui não apenas completar as tarefas de
design em mãos, mas também aprender ao longo do caminho.
Um belo dia, chegou a hora de consolidar minhas notas de design (algo que eu faço regularmente para a variedade de tópicos com os quais lido). Desta vez,
porém, quando o esforço foi concluído, tudo parecia. . . Nós vamos . . . como um livro. Este livro.
De qualquer forma . . .
Dado o estado atual do mercado de trabalho, estou profundamente convencido de que (desde meados da primeira década do século 21) conhecer as
complexidades da linguagem C / C ++ perfeitamente - e mesmo algoritmos, estruturas de dados e padrões de design - simplesmente não é o suficiente.
Na era do código aberto, a realidade da vida do programador profissional se torna cada vez menos sobre "saber como escrever o programa" e, em vez disso,
substancialmente mais sobre "saber como integrar os corpos de código existentes". Isso pressupõe não apenas ser capaz de ler o código de outra pessoa (escrito em
uma variedade de estilos e práticas de codificação), mas também saber a melhor maneira de integrar o código aos pacotes existentes que estão disponíveis
principalmente na forma binária (bibliotecas) acompanhados pela exportação arquivos de cabeçalho.
Esperançosamente, este livro irá educar (aqueles que podem precisar), bem como fornecer uma referência rápida para a maioria das tarefas relacionadas à análise
dos binários C / C ++.
Na verdade, quem me conhece sabe o quanto (na época em que era minha plataforma de design preferida) eu gostava e respeitava o ambiente de design do
Windows - o fato de ser bem documentado, bem suportado e até que ponto os componentes certificados funcionaram de acordo com a especificação. Uma série de
aplicativos de nível profissional que projetei (GraphEdit para Windows Mobile for Palm, Inc., projetado do zero e repleto de recursos extras, sendo provavelmente
o mais complexo, seguido por vários formatos de mídia / aplicativos de análise de DSP) lideraram em relação à compreensão completa e, em última análise, ao
respeito pela tecnologia Windows da época.
Nesse ínterim, a era do Linux chegou e isso é um fato da vida. O Linux está em todo lugar e há poucas chances de um programador ser capaz de ignorá-lo e evitá-
lo.
O ambiente de design de software Linux provou ser aberto, transparente, simples e direto ao ponto. O controle sobre estágios de programação individuais, a
disponibilidade de documentação bem escrita e ainda mais "línguas ao vivo" na Web tornam o trabalho com a cadeia de ferramentas GNU um prazer.
Mas espere! Linux e GNU não são exatamente a mesma coisa !!!
Sim eu conheço. Linux é um kernel, enquanto GNU cobre muitas coisas acima dele. Apesar do fato de que o compilador GNU pode ser usado em outros sistemas
operacionais (por exemplo, MinGW no Windows), na maioria das vezes o GNU e o Linux andam de mãos dadas. Para simplificar toda a história e chegar mais
perto de como o programador médio percebe a cena da programação, e especialmente em contraste com o lado do Windows, irei me referir coletivamente ao GNU
+ Linux simplesmente como "Linux".
Os capítulos 6-12 fornecem uma visão essencial do tópico. Investi muito esforço para ser conciso e tentar combinar palavras e imagens de objetos familiares da
vida real para explicar os conceitos mais vitais cuja compreensão é fundamental. Para aqueles sem formação formal em ciência da computação, a leitura e a
compreensão desses capítulos são altamente recomendadas. Na verdade, esses capítulos representam a essência de toda a história.
Os capítulos 13-15 são uma espécie de folha de referência prática, uma forma de lembretes rápidos e legais. O conjunto de ferramentas específico da plataforma
para a análise de arquivos binários é discutido, seguido pela referência cruzada "Como fazer" parte que contém receitas rápidas de como realizar certas tarefas
isoladas.
O Apêndice A contém os detalhes técnicos dos conceitos mencionados no Capítulo 8. O Apêndice A está disponível online apenas em www .apress .com . Para
obter informações detalhadas sobre como localizá-lo, vá para www.apress.com/source-code/ . Depois de compreender os conceitos do Capítulo 8, pode ser
muito útil tentar seguir as explicações práticas de como e por que certas coisas realmente funcionam. Espero que um pequeno exercício possa servir de
treinamento prático para o leitor ávido.
CAPÍTULO 1
O primeiro passo obrigatório nessa direção é entender o entorno em que os programas operam. O objetivo deste capítulo é fornecer em esboços gerais os detalhes
mais poderosos da funcionalidade de um sistema operacional multitarefa moderno.
Os sistemas operacionais multitarefa modernos são, em muitos aspectos, muito próximos uns dos outros em termos de como a funcionalidade mais importante é
implementada. Como resultado, um esforço consciente será feito para ilustrar os conceitos de maneiras independentes de plataforma primeiro. Além disso, será
dada atenção aos meandros das soluções específicas de plataforma (onipresentes Linux e formato ELF vs. Windows) e estes serão analisados em grande detalhe.
Abstrações úteis
Mudanças no domínio da tecnologia de computação tendem a acontecer em um ritmo muito rápido. A tecnologia de circuitos integrados fornece componentes
que não são apenas ricos em variedade (óptico, magnético, semicondutor), mas também são continuamente atualizados em termos de capacidades. De acordo com
a Lei de Moore, o número de transistores em circuitos integrados dobra aproximadamente a cada dois anos. A potência de processamento, que está fortemente
associada ao número de transistores disponíveis, tende a seguir uma tendência semelhante.
Como foi descoberto muito cedo, a única maneira de se adaptar substancialmente ao ritmo da mudança é definir os objetivos gerais e a arquitetura dos sistemas de
computador de uma forma abstrata / generalizada, no nível acima dos detalhes das implementações em constante mudança. A parte crucial desse esforço é
formular a abstração de tal forma que qualquer nova implementação real se encaixe na definição essencial, deixando de lado os detalhes da implementação real
como relativamente sem importância. A arquitetura geral do computador pode ser representada como um conjunto estruturado de abstrações, conforme mostrado
na Figura 1-1.
Máquina virtual
<-
-►
Processo
Memória Virtual r
v v
r Memória principal
Dispositivos I / O
A abstração no nível mais baixo lida com a vasta variedade de dispositivos de E / S (mouse, teclado, joystick, trackball, caneta de luz, scanner, leitores de código de
barras, impressora, plotter, câmera digital, câmera da web), representando-os com sua quintessência propriedade do fluxo de bytes. Na verdade,
independentemente das diferenças entre os vários objetivos, implementações e capacidades dos dispositivos, são os fluxos de bytes que esses dispositivos
produzem ou recebem (ou ambos) que são os detalhes de extrema importância do ponto de vista do projeto de sistema de computador.
O próximo nível de abstração, o conceito de memória virtual, que representa a ampla variedade de recursos de memória tipicamente encontrados no sistema, é o
assunto de extraordinária importância para o tópico principal deste livro. A maneira como essa abstração em particular realmente representa a variedade de
dispositivos de memória física não apenas impacta o design do hardware e software reais, mas também estabelece uma base na qual o design do compilador,
linker e carregador depende.
O conjunto de instruções que abstrai a CPU física é a abstração do próximo nível. Compreender os recursos do conjunto de instruções e a promessa do poder de
processamento que ele carrega é definitivamente o tópico de interesse do programador mestre. Do ponto de vista de nosso tópico principal, esse nível de abstração
não é de importância primária e não será discutido em grandes detalhes.
As complexidades do sistema operacional representam o nível final de abstração. Certos aspectos do design do sistema operacional (mais notavelmente,
multitarefa) têm um impacto decisivo na arquitetura do software em geral. Os cenários em que as várias partes tentam acessar o recurso compartilhado requerem
uma implementação cuidadosa em que a duplicação desnecessária de código seria evitada - o fator que levou diretamente ao projeto de bibliotecas
compartilhadas.
Vamos fazer um pequeno desvio em nossa jornada de análise dos meandros do sistema de computador geral e, em vez disso, prestar atenção especial às questões
importantes relacionadas ao uso da memória.
• A necessidade de memória parece insaciável. Sempre há necessidade de muito mais do que está disponível atualmente. Cada salto quântico no fornecimento de
quantidades maiores (de memória mais rápida) é imediatamente atendido com a demanda há muito aguardada das tecnologias que estiveram conceitualmente
prontas por algum tempo, e cuja realização foi atrasada até o dia em que a memória física tornou-se disponível em quantidades suficientes .
• A tecnologia parece ser muito mais eficiente em superar as barreiras de desempenho dos processadores do que a memória. Esse fenômeno é normalmente
conhecido como "lacuna entre processador e memória".
• A velocidade de acesso à memória é inversamente proporcional à capacidade de armazenamento. Os tempos de acesso dos dispositivos de armazenamento de
maior capacidade são normalmente várias ordens de magnitude maiores do que os dos dispositivos de memória de menor capacidade.
Agora, vamos dar uma olhada rápida no sistema do ponto de vista do programador / designer / engenheiro. Idealmente, o sistema precisa acessar toda a memória
disponível o mais rápido possível - o que sabemos que nunca será possível alcançar. A próxima pergunta imediata é: há algo que possamos fazer a respeito?
O detalhe que traz um grande alívio é o fato de o sistema não usar toda a memória o tempo todo, mas apenas alguma memória por algum tempo. Nesse caso, tudo
o que realmente precisamos fazer é reservar a memória mais rápida para executar a execução imediata e usar os dispositivos de memória mais lentos para o
código / dados que não são executados imediatamente. Enquanto a CPU busca da memória rápida as instruções agendadas para a execução imediata, o hardware
tenta adivinhar qual parte do programa será executada a seguir e fornece essa parte do código para a memória mais lenta para aguardar a execução. Pouco antes
de chegar a hora de executar as instruções armazenadas na memória mais lenta, elas são transferidas para a memória mais rápida. Este princípio é conhecido
como cache.
A analogia da vida real com o armazenamento em cache é algo que uma família comum faz com seu suprimento de comida. A menos que vivamos em lugares
muito isolados, normalmente não compramos e levamos para casa todos os alimentos necessários durante um ano inteiro. Em vez disso, mantemos
principalmente um armazenamento moderadamente grande em casa (geladeira, despensa, prateleiras), no qual mantemos um suprimento de comida suficiente
para uma ou duas semanas. Quando percebemos que essas pequenas reservas estão prestes a se esgotar, fazemos uma viagem ao armazém e compramos apenas a
quantidade de comida necessária para encher o estoque local.
O fato de a execução de um programa ser tipicamente afetada por uma série de fatores externos (as configurações do usuário sendo apenas um deles) torna o
mecanismo de armazenamento em cache uma forma de adivinhação ou um jogo de acerto ou erro. Quanto mais previsível for o fluxo de execução do programa
(medido pela falta de saltos, interrupções, etc.), mais suave o mecanismo de armazenamento em cache funcionará. Por outro lado, sempre que um programa
encontra a mudança de fluxo, as instruções que foram anteriormente acumuladas acabam sendo descartadas por não serem mais necessárias, e uma parte nova e
mais apropriada do programa precisa ser fornecida da memória mais lenta.
A implementação de um princípio de armazenamento em cache é onipresente e se estende por vários níveis de memória, conforme ilustrado na Figura 1-2.
Memória virtual
A abordagem genérica do cache de memória obtém a implementação real no próximo nível de arquitetura, no qual o programa em execução é representado pela
abstração chamada processo.
Os sistemas operacionais multitarefa modernos são projetados com a intenção de permitir que um ou mais usuários executem vários programas simultaneamente.
Não é incomum para o usuário médio ter vários aplicativos (por exemplo, navegador da web, editor, reprodutor de música, calendário) rodando
simultaneamente.
A desproporção entre as necessidades de memória e a limitada disponibilidade de memória foi resolvida pelo conceito de memória virtual, que pode ser
delineada pelo seguinte conjunto de diretrizes:
• As permissões de memória do programa são fixas, iguais para todos os programas e de natureza declarativa.
N
Os sistemas operacionais normalmente permitem que o programa (processo) use 2 bytes de memória, onde N hoje é 32 ou 64. Este valor é fixo e é independente
da disponibilidade de memória física no sistema
• A quantidade de memória física pode variar. Normalmente, a memória está disponível em quantidades várias vezes menores do que o espaço de endereço do
processo declarado. Não é incomum que a quantidade de memória física disponível para a execução de programas seja um número ímpar.
• A memória física em tempo de execução é dividida em pequenos fragmentos (páginas), com cada página sendo usada para programas rodando
simultaneamente.
• O layout completo da memória do programa em execução é mantido na memória lenta (disco rígido). Apenas as partes da memória (código e dados) que estão
prestes a serem executadas são carregadas na página da memória física.
A implementação real do conceito de memória virtual requer a interação de vários recursos do sistema, como hardware (exceções de hardware, tradução de
endereço de hardware), disco rígido (arquivos de troca), bem como o software de sistema operacional de nível mais baixo (kernel). O conceito de memória virtual
é ilustrado na Figura 1-3.
Processo A
Processo B
Memória física
Endereçamento Virtual
O conceito de endereçamento virtual está na base da implementação da memória virtual e, de muitas maneiras, impacta significativamente o design de
compiladores e vinculadores.
Como regra geral, o designer do programa está completamente livre de se preocupar com a faixa de endereçamento que seu programa ocupará em tempo de
execução (pelo menos isso é verdade para a maioria dos aplicativos do espaço do usuário; os módulos do kernel são um tanto excepcionais neste sentido). Em vez
N
disso, o modelo de programação assume que a faixa de endereço está entre 0 e 2 (faixa de endereço virtual) e é a mesma para todos os programas.
A decisão de conceder um esquema de endereçamento simples e unificado para todos os programas tem um enorme impacto positivo no processo de
desenvolvimento de código. A seguir estão os benefícios:
• O carregamento é simplificado.
A colocação real do tempo de execução da memória do programa em um intervalo de endereço concreto é realizada pelo sistema operacional por meio do
mecanismo de tradução de endereço. Sua implementação é realizada pelo módulo de hardware denominado unidade de gerenciamento de memória (MMU), que
não requer nenhum envolvimento do próprio programa.
A Figura 1-4 compara o mecanismo de endereçamento virtual com um esquema de endereçamento físico simples e simples (usado até hoje no domínio de sistemas
de microcontroladores simples).
Vários sistemas operacionais multitarefa / multiusuário especificam diferentes layouts de mapa de memória. Em particular, o mapa de memória virtual do
processo Linux segue o esquema de mapeamento mostrado na Figura 1-5.
variáveis ambientais
MEMORIA COMPARTILHADA
TEXTO
o
HEAP
dados inicializados
0x00000000
Independentemente das peculiaridades do esquema de divisão de memória do processo de uma determinada plataforma, as seguintes seções do mapa de
memória devem ser sempre suportadas:
• Seção de código contendo as instruções de código de máquina para a CPU executar ( seção de texto )
• Seções de dados contendo os dados nos quais a CPU irá operar. Normalmente, seções separadas são mantidas para dados inicializados ( seção .data ), para
dados não inicializados ( seção .bss ), bem como para dados constantes ( seção .rdata )
• A parte superior pertencente ao kernel onde (entre outras coisas) as variáveis de ambiente específicas do processo são armazenadas
Uma discussão lindamente detalhada sobre este tópico específico escrita por Gustavo Duarte pode ser encontrada em
http://duartes.org/gustavo/blog/post/anatomy-of-a-program-in-memory .
Em um esboço,
• O esqueleto de um arquivo binário é criado pelo vinculador. Para completar sua tarefa, o vinculador combina os arquivos binários criados pelo compilador para
preencher a variedade de seções do mapa de memória (código, dados, etc.).
• A tarefa de criação inicial do mapa de memória do processo é executada pelo utilitário do sistema chamado carregador de programa. No sentido mais simples, o
carregador abre o arquivo executável binário, lê as informações relacionadas às seções e preenche a estrutura do mapa de memória do processo.
Esteja ciente de que esta descrição mais simples está longe de fornecer o quadro completo e completo. Deve ser visto como uma introdução suave às discussões
subsequentes, por meio das quais substancialmente mais detalhes sobre o tópico de binários e carregamento de processo serão transmitidos à medida que
avançamos no tópico.
Resumo
Este capítulo forneceu uma visão geral dos conceitos que mais afetam fundamentalmente o design de sistemas operacionais multitarefa modernos. Os conceitos
básicos de memória virtual e endereçamento virtual não afetam apenas a execução do programa (que será discutido em detalhes no próximo capítulo), mas
também impactam diretamente como os arquivos executáveis do programa são construídos (o que será explicado em detalhes posteriormente neste livro )
CAPÍTULO 2
Assim como a vida útil de uma borboleta é determinada por seu estágio de lagarta, a vida útil de um programa é amplamente determinada pela estrutura interna
do binário, que o carregador do SO carrega, descompacta e coloca seu conteúdo na execução. Não deve ser uma grande surpresa que a maioria de nossas
discussões subsequentes serão dedicadas à arte de preparar um projeto e incorporá-lo adequadamente ao corpo do (s) arquivo (s) executável (s) binário (s).
Assumiremos que o programa foi escrito em C / C ++.
1 Criação do código-fonte
2 Compilando
3 Linking
4 Carregando
5 Executando
Verdade seja dita, este capítulo cobrirá muito mais detalhes sobre o estágio de compilação do que sobre os estágios subsequentes. A cobertura dos estágios
subsequentes (especialmente o estágio de ligação) só começa neste capítulo, no qual você verá apenas a proverbial "ponta do iceberg". Após a introdução mais
básica de ideias por trás do estágio de vinculação, o restante do livro tratará dos meandros da vinculação, bem como do carregamento e da execução do programa.
Premissas iniciais
Embora seja muito provável que uma grande porcentagem de leitores pertença à categoria de programadores avançados a especialistas, começarei com exemplos
iniciais bastante simples. As discussões neste capítulo serão pertinentes ao caso muito simples, mas muito ilustrativo. O projeto de demonstração consiste em dois
arquivos de origem simples, que serão primeiro compilados e, em seguida, vinculados. O código é escrito com a intenção de manter a complexidade de compilar e
vincular no nível mais simples possível.
Em particular, nenhum link de bibliotecas externas, particularmente o link dinâmico, estará ocorrendo neste exemplo de demonstração. A única exceção será a
vinculação com a biblioteca de tempo de execução C (que de qualquer forma é necessária para a grande maioria dos programas escritos em C). Sendo um
elemento tão comum na vida da execução do programa C, por uma questão de simplicidade, intencionalmente fecharei os olhos aos detalhes particulares da
ligação com a biblioteca de tempo de execução C e assumirei que o programa é criado de tal forma que todos o código da biblioteca de tempo de execução C é
inserido "automagicamente" no corpo do mapa de memória do programa.
Seguindo essa abordagem, ilustrarei os detalhes dos problemas essenciais da construção do programa de uma forma simples e limpa.
Escrita de Código
Dado que o principal tópico deste livro é o processo de construção do programa (ou seja, o que acontece depois que o código-fonte é escrito), não vou gastar muito
tempo no processo de criação do código-fonte.
Exceto em alguns casos raros quando o código-fonte é produzido por um script, presume-se que um usuário o faça digitando os caracteres ASCII em seu editor de
escolha em um esforço para produzir as instruções escritas que satisfaçam as regras de sintaxe do linguagem de programação de escolha (C / C ++ em nosso caso).
O editor de escolha pode variar desde o editor de texto ASCII mais simples possível até a ferramenta IDE mais avançada. Supondo que o leitor médio deste livro
seja um programador bastante experiente, não há realmente nada de especial a dizer sobre esse estágio do ciclo de vida do programa.
No entanto, existe uma prática de programação específica que impacta significativamente o rumo da história a partir deste ponto, e vale a pena prestar atenção
extra a ela. Para organizar melhor o código-fonte, os programadores normalmente seguem a prática de manter as várias partes funcionais do código em arquivos
separados, resultando em projetos geralmente compostos de muitos arquivos-fonte e de cabeçalho diferentes.
Essa prática de programação foi adotada muito cedo, desde a época dos ambientes de desenvolvimento feitos para os primeiros microprocessadores. Por ser uma
decisão de design muito sólida, ela tem sido praticada desde então, pois está comprovado que fornece uma organização sólida do código e torna as tarefas de
manutenção de código significativamente mais fáceis.
Esta prática de programação, sem dúvida útil, tem consequências de longo alcance. Como você verá em breve, praticá-lo leva a certa quantidade de
indeterminismo nos estágios subsequentes do processo de construção, cuja resolução requer algum pensamento cuidadoso.
Para ilustrar melhor as complexidades do processo de compilação, bem como a experiência prática de aquecimento, um projeto de demonstração simples foi
fornecido. composto de, no máximo, um cabeçalho e dois arquivos de origem. No entanto, é extremamente importante para a compreensão do quadro mais
amplo. Os seguintes arquivos fazem parte do projeto:
• Arquivo de cabeçalho function.h, que declara as funções chamadas e os dados acessados pela função main () .
• Arquivo-fonte function.c, que contém as implementações do código-fonte das funções e instanciação dos dados referenciados pela função main () .
O ambiente de desenvolvimento usado para construir este projeto simples será baseado no compilador gcc rodando no Linux. As listagens 2-1 a 2-3 contêm o
código usado no projeto de demonstração.
para fornecer um pouco ao leitor. O código é excepcionalmente simples; é cuidadosamente projetado para ilustrar os pontos de
file:///C:/Users/noels/Desktop/Advanced C and C++ Compiling ( PDFDrive ).html 9/235
21/09/2021 22:07 Advanced C and C++ Compiling ( PDFDrive ).html
Listagem 2-2. function.c int nCompletionStatus = 0;
float z = x + y; return z;
#include "function.h"
Compilando
Depois de escrever seu código-fonte, é hora de mergulhar no processo de construção do código, cuja primeira etapa obrigatória é o estágio de compilação. Antes
de entrar nos meandros da compilação, alguns termos introdutórios simples serão apresentados primeiro.
Definições introdutórias
Compilar em sentido amplo pode ser definido como o processo de transformar o código-fonte escrito em uma linguagem de programação em outra linguagem de
programação. O seguinte conjunto de fatos introdutórios é importante para sua compreensão geral do processo de compilação:
• A entrada para o compilador é uma unidade de tradução. Uma unidade de tradução típica é um arquivo de texto contendo o código-fonte.
• A saída da compilação é uma coleção de arquivos de objetos binários , um para cada uma das unidades de tradução de entrada.
• Para se tornarem adequados para execução, os arquivos-objeto precisam ser processados por meio de outro estágio de construção do programa, chamado de
vinculação.
O
int function_sum (int x, int y)
retorno (x + y)
main.c
o ° 1001001010010010
1010010101001010101 0101010111011011011 111
Definições Relacionadas
• Compilação no sentido estrito denota o processo de tradução do código de uma linguagem de nível superior para o código de arquivos de produção de uma
linguagem de nível inferior (normalmente, assembler ou mesmo código de máquina).
• Se a compilação é realizada em uma plataforma (CPU / OS) para produzir código a ser executado em alguma outra plataforma (CPU / OS), é chamada de
compilação cruzada. A prática usual é usar alguns dos sistemas operacionais de desktop (Linux, Windows) para gerar o código para dispositivos embarcados ou
móveis.
• Descompilação (desmontagem) é o processo de conversão do código-fonte de uma linguagem de nível inferior para a linguagem de nível superior.
• Tradução de linguagem é o processo de transformar o código-fonte de uma linguagem de programação em outra linguagem de programação do mesmo nível e
complexidade.
• A reescrita de linguagem é o processo de reescrever as expressões de linguagem em uma forma mais adequada para certas tarefas (como otimização).
As fases da compilação
O processo de compilação não é monolítico por natureza. Na verdade, ele pode ser dividido em vários estágios
(pré-processamento, análise linguística, montagem, otimização, emissão de código), cujos detalhes serão
discutido a seguir.
Pré-processando
A primeira etapa padrão no processamento dos arquivos de origem é executá-los por meio de um programa especial de processamento de texto chamado pré -
processador, que executa uma ou mais das seguintes ações:
• Inclui os arquivos que contêm definições (arquivos de inclusão / cabeçalho) nos arquivos de origem, conforme especificado pela palavra-chave #include .
• Converte as definições de macro em código na variedade de locais em que as macros são chamadas.
• Inclui ou exclui condicionalmente certas partes do código, com base na posição das diretivas #if, #elif e #endif .
A saída do pré-processador é o código C / C ++ em sua forma final, que será passado para a próxima etapa, a análise de sintaxe.
O compilador gcc fornece o modo em que apenas o estágio de pré-processamento é executado nos arquivos de origem de entrada:
A menos que seja especificado de outra forma, a saída do pré-processador é o arquivo que tem o mesmo nome do arquivo de entrada e cuja extensão de arquivo é
.i. O resultado da execução do pré-processador no arquivo function.c é semelhante ao da Listagem 2-4.
# 1 "function.c"
# 1 "# 1"
# 1 "function.h" 1
# 11 "function.h"
# 2 "function.c" 2
int nCompletionStatus = 0;
float z = x + y; return z;
Uma saída de pré-processador mais compacta e significativa pode ser obtida se alguns sinalizadores extras forem passados para o gcc, como
gcc -E -P -i <arquivo de entrada> -o <arquivo de saída pré-processado> .i que resulta no arquivo pré-processado visto na Listagem 2-5.
float z = x + y; return z;
Obviamente, o pré-processador substituiu o símbolo MULTIPLIER, cujo valor real, a partir do fato de que a variável USE_FIRST_OPTION foi definida, acabou
sendo 3,0.
Análise Lingüística
Durante esse estágio, o compilador primeiro converte o código C / C ++ em uma forma mais adequada para processamento (eliminando comentários e espaços em
branco desnecessários, extraindo tokens do texto, etc.). Essa forma otimizada e compactada de código-fonte é analisada lexicamente, com a intenção de verificar se
o programa satisfaz as regras de sintaxe da linguagem de programação na qual foi escrito. Se desvios das regras de sintaxe forem detectados, erros ou avisos serão
relatados. Os erros são causa suficiente para o término da compilação, enquanto os avisos podem ou não ser suficientes, dependendo das configurações do
usuário.
Uma visão mais precisa deste estágio do processo de compilação revela três estágios distintos:
• Análise lexical, que divide o código-fonte em tokens não divisíveis. O próximo estágio,
e verifica se sua ordem faz sentido do ponto de vista das regras da linguagem de programação. Finalmente,
Durante o estágio de análise lingüística, o compilador provavelmente mais merece ser chamado de "reclamante", pois tende a reclamar mais de erros de digitação
ou outros erros que encontra do que realmente compilar o código.
Montagem
O compilador atinge esse estágio somente depois que o código-fonte é verificado para não conter erros de sintaxe. Neste estágio, o compilador tenta converter as
construções de linguagem padrão em construções específicas para o conjunto real de instruções da CPU. Diferentes CPUs apresentam diferentes recursos de
funcionalidade e, em geral, diferentes conjuntos de instruções, registros e interrupções disponíveis, o que explica a grande variedade de compiladores para uma
variedade ainda maior de processadores.
O compilador gcc fornece o modo de operação no qual o código-fonte dos arquivos de entrada é convertido em um arquivo de texto ASCII contendo as linhas de
instruções do montador específicas para o chip e / ou sistema operacional.
A menos que seja especificado de outra forma, a saída do pré-processador é o arquivo que tem o mesmo nome do arquivo de entrada e cuja extensão de arquivo é
.s.
O arquivo gerado não é adequado para execução; é apenas um arquivo de texto contendo os mnemônicos legíveis por humanos das instruções do assembler, que
podem ser usados pelo desenvolvedor para obter uma visão melhor dos detalhes do funcionamento interno do processo de compilação.
No caso particular da arquitetura do processador X86, o código assembler pode estar em conformidade com um dos dois formatos de impressão de instrução
suportados,
• formato AT&T
• formato Intel
a escolha de qual pode ser especificada passando um argumento de linha de comando extra para o assembler gcc. A escolha do formato depende principalmente
do gosto pessoal do desenvolvedor.
Quando o arquivo function.c é montado no formato AT&T executando o seguinte comando $ gcc -S -masm = att function.c -o function.s
ele cria o arquivo assembler de saída, que se parece com o código mostrado na Listagem 2-6.
.file "function.c"
.align 4
.zero 4 .text
.globl add
adicionar: .LFB0:
.cfi_startproc pushl% ebp .cfi_def_cfa_offset 8 .cfi_offset 5, -8 movl% esp,% ebp .cfi_def_cfa_register 5 subl $ 20,% esp flds 8
(% ebp) fadds 12 (% ebp) fstps -4 (% ebp) movl -4 (% ebp),% eax movl% eax, -20 (% ebp) flds -20 (% ebp) licença
.cfi_endproc
.LFE0:
fmulp% st,% st (1) fstps -4 (% ebp) movl -4 (% ebp),% eax movl% eax, -20 (% ebp) flds -20 (% ebp) licença
.cfi_endproc
add_and_multiply, .rodata
1077936128
-add_and_multiply
.grande
.ident
.seção
.LC1:
.LFE1:
O mesmo arquivo (function.c) pode ser montado no formato assembler Intel executando o seguinte comando, $ gcc -S -masm = intel function.c -o
function.s que resulta com o arquivo assembler mostrado na Listagem 2-7 .
.align 4
.globl add
adicionar: .LFB0:
fadd DWORD PTR [ebp + 12] fstp DWORD PTR [ebp-4] mov eax, DWORD PTR [ebp-4]
sair
.cfi_endproc
.LFE0:
e
.size .globl .typ add_and_multiply: .LFB1:
.cfi_endproc
.LFE1:
add_and_multiply, .rodata
1077936128
.-add_and_multiply
.grande
.ident
.seção
.LC1:
Otimização
Uma vez criada a primeira versão do assembler correspondente ao código-fonte original, inicia-se o esforço de otimização, no qual o uso dos registradores é
minimizado. Além disso, a análise pode indicar que certas partes do código não precisam de fato ser executadas e essas partes do código são eliminadas.
Emissão de código
Finalmente, chegou o momento de criar a saída da compilação: arquivos-objeto, um para cada unidade de tradução. As instruções de montagem (escritas em
código ASCII legível) são neste estágio convertidas nos valores binários das instruções de máquina correspondentes (opcodes) e escritas nos locais específicos nos
arquivos de objeto.
O arquivo objeto ainda não está pronto para ser servido como refeição para o processador faminto. Os motivos são o tópico essencial de todo o livro. O tópico
interessante neste momento é a análise de um arquivo-objeto.
Ser um arquivo binário torna o arquivo objeto substancialmente diferente das saídas dos procedimentos de pré-processamento e montagem, ambos os quais são
arquivos ASCII, inerentemente legíveis por humanos. As diferenças se tornam mais óbvias quando nós, os humanos, tentamos olhar mais de perto o conteúdo.
Além da escolha óbvia de usar o editor hexadecimal (não é muito útil, a menos que você escreva compiladores para viver), um procedimento específico chamado
desmontagem é usado para obter uma visão detalhada do conteúdo de um arquivo de objeto.
No caminho geral dos arquivos ASCII em direção aos arquivos binários adequados para execução na máquina de concreto, a desmontagem pode ser vista como
um pequeno desvio em que o arquivo binário quase pronto é convertido no arquivo ASCII para ser servido a os olhos curiosos do desenvolvedor de software.
Felizmente, esse pequeno desvio serve apenas para fornecer ao desenvolvedor uma melhor orientação e normalmente não é realizado sem uma causa real.
file:///C:/Users/noels/Desktop/Advanced C and C++ Compiling ( PDFDrive ).html 15/235
21/09/2021 22:07 Advanced C and C++ Compiling ( PDFDrive ).html
A menos que seja especificado de outra forma, a saída do pré-processador é o arquivo que tem o mesmo nome do arquivo de entrada e cuja extensão de arquivo é
.o.
O conteúdo do arquivo de objeto gerado não é adequado para visualização em um editor de texto. O editor / visualizador hexadecimal é um pouco mais
adequado, pois não será confundido com caracteres não imprimíveis e ausências de caracteres de nova linha. A Figura 2-2 mostra o conteúdo binário do arquivo
objeto function.o gerado pela compilação do arquivo function.c deste projeto de demonstração.
00000010 01 00 03 00 Ol 00 00 00 00 00 00 00 00 00 00 00 1 .......
00000050 e5 83 ec lc 8b 45 0C 89 44 24 04 8b 45 08 89 04 | ..... E. .D $ .. E. .. |
00000060 24 e8 fc ff ff ff d9 5d fc d9 45 fc d9 05 00 00 1 $ ...... ] .. E ..... |
00000070 00 00 de C9 d9 5 <i fc 8b 45 fc 89 45 ec 09 45 ec 1 .....] • .E., E..E. |
00000080 C9 c3 00 00 00 00 40 40 00 47 43 43 3a 20 28 55 1 ...... @ .GCC: (U |
00000090 62 75 5e 74 75 2f 4c 69 6e 61 72 6f 20 34 2e 36 | buntu / Linaro 4.6 |
OOOOOOaO 2e 33 2d 31 75 62 75 6e 74 75 35 29 20 34 2e 36 | .3-lubuntuS) 4.6 |
OOOOOObO 2e 33 00 00 14 00 00 00 00 00 00 00 01 7a 52 00 | .3 ..... ...... ZR. j
O0O0O0C0 01 7c 08 01 Libra 0C 04 04 88 Ol 00 00 lc 00 00 00 M .....
0000OOf0 3c 00 00 00 la 00 00 00 34 00 00 00 00 41 Oe 08
00000190 00 00 00 00 Se 00 00 00 01 00 00 00 06 00 00 00 1 .......
OOOOOlfO 00 00 00 00 84 00 00 00 00 00 00 00 00 00 00 00 1 .......
00000200 00 00 00 00 04 00 00 00 00 00 00 00 2b 00 00 00 1 .......
00000210 08 00 00 00 03 00 00 00 00 00 00 00 84 00 00 00 1 .......
00000220 04 00 00 00 00 00 00 00 00 00 00 00 04 00 00 00 1 .......
00000230 00 00 00 00 30 00 00 00 Ol 00 00 00 02 00 00 00 | ____ 0. .
00000240 00 00 00 00 84 00 00 00 04 00 00 00 00 00 00 00 1 .......
00000270 2b 00 00 00 00 00 00 00 00 00 00 00 01 00 00 00
00000280 01 00 00 00 41 00 00 00 01 00 00 00 00 00 00 00 | . ..UMA..
00000290 00 00 00 00 b3 00 00 00 00 00 00 00 00 00 00 00 ! .......
0O0OO2d0 00 00 00 00 51 00 00 00 09 00 00 00 00 00 00 00
0OO0O2e0 00 00 00 00 78 04 00 00 10 00 00 00 Ob 00 00 00
Obviamente, apenas dar uma olhada nos valores hexadecimais do arquivo objeto não nos diz muito. O procedimento de desmontagem tem o potencial de nos
dizer muito mais.
A ferramenta Linux chamada objdump (parte do popular pacote binutils ) é especializada em desmontar os arquivos binários, entre muitas outras coisas. Além de
converter a sequência de instruções binárias de máquina específicas para uma determinada plataforma, ele também especifica os endereços nos quais as instruções
residem.
Não deve ser uma grande surpresa que ele suporta AT&T (padrão), bem como os sabores Intel de impressão do código assembler.
0 55 Empurre % ebp
1 89 e5 mov % esp,% ebp
3 83 ec 14 sub $ 0x14,% esp
6 d9 45 08 flds 0x8 (% ebp)
9 d8 45 0c modismos 0xc (% ebp)
c d9 5d fc fstps -0x4 (% ebp)
f 8b 45 fc mov -0x4 (% ebp),% eax
12 89 45 ec mov % eax, -0x14 (% ebp)
15 d9 45 ec flds -0x14 (% ebp)
18 c9 sair
19 c3 ret
0000001a <add_and_multiply>:
1a 55 Empurre % ebp
1b 89 e5 mov % esp,% ebp
1d 83 ec 1c sub $ 0x1c,% esp
20 8b 45 0c mov 0xc (% ebp),% eax
23 89 44 24 04 mov % eax, 0x4 (% esp)
27 8b 45 08 mov 0x8 (% ebp),% eax
2a 89 04 24 mov % eax, (% esp)
2d e8 fc ff ff ff ligar 2e <adicionar e multiplicar + 0x14>
32 d9 5d fc fstps -0x4 (% ebp)
35 d9 45 fc flds -0x4 (% ebp)
38 d9 05 00 00 00 00 flds 0x0
3e de c9 fmulp % st,% st (1)
40 d9 5d fc fstps -0x4 (% ebp)
43 8b 45 fc mov -0x4 (% ebp),% eax
46 89 45 ec mov % eax, -0x14 (% ebp)
49 d9 45 ec flds -0x14 (% ebp)
4c c9 sair
4d c3 ret
00000000 <.rodata>: 0: 00 00 2: 40 3: 40
3: 43
4: 3a 20
6: 28 55 62
9: 75 6e
b: 74 75
d: 2f
e: 4c
f: 69 6e 61 72 6f
16: 2e 36 2e 33 2d
1d: 62 75
1f: 6e
20: 74 75
22: 35 29 20 34 2e
27: 36 2e 33 00
inc% ebx
cmp (% eax),% ah
je 82 <add_and_multiply + 0x68>
das
dez% esp
je 97 <add_and_multiply + 0x7d>
0: 14 00 adc $ 0x0,% al
2: 00 00 adicionar % al, (% eax)
4: 00 00 adicionar % al, (% eax)
6: 00 00 adicionar % al, (% eax)
8: 01 7a 52 adicionar % edi, 0x52 (% edx)
b: 00 01 adicionar % al, (% ecx)
d: 7c 08 jl 17 <.eh frame + 0x17>
f: 01 1b adicionar % ebx, (% ebx)
11: 0c 04 ou $ 0x4,% al
24 1a 00 sbb (% eax),% al
3c 3c 00 cmp $ 0x0,% al
40 1a 00 sbb (% eax),% al
44 34 00 xor $ 0x0,% al
53 0c 04 ou $ 0x4,% al
55 04 00 adicionar $ 0x0,% al
Da mesma forma, ao especificar o tipo Intel, $ objdump -D -M intel <arquivo de entrada> .o, você obtém o seguinte conteúdo impresso na tela do
terminal:
0: 55 Empurre vazante
19: c3 ret
0000001a <add_and_multiply>:
1a 55 Empurre vazante
4d c3 ret
adicionar
0: 00 00
00000000 <.rodata>: 0: 00 00 2: 40 3: 40
eax
eax
00000000 <.comment>:
0: 00 47 43
3: 43
4: 3a 20
6: 28 55 62
9: 75 6e
b: 74 75
d: 2f
e: 4c
f: 69 6e 61 72
16: 2e 36 2e 33
1d: 62 75
1f: 6e
20: 74 75
22: 35 29 20 34
27: 36 2e 33 00
jne je das
dez esp
outs dx, BYTE PTR ds: [esi] je 97 <add_and_multiply + 0x7d> xor eax, 0x2e342029 ss xor eax, DWORD PTR cs: ss: [eax]
ah, BYTE PTR [eax] BYTE PTR [ebp + 0x62], dl 79 <add_and_multiply + 0x5f> 82 <add_and_multiply + 0x68>
20 34 31 75
0 14 00 adc
2 00 00 adicionar
4 00 00 adicionar
6 00 00 adicionar
8 01 7a 52 adicionar
b 00 01 adicionar
d 7c 08 jl
f 01 1b adicionar
11 0c 04 ou
13 04 88 adicionar
15 01 00 adicionar
17 00 1c 00 adicionar
1a 00 00 adicionar
1c 1c 00 sbb
1e 00 00 adicionar
20 00 00 adicionar
22 00 00 adicionar
24 1a 00 sbb
26 00 00 adicionar
28 00 41 0e adicionar
2b 08 85 02 42 0d 05 ou
31 56 Empurre
32 c5 0c 04 lds
35 04 00 adicionar
37 00 1c 00 adicionar
3a 00 00 adicionar
3c 3c 00 cmp
3e 00 00 adicionar
40 1a 00 sbb
42 00 00 adicionar
44 34 00 xor
46 00 00 adicionar
48 00 41 0e adicionar
4b 08 85 02 42 0d 05 ou
51 70 c5 jo
53 0c 04 ou
55 04 00 adicionar
al, 0x0
al, 0x88
DWORD PTR [eax], eax BYTE PTR [eax + eax * 1], bl BYTE PTR [eax], al al, 0x0
esi
BYTE PTR [eax + eax * 1], bl BYTE PTR [eax], al al, 0x0
BYTE PTR [eax], al al, BYTE PTR [eax] BYTE PTR [eax], al al, 0x0
al, 0x0
A saída do processo de compilação é um ou mais arquivos de objetos binários, cuja estrutura é o próximo tópico de interesse natural. Como você verá em breve, a
estrutura dos arquivos de objetos contém muitos detalhes importantes no caminho para realmente compreender o quadro mais amplo.
Em um esboço,
• Um arquivo-objeto é o resultado da tradução de seu arquivo-fonte original correspondente. O resultado da compilação é a coleção de tantos arquivos de objeto
quantos arquivos de origem no projeto.
Após a conclusão da compilação, o arquivo-objeto continua representando seu arquivo-fonte original em estágios subsequentes do processo de construção do
programa.
• Os ingredientes básicos de um arquivo de objeto são os símbolos (referências aos endereços de memória no programa ou memória de dados), bem como as
seções.
Entre as seções mais freqüentemente encontradas nos arquivos de objeto estão o código (.texto), dados inicializados (.data), dados não inicializados (.bss) e
algumas das seções mais especializadas (informações de depuração, etc.).
• A intenção final por trás da ideia de construir o programa é que as seções obtidas pela compilação de arquivos-fonte individuais sejam combinadas (lado a lado)
em um único arquivo executável binário.
Esse arquivo binário conteria as seções do mesmo tipo (.text, .data, .bss,...) Obtidas agrupando as seções dos arquivos individuais. Falando figurativamente, um
arquivo de objeto pode ser visto como um simples ladrilho esperando para encontrar seu lugar no gigante mosaico do mapa de memória do processo.
• A estrutura interna do arquivo de objeto não sugere, entretanto, onde as seções individuais residirão no mapa de memória do programa. Por esse motivo, os
intervalos de endereço de cada seção em cada um dos arquivos de objeto são provisoriamente configurados para começar a partir de um valor zero.
O intervalo de endereços real no qual uma seção de um arquivo de objeto residirá no mapa do programa será determinado nos estágios subsequentes (vinculação)
do processo de construção do programa.
• No processo de agrupar as seções dos arquivos de objeto no mapa de memória do programa resultante, o único parâmetro verdadeiramente importante é o
comprimento de suas seções ou, para ser mais preciso, seu intervalo de endereços.
• A contribuição do arquivo objeto para a seção .bss (dados não inicializados) do programa é muito rudimentar; a seção .bss é descrita meramente por seu
comprimento de byte. Essas poucas informações são exatamente o que é necessário para o carregador estabelecer a seção .bss como uma parte da memória na qual
alguns dados serão armazenados.
Em geral, as informações são armazenadas nos arquivos objeto de acordo com um determinado conjunto de regras resumidas na forma de especificação de
formato binário, cujos detalhes variam entre as diferentes plataformas (Windows vs. Linux, 32 bits vs. 64 bits, x86 vs. família de processadores ARM).
Normalmente, as especificações de formato binário são projetadas para suportar as construções da linguagem C / C ++ e os problemas de implementação
associados. Muito frequentemente, a especificação do formato binário cobre uma variedade de modos de arquivo binário, como executáveis, bibliotecas estáticas e
bibliotecas dinâmicas.
No Linux, o Executable and Linkable Format (ELF) ganhou prevalência. No Windows, os binários geralmente estão em conformidade com a especificação de
formato PE / COFF.
Passo a passo, as peças do gigantesco quebra-cabeça do processo de construção do programa estão começando a se encaixar e a imagem ampla e clara de toda a
história emerge lentamente. Até agora, você aprendeu que o processo de compilação converte os arquivos de origem ASCII na coleção correspondente de arquivos
de objetos binários. Cada um dos arquivos objeto contém seções, o destino de cada uma é, em última análise, tornar-se parte do gigantesco quebra-cabeça do mapa
de memória do programa, conforme ilustrado na Figura 2-3.
filel.o
sem Arquivo
file2.o
A tarefa que resta é agrupar as seções individuais armazenadas em arquivos de objetos individuais no corpo do mapa de memória do programa. Conforme
mencionado nas seções anteriores, essa tarefa precisa ser deixada para outro estágio do processo de construção do programa, denominado vinculação.
A pergunta que um observador cuidadoso não pode deixar de fazer (antes de entrar nos detalhes do procedimento de vinculação) é exatamente por que precisamos
de um novo estágio do processo de construção, ou mais precisamente, exatamente por que o processo de compilação descrito não pode até agora, completou a parte de mosaico da
tarefa?
Existem algumas razões muito sólidas para dividir o procedimento de construção e o restante desta seção tentará esclarecer as circunstâncias que levaram a tal
decisão.
Em segundo lugar, o princípio de reutilização de código aplicado ao processo de construção do programa (e a capacidade de combinar as partes binárias
provenientes de vários projetos) afirmou definitivamente a decisão de implementar a construção C / C ++ como um procedimento de duas etapas (compilar e
vincular) .
No entanto, há uma parte de toda a história que pode causar alguns problemas: embora o código-fonte esteja agrupado nos arquivos-fonte dedicados, fazer parte
do mesmo programa implica que certas conexões mútuas devem existir. Na verdade, as conexões entre as partes distintas do código são normalmente
estabelecidas por meio das duas opções a seguir:
Por exemplo, uma função no arquivo de origem relacionado à GUI de um aplicativo de bate-papo pode chamar uma função no arquivo de origem de rede TCP /
IP, que por sua vez pode chamar uma função localizada no arquivo de origem de criptografia.
• Variáveis externas:
No domínio da linguagem de programação C (substancialmente menos no domínio C ++), era uma prática comum reservar variáveis globalmente visíveis para
manter o estado de interesse para várias partes do código. Uma variável destinada a um uso mais amplo é geralmente declarada em um arquivo de origem como
variável global e referenciada a partir de todos os outros arquivos de origem como variável externa.
Um exemplo típico é a variável errno usada em bibliotecas C padrão para manter o valor do último erro encontrado.
Para acessar qualquer um dos dois (comumente chamados de símbolos), seus endereços (mais precisamente, o endereço da função na memória do programa e /
ou o endereço da variável global na memória de dados) devem ser conhecidos.
No entanto, o endereço real não pode ser conhecido antes que as seções individuais sejam incorporadas na seção do programa correspondente (ou seja, antes que a
seção ladrilhada seja concluída !!!). Até então, uma conexão significativa entre uma função e seu chamador e / ou acesso à variável externa é impossível de
estabelecer, ambos devidamente relatados como referências não resolvidas. Observe que esse problema não ocorre quando a função ou variável global é
referenciada a partir do mesmo arquivo de origem em que foi definida. Nesse caso particular, tanto a função / variável quanto seu chamador / usuário acabam
fazendo parte da mesma seção, e suas posições em relação umas às outras são conhecidas antes da "conclusão do grande quebra-cabeça". Nesses casos, assim que
o revestimento das seções for concluído,
Conforme mencionado anteriormente nesta seção, resolver esse tipo de problema ainda não exige que um procedimento de construção seja dividido em dois
estágios distintos. Na verdade, muitas linguagens diferentes implementam com sucesso um procedimento de compilação de uma passagem. No entanto, o
conceito de reutilização (reutilização binária, neste caso) aplicado ao domínio da construção do programa (e o conceito de bibliotecas), em última análise, confirma
a decisão de dividir a construção do programa em dois estágios (compilar e vincular).
Linking
A segunda etapa do processo de construção do programa é a vinculação.A entrada para o processo de vinculação é a coleção de arquivos-objeto criados pelo
estágio de compilação concluído anteriormente. Cada arquivo de objeto pode ser visto como armazenamento binário de contribuições de arquivos de origem
individuais para as seções do mapa de memória do programa de todos os tipos (código, dados inicializados, dados não inicializados, informações de depuração,
etc.). A tarefa final do vinculador é formar a seção do mapa de memória do programa resultante a partir de contribuições individuais e resolver todas as
referências. Como um lembrete, o conceito de memória virtual simplificou a tarefa do vinculador na medida em que permite assumir que o mapa de memória do
programa que o vinculador precisa preencher é um intervalo de endereço baseado em zero de tamanho idêntico para cada programa, independentemente do que
faixa de endereços o processo será fornecido pelo sistema operacional em tempo de execução.
Para simplificar, cobrirei neste exemplo o caso mais simples possível, no qual as contribuições para as seções do mapa de memória do programa vêm
exclusivamente dos arquivos pertencentes ao mesmo projeto. Na realidade, devido ao avanço do conceito de reutilização de binários, isso pode não ser verdade.
Estágios de ligação
O processo de vinculação acontece por meio de uma sequência de estágios (realocação, resolução de referência), que serão discutidos em detalhes a seguir.
Relocação
O primeiro estágio de um procedimento de vinculação nada mais é do que tiling, um processo no qual seções de vários tipos contidos em arquivos de objetos
individuais são combinados para criar as seções do mapa de memória do programa (consulte a Figura 2-4). Para completar esta tarefa, os intervalos de endereços
baseados em zero, anteriormente neutros, das seções de contribuição são traduzidos em intervalos de endereços mais concretos do mapa de memória do programa
resultante.
Dados
Código
Arquivo de objeto
A expressão "mais concreto" é usada para enfatizar o fato de que a imagem do programa resultante criada pelo vinculador ainda é neutra por si só. Lembre-se de
que o mecanismo de endereçamento virtual torna possível que todo e qualquer programa tenha a mesma visão simples e idêntica do espaço de endereço do
N
programa (que reside entre 0 e 2 ), enquanto o endereço físico real no qual o programa é executado é determinado em tempo de execução pelo sistema
operacional, invisível para o programa e o programador.
Assim que o estágio de realocação for concluído, a maior parte (mas não todos!) Do mapa de memória do programa foi criada.
Resolvendo Referências
Agora vem a parte difícil. Tomar seções, traduzir linearmente seus intervalos de endereços em intervalos de endereços do mapa de memória do programa foi uma
tarefa bastante fácil. Uma tarefa muito mais difícil é estabelecer as conexões necessárias entre as várias partes do código, tornando o programa homogêneo.
Vamos supor (com razão, dada a simplicidade deste programa de demonstração) que todos os estágios de construção anteriores (compilação completa, bem como
realocação de seção) foram concluídos com êxito. Agora é o momento de apontar exatamente quais tipos de problemas são deixados para a última etapa de
conexão resolver.
Como mencionado anteriormente, a causa raiz dos problemas de ligação é bastante simples: pedaços de código originados de unidades de tradução diferentes (ou
seja, arquivos de origem) e estão tentando fazer referência uns aos outros, mas não podem saber onde na memória esses itens irão residir até o os arquivos de
objeto são colocados lado a lado no corpo do mapa de memória do programa. Os componentes do código que causam mais problemas são aqueles fortemente
ligados ao endereço nas variáveis de memória do programa (pontos de entrada de função) ou na memória de dados (global / estático / externo).
• A função add_and_multiply chama a função add, que reside no mesmo arquivo de origem (ou seja, a mesma unidade de tradução no mesmo arquivo objeto).
Nesse caso, o endereço na memória do programa da função add () é, até certo ponto, uma quantidade conhecida e pode ser expresso por seu deslocamento
relativo da seção de código do arquivo-objeto function.o.
• Agora, a função principal chama a função add_and_multiply e também faz referência à variável externa nCompletionStatus e tem grandes problemas
para descobrir o endereço real da memória do programa em que residem. Na verdade, ele apenas pode assumir que esses dois símbolos em algum ponto no futuro
residirão em algum lugar no mapa de memória do processo. Mas, até que o mapa de memória seja formado, dois itens não podem ser considerados nada mais do
que referências não resolvidas.
function.o
main.o
O
D-
a Principal
n Status de conclusão
i = (d = i
add_and_multiply () (?) > '
Referências não resolvidas
Figura 2-5. O problema das referências não resolvidas em sua forma essencial
Para resolver esse tipo de problema, deve ocorrer uma etapa de encadeamento de resolução das referências. O que o vinculador precisa fazer nessa situação é
• Descubra qual parte do código faz chamadas fora de sua seção original.
• Descobrir onde exatamente (em qual endereço no mapa de memória) reside a parte referenciada do código.
Depois que o vinculador conclui sua mágica, a situação pode ser semelhante à Figura 2-6.
Na abordagem tudo de uma vez, a mesma operação pode ser concluída invocando o compilador e o vinculador com apenas um comando.
Para os fins desta demonstração, vamos seguir a abordagem passo a passo, pois isso irá gerar o arquivo de objeto main.o , que contém detalhes muito
importantes que desejo demonstrar aqui. A desmontagem do arquivo main.o,
0: 55 Empurre vazante
3: 83 e4 f0 e esp, 0xfffffff0
42: c9 sair
43: c3 ret
A linha 2a apresenta uma instrução de chamada que salta para si mesma (estranho, não é?), Enquanto a linha 33 apresenta o acesso da variável residente no
endereço 0x0 (ainda mais estranho). Obviamente, esses dois valores obviamente estranhos foram inseridos pelo linker propositalmente.
A saída desmontada do executável de saída, no entanto, mostra que não apenas o conteúdo do arquivo de objeto main.o foi realocado para a faixa de endereço
começando no endereço 0x08048404, mas também esses dois pontos problemáticos foram resolvidos pelo vinculador.
080483ce <add_and_multiply>:
8048400 c9 sair
8048401 c3 ret
8048402 90 nop
8048403 90 nop
08048404 <main>:
A linha no endereço do mapa de memória 0x8048437 faz referência à variável que reside no endereço 0x804a018. A única questão em aberto agora é o que reside
naquele endereço específico?
A versátil ferramenta objdump pode ajudá-lo a obter a resposta a essa pergunta (uma parte decente dos capítulos subsequentes é dedicada a essa ferramenta
excepcionalmente útil). Executando o seguinte comando
você pode desmontar a seção .bss carregando os dados não inicializados, o que revela que sua variável nCompletionStatus reside exatamente no endereço
0x804a018, conforme mostrado na Figura 2-7.
TABELA DE SÍMBOLOS:
08043010 eu d .bss
08043010 eu 0 . bss
08043014 eu 0 .bss
08043018 9 0 .bss
.bss
completado.6159
dtor_idx.6l61
file:///C:/Users/noels/Desktop/Advanced C and C++ Compiling ( PDFDrive ).html 28/235
21/09/2021 22:07 Advanced C and C++ Compiling ( PDFDrive ).html
nConpletionStatus
Oüo
Agora que você conhece os meandros da tarefa de vinculação, é útil diminuir um pouco o zoom e tentar resumir a filosofia que orienta o vinculador durante a
execução de suas tarefas usuais. Na verdade, o vinculador é uma ferramenta específica que, ao contrário de seu irmão mais velho, o compilador, não está
interessada nos mínimos detalhes do código escrito. Em vez disso, ele vê o mundo como um conjunto de arquivos de objetos que (bem como peças de um quebra-
cabeça) estão prestes a ser combinados em uma imagem mais ampla do mapa de memória do programa, conforme ilustrado na Figura 2-8.
oooooo
Os símbolos precisam ser resolvidos; eles estão localizados dentro de outras unidades de tradução
C
TTT ...... ??
:(? Y
TTT ...... ??
: <? X: <? X
Segmento de código (.text)
EH
TTT ...... ??
Outros segmentos ...
.TTT — YT
TTT ...... ??
TTT ...... ??
TTT ...... ??
ßlX
Não é preciso muita imaginação para descobrir que a Figura 2-8 tem muita semelhança com a parte esquerda da Figura 2-9, ao passo que a tarefa final do linkador
poderia ser representada pela parte direita da mesma figura.
D
3H
Entre todos os símbolos contidos no arquivo executável, um lugar muito único pertence à função principal , pois do ponto de vista dos programas C / C ++ é a
função a partir da qual toda a execução do programa começa. No entanto, esta não é a primeira parte do código que é executada quando o programa é iniciado.
Um detalhe excepcionalmente importante que precisa ser apontado é que o arquivo executável não é inteiramente feito de código compilado dos arquivos-fonte
do projeto. Na verdade, um trecho de código estrategicamente importante responsável por iniciar a execução do programa é adicionado no estágio de vinculação
ao mapa de memória do programa. Este código de objeto, que o vinculador normalmente armazena no início do mapa de memória do programa, vem em duas
variantes:
• ertO é o ponto de entrada "plain vanilla", a primeira parte do código do programa que é executado sob o controle do kernel.
• crtl é a rotina de inicialização mais moderna com suporte para tarefas a serem concluídas antes que a função principal seja executada e depois que o
programa terminar.
Tendo esses detalhes em mente, a estrutura geral do executável do programa pode ser representada simbolicamente pela Figura 2-10.
% esp -> ■
brk
Estruturas de dados específicas do processo (por exemplo, tabelas de página, tarefas e estruturas de mm, pilha de kernel)
Memória física
Pilha de usuário
Como você verá mais adiante no capítulo dedicado a bibliotecas dinâmicas e vinculação dinâmica, essa parte extra de código, que é fornecida pelo sistema
operacional, faz quase toda a diferença entre um executável e uma biblioteca dinâmica; o último não tem essa parte específica do código.
Mais detalhes sobre a sequência de etapas que acontecem quando a execução de um programa começa serão discutidos no próximo capítulo.
Assim como dirigir um automóvel não pode ser imaginado sem o motor e um conjunto de quatro rodas, a execução do programa não pode ser imaginada sem as
seções de código (.text) e de dados (.data e / ou .bss). Esses ingredientes são naturalmente a parte essencial da funcionalidade mais básica do programa.
No entanto, assim como o automóvel não é apenas o motor e as quatro rodas, o arquivo binário contém muito mais seções. Para sincronizar com precisão a
variedade de tarefas operacionais, o vinculador cria e insere no arquivo binário muitos outros tipos de seção diferentes.
Por convenção, o nome da seção começa com o caractere ponto (.). Os nomes dos tipos de seção mais importantes são independentes da plataforma; eles são
chamados da mesma forma, independentemente da plataforma e do formato binário ao qual pertencem.
Ao longo deste livro, os significados e funções de certos tipos de seção no esquema geral das coisas serão discutidos longamente. Esperançosamente, quando o
livro for lido, o leitor terá uma compreensão substancialmente mais ampla e focada das seções do arquivo binário.
Na Tabela 2-1, a especificação do formato binário ELF predominante do Linux traz a seguinte ( http://man7.org/ linux / man-pages / man5 /
elf.5.html) lista de vários tipos de seção fornecidos no ordem alfabética. Mesmo que as descrições das seções individuais sejam um pouco escassas, dar uma
olhada na variedade de seções neste ponto pode dar ao leitor uma ideia bastante boa sobre a variedade disponível.
.bss Esta seção contém os dados não inicializados que contribuem para a imagem da memória do programa.
.comment Esta seção contém informações de controle de versão. Esta seção é do tipo SHT_PROGBITS.
Esta seção contém ponteiros inicializados para as funções do construtor C ++. Esta seção é do tipo SHT_PROGBITS. Os tipos de atributos são SHF_ALLOC e
SHF_WRITE.
Esta seção contém dados inicializados que contribuem para a imagem da memória do programa. Esta seção é do tipo SHT_PROGBITS. Os tipos de atributos são
SHF_ALLOC e SHF_WRITE.
Esta seção contém dados inicializados que contribuem para a imagem da memória do programa. Esta seção é do tipo SHT_PROGBITS. Os tipos de atributos são
SHF_ALLOC e SHF_WRITE.
Esta seção contém informações para depuração simbólica. Os conteúdos não são especificados. Esta seção é do tipo SHT_PROGBITS. Nenhum tipo de atributo é
usado.
Esta seção contém ponteiros inicializados para as funções destruidoras do C ++. Esta seção é do tipo SHT_PROGBITS. Os tipos de atributos são SHF_ALLOC e
SHF_WRITE.
Esta seção contém informações de links dinâmicos. Os atributos da seção incluem o bit SHF_ALLOC . Se o bit SHF_WRITE está definido é específico do processador.
Esta seção é do tipo SHT_DYNAMIC. Veja os atributos acima
(continuação) 38
.ctors
.dados
.data1
.depurar
.dtors
.dinâmico
Esta seção contém as strings necessárias para a vinculação dinâmica, mais comumente as strings que representam os nomes associados às entradas da tabela de
símbolos. Esta seção é do tipo SHT_STRTAB. O tipo de atributo usado é SHF_ALLOC.
Esta seção contém a tabela de símbolos de vínculo dinâmico. Esta seção é do tipo SHT_DYNSYM. O atributo usado é SHF_ALLOC.
Esta seção contém instruções executáveis que contribuem para o código de encerramento do processo. Quando um programa é encerrado normalmente, o sistema
organiza a execução do código nesta seção. Esta seção é do tipo SHT_PROGBITS. Os atributos usados são SHF_ALLOC e SHF_EXECINSTR.
Esta seção contém a tabela de símbolos de versão, uma matriz de elementos ElfN_Half. Esta seção é do tipo SHT_GNU_versym. O tipo de atributo usado é
SHF_ALLOC.
Esta seção contém as definições de símbolo de versão, uma tabela de estruturas ElfN_Verdef. Esta seção é do tipo SHT_GNU_verdef . O tipo de atributo usado é
SHF_ALLOC.
Esta seção contém os elementos necessários do símbolo de versão, uma tabela de estruturas ElfN_Verneed. Esta seção é do tipo SHT_GNU_versym. O tipo de
atributo usado é SHF_ALLOC.
Esta seção contém a tabela de deslocamento global. Esta seção é do tipo SHT_PROGBITS. Os atributos são específicos do processador.
Esta seção contém a tabela de ligação do procedimento. Esta seção é do tipo SHT_PROGBITS. Os atributos são específicos do processador.
Esta seção contém uma tabela de hash de símbolos. Esta seção é do tipo SHT_HASH. O atributo usado é SHF_ALLOC.
Esta seção contém instruções executáveis que contribuem para o código de inicialização do processo. Quando um programa começa a ser executado, o sistema
organiza a execução do código nesta seção antes de chamar o ponto de entrada do programa principal. Esta seção é do tipo SHT_PROGBITS. Os atributos usados
são SHF_ALLOC e SHF_EXECINSTR.
Esta seção contém o nome do caminho de um interpretador de programa. Se o arquivo tiver um segmento carregável que inclui a seção, os atributos da seção
incluirão o bit SHF_ALLOC . Caso contrário, esse bit estará desligado. Esta seção é do tipo SHT_PROGBITS.
Esta seção contém as informações do número da linha para depuração simbólica, que descreve a correspondência entre a fonte do programa e o código de
máquina. O conteúdo não foi especificado. Esta seção é do tipo SHT_PROGBITS. Nenhum tipo de atributo é usado.
Esta seção é usada em arquivos de objeto Linux para declarar atributos de pilha. Esta seção é do tipo SHT_PROGBITS. O único atributo usado é SHF_EXECINSTR.
Isso indica ao vinculador GNU que o arquivo-objeto requer uma pilha executável.
.dynstr
.dynsym .fini
.gnu.version
.gnu.version_d
.gnu_version.r
.conseguiu
.got.plt
.cerquilha
.iniciar
.interp
.linha
.Nota
.note.GNU-stack .plt
Descrição
Nome da Seção
Esta seção contém a tabela de ligação do procedimento. Esta seção é do tipo SHT_PROGBITS. Os atributos são específicos do processador.
(contínuo)
Esta seção contém informações de realocação conforme descrito abaixo. Se o arquivo tiver um segmento carregável que inclui realocação, os atributos da seção
incluirão o bit SHF_ALLOC . Caso contrário, a broca estará desligada. Por convenção, "NOME" é fornecido pela seção à qual se aplicam as realocações. Portanto,
uma seção de realocação para .text normalmente teria o nome .rel.text. Esta seção é do tipo SHT_REL.
Esta seção contém informações de realocação conforme descrito abaixo. Se o arquivo tiver um segmento carregável que inclui realocação, os atributos da seção
incluirão o bit SHF_ALLOC . Caso contrário, a broca estará desligada. Por convenção, "NOME" é fornecido pela seção à qual se aplicam as realocações. Portanto,
uma seção de realocação para .text normalmente teria o nome .rela.text. Esta seção é do tipo SHT_RELA.
Esta seção contém dados somente leitura que normalmente contribuem para um segmento não gravável na imagem do processo. Esta seção é do tipo
SHT_PROGBITS. O atributo usado é SHF_ALLOC.
Esta seção contém dados somente leitura que normalmente contribuem para um segmento não gravável na imagem do processo. Esta seção é do tipo
SHT_PROGBITS. O atributo usado é SHF_ALLOC.
Esta seção contém nomes de seção. Esta seção é do tipo SHT_STRTAB. Nenhum tipo de atributo é usado.
Esta seção contém strings, mais comumente as strings que representam os nomes associados às entradas da tabela de símbolos. Se o arquivo tiver um segmento
carregável que inclua a tabela de sequência de símbolos, os atributos da seção incluirão o bit SHF_ALLOC . Caso contrário, a broca estará desligada. Esta seção é do
tipo SHT_STRTAB.
Esta seção contém uma tabela de símbolos. Se o arquivo tiver um segmento carregável que inclui a tabela de símbolos, os atributos da seção incluirão o bit
SHF_ALLOC . Caso contrário, a broca estará desligada. Esta seção é do tipo SHT_SYMTAB.
Esta seção contém o "texto" ou instruções executáveis de um programa. Esta seção é do tipo SHT PROGBITS. Os atributos usados são SHF ALLOC e SHF
EXECINSTR.
.relNAME
.relaNAME
.symtab .text
Descrição
Nome da Seção
O formato ELF oferece uma grande variedade de tipos de símbolos de vinculação, muito maiores do que você pode imaginar neste estágio inicial de seu caminho
para a compreensão dos meandros do processo de vinculação. No momento, você pode distinguir claramente que os símbolos podem ser de escopo local ou de
visibilidade mais ampla, normalmente necessários para os outros módulos. Ao longo do material do livro, mais tarde, os vários tipos de símbolos serão discutidos
em substancialmente mais detalhes.
A Tabela 2-2 apresenta a variedade de tipos de símbolos, conforme mostrado nas páginas do manual ( http://linux.die.net/man/1/nm ) do útil programa
utilitário de exame de símbolo nm . Como regra geral, a menos que explicitamente indicado (como no caso de "U" vs. "u"), a letra minúscula denota símbolos
locais, enquanto a letra maiúscula indica melhor visibilidade do símbolo (externo, global).
O valor do símbolo é absoluto e não será alterado por links adicionais. O símbolo está na seção de dados não inicializados (.bss).
O símbolo é comum. Os símbolos comuns são dados não inicializados. Ao vincular, vários símbolos comuns podem aparecer com o mesmo nome. Se o símbolo for
definido em qualquer lugar, os símbolos comuns serão tratados como referências indefinidas.
O símbolo está em uma seção de dados inicializada para pequenos objetos. Alguns formatos de arquivo de objeto permitem um acesso mais eficiente a pequenos
objetos de dados, como uma variável int global em oposição a uma grande matriz global.
Para arquivos de formato PE, isso indica que o símbolo está em uma seção específica para a implementação de DLLs. Para arquivos no formato ELF, isso indica
que o símbolo é uma função indireta. Esta é uma extensão GNU para o conjunto padrão de tipos de símbolos ELF. Indica um símbolo que, se referenciado por
uma relocação, não avalia seu endereço, mas deve ser invocado em tempo de execução. A execução do tempo de execução retornará então o valor a ser usado na
realocação.
O símbolo está em uma seção de dados não inicializada para pequenos objetos.
O símbolo é indefinido. Na verdade, esse binário não define esse símbolo, mas espera que ele eventualmente apareça como resultado do carregamento das
bibliotecas dinâmicas.
O símbolo é um símbolo global único. Esta é uma extensão GNU para o conjunto padrão de associações de símbolos ELF. Para tal símbolo, o vinculador dinâmico
se certificará de que em todo o processo haja apenas um símbolo com esse nome e tipo em uso.
O símbolo é um objeto fraco. Quando um símbolo definido como fraco é vinculado a um símbolo definido normal, o símbolo definido normal é usado sem erro.
Quando um símbolo fraco indefinido é vinculado e o símbolo não é definido, o valor do símbolo fraco torna-se zero sem erro. Em alguns sistemas, letras
maiúsculas indicam que um valor padrão foi especificado.
O símbolo é um símbolo fraco que não foi especificamente marcado como um símbolo de objeto fraco. Quando um símbolo definido fraco é vinculado a um
símbolo definido normal, o símbolo definido normal é usado sem erro. Quando um símbolo indefinido fraco é vinculado e o símbolo não é definido, o valor do
símbolo é determinado de uma maneira específica do sistema sem erros. Em alguns sistemas, maiúsculas indica que um valor padrão foi especificado.
"UMA"
"N" "p"
"VOCÊ"
"W" ou "w"
O símbolo é um símbolo de punhaladas em um arquivo de objeto a.out . Nesse caso, os próximos valores impressos são o campo stabs other, o campo stabs desc
e o tipo de stab. Os símbolos Stabs são usados para armazenar informações de depuração.
CAPÍTULO 3
Importância da Shell
A execução do programa sob o controle do usuário normalmente ocorre por meio de um shell, um programa que monitora as ações do usuário no teclado e no
mouse. O Linux apresenta muitos shells diferentes, sendo os mais populares sh, bash e tcsh.
Depois que o usuário digita o nome do comando e pressiona a tecla Enter, o shell tenta primeiro comparar o nome do comando digitado com o de seus próprios
comandos embutidos. Se o nome do programa for confirmado como não sendo nenhum dos comandos suportados pelo shell, o shell tentará localizar o binário
cujo nome corresponda à string de comando. Se o usuário digitou apenas o nome do programa (ou seja, não o caminho completo para o binário executável), o shell
tenta localizar o executável em cada uma das pastas especificadas pela variável de ambiente PATH. Uma vez que o caminho completo do binário executável é
conhecido, o shell ativa o procedimento para carregar e executar o binário.
A primeira ação obrigatória do shell é criar um clone de si mesmo bifurcando o processo filho idêntico. Criar o novo mapa de memória de processo copiando o
mapa de memória existente do shell parece um movimento estranho, pois é muito provável que um novo mapa de memória de processo não tenha nada em
comum com o mapa de memória do shell. Essa estranha manobra é feita por um bom motivo: assim, o shell efetivamente passa todas as suas variáveis de
ambiente para o novo processo. De fato, logo após o novo mapa de memória do processo ser criado, a maioria de seu conteúdo original é apagado / zerado (exceto
a parte que carrega as variáveis de ambiente herdadas) e sobrescrito com o mapa de memória do novo processo, que fica pronto para a execução estágio. A Figura
3-1 ilustra a ideia.
Processar estruturas de dados específicas (por exemplo, tabelas de página, tarefas e estruturas de mm, pilha de kernel)
Processar estruturas de dados específicas (por exemplo, tabelas de página, tarefas e estruturas de mm, pilha de kernel)
^ Meio Ambiente
1
O® ^ milan @ milan
O Shell primeiro cria uma cópia de seu próprio mapa de memória de processo, que será usado por um novo processo.
Logo depois disso, o kernel irá limpar o mapa de memória recém-criado, retendo apenas a parte que carrega as variáveis de ambiente.
A partir daí, tudo está pronto para o carregador preencher o mapa de memória vazio com o conteúdo encontrado no arquivo binário do programa
iniciado.
Figura 3-1. O shell começa a criar o novo mapa de memória de processo copiando seu próprio mapa de memória de processo, com a intenção de passar suas próprias variáveis de
ambiente para o novo processo
Deste ponto em diante, o shell pode seguir um dos dois cenários possíveis. Por padrão, o shell espera que seu processo de clonagem bifurcada conclua o comando
(ou seja, que o programa iniciado conclua a execução). Como alternativa, se o usuário digitar um "e" comercial após o nome do programa, o processo filho será
colocado em segundo plano e o shell continuará monitorando os comandos digitados subseqüentemente pelo usuário. O mesmo modo pode ser alcançado pelo
usuário não anexando o "e" comercial após o nome do executável; em vez disso, após o programa ser iniciado, o usuário pode pressionar Ctrl-Z (que emite o sinal
SIGSTOP para o processo filho) e imediatamente após digitar "bg" (que emite o sinal SIGCONT para o processo filho) na janela do shell, que irá causar o efeito
idêntico (colocar o processo filho do shell em segundo plano).
Um cenário muito semelhante de inicialização do programa ocorre quando o usuário aplica um clique do mouse no ícone do aplicativo. O programa que fornece o
ícone (como uma sessão gnome e / ou Nautilus File Explorer no Linux) assume a responsabilidade de traduzir o clique do mouse na chamada do sistema () , o
que faz com que uma sequência de eventos muito semelhante aconteça como se o aplicativo foram chamados digitando na janela do shell.
Papel do Kernel
Assim que o shell delega a tarefa de executar o programa, o kernel reage invocando uma função da família de funções exec , todas as quais fornecem
praticamente a mesma funcionalidade, mas diferem nos detalhes de como os parâmetros de execução são especificados. Independentemente de qual função do
tipo exec particular é escolhida, cada um deles faz uma chamada para a função sys_execve , que inicia o trabalho real de execução do programa.
A próxima etapa imediata (que acontece na função search_binary_handler (arquivo fs / exec.c) é identificar o formato executável. Além de suportar o
formato binário executável ELF mais recente, o Linux oferece compatibilidade com versões anteriores suportando vários outros formatos binários. O formato ELF
é identificado, o foco da ação passa para a função load_elf_binary (arquivo fs / binfmt_elf.c).
Depois que o formato executável é identificado como um dos formatos suportados, o esforço de preparação do mapa de memória do processo para a execução
começa. Em particular, o processo filho criado pelo shell (um clone do próprio shell) é passado do shell para o kernel com as seguintes intenções:
• O kernel obtém a sandbox (o ambiente de processo) e, mais importante, a memória associada, que pode ser usada para iniciar o novo programa.
A primeira coisa que o kernel fará é limpar completamente a maior parte do mapa de memória. Imediatamente depois, ele delegará ao carregador o processo de
preencher o mapa de memória apagado com os dados lidos do arquivo executável binário do novo programaSharePoint.
• Ao clonar o processo shell (por meio da chamada fork () ), as variáveis de ambiente definidas no shell são passadas para o processo filho, o que ajuda a que a
cadeia de herança das variáveis de ambiente não seja quebrada.
Função do carregador
Antes de entrar nos detalhes da funcionalidade do carregador, é importante ressaltar que o carregador e o vinculador têm perspectivas um tanto diferentes sobre o
conteúdo do arquivo binário.
O vinculador pode ser considerado um módulo altamente sofisticado, capaz de distinguir com precisão uma ampla variedade de seções de várias naturezas
(código, dados não inicializados, dados inicializados, construtores, informações de depuração, etc.). Para resolver as referências, deve conhecer intimamente os
detalhes de sua estrutura interna.
■ Nota Como será mostrado mais tarde em discussões sobre o processo de vinculação dinâmica, os recursos do carregador são um pouco mais complexos
do que meros blocos de cópia de dados.
Portanto, não é uma grande surpresa que o carregador tenda a agrupar as seções criadas pelo vinculador em segmentos com base em seus requisitos de
carregamento comuns. Conforme mostrado na Figura 3-2, os segmentos do carregador normalmente carregam várias seções que possuem atributos de acesso
comuns (leitura ou leitura e gravação, ou o mais importante, para serem corrigidos ou não).
Conforme mostrado na Figura 3-3, usar o utilitário readelf para examinar os segmentos ilustra o agrupamento de muitas seções diferentes do linker nos
segmentos do carregador.
O tipo de arquivo Elf é DYN (arquivo de objeto compartilhado) Ponto de entrada 0x399
GNU_EH_FRAME 0X0004C4 OX0O00O4C4 0X00O004C4 0x0001c 0x0001c R 0x4 GNU STACK 0X000000 0X00000009 0X00000000 0X00000 9x90000 RW 0X4
00 .note.gnu.build-Id .gnu.hash .dynsyn .dynstr .gnu.version .gnu.verslon_r .rel, dy n .rel.pit .Init .pit .text .flnl
.eh_frane_hdr .eh ^ frsme
02 .dynamic
03 .note.gnu.build-Id
04 .eh_franehdr
05
nllan @ mllan $
Uma vez que o formato binário é identificado, a função do módulo carregador do kernel entra em ação. O carregador tenta primeiro localizar o segmento
PT_INTERP no arquivo binário executável, o que o ajudará na tarefa de carregamento dinâmico.
A fim de evitar as armadilhas da proverbial situação de "carroça à frente do cavalo" - uma vez que o carregamento dinâmico ainda não foi explicado - vamos
assumir o cenário mais simples possível em que o programa está estaticamente vinculado e não há necessidade de carregamento dinâmico de qualquer tipo.
O termo construção estática é usado para indicar o executável, que não possui nenhuma das dependências de vinculação dinâmica. Todas as bibliotecas
externas necessárias para criar esse executável são vinculadas estaticamente. Como consequência, o binário obtido é totalmente portável, pois não requer a
presença de nenhuma biblioteca compartilhada do sistema (nem mesmo a libc) para ser executado. O benefício da portabilidade total (que raramente
requer tais medidas drásticas) vem com o preço do tamanho de byte muito maior do executável.
além da portabilidade total, a razão para o executável estaticamente construído pode ser puramente educacional, já que se presta bem ao processo de
explicar as funções originais e mais simples possíveis do carregador.
O efeito da construção estática pode ser ilustrado com o exemplo do exemplo "Hello World" puro e simples. Vamos usar o mesmo arquivo de origem para
construir os dois aplicativos, um dos quais é construído com o sinalizador de vinculador -static ; consulte as Listagens 3-1 e 3-2.
A comparação dos tamanhos de bytes dos dois executáveis mostrará que o tamanho dos bytes do executável construído estaticamente é muito maior (cerca
de 100 vezes neste exemplo específico). _
O carregador continua lendo os cabeçalhos dos segmentos do arquivo binário do programa para determinar os endereços e comprimentos de byte de cada um dos
segmentos. Um detalhe importante a destacar é que, neste estágio, o carregador ainda não grava nada no mapa de memória do programa. Tudo o que o
carregador faz neste estágio é estabelecer e manter um conjunto de estruturas (vm_are_struct por exemplo), carregando os mapeamentos entre os segmentos do
arquivo executável (na verdade, partes da página inteira de cada um dos segmentos) e o mapa de memória do programa.
A cópia real dos segmentos do executável ocorre após o início da execução do programa. Nessa altura, o mapeamento de memória virtual entre a página de
memória física concedida ao processo e o mapa de memória do programa foi estabelecido; as primeiras solicitações de paginação começam a chegar do kernel,
solicitando que intervalos de toda a página de segmentos de programa estejam disponíveis para execução. Como consequência direta dessa política, apenas as
partes do programa que são realmente necessárias no tempo de execução são carregadas (Figura 3-4).
Vamos examinar mais de perto o que normalmente acontece no Linux entre o carregamento do programa e a execução da primeira linha de código da função main
() .
Depois de carregar o programa (isto é, preparar o blueprint do programa e copiar as seções necessárias para a memória para sua execução), o carregador dá uma
olhada rápida no valor do campo e_entry do cabeçalho ELF. Este valor contém o endereço de memória do programa a partir do qual a execução será iniciada.
A desmontagem do arquivo binário executável normalmente mostra que o valor e_entry carrega nada menos do que o primeiro endereço da seção do código
(.text). Coincidentemente, este endereço de memória de programa normalmente denota a origem da função de inicialização .
08048320 <_início>:
O papel da função _start é preparar os argumentos de entrada para a função_libc_start_main que será
int _libc_start_main (int (* main) (int, char * *, char * *), / * endereço da função principal * /
void (* fini) (void), / * endereço da função fini * / void (* rtld_fini) (void), / * endereço da função fini do vinculador
dinâmico * /
Na verdade, tudo o que as instruções anteriores à instrução de chamada fazem é empilhar os argumentos necessários para a chamada na ordem esperada.
Para entender exatamente o que essas instruções fazem e por quê, dê uma olhada na próxima seção, que é dedicada a explicar o mecanismo de pilha. Mas antes de
irmos lá, vamos primeiro completar a história sobre como iniciar a execução do programa.
O papel da função__libc_start_main ()
Esta função é a peça chave no processo de preparação do ambiente para a execução do programa. Ele não apenas configura as variáveis de ambiente para o
programa durante a execução do programa, mas também faz o seguinte:
• Chama a função _init () , que executa as inicializações necessárias para serem concluídas antes do início da função main () .
O compilador GCC por meio da palavra-chave _attribute_ ((construtor)) oferece suporte personalizado
design das rotinas que você pode desejar concluir antes do início do programa.
• Registra as funções _fini () e _rtld_fini () a serem chamadas para limpeza após o término do programa. Normalmente, a ação de _fini () é inversa
às ações da função _init () .
O compilador GCC, por meio da palavra- chave_ attribute_ ((destructor)) , oferece suporte a custom
design das rotinas que você pode querer concluir antes de iniciar o programa.
Finalmente, depois que todas as ações de pré-requisito foram concluídas, the_libc_start_main () chama o main ()
Como qualquer pessoa com experiência em programação acima do nível de iniciante absoluto sabe, o fluxo de programa típico é, na verdade, uma sequência de
chamadas de função. Normalmente, a função principal chama pelo menos uma função, que por sua vez pode chamar um grande número de outras funções.
O conceito de pilha é a base do mecanismo de chamadas de função. Este aspecto particular da execução do programa não é de suma importância para o tópico
geral deste livro, e não gastaremos muito mais tempo discutindo os detalhes de como a pilha funciona. Esse assunto é lugar-comum há muito tempo e não há
necessidade de reiterar os fatos conhecidos.
Em vez disso, apenas alguns pontos importantes relacionados à pilha e às funções serão apontados.
• A quantidade de memória de pilha usada em tempo de execução realmente varia; quanto maior a sequência de chamadas de função, mais memória de pilha está
em uso.
• A pilha de memória não é ilimitada. Em vez disso, a quantidade de memória de pilha disponível é vinculada à quantidade de memória disponível para alocação
(que é a parte da memória do processo conhecida como heap).
Na verdade, muitas convenções de chamada diferentes foram desenvolvidas para a arquitetura X86, como cdecl, stdcall, fastcall, thiscall, para citar
apenas alguns. Cada um deles é feito sob medida para um cenário específico de uma variedade de pontos de vista de design. O artigo intitulado "Calling
Conventions Demystified" por Nemanja Trifunovic ( www.codeproject.com/Articles/1388/Calling-Conventions-Demystified ) fornece uma visão
Sem gastar muito tempo neste tópico em particular, um detalhe de particular importância é que entre todas as convenções de chamada disponíveis, uma delas em
particular, a convenção de chamada cdecl , é fortemente preferida para implementar a interface de bibliotecas dinâmicas exportadas para o outro mundo . Fique
ligado para mais detalhes, pois as discussões no Capítulo 6 sobre as funções da biblioteca ABI fornecerão uma visão melhor do tópico.
CAPÍTULO 4
Os motivos iniciais para dividir as tarefas entre compilador e vinculador já foram descritos nos capítulos anteriores. Resumidamente, tudo começou com o hábito
útil de manter o código em arquivos de origem separados; então, no tempo de compilação, tornou-se óbvio que o compilador não poderia completar facilmente a
tarefa de resolver as referências simplesmente porque o agrupamento das seções de código no quebra-cabeça final do mapa de memória do programa tinha que
acontecer primeiro.
A ideia de reutilização de código acrescentou um argumento extra à decisão de dividir os estágios de compilação e vinculação. A quantidade de indeterminismo
trazida pelos arquivos objeto (todas as seções tendo intervalos de endereços baseados em zero mais referências não resolvidas), que inicialmente certamente
parecia uma desvantagem, à luz da ideia de compartilhamento de código, na verdade, começou a parecer uma nova qualidade preciosa.
O conceito de reutilização de código aplicado ao domínio da construção dos executáveis do programa encontrou sua primeira realização na forma de bibliotecas
estáticas, que são coleções agrupadas de arquivos-objeto. Mais tarde, com o advento dos sistemas operacionais multitarefa, outra forma de reutilização, chamada
de bibliotecas dinâmicas, ganhou destaque. Hoje em dia, ambos os conceitos (bibliotecas estáticas e dinâmicas) estão em uso, cada um com prós e contras,
exigindo, portanto, uma compreensão mais profunda dos detalhes internos de sua funcionalidade. Este capítulo descreve detalhadamente esses dois conceitos um
tanto semelhantes, mas também substancialmente diferentes.
Bibliotecas estáticas
A ideia por trás do conceito de bibliotecas estáticas é excepcionalmente simples: uma vez que o compilador traduz uma coleção de unidades de tradução (ou seja,
arquivos de origem) em arquivos de objetos binários, você pode querer manter os arquivos de objetos para uso posterior em outros projetos, onde eles podem ser
prontamente combinados em tempo de link com os arquivos de objetos inerentes a esse outro projeto.
Para que seja possível integrar os arquivos de objetos binários em algum outro projeto, pelo menos um requisito adicional precisa ser satisfeito: que os arquivos
binários sejam acompanhados pelo arquivo de inclusão de cabeçalho de exportação, que fornecerá a variedade de definições e funções declarações de pelo menos
essas funções que podem ser usadas como pontos de entrada. A seção intitulada "A Conclusão: O Impacto do Conceito de Reutilização Binária" explica por que
algumas funções são mais importantes do que outras.
Existem várias maneiras pelas quais um conjunto de arquivos de objeto pode ser usado em vários projetos:
(recortar e colar) ou transferir de qualquer maneira possível para um projeto que precisa deles (onde serão vinculados ao lado de outros arquivos-objeto no
executável), conforme mostrado na Figura 4-1.
Figura 4-1. Um método trivial de reutilização de código binário, o precursor de bibliotecas estáticas
COMPILADOR i = D>
PROJETO 2
• A melhor maneira é agrupar os arquivos de objeto em um único arquivo binário, uma biblioteca estática.
É muito mais simples e muito mais elegante entregar um único arquivo binário para o outro projeto do que cada arquivo de objeto separadamente (Figura 4-2).
• O requisito óbvio neste caso é que o vinculador entenda o formato do arquivo da biblioteca estática e seja capaz de extrair seu conteúdo (ou seja, arquivos-objeto
agrupados) para vinculá-los. Felizmente, esse requisito foi atendido por provavelmente todos e cada um vinculador desde os primórdios da programação de
microprocessadores.
• Observe também que o processo de criação de uma biblioteca estática não é irreversível. Mais especificamente, uma biblioteca estática é meramente um arquivo
de arquivos de objeto, que pode ser manipulado de várias maneiras. Por meio do uso fácil de ferramentas apropriadas, uma biblioteca estática pode ser
desmontada na coleção de arquivos de objetos originais; um ou mais arquivos-objeto podem ser descartados da biblioteca, novos arquivos-objeto podem ser
adicionados e, finalmente, os arquivos-objeto existentes podem ser substituídos por uma versão mais recente.
Qualquer que seja uma dessas duas abordagens que você decida seguir, a trivial ou a abordagem de biblioteca estática mais sofisticada, você essencialmente terá o
processo de reutilização de código binário acontecendo, já que os arquivos binários gerados em um projeto são usados em outros projetos. O impacto geral da
reutilização de código binário na paisagem do design de software será discutido em detalhes posteriormente.
Bibliotecas Dinâmicas
Ao contrário do conceito de bibliotecas estáticas, que existe desde os primeiros dias da programação em assembler, o conceito de bibliotecas dinâmicas foi
totalmente aceito muito mais tarde. As circunstâncias que levaram à sua criação e adoção estão intimamente relacionadas ao surgimento de sistemas operacionais
multitarefa.
Em qualquer análise do funcionamento de um sistema operacional multitarefa, uma noção particular rapidamente ganha destaque: independentemente da
variedade de tarefas simultâneas, certos recursos do sistema são únicos e devem ser compartilhados por todos. Os exemplos típicos de recursos compartilhados no
sistema de desktop são o teclado, o mouse, o adaptador gráfico de vídeo, a placa de som, a placa de rede e assim por diante.
Seria contraproducente e até desastroso se cada aplicativo que pretende acessar os recursos comuns tivesse que incorporar o código (seja como uma fonte ou como
uma biblioteca estática) que fornece controle sobre o recurso. Isso seria muito ineficiente, desajeitado e muito espaço de armazenamento (disco rígido e memória)
seria desperdiçado no armazenamento de duplicatas do mesmo código.
O sonho de sistemas operacionais melhores e mais eficientes levou à ideia de ter um mecanismo de compartilhamento que não pressuporia a compilação dos
arquivos de origem duplicados nem a vinculação dos arquivos de objetos duplicados. Em vez disso, seria implementado como algum tipo de compartilhamento
de tempo de execução. Em outras palavras, o aplicativo em execução seria capaz de integrar em seu mapa de memória de programa as partes compiladas e
vinculadas de algum outro executável, onde a integração ocorreria sob demanda, conforme a necessidade, em tempo de execução. Esse conceito é conhecido como
vinculação dinâmica / carregamento dinâmico, que será ilustrado com mais detalhes na próxima seção.
Desde os primeiros estágios de design, um fato importante se tornou óbvio: de todas as partes da biblioteca dinâmica, só faz sentido compartilhar sua seção de código
(.texto), mas não os dados com os outros processos. Na analogia culinária, vários chefs diferentes podem compartilhar o mesmo livro de receitas (código). No
entanto, dado que chefs diferentes podem estar preparando simultaneamente pratos totalmente diferentes do mesmo livro de receitas, seria desastroso se eles
compartilhassem os mesmos utensílios de cozinha (dados).
Obviamente, se vários processos diferentes tivessem acesso à seção de dados da biblioteca dinâmica, a substituição da variável aconteceria em momentos
arbitrários e a execução da biblioteca dinâmica seria imprevisível, o que tornaria a ideia toda sem sentido. Dessa forma, mapeando apenas a seção de código, os
vários aplicativos ficam livres para executar o código compartilhado cada um em seu próprio compartimento, separadamente um do outro.
As ambições dos designers de sistema operacional desde o início eram evitar a presença múltipla desnecessária das mesmas partes do código do sistema
operacional nos binários de cada aplicativo que pudesse precisar deles. Por exemplo, cada aplicativo que precisasse imprimir o documento teria que incorporar a
pilha de impressão completa, terminando com o driver da impressora, a fim de fornecer o recurso de impressão. Se o driver da impressora mudasse, todo o
exército de designers de aplicativos precisaria recompilar seus aplicativos; caso contrário, um caos surgiria devido à presença de uma infinidade de versões de
driver de impressora diferentes no tempo de execução.
Obviamente, a solução certa seria implementar o sistema operacional de forma que aconteça o seguinte:
• O aplicativo que precisa de acesso à funcionalidade comum precisa simplesmente carregar a biblioteca dinâmica em tempo de execução.
A ideia básica por trás do conceito de bibliotecas dinâmicas é ilustrada na Figura 4-3.
TEMPO DE CONSTRUÇÃO
rotina
A primeira solução para esse problema (ou seja, a primeira versão de implementação de vinculação dinâmica, conhecida como relocação de tempo de
carregamento (LTR)), atingiu a meta com sucesso parcial. A boa notícia é que os aplicativos foram dispensados de carregar a bagagem desnecessária do código do
sistema operacional em seus binários; em vez disso, eles foram implantados apenas com o código específico do aplicativo, enquanto todas as necessidades
relacionadas ao sistema foram satisfeitas vinculando dinamicamente os módulos fornecidos pelo sistema operacional.
A má notícia, entretanto, era que se vários aplicativos precisassem de certa funcionalidade do sistema em tempo de execução, cada um dos aplicativos teria que
carregar sua própria cópia da biblioteca dinâmica. A causa subjacente dessa limitação foi o fato de que a técnica de realocação do tempo de carregamento
modificou os símbolos da seção .text da biblioteca dinâmica para se ajustar ao mapeamento de endereço específico do aplicativo fornecido. Para outro aplicativo,
que carregaria a biblioteca dinâmica no intervalo de endereços possivelmente diferente, o código de biblioteca modificado simplesmente não se ajustaria ao layout
de memória diferente.
Como resultado, várias cópias das bibliotecas dinâmicas residiam nos mapas de memória dos processos em tempo de execução. Isso é algo com que poderíamos
conviver por algum tempo, mas os objetivos de longo prazo do design eram muito mais ambiciosos: fornecer um mecanismo mais eficiente que permitiria que a
biblioteca dinâmica fosse carregada apenas uma vez (por qualquer aplicativo que aconteça para carregá-la primeiro) e ser disponibilizado para qualquer outro
aplicativo que tente carregá-lo em seguida.
Esse objetivo foi alcançado por meio do conceito conhecido como código independente de posição (PIC). Alterando como o código da biblioteca dinâmica
acessava os símbolos, apenas uma cópia da biblioteca dinâmica carregada no mapa de memória de qualquer processo se tornaria compartilhável por mapeamento
de memória para qualquer mapa de memória de processo do aplicativo (Figura 4-4).
TEMPO DE EXECUÇÃO
Estruturas de dados específicas de processos (por exemplo, tabelas de páginas, tarefas e estruturas de mm, pilha de kernel)
Pilha de usuário
Além disso, não é incomum que o sistema operacional carregue certos recursos comuns do sistema (drivers de nível superior, por exemplo) na memória física,
sabendo que provavelmente serão necessários pela abundância de processos em execução. O efeito da vinculação dinâmica é que cada um dos processos tem a
ilusão perfeita de que são os únicos proprietários do driver.
Desde a invenção do conceito PIC, as bibliotecas dinâmicas projetadas para suportá-lo foram chamadas de bibliotecas compartilhadas. Hoje em dia, o conceito PIC
é predominante, e em sistemas de 64 bits é fortemente favorecido pelos compiladores, então a distinção de nomenclatura entre os termos biblioteca dinâmica e
biblioteca compartilhada está desaparecendo, e os dois nomes são usados mais ou menos indistintamente.
O conceito de memória virtual pavimentou a base para o sucesso da ideia de compartilhamento de tempo de execução (sintetizado pelo conceito de código
independente de posição). A ideia inicial é bastante simples: se o mapa de memória do processo real (com endereços reais e concretos) nada mais é do que o
resultado do mapeamento 1: 1 do mapa de memória do processo baseado em zero, o que realmente nos impede de criar um monstro, um real mapa de memória
de processo obtido mapeando partes de mais de um processo diferente? Na verdade, é exatamente assim que funciona o mecanismo de compartilhamento em
tempo de execução de bibliotecas dinâmicas.
A implementação bem-sucedida do conceito PIC representa a pedra angular dos modernos sistemas operacionais multitarefa.
Estruturas de dados específicas de processos (por exemplo, tabelas de páginas, tarefas e estruturas de mm, pilha de kernel)
Estruturas de dados específicas de processos (por exemplo, tabelas de páginas, tarefas e estruturas de mm, pilha de kernel)
O conceito de vinculação dinâmica está no cerne do conceito de bibliotecas dinâmicas. É praticamente impossível entender completamente como as bibliotecas
dinâmicas funcionam sem entender a complexa interação entre a biblioteca dinâmica, o executável do cliente e o sistema operacional. O foco desta seção é fornecer
o amplo nível de compreensão necessário do processo de vinculação dinâmica. Uma vez que sua essência seja compreendida, as seções subsequentes deste
documento prestarão a devida atenção aos detalhes.
Então, vamos ver o que realmente acontece durante o processo de vinculação dinâmica.
Como as figuras anteriores sugerem, o processo de construção de uma biblioteca dinâmica é uma construção completa, pois abrange tanto a compilação
(conversão da fonte em arquivos de objetos binários) quanto a resolução das referências. O produto do processo de construção da biblioteca dinâmica é o arquivo
binário cuja natureza é idêntica à natureza do executável, a única diferença é que a biblioteca dinâmica não possui as rotinas de inicialização que permitiriam que
ela fosse iniciada como um programa independente (Figura 4-5).
Símbolos de exportação
• No Windows, construir uma biblioteca dinâmica requer estritamente que todas as referências sejam resolvidas. Se o código da biblioteca dinâmica chamar uma
função em alguma outra biblioteca dinâmica, essa outra biblioteca e o símbolo de referência que ela contém devem ser conhecidos no momento da construção.
• No Linux, no entanto, a opção padrão permite um pouco mais de flexibilidade, permitindo que alguns símbolos não sejam resolvidos com a expectativa de que
eventualmente apareçam no binário final depois que alguma outra biblioteca dinâmica for vinculada. Além disso, o vinculador Linux fornece o opção para
corresponder totalmente à rigidez do vinculador do Windows.
• No Linux, é possível modificar a biblioteca dinâmica para torná-la executável por si mesma (ainda pesquisando se tal opção existe no Windows). Na verdade, a
libc (biblioteca de tempo de execução C) é executável por si só; quando invocado digitando seu nome de arquivo na janela do shell, ele imprime uma mensagem
na tela e termina. Para obter mais detalhes sobre como implementar esse recurso, consulte o Capítulo 14.
Parte 2: Jogando pela confiança ao construir o executável do cliente (procurando apenas os símbolos)
A próxima etapa no cenário de uso de uma biblioteca dinâmica acontece quando você tenta construir o executável que pretende usar a biblioteca dinâmica em
tempo de execução. Ao contrário do cenário de bibliotecas estáticas em que o vinculador está criando o executável por conta própria à vontade, o cenário de
vinculação das bibliotecas dinâmicas é peculiar, pois o vinculador tenta combinar seu trabalho atual com os resultados existentes do procedimento de vinculação
concluído anteriormente que criou binário de biblioteca dinâmica.
O detalhe crucial nesta parte da história é que o vinculador presta quase toda a sua atenção aos símbolos da biblioteca dinâmica. Parece que, neste estágio, o
vinculador quase não está interessado em nenhuma das seções, nem em código (.texto), nem em dados (.data / .bss).
Ele não examina o binário da biblioteca dinâmica completamente; ele não tenta encontrar as seções ou seus tamanhos, nem tenta integrá-los no binário resultante.
Em vez disso, ele tenta apenas verificar se a biblioteca dinâmica contém os símbolos necessários para o binário resultante. Depois de encontrá-lo, ele conclui a
tarefa e cria o binário executável (consulte a Figura 4-6).
A abordagem de "jogar por confiança" não é completamente não intuitiva. Vamos considerar um exemplo da vida real: se você disser a alguém que para enviar
uma carta, ele precisa ir ao quiosque na praça próxima e comprar um selo postal, você está basicamente baseando seu conselho em uma quantidade razoável de
confiança. Você sabe que deveria haver um quiosque na praça e que ele tem selos postais. O fato de você não saber os detalhes particulares do funcionamento do
quiosque (horário de funcionamento, quem trabalha lá, o preço dos selos) não diminui a validade do seu conselho, pois em tempo de execução todos esses
detalhes menos importantes serão resolvidos. A ideia de link dinâmico é baseada em suposições completamente análogas.
Note, entretanto, que essa quantidade de confiança deixa portas abertas para muitos cenários interessantes, todos os quais se enquadram no paradigma "construir
com um, carregar o outro". As implicações práticas variam de truques peculiares de design de software até todo o novo paradigma (plug-ins), ambos os quais
serão discutidos posteriormente neste livro.
Os eventos que acontecem no momento do carregamento são de importância crucial, pois este é o momento em que a confiança que o vinculador tinha nas
promessas da biblioteca dinâmica precisa ser confirmada. Anteriormente, o procedimento de construção (possivelmente concluído em uma máquina de
construção "A") examinava a cópia do binário da biblioteca dinâmica na busca por símbolos que o executável precisa. Agora, o que precisa acontecer no tempo de
execução (possivelmente em uma máquina de tempo de execução "B" diferente) é o seguinte:
Cada sistema operacional possui um conjunto de regras que estipulam em qual diretório o carregador deve procurar os binários das bibliotecas dinâmicas.
Este é o momento em que a promessa de vinculação em tempo de construção deve ser cumprida em tempo de execução.
Na verdade, a biblioteca dinâmica carregada em tempo de execução deve conter o conjunto idêntico de símbolos prometidos para estar disponíveis em tempo de construção.
Mais especificamente, no caso de símbolos de função, o termo "idêntico" significa que os símbolos de função encontrados na biblioteca dinâmica em tempo de
execução devem corresponder exatamente à assinatura de função completa (afiliações, nome, lista de argumentos, ligação / convenção de chamada) prometida na
construção Tempo.
Curiosamente, ele é não exigido que o código de montagem real (ou seja, o conteúdo seções) da biblioteca dinâmica encontrada em tempo de execução
corresponde ao código encontrado no binário biblioteca dinâmica usada durante o tempo de compilação. Isso abre muitos cenários interessantes que serão
discutidos em detalhes mais adiante.
3. Os símbolos executáveis precisam ser resolvidos para apontar para o endereço correto na parte do processo do mapa de memória onde a biblioteca dinâmica é
mapeada.
É neste estágio que a integração da biblioteca dinâmica no mapa de memória do processo realmente merece ser chamada de vinculação dinâmica, pois , ao
contrário da vinculação convencional, ela ocorre no momento do carregamento.
Se todas as etapas deste estágio forem concluídas com sucesso, você pode ter seu aplicativo executando o código contido na biblioteca dinâmica, conforme
ilustrado na Figura 4-7.
Como é verdade que a vinculação dinâmica acontece em duas fases (tempo de construção vs. tempo de execução) nas quais o vinculador se concentra em
diferentes detalhes do binário da biblioteca dinâmica, não há nenhuma razão substancial para que a mesma cópia idêntica do binário da biblioteca dinâmica não
pudesse não pode ser usado em ambas as fases.
Mesmo que na fase de vinculação dinâmica em tempo de construção apenas os símbolos da biblioteca desempenhem uma função, não há nada de errado se a
mesma cópia exata do arquivo binário for usada na fase de tempo de execução também.
Este princípio é seguido principalmente em uma variedade de sistemas operacionais, incluindo Linux. No Windows, no entanto, na tentativa de tornar mais clara
a separação entre os estágios de vinculação dinâmica, as coisas são um pouco mais complicadas de uma forma que pode confundir um pouco os iniciantes.
Este tipo de arquivo é na verdade a biblioteca dinâmica, um objeto compartilhado usado em tempo de execução pelos processos por meio do mecanismo de
vinculação dinâmica. Mais especificamente, a maioria dos fatos apresentados até agora sobre os princípios nos quais as funções da biblioteca dinâmica são
totalmente aplicáveis aos arquivos DLL.
Um arquivo binário de biblioteca de importação dedicado (.lib) é usado no Windows especificamente na fase "Parte 2" da vinculação dinâmica (Figura 4-8). Ele
contém apenas a lista de símbolos DLL e nenhuma de suas seções de vinculador, e seu objetivo é apenas apresentar o conjunto de símbolos exportados da
biblioteca dinâmica para o binário do cliente.
Biblioteca de importação
A extensão do arquivo da biblioteca de importação (.lib) é a fonte potencial de confusão, pois a mesma extensão de arquivo também é usada para indicar as
bibliotecas estáticas.
Outro detalhe que merece um pouco de discussão é o fato desse arquivo ser denominado biblioteca de importação, mas na verdade desempenha um papel no
processo de exportação dos símbolos DLL. Como é verdade que a escolha da nomenclatura depende do lado de onde olhamos para o processo de vinculação
dinâmica, também é verdade que este arquivo pertence ao projeto DLL, é criado pela construção do projeto DLL, e pode ser disseminado para inúmeras
aplicações. Por todos esses motivos, não deve ser errado adotar a direção "da DLL para fora" e, portanto, usar o nome biblioteca de exportação.
A prova óbvia de que outra pessoa na Microsoft compartilhava desse ponto de vista, pelo menos até certo ponto, pode ser encontrada no
seção que discute o uso da palavra-chave of_ declspec , onde a nomenclatura (_ declspec (dllexport)) é usada para indicar
Um dos motivos pelos quais o pessoal da Microsoft decidiu manter essa convenção de nomenclatura específica é que o projeto DLL produz outro tipo de arquivo
de biblioteca que pode ser usado em vez deste nos cenários de dependências circulares. Esse outro tipo de arquivo é chamado de arquivo de exportação (.exp)
(veja abaixo), e para distinguir entre os dois, a nomenclatura existente foi mantida.
O arquivo de exportação tem a mesma natureza do arquivo de biblioteca de importação . No entanto, é normalmente usado no cenário em que dois executáveis têm
dependências circulares que impossibilitam a conclusão da construção de qualquer um. Nesse caso, o arquivo exp é fornecido com a intenção de possibilitar que
pelo menos um dos binários seja compilado com êxito, o que, por sua vez, pode ser usado por outros binários dependentes para concluir suas compilações.
■ Nota As DLLs do Windows são estritamente necessárias para resolver todos os símbolos no momento da criação. no Linux, no entanto, é possível deixar
alguns símbolos de biblioteca dinâmica não resolvidos com a expectativa de que os símbolos ausentes eventualmente apareçam no mapa de memória do
processo como resultado da vinculação dinâmica em algumas outras bibliotecas dinâmicas.
É importante entender desde o início que, no conjunto de tipos binários, a biblioteca dinâmica tem uma natureza bastante única, cujos detalhes são importantes
para se ter em mente ao lidar com os problemas de design relacionados usuais.
Ao olhar para os outros tipos binários, as naturezas opostas dos executáveis e das bibliotecas estáticas tornam-se óbvias quase imediatamente. A criação de uma
biblioteca estática não envolve a etapa de vinculação, enquanto no caso do executável é a última etapa obrigatória. Como consequência, a natureza do executável é
muito mais completa, pois contém referências resolvidas e, devido às rotinas de início extras incorporadas, está pronto para execução.
Nesse sentido, apesar da palavra "biblioteca", que sugere semelhanças entre as bibliotecas estáticas e dinâmicas, é o fato de que a natureza da biblioteca dinâmica
está muito mais próxima da natureza do executável.
■ Nota Decidi, a partir de agora, usar o termo "binário do cliente" para indicar o executável ou a biblioteca dinâmica que carrega uma biblioteca dinâmica.
Quando o conceito de interface é aplicado ao domínio das linguagens de programação, ele é normalmente usado para denotar a estrutura dos ponteiros de função.
C ++ adiciona alguns significados extras, definindo-o como uma classe de ponteiros de função; além disso, ao declarar os ponteiros de função como iguais a NULL,
a interface recebe um impulso extra de abstração, pois se torna inadequada para instanciação, mas pode ser usada como modelo idealista para outras classes
implementarem.
A interface exportada por um módulo de software para os clientes é normalmente chamada de interface de programação de aplicativo (API). Quando aplicado ao
domínio de binários, o conceito de uma interface obtém um sabor específico de domínio adicional chamado interface binária de aplicativo (ABI). Não é errado
pensar na ABI como um conjunto de símbolos (principalmente um conjunto de pontos de entrada de função) criado no processo de compilação / vinculação da
interface do código-fonte.
O conceito de ABI é útil para explicar com mais precisão o que acontece durante a vinculação dinâmica.
• Durante a primeira fase (tempo de construção) da vinculação dinâmica, o binário do cliente de fato é vinculado à ABI exportada da biblioteca.
Como mencionei, no momento da construção, o binário do cliente, na verdade, apenas verifica se a biblioteca dinâmica exporta os símbolos (ponteiros de função,
como o ABI) e não se importa em nada com as seções (os corpos da função).
• Para concluir com êxito a segunda fase (tempo de execução) de vinculação dinâmica, o espécime binário da biblioteca dinâmica disponível em tempo de
execução deve exportar a ABI inalterada, idêntica à encontrada em tempo de construção.
A diferença mais interessante entre as bibliotecas estáticas e dinâmicas é a diferença nos critérios de seletividade aplicados por um binário cliente que tenta
vinculá-las.
Cliente binário
c =? h =?
-O 9
abcdefghij
? rnmrn
c
Biblioteca estática
O comprimento do byte do binário do cliente aumenta, embora apenas pela quantidade de código relevante ingerido da biblioteca estática.
■ Nota Apesar do fato de que o algoritmo de vinculação é seletivo ao escolher quais arquivos de objeto para vincular, a seletividade não vai além da
granularidade de arquivos de objeto individuais. Ainda pode acontecer que além dos símbolos realmente necessários, o arquivo objeto escolhido contenha
alguns símbolos desnecessários.
Quando o binário do cliente vincula a biblioteca dinâmica, ele apresenta a seletividade apenas no nível da tabela de símbolos, na qual apenas os símbolos da
biblioteca dinâmica realmente necessários são mencionados na tabela de símbolos.
Em todos os outros aspectos, a seletividade é praticamente inexistente. Independentemente de quão pequena uma parte da funcionalidade da biblioteca dinâmica
seja concretamente necessária, toda a biblioteca dinâmica é vinculada dinamicamente (Figura 4-10).
Cliente binário
99
abcdefghij
Biblioteca dinâmica
O aumento da quantidade de código só acontece em tempo de execução. O comprimento do byte do binário do cliente não aumenta significativamente. Os bytes
extras necessários para a contabilidade de novos símbolos tendem a somar pequenas contagens de bytes. No entanto, vincular a biblioteca dinâmica impõe o
requisito de que o binário da biblioteca dinâmica precisa estar disponível em tempo de execução na máquina de destino.
Uma reviravolta interessante ocorre quando a funcionalidade da biblioteca estática precisa ser apresentada aos clientes binários por meio da biblioteca dinâmica
intermediária (Figura 4-11).
Cliente binário
Biblioteca dinâmica
abcdefghij î l î l î l î l î l î l î l î l î l î
Biblioteca estática
A própria biblioteca dinâmica intermediária não precisa de nenhuma funcionalidade da biblioteca estática. Portanto, de acordo com as regras de seletividade de
importação formuladas, ele não vinculará em nada da biblioteca estática. Ainda, a única razão pela qual a biblioteca dinâmica é projetada é ingerir a
funcionalidade da biblioteca estática e exportar seus símbolos para o resto do mundo usar.
Felizmente, esse cenário foi identificado antecipadamente e o suporte de vinculador adequado foi fornecido por meio do sinalizador de vinculador --whole-
archive . Quando especificado, este sinalizador de vinculador indica que uma ou mais bibliotecas listadas a partir de então serão totalmente vinculadas
incondicionalmente, independentemente de o binário do cliente que as vincula precisar ou não de seus símbolos.
file:///C:/Users/noels/Desktop/Advanced C and C++ Compiling ( PDFDrive ).html 50/235
21/09/2021 22:07 Advanced C and C++ Compiling ( PDFDrive ).html
Em reconhecimento a este cenário, o sistema de desenvolvimento nativo Android, além de suportar a variável de construção LOCAL_STATIC_LIBRARIES , o
sistema de construção nativo também suporta a variável de construção LOCAL_WHOLE_STATIC_LIBRARIES , assim:
$ gcc -fPIC <arquivos de origem> -Wl, - whole-archive -l <bibliotecas estáticas> -o <nome do arquivo shlib>
Curiosamente, há um sinalizador de linker de contra-ação (--no-whole-archive). Seu efeito é neutralizar o efeito de --whole-archive para todas as
bibliotecas subsequentes sendo especificadas para vinculação na mesma linha de comando do vinculador.
De natureza semelhante ao sinalizador - -whole-archive é o sinalizador do vinculador -rdynamic . Ao passar esse sinalizador de vinculador, você
basicamente está solicitando que o vinculador exporte todos os símbolos (presentes na seção .symtab ) para a seção dinâmica (.dynsym) , o que basicamente os
torna utilizáveis para fins de vinculação dinâmica. Curiosamente, esse sinalizador não parece exigir o prefixo -Wl .
Ao projetar os pacotes de implantação de software, os engenheiros de construção normalmente enfrentam um requisito para minimizar o tamanho em bytes do
pacote de implantação. Em um dos cenários mais simples possíveis, o produto de software que precisa ser implantado é composto de um executável que delega a
tarefa de fornecer certa parte de sua funcionalidade a uma biblioteca. Digamos que a biblioteca possa vir em ambos os tipos, a biblioteca estática e a dinâmica. A
questão básica que o engenheiro de compilação enfrenta é que tipo de cenários de vinculação utilizar para minimizar o tamanho do byte do pacote de software
implantado.
Uma das opções que um engenheiro de compilação enfrenta é vincular o executável à versão estática da biblioteca. Essa decisão vem com prós e contras.
• Prós: o executável é totalmente autocontido, pois carrega todo o código de que precisa.
• Contras: o tamanho do byte executável é aumentado pela quantidade de código ingerido da biblioteca estática.
• Prós: O tamanho do byte executável não é alterado (exceto talvez pela pequena despesa de contabilidade de símbolos).
• Contras: sempre há uma chance de que a biblioteca dinâmica necessária, por qualquer motivo, não esteja fisicamente disponível na máquina de destino. Se
precauções forem tomadas e a biblioteca dinâmica necessária for implantada junto com o executável, vários problemas potenciais podem surgir.
• Primeiro, o tamanho geral em bytes do pacote de implantação definitivamente fica maior, pois agora você implanta um executável e uma biblioteca dinâmica.
• Em segundo lugar, a versão da biblioteca dinâmica implantada pode não corresponder aos requisitos de outros aplicativos que podem depender dela.
• Terceiro, quarto e assim por diante, há todo um conjunto de problemas que podem acontecer ao lidar com as bibliotecas dinâmicas, conhecido pelo nome de
"DLL hell".
Veredicto Final
A vinculação com bibliotecas estáticas é uma boa escolha quando o aplicativo é vinculado a porções relativamente menores de um número relativamente menor
de bibliotecas estáticas.
A vinculação com bibliotecas dinâmicas vem como uma boa escolha quando o aplicativo depende das bibliotecas dinâmicas esperadas com grande certeza que
existam em tempo de execução na máquina de destino.
Os prováveis candidatos são bibliotecas dinâmicas específicas do sistema operacional, como biblioteca de tempo de execução C, subsistemas gráficos, drivers de
dispositivo de nível superior do espaço do usuário e / ou bibliotecas provenientes de pacotes de software muito populares. A Tabela 4-1 resume as diferenças entre
lidar com bibliotecas estáticas e dinâmicas.
Categoria de
Bibliotecas estáticas Bibliotecas Dinâmicas
Comparação
Procedimento
Incompleto: Completo:
de construção
Compilando:
Compilando: simLinking: sim
simLinking: não
Natureza do Arquivo de arquivo (s)
O executável sem a inicialização
binário objeto
Todas as seções existem, rotinas.
Eficiente: apenas os
arquivos de objeto 2) Símbolos e integração de seções em
necessários
O tamanho do byte do
carregado no processo, independentemente de
binário do cliente obtém
qual parte da biblioteca é realmente necessária. O tamanho do byte do binário do cliente quase não muda. No
aumentou, no entanto. entanto, a disponibilidade do binário da biblioteca dinâmica em tempo de execução é uma coisa a mais com que se
preocupar.
Impacto no Aumenta o tamanho do
Reduz o tamanho do executável, pois apenas
executável executável, como seções
seja adicionado às seções o código específico do aplicativo reside no executável do aplicativo, enquanto as partes compartilháveis são extraídas
Tamanho
executáveis. para a biblioteca dinâmica.
Excelente, pois tudo que
Portabilidade Varia.
o aplicativo precisa é
dentro de seu binário. Bom para bibliotecas dinâmicas padrão do sistema operacional
A ausência de
(libc, drivers de dispositivo, etc.), como eles são
dependências externas
garantia de existência na máquina de tempo de execução.
(contínuo)
Categoria de
Bibliotecas estáticas Bibliotecas Dinâmicas
Comparação
Facilidade de
Muito limitado. Excelente.
combinação
Não é possível criar uma
biblioteca estática usando Uma biblioteca dinâmica pode se conectar a um ou mais
o
outras bibliotecas (nem
bibliotecas estáticas e / ou uma das mais dinâmicas
estáticas nem dinâmicas).
Só pode ligar todos eles
bibliotecas.
no
Na verdade, o Linux pode ser visto como "Legoland", um conjunto de construções feitas por bibliotecas
mesmo executável. dinâmicas vinculadas a outras bibliotecas dinâmicas. A magnitude da integração é muito facilitada pela
disponibilidade do código-fonte.
Facilidade de Bastante fácil. Praticamente impossível para a maioria dos mortais.
conversão
file:///C:/Users/noels/Desktop/Advanced C and C++ Compiling ( PDFDrive ).html 52/235
21/09/2021 22:07 Advanced C and C++ Compiling ( PDFDrive ).html
A função padrão do
Algumas soluções comerciais foram vistas
arquivador
utilidade é a extração do
aquela tentativa com vários graus de sucesso
objeto ingrediente
arquivos. Uma vez
para implementar a conversão de uma dinâmica
extraídos, eles podem ser
eliminado, substituído ou
para uma biblioteca estática.
recombinado
em uma nova biblioteca
estática ou dinâmica.
Apenas em casos muito
excepcionalmente
especiais (em
a seção "Dicas e truques",
consulte o tópico
sobre ligar a biblioteca
estática a uma dinâmica
lib no Linux de 64 bits)
isso pode não ser bom
o suficiente, e você pode
precisar recompilar
as fontes originais.
Em geral, é escrito de uma forma indeterminista. Por exemplo: se uma pessoa (que pessoa?) For condenada por cometer uma contravenção de classe A (que
contravenção específica? O que exatamente a pessoa fez?), Ela será condenada a pagar uma multa que não exceda 2.000 dólares (exatamente como muito?), ou para
cumprir a pena de prisão não superior a 6 meses (exatamente quanto tempo?) ou ambos (qual das três combinações possíveis?).
John Smith é condenado por resistir à prisão e desobedecer ao policial. A promotoria pede que ele pague uma multa de US $ 1.500 e passe 30 dias na prisão.
Todas as referências (quem, o quê, quando e possivelmente por quê) são resolvidas: as violações da lei foram provadas no tribunal, o juiz sentenciou John Smith de
acordo com a letra da lei e tudo está pronto para ele cumprir sua pena na instalação de correção estadual próxima.
Biblioteca estática Ingredientes de alimentos crus (por exemplo, carne crua ou vegetais crus)
Pronto para o consumo, mas servir como está não faz muito sentido. No entanto, se o resto do almoço estiver pronto, será um ótimo complemento para a refeição
servida.
Consiste no pão fresco do dia, na salada da hora e no prato principal preparado, que pode ser enriquecido com uma certa quantidade de um prato aquecido
cozinhado há poucos dias.
Veterano de combate condecorado, conhecido por suas excelentes habilidades e instintos de sobrevivência. Designado pela British Geographic Society para
investigar rumores de que nas profundezas das selvas tropicais existem templos de civilizações avançadas há muito perdidas, escondendo numerosos tesouros
materiais e científicos. Ele tem direito a apoio logístico do departamento consular britânico local, que se encarrega de coordenar os esforços com as autoridades
locais e oferece todo tipo de ajuda com suprimentos, dinheiro, logística e transporte.
Esse cara nasceu e cresceu na área geográfica alvo da expedição. Ele fala todas as línguas locais, conhece todas as religiões e culturas tribais; tem muitas conexões
pessoais na área; conhece todos os lugares perigosos e como evitá-los; tem habilidades de sobrevivência excepcionais; é um bom caçador, excelente desbravador e
pode prever as mudanças climáticas. Altamente especializado em tudo relacionado à selva, podendo cuidar de si mesmo na íntegra. A maior parte de seu tempo
adulto foi gasto como guia contratado para expedições como esta. Entre as expedições, ele não faz quase nada, a não ser passar o tempo com a família, ir pescar e
caçar, etc. Não tem ambição nem poder financeiro para começar nada sozinho.
Jovem rapaz britânico da família aristocrática. Pouca ou nenhuma experiência na vida real, mas o diploma de Oxford em arqueologia e conhecimento de línguas
antigas, bem como o conhecimento operacional de estenografia, telegrafia e código Morse, rendeu a ele um lugar na equipe. Embora suas habilidades sejam
potencialmente aplicáveis a muitas funções e muitos cenários, ele nunca esteve nas áreas tropicais, não fala as línguas locais e, na maior parte, dependerá de
autoridade superior e / ou especialização superior de vários tipos. Muito provavelmente, ele não terá autoridade formal sobre o curso da expedição e nenhum
poder de tomar decisões de qualquer tipo, exceto no domínio de sua especialidade imediata e apenas quando solicitado a fazê-lo.
■ Nota Na analogia culinária, você (o designer do software) está administrando um restaurante no qual (por meio do processo de construção dos
executáveis) prepara uma refeição para o CPU faminto que mal espera para começar a mastigar a refeição.
• O aparecimento de projetos dedicados cuja intenção não é construir código executável, mas sim construir um pacote binário de código reutilizável.
• Assim que a prática de construir o código para outros usarem começou a ganhar impulso, a necessidade de seguir o princípio do encapsulamento passou a se
destacar.
A essência da ideia de encapsulamento é que, se estamos construindo algo para que outros usem, é sempre bom se esses produtos de exportação vierem com
recursos essenciais claramente separados dos detalhes de funcionalidade interna menos importantes. Uma das maneiras obrigatórias de conseguir isso é declarar a
interface, o conjunto de funções essenciais que o usuário está mais interessado em usar.
• A interface (conjunto de funções quintessenciais / mais importantes) é normalmente declarada no arquivo de cabeçalho de exportação (um arquivo de inclusão
que fornece a interface de nível superior entre o código binário reutilizável e o usuário potencial).
Resumindo, a maneira de distribuir o código para que outros usem é entregar o pacote de software que contém o conjunto de arquivos binários e o conjunto de
arquivos de cabeçalho de exportação. Os arquivos binários exportam a interface, principalmente o conjunto de funções quintessenciais para usar o pacote.
• O aparecimento de SDKs (kits de desenvolvimento de software), que são, na versão mais básica,
um conjunto de cabeçalhos e binários de exportação (bibliotecas estáticas e / ou dinâmicas) destinados à integração com binários criados durante a compilação de
arquivos de origem originais do projeto do cliente.
Existem muitos exemplos em que o mecanismo popular é usado por diferentes aplicativos, apresentando as diferentes GUIs ao usuário, mas executando o mesmo
mecanismo (carregado das mesmas bibliotecas dinâmicas) em segundo plano. Exemplos típicos no domínio da multimídia são ffmpeg e avisynth.
Ao entregar binários em vez de código-fonte, as empresas de software podem entregar sua tecnologia sem revelar as ideias por trás dela. A disponibilidade de
desmontadores torna essa história um pouco mais complicada, mas no longo prazo a ideia básica ainda se aplica.
CAPÍTULO 5
No Linux, a ferramenta de arquivamento, chamada simplesmente ar, está disponível como parte da cadeia de ferramentas do GCC. O exemplo simples a seguir
demonstra o processo de criação de biblioteca estática a partir de dois arquivos de origem:
Pela convenção do Linux, os nomes das bibliotecas estáticas começam com o prefixo lib e têm a extensão de arquivo .a. Além de realizar sua tarefa básica de
agrupar os arquivos objeto no arquivo (biblioteca estática), o ar pode realizar várias tarefas adicionais:
A lista completa de recursos suportados pode ser encontrada na página do manual da ferramenta ar ( http://linux.die.net/man/l7ar ).
A tarefa de criar a biblioteca estática no Windows não difere substancialmente da mesma tarefa executada no Linux. Mesmo que possa ser concluído a partir da
linha de comando, o fato da vida é que na grande maioria dos casos a tarefa de criar a biblioteca estática é realizada criando um projeto Visual Studio dedicado (ou
outra ferramenta IDE semelhante) com a opção de construir a biblioteca estática. Ao examinar a linha de comando do projeto, você pode ver na Figura 5-1 que a
tarefa se resume essencialmente ao mesmo uso de uma ferramenta de arquivamento (embora seja uma versão do Windows).
As bibliotecas estáticas são a forma mais básica de compartilhamento binário do código, que já estava disponível por muito tempo antes da invenção das
bibliotecas dinâmicas. Nesse ínterim, o paradigma mais sofisticado de bibliotecas dinâmicas assumiu o domínio do compartilhamento de código binário. No
entanto, ainda existem alguns cenários em que o recurso ao uso de bibliotecas estáticas ainda faz sentido.
As bibliotecas estáticas são perfeitamente adequadas para todos os cenários que implementam o núcleo de vários algoritmos (principalmente proprietários), desde
algoritmos elementares, como busca e classificação, até algoritmos científicos ou matemáticos muito complexos. Os seguintes fatores podem fornecer um impulso
extra para a decisão de usar a biblioteca estática como a forma de entrega do código:
• A arquitetura geral do código pode ser descrita mais como uma "ampla coleção de várias habilidades" em vez de um "módulo com a interface estritamente
definida".
• O cálculo real não depende de um recurso de sistema operacional específico (como um driver de dispositivo de placa gráfica ou temporizadores de sistema de
alta prioridade, etc.) que requer o carregamento de bibliotecas dinâmicas.
• O usuário final deseja usar seu código, mas não necessariamente deseja compartilhá-lo com mais ninguém.
• Os requisitos de implantação de código sugerem a necessidade de implantação monolítica (ou seja, pequeno número geral de arquivos binários entregues à
máquina do cliente).
Usar a biblioteca estática sempre significa controle mais rígido sobre o código, embora ao preço de flexibilidade reduzida. A modularidade é normalmente
reduzida e o aparecimento de novas versões de código normalmente significa recompilar cada aplicativo que o utiliza.
No domínio da multimídia, as rotinas de processamento de sinal (análise, codificação, decodificação, DSP) são normalmente entregues na forma de bibliotecas
estáticas. Por outro lado, sua integração com os frameworks de multimídia (DirectX, GStreamer, OpenMAX) são implementados na forma de bibliotecas
dinâmicas que se ligam nas bibliotecas estáticas relacionadas ao algoritmo. Neste esquema, as funções simples e estritas de se comunicar com a estrutura são
delegadas à camada fina da parte da biblioteca dinâmica, enquanto as complexidades de processamento de sinal pertencem à parte da biblioteca estática.
A maneira como o vinculador integra as seções e símbolos da biblioteca estática no binário do cliente é genuinamente bastante simples e direta. Quando
vinculadas ao binário do cliente, as seções da biblioteca estática combinam-se perfeitamente com as seções provenientes dos arquivos de objetos nativos do binário
do cliente. Os símbolos da biblioteca estática tornam-se parte da lista de símbolos binários do cliente e mantêm sua visibilidade original; os símbolos globais da
biblioteca estática tornam-se os símbolos globais do binário do cliente e os símbolos locais da biblioteca estática tornam-se os símbolos locais do binário do cliente.
Quando o binário do cliente é a biblioteca dinâmica (ou seja, não o aplicativo), o resultado dessas regras simples e diretas de integração pode ser comprometido
pelas regras de design de outras bibliotecas dinâmicas.
O pressuposto implícito no conceito de bibliotecas dinâmicas é a modularidade. Não é errado pensar em uma biblioteca dinâmica como um módulo que é
projetado para ser facilmente substituído quando surgir a necessidade. Para implementar adequadamente o conceito de modularidade, o código da biblioteca
dinâmica é tipicamente estruturado em torno da interface, o conjunto de funções que expõe a funcionalidade do módulo para o mundo externo, enquanto os
internos da biblioteca dinâmica são normalmente mantidos longe dos olhares curiosos de os usuários da biblioteca.
Por sorte, as bibliotecas estáticas são normalmente projetadas para fornecer o "coração e a alma" das bibliotecas dinâmicas. Independentemente de quão preciosa é
a contribuição da biblioteca estática para a funcionalidade geral de sua biblioteca dinâmica hospedeira, as regras de design das bibliotecas dinâmicas estipulam
que elas devem exportar (ou seja, tornar visíveis) apenas o mínimo necessário para a biblioteca se comunicar com o mundo exterior .
Como consequência direta de tais regras de design (como você verá nos próximos capítulos), a visibilidade dos símbolos da biblioteca estática acaba sendo
reduzida. Em vez de permanecerem visíveis globalmente (o que estavam imediatamente após a conclusão da vinculação), os símbolos da biblioteca estática
tornam-se imediatamente rebaixados para os privados ou podem até mesmo ser removidos (ou seja, completamente eliminados da lista de símbolos da biblioteca
dinâmica).
Por outro lado, um detalhe peculiar, mas muito importante, é que as bibliotecas dinâmicas gozam de total autonomia sobre seus símbolos locais. Na verdade,
várias bibliotecas dinâmicas podem ser carregadas no mesmo processo, cada biblioteca dinâmica apresentando símbolos locais que têm os mesmos nomes que os
símbolos locais de outras bibliotecas dinâmicas. No entanto, o vinculador consegue evitar quaisquer conflitos de nomenclatura.
A existência permitida de várias instâncias dos símbolos com o mesmo nome pode levar a uma série de consequências indesejadas. Um cenário é conhecido como
as múltiplas instâncias do paradoxo da classe singleton, que será ilustrado com mais detalhes no Capítulo 10.
Digamos que você tenha um trecho de código que fornece certa funcionalidade e deve decidir se vai ou não encapsulá-lo na forma de uma biblioteca estática. Aqui
estão alguns cenários típicos em que o caso da biblioteca estática é contra-indicado:
• Quando a vinculação da biblioteca estática requer a vinculação de várias bibliotecas dinâmicas (exceto talvez a libc), então a biblioteca estática provavelmente
não deve ser usada, e a opção de biblioteca dinâmica correspondente deve ser favorecida.
• O código-fonte da biblioteca (se disponível) deve ser reconstruído para criar a biblioteca dinâmica. ou
• A biblioteca estática disponível deve ser desmontada nos arquivos objeto, que (exceto em alguns casos raros) podem ser usados no projeto de construção que
constrói a biblioteca dinâmica.
Isso é completamente análogo à situação que acontece quando uma pessoa com necessidades especiais (hábitos alimentares especiais ou requisitos de condições
médicas / ambientais especiais) decide ficar na casa de um amigo ao visitar a cidade em que o amigo mora. Para que o amigo possa atender às necessidades
especiais do hóspede, ele precisa reorganizar significativamente sua vida cotidiana, a fim de fazer viagens extras incomuns às lojas de alimentos especializados, ou
fornecer condições especiais de que ele mesmo não precisa realmente no seu dia a dia. Faz muito mais sentido que o visitante assuma um papel mais
independente, como conseguir um quarto de hotel ou providenciar o suporte para suas necessidades específicas; e assim que suas próprias referências forem
resolvidas, entre em contato com o amigo cuja cidade ele está visitando.
• Se a funcionalidade que você implementa requer a existência de uma única instância de uma classe (padrão singleton), seguir as boas práticas de design de
biblioteca dinâmica acabará levando à forte sugestão de encapsular seu código em uma biblioteca dinâmica em vez de estática. A razão por trás disso foi explicada
no parágrafo anterior.
Um bom exemplo da vida real desse cenário é o projeto de um utilitário de registro. Normalmente apresenta uma única instância de uma classe visível para uma
variedade de módulos de funcionalidade, especializada em serializar todas as instruções de log possíveis e enviar o fluxo de log para a mídia de gravação (stdout,
disco rígido ou arquivo de rede, etc.).
Se os módulos de funcionalidade forem implementados como bibliotecas dinâmicas, é altamente recomendável hospedar a classe do criador de logs em outra
biblioteca dinâmica.
• A vinculação de bibliotecas estáticas acontece sequencialmente, uma biblioteca estática por uma.
• A vinculação de bibliotecas estáticas começa na última biblioteca estática na lista de bibliotecas estáticas passadas para o vinculador (da linha de comando ou
por meio do makefile) e retrocede em direção à primeira biblioteca da lista.
• O vinculador pesquisa as bibliotecas estáticas em detalhes e, de todos os arquivos-objeto contidos na biblioteca estática, ele vincula apenas ao arquivo-objeto,
que contém símbolos realmente necessários ao binário do cliente.
Como resultado dessas regras específicas, às vezes é necessário especificar a mesma biblioteca estática mais de uma vez na mesma lista de bibliotecas estáticas transmitidas ao
vinculador. As chances de isso acontecer aumentam quando uma biblioteca estática fornece vários conjuntos de funcionalidades não relacionados.
A biblioteca estática pode ser convertida em biblioteca dinâmica de forma bastante simples. Tudo que você precisa fazer é o seguinte:
• Use a ferramenta archiver (ar) para extrair todos os arquivos-objeto da biblioteca, como
$ ar -x <biblioteca estática> .a
que resulta com a coleção de arquivos-objeto extraídos da biblioteca estática para a pasta atual.
No Windows, você pode usar a ferramenta lib.exe que está disponível no console do Visual Studio. Com base na documentação online do MSDN (
http://support.microsoft.com/ kb / 31339) , é possível extrair pelo menos um arquivo de objeto (primeiro você precisa listar o conteúdo da biblioteca
estática, o que também pode ser obtido usando o ferramenta lib.exe ).
• Construir a biblioteca dinâmica a partir do conjunto de arquivos de objetos extraídos para o vinculador.
Esta receita funciona em quase todos os casos. Os casos especiais em que requisitos adicionais devem ser satisfeitos são apresentados a seguir.
O uso de bibliotecas estáticas no Linux de 64 bits vem com um cenário interessante de caso secundário. Aqui está o esboço:
• No entanto, vincular a biblioteca estática à biblioteca compartilhada requer que a biblioteca estática seja construída com o sinalizador do compilador -fPIC
(sugerido pela impressão de erro do compilador) ou com o sinalizador do compilador -mcmodel = large .
Primeiro, uma mera menção do sinalizador do compilador -fPIC no contexto de bibliotecas estáticas pode ser um pouco confuso. Como discutirei no próximo
capítulo, lidando com bibliotecas dinâmicas, o uso do sinalizador -fPIC tem sido tradicionalmente associado à construção de bibliotecas dinâmicas.
É uma crença popular que passar o sinalizador -fPIC para o compilador é um dos dois requisitos principais estritamente exigidos por bibliotecas dinâmicas, mas
nunca exigido para compilar bibliotecas estáticas. Qualquer menção ao sinalizador do compilador -fPIC no contexto de bibliotecas estáticas é um pouco chocante.
Na verdade, essa crença não é exatamente correta, mas é bastante segura contra erros. A verdade é que o uso do sinalizador -fPIC não é o fator decisivo se a
biblioteca estática ou dinâmica será criada; é o sinalizador de vinculador -shared .
De volta à dura realidade. A verdadeira razão pela qual o compilador insiste em compilar a biblioteca estática com o sinalizador -fPIC é que na plataforma de 64
bits o intervalo de deslocamentos de endereço não pode ser coberto pelas construções montadoras de compilador usuais em que os registros de 32 bits são usados.
O compilador precisa de uma espécie de kick (o uso de -fPIC ou o -mcmodel = sinalizadores de compilador grandes ) para implementar o mesmo código com
os registradores de 64 bits.
Tentar desmontar a biblioteca estática no arquivo objeto não muda a situação nem por um pouquinho; os arquivos de objeto não foram compilados com os
sinalizadores de compilador necessários para este cenário específico, e nenhuma mágica de conversão de biblioteca pode ajudar a evitar a necessidade de
recompilar as fontes de biblioteca estática.
A única solução verdadeira para esse tipo de problema é que alguém que tem o código-fonte (o distribuidor do código ou o usuário final) modifica os parâmetros
de construção (edite o Makefile) adicionando os sinalizadores necessários ao conjunto de sinalizadores do compilador.
Se isso servir de consolo, imagine que você não tem o código-fonte da biblioteca. Agora, isso seria assustador, hein?
CAPÍTULO 6
O processo de construção de bibliotecas dinâmicas tradicionalmente consiste no seguinte conjunto mínimo de sinalizadores:
O exemplo simples a seguir demonstra o processo de criação da biblioteca dinâmica a partir de dois arquivos de origem:
Pela convenção do Linux, as bibliotecas dinâmicas começam com o prefixo lib e têm a extensão de nome de arquivo .so. Se você seguir esta receita, há poucas
chances de se perder. Se esses sinalizadores forem passados para o compilador e o vinculador, respectivamente, sempre que você pretende construir uma
biblioteca dinâmica, o resultado final será a biblioteca dinâmica correta e utilizável. No entanto, aceitar essa receita como verdade incontestável e universal não é a
O restante desta seção será focado principalmente no lado do Linux (embora alguns dos conceitos também existam no Windows).
Os detalhes sobre o uso do sinalizador -fPIC podem ser melhor ilustrados por meio da sequência de perguntas e respostas a seguir.
O "PIC" em -fPIC é a sigla para código independente de posição. Antes que o conceito de código independente de posição ganhasse destaque, era possível criar
bibliotecas dinâmicas que o carregador era capaz de carregar no espaço de memória do processo. No entanto, apenas o processo que carregou pela primeira vez a
biblioteca dinâmica poderia aproveitar os benefícios de sua presença; todos os outros processos em execução que precisavam carregar a mesma biblioteca
dinâmica não tinham escolha a não ser carregar outra cópia da mesma biblioteca dinâmica na memória. Quanto mais processos são necessários para carregar uma
biblioteca dinâmica específica, mais cópias devem existir na memória.
A causa subjacente de tais limitações era um projeto de procedimento de carregamento abaixo do ideal. Ao carregar a biblioteca dinâmica no processo, o
carregador alterou o segmento de código (.text) da biblioteca dinâmica de uma forma que tornou todos os símbolos da biblioteca dinâmica significativos
apenas dentro do domínio do processo que carregou a biblioteca. Embora essa abordagem fosse adequada para as necessidades de tempo de execução mais
básicas, o resultado final foi que a biblioteca dinâmica carregada foi irreversivelmente alterada de forma que seria bastante difícil para qualquer outro processo
reutilizar a biblioteca já carregada. Esta abordagem de projeto de carregador original é conhecida como realocação de tempo de carregamento e será discutida em
mais detalhes nos parágrafos subsequentes.
O conceito PIC foi claramente um grande passo à frente. Ao redesenhar o mecanismo de carregamento para evitar amarrar o segmento de código (.text) da
biblioteca carregada ao mapa de memória do primeiro processo que o carregou, o entalhe de funcionalidade extra desejado foi alcançado fornecendo o caminho
para vários processos mapearem perfeitamente para seu mapa de memória a biblioteca dinâmica já carregada.
Pergunta 2: O uso do sinalizador do compilador -fPIC é estritamente necessário para construir a biblioteca dinâmica?
A resposta não é única. Na arquitetura de 32 bits (X86), não é necessário. Se não for especificado, no entanto, a biblioteca dinâmica estará em conformidade com o
mecanismo de carregamento de realocação de tempo de carregamento mais antigo, no qual apenas o processo que carrega a biblioteca dinâmica primeiro será
capaz de mapeá-la em seu mapa de memória de processo.
Em arquiteturas de 64 bits (X86_64 e I686), a simples omissão do sinalizador do compilador -fPIC (em uma tentativa de implementar o mecanismo de realocação
de tempo de carregamento) resultará no erro do vinculador. Uma discussão sobre por que isso acontece e como contornar o problema será fornecida
posteriormente neste livro. A solução para esse tipo de situação é passar o sinalizador -fPIC ou -mcmodel = large para o compilador.
Pergunta 3: O uso do sinalizador do compilador -fPIC está estritamente restrito ao domínio de bibliotecas dinâmicas?
Ele pode ser usado ao construir a biblioteca estática?
É crença popular que o uso do sinalizador -fPIC é estritamente confinado ao domínio das bibliotecas dinâmicas. A verdade é um pouco diferente.
Na arquitetura de 32 bits (X86), realmente não importa se você compila a biblioteca estática com o sinalizador -fPIC ou não. Isso terá um certo impacto na
estrutura do código compilado; no entanto, terá um impacto insignificante na vinculação e no comportamento geral do tempo de execução da biblioteca.
Na arquitetura de 64 bits (X86_64 com certeza), as coisas são ainda mais interessantes.
• A biblioteca estática vinculada ao executável pode ser compilada com ou sem o sinalizador do compilador -fPIC (ou seja, não importa se você especifica ou
não).
Contudo:
• A biblioteca estática vinculada à biblioteca dinâmica deve ser compilada com o sinalizador -fPIC !!! (Alternativamente, em vez do sinalizador -fPIC , você
pode especificar o sinalizador do compilador -mcmodel = large .)
Se a biblioteca estática não foi compilada com nenhum dos dois sinalizadores, a tentativa de vinculá-la à biblioteca dinâmica resulta no erro do vinculador
mostrado na Figura 6-1.
1
/ usr / bin / Id:. JstaticLib / libstatlclinklngderio.aCtestStaticLlriklng.o): relocação R_X86_64_32 contra .rodata não pode ser usado ao
fazer um
../stattcLlb/llbstatlellnklngdemo.a: não foi possível ler synbols: Valor incorreto Figura 6-1. Erro de linker
Uma discussão técnica interessante relacionada a esse problema pode ser encontrada no seguinte artigo da web: www.technovelty.org/c/position-
independent-code-and-x86-64-libraries.html .
O processo de construção de uma biblioteca dinâmica simples no Windows requer seguir uma receita bastante simples. A sequência de capturas de tela (Figuras 6-
2 a 6-6) ilustra o processo de criação do projeto DLL. Uma vez que o projeto é criado, construir a DLL não requer nada mais do que lançar o comando Build.
| Modelos Recentes | .NET Framework 4 'Classificar por: Padrão -J jj jj Pesquisar Modelos Instalados P |
Modelos Instalados Tipo: Visual C ++
Aplicativo de console Win32 Visual C ++
um Visual C ++ Um projeto para criar um aplicativo Win32.
ATL
Projeto Win32 Visual C ++ aplicativo de console, DLL ou biblioteca estática
CLR
Em geral
MFC
Teste
Win32
Outras línguas
Base de dados
b Projetos de teste
1 Modelos Online
Nome: DemoDLL
Localização: C: \ Users \ miian \ Navegar...
Por sua natureza, uma biblioteca dinâmica em geral fornece uma funcionalidade específica para o mundo externo, cuja maneira deve minimizar o envolvimento
do cliente nos detalhes da funcionalidade interna. A forma como isso é feito é através da interface, onde o cliente fica ao máximo aliviado por saber de algo com
que não precisa se preocupar.
O conceito de interface, que é onipresente no domínio da programação orientada a objetos, obtém um sabor extra no domínio da reutilização de código binário.
Conforme explicado na seção "O impacto do conceito de reutilização binária" no Capítulo 5, a imutabilidade da interface binária do aplicativo (ABI) entre as fases
de tempo de construção e tempo de execução da vinculação dinâmica é o requisito mais básico de uma vinculação dinâmica bem-sucedida.
À primeira vista, o design da ABI não difere muito do design da API. O significado básico do conceito de interface permanece inalterado: um conjunto de funções
que precisam ser disponibilizadas ao cliente para utilizar os serviços prestados por um módulo especializado.
Na verdade, enquanto o programa não for escrito em C ++, o esforço de design da ABI da biblioteca dinâmica não exige mais raciocínio do que projetar a API de
um módulo de software reutilizável. O fato de a ABI ser apenas um conjunto de símbolos de vinculação que precisam ser carregados em tempo de execução não
torna as coisas substancialmente diferentes.
No entanto, o impacto da linguagem C ++ (mais notavelmente, a falta de padronização estrita) requer pensamento adicional ao projetar a biblioteca dinâmica ABI.
Problemas C ++
Um fato infeliz da vida é que o progresso no domínio das linguagens de programação não foi seguido simetricamente pelo design de linkers, ou, para dizer
exatamente, pelo rigor dos corpos normativos que trazem os padrões no domínio do software. As boas razões para não fazer isso serão apontadas ao longo desta
seção. Um excelente artigo que ilustra essas questões é o "Guia do Iniciante para Linkers " em www.lurklurk.org/linkers/linkers.html . Vamos começar
com os fatos simples e revisar algumas das questões.
• Em geral, as funções C ++ raramente são independentes; em vez disso, tendem a ser afiliados a várias entidades de código.
A primeira coisa que vem à mente é sim, em C ++, as funções geralmente pertencem às classes (e, como tal, até têm o nome especial: métodos). Além disso, as
classes (e, portanto, seus métodos) podem pertencer aos namespaces. A situação fica ainda mais complicada quando os modelos entram em cena.
• O mecanismo de sobrecarga do C ++ permite que métodos diferentes da mesma classe tenham o mesmo nome, o mesmo valor de retorno, mas diferem em
termos de listas de argumentos de entrada.
Para identificar exclusivamente as funções (métodos) que compartilham o mesmo nome, o vinculador deve de alguma forma adicionar as informações sobre os
argumentos de entrada ao símbolo que ele cria para o ponto de entrada da função.
Os esforços de design do linker para responder a esses requisitos substancialmente mais complexos resultaram na técnica conhecida como name muting. Em
poucas palavras, a mutilação de nome é o processo de combinar o nome da função, as informações de afiliação da função e a lista de argumentos da função para
criar o nome do símbolo final. Normalmente, a afiliação da função é anexada (prefixada), enquanto as informações de assinatura da função são anexadas (pós-
fixadas) ao nome da função.
A principal fonte de problemas é que as convenções de mutilação de nomes não são padronizadas exclusivamente e, até hoje, são específicas do fornecedor. O
artigo da Wikipedia ( http://en.wikipedia.org/wiki/Name_mangling#How_ different_compilers_mangle_the_same_functions) ilustra as diferenças
nas implementações de mutilação de nomes em diferentes vinculadores. Conforme declarado no artigo, muitos outros fatores além da ABI desempenham um
papel na implementação do mecanismo de mutilação (pilha de manipulação de exceções, layout de tabelas virtuais, estrutura e preenchimento de quadro de
pilha). Dada a grande variedade de requisitos, o Manual de Referência C ++ Anotado ainda recomenda a manutenção de esquemas individuais de mutilação.
FUNÇÕES ESTILO C
Ao usar o compilador C ++, coisas interessantes acontecem ao usar funções de estilo C. mesmo que as funções C não exijam mutilação, o vinculador por
padrão cria nomes mutilados para elas. nos casos em que se deseja evitar o mutilamento, deve-se aplicar uma palavra-chave especial para sugerir ao
vinculador que não o faça.
a técnica é baseada no uso da palavra-chave extern "C" . Quando uma função é declarada (normalmente em um arquivo de cabeçalho) da seguinte
maneira
#ifdef_cplusplus
extern "C" {
#endif // _cplusplus
#ifdef_cplusplus
#endif // _cplusplus
o resultado final é que o linker cria seu símbolo privado de qualquer mutilação. Posteriormente neste capítulo, a seção sobre exportação de ABI conterá uma
explicação mais detalhada de por que essa técnica é tão importante.
Um dos legados das linguagens C é que o linker pode lidar com variáveis inicializadas de maneira bastante simples, sejam eles tipos de dados simples ou as
estruturas. Tudo o que o vinculador precisa fazer é reservar o armazenamento na seção .data e gravar o valor inicial nesse local. No domínio da linguagem C, a
ordem em que as variáveis são inicializadas geralmente não tem nenhuma importância particular. Tudo o que importa é que a inicialização das variáveis seja
concluída antes do início do programa.
Em C ++, entretanto, o tipo de dados é um objeto em geral, e sua inicialização é concluída em tempo de execução por meio do processo de construção do objeto,
que é concluído quando o método construtor da classe completa sua execução. Obviamente, o vinculador precisa fazer muito mais coisas para inicializar os objetos
C ++. Para facilitar o trabalho do vinculador, o compilador incorpora no arquivo-objeto a lista de todos os construtores que precisam ser executados para um
arquivo específico e armazena essas informações no segmento de arquivo-objeto específico. No momento do link, o vinculador examina todos os arquivos-objeto e
combina essas listas de construção na lista final que será executada no tempo de execução.
É importante mencionar neste ponto que os ligadores observam a ordem de execução dos construtores com base na cadeia de herança. Em outras palavras, é
garantido que o construtor da classe base será executado primeiro, seguido pelos construtores das classes derivadas. Essa lógica incorporada ao vinculador é
suficiente para a maioria dos cenários possíveis.
O vinculador, no entanto, não é indefinidamente inteligente. Infelizmente, há toda uma categoria de casos em que um programador não se desvia de forma
alguma das regras de sintaxe do C ++, mas a lógica limitada do vinculador causa travamentos muito desagradáveis que acontecem antes de o programa ser
carregado, muito antes que qualquer depurador possa detectá-lo.
O cenário típico desse tipo acontece quando a inicialização de um objeto depende de algum outro objeto sendo inicializado de antemão. Vou primeiro explicar o
mecanismo subjacente do problema e, em seguida, sugerir maneiras para o programador evitá-los. Em círculos de programadores C ++, essa classe de problemas é
normalmente referida como um fiasco de ordem de inicialização estática.
Descrição do Problema
Objetos estáticos não locais são as instâncias de uma classe C ++ cujo escopo de visibilidade excede os limites de uma classe. Mais especificamente, tais objetos
podem ser um dos seguintes:
Esses objetos são inicializados rotineiramente pelo vinculador antes de o programa começar a ser executado. Para cada um desses objetos, o vinculador mantém a
lista de construtores necessários para criar tal objeto e os executa na ordem especificada pela cadeia de herança.
Infelizmente, este é o único esquema de ordenação de inicialização de objeto que o vinculador reconhece e implementa. Agora é a hora da reviravolta especial em
toda a história.
Vamos supor que um desses objetos dependa de algum outro objeto sendo inicializado de antemão. Suponha, por exemplo, que você tenha dois objetos estáticos:
• Objeto A (instância da classe a), que inicializa a infraestrutura de rede, consulta a lista de redes disponíveis, inicializa os soquetes e estabelece a conexão inicial
com o servidor de autenticação.
• Objeto B (instância da classe b), que é necessário para enviar a mensagem pela rede ao servidor de autenticação remoto, chamando os métodos de interface na
instância da classe b.
Obviamente, a ordem correta de inicialização é que o objeto B seja inicializado após o objeto A. É óbvio que violar a ordem de inicialização do objeto tem um
potencial muito real de causar estragos. Mesmo que os designers tenham sido cuidadosos o suficiente para prever casos em que a inicialização não foi concluída
(ou seja, verificar os valores do ponteiro antes de fazer as chamadas reais), o melhor que pode acontecer é que a tarefa da classe B não seja concluída quando
esperado.
Na verdade, não existe uma regra que dite a ordem em que a inicialização de objetos estáticos acontecerá. As tentativas de implementar o algoritmo que
examinaria o código reconhecem tais cenários e sugerem a ordenação correta para o vinculador provaram pertencer à categoria de problemas que são muito
difíceis de resolver. A presença de outros recursos da linguagem C ++ (modelos) apenas agrava o caminho para a solução do problema.
Como consequência final, o vinculador pode decidir inicializar o objeto estático não local em qualquer ordem. Para piorar as coisas, a decisão do vinculador de
qual ordem seguir pode depender de um número inimaginável de circunstâncias de tempo de execução não relacionadas.
Na vida real, esses problemas são assustadores por vários motivos. Primeiro, eles são difíceis de rastrear, pois resultam em travamentos que acontecem antes que
o carregamento do processo seja conectado, muito antes que o depurador possa ajudar. Além disso, as ocorrências de travamento podem não ser persistentes; a
falha pode acontecer de vez em quando ou em alguns cenários sempre com sintomas diferentes.
Evitando o problema
Mesmo que o problema não seja para os fracos de coração, há uma maneira de evitar a confusão feia. As regras do vinculador não especificam a ordem de
inicialização das variáveis, mas a ordem é especificada com muita precisão para as variáveis estáticas declaradas dentro de um corpo de função. Ou seja, o objeto
declarado como estático dentro da função (ou método de classe) é inicializado quando sua definição é encontrada pela primeira vez durante uma chamada para
essa função.
A solução para esse problema se torna óbvia. As instâncias não devem ser mantidas em roaming livre na memória de dados. Em vez disso, eles deveriam ser
• Uma função deve ser convenientemente usada como a única maneira de acessar tal variável (retornando a referência ao objeto, por exemplo) definida estática no
escopo do arquivo.
Em resumo, as duas soluções possíveis a seguir são tradicionalmente aplicadas para resolver esses tipos de problemas:
• SOLUÇÃO 1: Proporcionar a implementação customizada do método _init () , um método padrão chamado imediatamente quando a biblioteca dinâmica é
carregada, no qual um método estático de classe instancia o objeto, forçando assim a inicialização pela construção. Conseqüentemente, a implementação
personalizada do _fini () padrão , um método padrão chamado imediatamente antes que a biblioteca dinâmica seja descarregada, pode ser fornecida em que
a desalocação do objeto pode ser concluída.
• SOLUÇÃO 2: Substituir o acesso direto a tal objeto por uma chamada a uma função personalizada. Essa função conterá uma instância estática da classe C ++ e
retornará a referência para
isto. Antes do primeiro acesso, será construída uma variável declarada como estática, garantindo que sua inicialização aconteça antes da primeira chamada real. O
compilador GNU, bem como o padrão C ++ 11, garantem que essa solução seja segura para threads.
Questão 3: Modelos
A essência do problema é que diferentes especializações de modelos têm representações de código de máquina completamente diferentes. Por sorte, uma vez
escrito, o modelo pode ser especializado em cerca de zilhões de maneiras, dependendo de como o usuário do modelo deseja usá-lo. O seguinte modelo
T max (T x, T y) {
pode ser especializado para tantos tipos de dados que suportam operador de comparação (tipos de dados simples variando de char até double são os
candidatos imediatos).
Quando o compilador encontra o modelo, ele precisa materializá-lo em alguma forma de código de máquina. Mas, isso não pode ser feito até que todos os outros
arquivos de origem tenham sido examinados para descobrir qual especialização específica ocorreu no código. Como isso pode ser relativamente fácil no caso de
aplicativos autônomos, a tarefa requer muita reflexão quando o modelo é exportado por uma biblioteca dinâmica.
• O compilador pode gerar todas as especializações de modelo possíveis e criar símbolos fracos para cada uma delas. A explicação completa do conceito de
símbolos fracos pode ser encontrada na discussão sobre os tipos de símbolo do linker. Observe que o vinculador tem a liberdade de descartar símbolos fracos, uma
vez que determina que eles não são realmente necessários na compilação final.
• A abordagem alternativa é que o vinculador não inclui as implementações de código de máquina de nenhuma das especializações de modelo até o final. Depois
que todo o resto estiver concluído, o vinculador pode examinar o código, determinar exatamente quais especializações são realmente necessárias, chamar o
compilador C ++ para criar as especializações de modelo necessárias e, finalmente, inserir o código de máquina no executável. Essa abordagem é favorecida pelo
conjunto de compiladores Solaris C ++.
Para minimizar possíveis problemas, melhorar a portabilidade para diferentes plataformas e até mesmo aumentar a interoperabilidade entre os módulos criados
por diferentes compiladores, é altamente recomendável praticar as seguintes diretrizes.
Existem muitas boas razões para explicar por que esse conselho faz muito sentido. Por exemplo, você pode
• Melhorar a interoperabilidade entre os binários produzidos por diferentes compiladores. (Alguns dos compiladores tendem a produzir binários que podem ser
usados por outros compiladores. Exemplos notáveis são os compiladores MinGW e Visual Studio.)
Para exportar os símbolos ABI como funções de estilo C, use a palavra-chave extern "C" para direcionar o vinculador a não aplicar a mutilação de nome nesses
símbolos.
A "declaração ABI completa" significa não apenas protótipos de função, mas também as definições do pré-processador, layouts de estruturas, etc.
Se a funcionalidade interna da biblioteca dinâmica for implementada por uma classe C ++, isso ainda não significa que você deve violar a diretriz nº 1. Em vez
disso, você deve seguir a chamada abordagem de fábrica de classes (Figura 6-7).
A fábrica de classes é uma função no estilo C que representa uma ou mais classes C ++ para o mundo externo (semelhante a um agente de Hollywood que
representa muitos atores famosos em negociações com estúdios de cinema).
Como regra, a fábrica de classes tem conhecimento íntimo do layout da classe C ++, que normalmente é obtido declarando-o como um método estático da mesma
classe C ++.
Quando chamada pelo cliente interessado, a fábrica de classes cria uma instância da classe C ++ que representa. Para manter os detalhes do layout da classe C ++
longe dos olhos curiosos do cliente, ele nunca encaminha a instância da classe de volta para o chamador. Em vez disso, ele converte a classe C ++ para uma
interface de estilo C e converte o ponteiro para o objeto C ++ criado como o ponteiro da interface.
Obviamente, para que esse esquema funcione corretamente, a classe C ++ representada pela fábrica de classes é obrigada a implementar a interface de exportação.
No caso particular de C ++, isso significa que a classe deve herdar publicamente a interface. Dessa forma, lançar o ponteiro da classe para o ponteiro da interface é
muito natural.
Finalmente, esse esquema requer que um certo mecanismo de rastreamento de alocação mantenha o controle de todas as instâncias alocadas pela função de fábrica
de classe. Na tecnologia Microsoft Component Object Model (COM), a contagem de referência garante que o objeto alocado seja destruído quando não estiver
mais sendo usado. Em outras implementações, sugere-se manter a lista de ponteiros para os objetos alocados. No momento do encerramento (delineado por uma
chamada para uma função de limpeza de um tipo), cada elemento da lista seria excluído e a lista finalmente limpa.
O equivalente em C da fábrica de classes é normalmente conhecido como módulo. É o corpo do código que fornece a funcionalidade para o mundo externo por
meio de um conjunto de funções de interface cuidadosamente projetadas.
O design modular é típico para módulos de kernel de baixo nível e drivers de dispositivo, mas sua aplicação não é de forma alguma limitada a esse domínio
específico. O módulo típico exporta funções como Open () (ou Initialize ()), uma ou mais funções de trabalho (Read () Write (), SetMode (), etc.)
e, finalmente, Close () (ou Deinitialize ()) .
Muito típico para módulos é o uso de handle, um tipo de identificador de instância de módulo, muito frequentemente implementado como um ponteiro void,
um predecessor desse ponteiro em C ++.
O identificador normalmente é criado dentro do método Open () e é retornado ao chamador. Em chamadas para outros métodos de interface de módulo, o
identificador é o primeiro argumento de função obrigatório.
Nos casos em que C ++ não é uma opção, projetar o módulo C é completamente viável, equivalente ao conceito orientado a objetos de uma fábrica de classes.
Por ser modular por natureza, a biblioteca dinâmica deve ser projetada de modo que sua funcionalidade seja exposta ao mundo exterior por meio de um conjunto
definido de símbolos de função (a interface binária do aplicativo, ABI), enquanto os símbolos de todas as outras funções usadas apenas internamente devem ser
acessível aos executáveis do cliente. Existem vários benefícios para esta abordagem:
• O tempo de carregamento da biblioteca pode ser tremendamente melhorado como resultado da redução significativa no número de símbolos exportados.
• A chance de símbolos em conflito / duplicados entre as diferentes bibliotecas dinâmicas torna-se significativamente reduzida.
A ideia é bastante simples: a biblioteca dinâmica deve exportar apenas os símbolos das funções e dados que são absolutamente necessários para quem carrega a
biblioteca, e todos os outros símbolos devem ficar invisíveis. A seção a seguir trará mais detalhes sobre como controlar a visibilidade dos símbolos da biblioteca
dinâmica.
Da perspectiva de alto nível, o mecanismo de exportar / ocultar os símbolos do vinculador é resolvido quase de forma idêntica no Windows e no Linux. A única
diferença substancial é que, por padrão, todos os símbolos do vinculador DLL do Windows ficam ocultos, enquanto no Linux todos os símbolos do vinculador da
biblioteca dinâmica são exportados por padrão.
Na prática, devido a um conjunto de recursos fornecidos pelo GCC em uma tentativa de alcançar uniformidade de plataforma cruzada, os mecanismos de
exportação de símbolo são muito semelhantes e fazem praticamente a mesma coisa, no sentido de que, em última análise, apenas os símbolos de vinculação que
compõem o aplicativo a interface binária é exportada, enquanto todos os símbolos restantes ficam ocultos / invisíveis.
É óbvio que algum tipo de controle sobre quais símbolos são exportados é necessário. Além disso, como esse controle já está implementado nas DLLs do
Windows, atingir o paralelismo facilitaria tremendamente os esforços de portabilidade.
Existem vários mecanismos para como o controle sobre a exportação de símbolos pode ser obtido no momento da construção. Além disso, a abordagem de força
bruta pode ser aplicada executando a ferramenta de linha de comando strip sobre o binário da biblioteca dinâmica. Finalmente, é possível combinar vários
métodos diferentes com o mesmo objetivo de controlar a visibilidade dos símbolos da biblioteca dinâmica.
O compilador GCC fornece vários mecanismos para configurar a visibilidade dos símbolos do vinculador: MÉTODO 1: (afetando todo o corpo do código)
-fvisibility = sinalizador do compilador oculto , é possível fazer com que todos os símbolos da biblioteca dinâmica não sejam exportados / invisíveis para
quem tentar vincular dinamicamente à biblioteca dinâmica.
Ao decorar a assinatura da função com a propriedade do atributo, você instrui o vinculador a permitir (padrão) ou não permitir (oculto) a exportação do símbolo.
MÉTODO 3: (afetando símbolos individuais ou um grupo de símbolos) #pragma visibilidade GCC [push | pop]
Esta opção é normalmente usada nos arquivos de cabeçalho. Fazendo algo assim
#pragma visibilidade push (oculto) void someprivatefunction_l (void); void someprivatefunction_2 (void);
você está basicamente tornando invisíveis / não exportadas todas as funções declaradas entre as instruções #pragma . Esses três métodos podem ser combinados
de qualquer maneira que o programador considere adequada.
Os Outros Métodos
O vinculador GNU suporta um método sofisticado de lidar com o controle de versão da biblioteca dinâmica, no qual um arquivo de script simples é passado para
o vinculador (por meio do sinalizador -Wl, - version-script, <nome do arquivo de script> vinculador). Por mais que o objetivo original do
mecanismo seja especificar as informações da versão, ele também tem o poder de afetar a visibilidade do símbolo. A simplicidade com que realiza a tarefa torna
esta técnica a forma mais elegante de controlar a visibilidade do símbolo. Mais detalhes sobre essa técnica podem ser encontrados no Capítulo 11 nas seções que
discutem o controle de versão das bibliotecas do Linux.
Para ilustrar o mecanismo de controle de visibilidade, criei um projeto de demonstração no qual duas bibliotecas dinâmicas idênticas foram construídas com
configurações de visibilidade diferentes. As bibliotecas são apropriadamente chamadas de libdefaultvisibility.so e libcontrolledvisibility.so.
Depois que as bibliotecas são construídas, seus símbolos são examinados usando o utilitário nm (que é abordado em detalhes nos Capítulos 12 e 13).
O exame dos símbolos presentes no binário da biblioteca construída não traz surpresas, pois os símbolos de todas as funções são exportados e visíveis, conforme
mostrado na Figura 6-8.
No caso de uma biblioteca dinâmica na qual você deseja controlar a visibilidade / exportabilidade do símbolo, o sinalizador do compilador -fvisibility foi
especificado no Makefile do projeto, conforme mostrado na Listagem 6-2.
# Compilador
INCLUDES = $ (COMMON_INCLUDES)
ifeq ($ (DEBUG), 1)
outro
fim se
CFLAGS + = $ (VISIBILITY_FLAGS)
COMPILAR = g ++ $ (CFLAGS)
Quando a biblioteca é construída exclusivamente com essa configuração de visibilidade de símbolo em particular, o exame dos símbolos indicou que os símbolos
de função não foram exportados (Figura 6-9).
nm -D ^ libcontrolledvisibility.so
Em seguida, quando a decoração de assinatura de função com os atributos de visibilidade é aplicada, conforme mostrado na Listagem 6-3,
o efeito líquido é que a função declarada com o_atributo_ ((visibilidade ("padrão"))) torna-se visível
Listagem 6-3. A função de decoração de assinatura com os atributos de visibilidade aplicados #include "sharedLibExports.h"
#if 1
//
// também compatível:
// mas isso não é compatível: // void printMessage FOR_EXPORT (void) // nem este:
//
// isto é, o atributo pode ser declarado em qualquer lugar // antes do nome da função
Outro mecanismo de controle da visibilidade dos símbolos está disponível. Não é tão sofisticado e não é programável. Em vez disso, ele é implementado
executando um utilitário de linha de comando strip (Figura 6-11). Esta abordagem é muito mais brutal, pois tem o poder de apagar completamente qualquer
informação sobre qualquer um dos símbolos da biblioteca, a tal ponto que nenhum dos utilitários de exame de símbolo usuais será capaz de ver qualquer um dos
símbolos, seja em a seção .dynamic ou não.
__FRAME_END__
_JCR_END_
_3CR_LIST__
__bss_start
_cxa_finalize @@ GLIBC_2.1.3
__do_global_ctors_aux
__do_global_dtors_aux
__dso_haridle
_gnon_start_
pulan @ milan
■ Nota Mais informações sobre o utilitário strip podem ser encontradas no Capítulo 13.
Felizmente, o mecanismo de exportação dos símbolos DLL está completamente sob o controle do programador. Na verdade, há dois mecanismos com suporte de
como os símbolos DLL podem ser declarados para exportação.
Esse mecanismo é fornecido por padrão pelo Visual Studio. Marque a caixa de seleção "Exportar símbolos" na caixa de diálogo de criação de novo projeto,
conforme mostrado na Figura 6-12.
Figura 6-12. Selecionando a opção "Exportsymbols" na caixa de diálogo Win32 DLL Wizzard
Aqui, você especifica que deseja que o assistente de projeto gere o cabeçalho de exportação da biblioteca contendo os trechos de código que se parecem com a
Figura 6-13.
// - O seguinte bloco ifdef é a maneira padrão de criar macros que tornam a exportação // • de uma DLL mais simples. Todos os arquivos
dentro desta DLL são compilados com o MI LANDLLDEMO_EXPORTS // - símbolo definido na linha de comando »Este símbolo não deve ser definido em
nenhum projeto // - que usa esta DLL. Desta forma, qualquer outro projeto cujos arquivos fonte incluam este arquivo veja // - as funções
MILANDLLDEMO_API como sendo importadas de um OIL; enquanto essa DLL vê símbolos // - definidos com esta macro como ■ sendo exportados.
#ifdef HILANOLLD £ MO_ £ XPOftTS
#outro
#fim se
público:
CailanDLLdem (vazio);
>;
extern MILAN DL LDEf »_API int nmilanDLLdemo; MI LAND LL DEi-10_A PI int frmilanDLLdemo (vazio);
Figura 6-13. O Visual Studio gera a declaração específica do projeto de palavras-chave_declspec (dllexport)
Como mostra a Figura 6-13, o cabeçalho de exportação pode ser usado tanto dentro do projeto DLL quanto pelo executável do cliente
projeto. Quando usada dentro do projeto DLL, a macro específica do projeto é avaliada como the_ declspec (dllexport)
palavra-chave dentro do projeto DLL, enquanto dentro do projeto executável do cliente ela avalia to_ declspec (dllimport).
Isso é imposto pelo Visual Studio, que insere automaticamente a definição do pré-processador no projeto DLL (Figura 6-14).
Figura 6-14. O Visual Studio gera automaticamente a definição de pré-processador específica do projeto
Quando a palavra-chave específica do projeto que avalia to_ declspec (dllexport) é adicionada à função
declaração, o símbolo do vinculador de função é exportado. Caso contrário, omitir essa palavra-chave específica do projeto evitará a exportação do símbolo de
função. A Figura 6-15 apresenta duas funções, das quais apenas uma é declarada para exportação.
■ * return -1; }
Ajuda e suporte
programas e arquivos s
Figura 6-16. Iniciando o prompt de comando do Visual Studio para acessar a coleção de ferramentas de linha de comando de análise binária
Figura 6-15. O Visual Studio gera automaticamente um exemplo de uso de palavras-chave de controle de exportação de símbolo específicas do projeto
Agora é o momento perfeito para apresentar o utilitário dumpbin do Visual Studio que você pode usar para analisar a DLL na busca por símbolos exportados. É
parte das ferramentas do Visual Studio e pode ser usado apenas executando o prompt de comando especial das Ferramentas do Visual Studio (Figura 6-16).
A Figura 6-17 mostra o que a ferramenta dumpbin (chamada com o sinalizador / EXPORT ) relata sobre os símbolos exportados por sua DLL.
e: \ milanDLLdema \ DebU £ ("> duripbin ✓EXPORTS milanDLLdemo.dll Microsoft <R> COFF ^ PE Dumper Uersion 10.00.-10219.01 Copyright <C> Microsoft Corporation
- direitos de preenchimento reservados.
5172E312 hora data stanp Sáb 20 de abril 11:48:50 2013 versão 0,00
2 1 00011172 ?? 4CmilanDLLdenoPPQflEAftU0 (? ABU0ePZ = PI LT + 365 <?? 4Cmilan DLL de nto 6 PQA Efl ft U 0 PA BU 0 PPZ>
HZ)
>a
LLdemo) Resumo
1000 .data 1000 .idata 2000 .rdata 1000. re loc 1000 .rsrc 4000 .text 10000 .textbss
Figura 6-17. Usando dumpbin.exe para visualizar a lista de símbolos exportados por DLL
Obviamente, o símbolo da função declarado com o símbolo de exportação específico do projeto acaba sendo exportado pela DLL. No entanto, o vinculador o
processou de acordo com as diretrizes C ++, que usam a mutilação de nomes. Os executáveis do cliente geralmente não têm problemas para interpretar tais
símbolos, mas se tiverem, você pode declarar a função como extern "C", o que resultará com o símbolo de função seguindo a convenção de estilo C (Figura 6-
18).
c: SnilanDLLdemoM> ebag> dunipbin / EXPORTS milanDLLderno.dll Microsoft <JO COFF / PE Dumper Uersion 10-00-40219.01 Copyright ^ C} Microsoft Corporation-
Todos os direitos reservados.
1 0 000110FF ?? 0Crii lan DLLde mo (? (? QAE (? XZ = P1MT * 250 <?? 0Cnil «nDLLdenoG (?
QAEGXZ>
2 1 00011172 ?? 4CmilanDLLde ™> CGQAEftAU0 (? FtBU0 [ ? EZ = <? I LI * 365 <?? 4Cmilan DLL de mo 0 5 QA EA AII00 AB U0 Z>
Lderao)
Sumwjf
1000 .data
1000 idata
2000 .rdata
1000 .reloc
1000 .rsrc
4000 .text
10000 .textbss
c: SmilanDLLdemoSl> ebug>
A maneira alternativa de controlar a exportação de símbolos DLL é por meio do uso de arquivos de definição de módulo (.def) .
Ao contrário do mecanismo descrito anteriormente (com base nas palavras- chave_declspec (dllexport) ), que pode ser especificado
por meio do assistente de criação de projeto, marcando a caixa de seleção "Exportar símbolos", o uso de um arquivo de definição de módulo requer algumas
medidas mais explícitas.
Para começar, se você planeja usar o arquivo .def , é recomendável não marcar a caixa de seleção "Exportar símbolos". Em vez disso, use o menu Arquivo ^ Novo
para criar um novo arquivo de definição (.def) . Se esta etapa for concluída corretamente, as configurações do projeto indicarão que o arquivo de definição do
módulo é oficialmente parte do projeto, conforme mostrado na Figura 6-19.
Alternativamente, você pode escrever manualmente o arquivo .def , adicioná-lo manualmente à lista de arquivos de origem do projeto e, finalmente, editar
manualmente a página de propriedades do vinculador para parecer conforme mostrado na Figura 6-19. O arquivo de definição de módulo que especifica a função
de demonstração para exportação se parece com a Figura 6-20.
um arquivo de cabeçalho
milanDLLdemo.h
stdafx.h
targetver.h
□ Arquivos de recursos
a Li Source Files
C "] dllmain.cpp
milanDLLdemo.cpp
milanDLLDemo.def
t3 stdafx.cpp
Q ReadMe.txt
Na linha EXPORTS, ele pode conter tantas linhas quantas forem as funções cujos símbolos você planeja exportar.
Um detalhe interessante é que o uso do arquivo de definição de módulo resulta em símbolos de função exportados como funções estilo C, sem a necessidade de
declarar a função como extern "C" . Se isso é uma vantagem ou desvantagem depende das preferências pessoais e das circunstâncias do projeto.
Uma vantagem particular de usar os arquivos de definição de módulo (.def) como método de exportação dos símbolos DLL é que, em certos cenários de
compilação cruzada, os compiladores não Microsoft tendem a oferecer suporte a essa opção.
Um exemplo é usar o compilador MinGW, que compila um projeto de código aberto (por exemplo, ffmpeg) para criar DLLs do Windows e arquivos .def
associados . Para que o DLL seja vinculado dinamicamente no momento da construção, você precisa usar sua biblioteca de importação, que infelizmente não foi
gerada pelo compilador MinGW.
X: \ MilanFFMpegWin32Build> dir * .def Uolume na unidade X é UBOX_U Bo xS bared Uolume O número de série é 9AE7-0879
14/02/2013 11h51
14/02/2013 11h51
14/02/2013 11h51
14/02/2013 11 = 51 AM
14/02/2013 11h51
14/02/2013 11h51
14/02/2013 11h51
7.012 avcodec-53.def
115 audeuice-53_def 5.107 auf ilter — 2 _ def 5.119 auformat-53.def 4.762 aoutil-5i.def
232 postproc-Sl-def
14/02/2013 11h51
8 Arquivo <s>
X: sMilanFFMpegVlin32Build> lib / máquina = X86 / def: aocodec-S3 .def / out: avcodec. lib
Microsoft <R> Library Manager Uersion 10.00.40219.01 Copyright <C> Microsoft Corporation. Todos os rigJits reserados.
Microsoft <R> Library Manager Uersion 10.00.40219.01 Copyright <C> Microsoft Corporation. Todos os direitos reservados.
Microsoft <R) Library Manager Uersion 10.00.40219.01 Copyright <C> Microsoft Corporation. Todos os direitos reservados.
Microsoft <R> Library Manager Uersion 10.00.40219.01 Copyright <C> Microsoft Corporation. Todos os direitos reservados.
i: \ MilanFFMpegWin32Build> lib / ntacliine: X86 /def:auutil-51.def / out: auutil.lib Microsoft (R> Library Manager Uersion 10.00.40219.01 Copyright <C> Microsoft
Corporation. Todos os direitos »• eserued.
Microsoft CR) Library Manager Uersion 10.00.40219.01 Copyright <C> Microsoft Corporation. Todos os direitos reservados.
<: \ MilanFFMpegl) in32Build> lib / máquina: X86 / def: suresample-0. def / out: suresample. lib
X: xMilanFFMpegVlin32Build> lib / machine: X86 /def:swscale~2.def / out: swscale.lib Microsoft <R> Library Manager Uersion 10.00.40219.01 Copyright <C> Microsoft
Corporation. Todos os direitos reservados.
Figura 6-21. Gerando arquivos de biblioteca de importação para DLLs gerados pelo compilador MingW com base em arquivos de definição de módulo (.def) especificados
• Incapacidade de discernir os métodos da classe C ++ das funções C: se dentro de uma DLL você tiver uma classe, e a classe tiver um método com o mesmo nome da
função C que você especificou para exportação no arquivo .def , o compilador irá relatar um conflito ao tentar descobrir qual dos dois deve ser exportado.
• peculiaridades extern "C" : Em geral, a função declarada para exportação dentro do arquivo .def não precisa ser declarada como extern "C", pois o
vinculador tomará cuidado para que seu símbolo siga a convenção C. No entanto, se você decidir decorar a função como extern "C", certifique-se de fazê-lo no
arquivo de cabeçalho e no arquivo .cpp de origem (o último dos quais normalmente não deve ser exigido). Não fazer isso irá de alguma forma confundir o
vinculador e o aplicativo cliente não será capaz de vincular seu símbolo de função exportado. Para que o problema seja mais difícil, a saída do utilitário dumpbin
não indicará nenhuma diferença, tornando o problema mais difícil de resolver.
O processo de criação de biblioteca dinâmica é um procedimento de construção completo, pois envolve tanto a fase de compilação quanto a de vinculação. Em
geral, o estágio de vinculação é concluído depois que cada símbolo do vinculador é resolvido, e esse critério deve ser observado independentemente de o destino
ser uma biblioteca executável ou dinâmica.
No Windows, essa regra é estritamente aplicada. O processo de vinculação nunca é considerado completo e o binário de saída nunca é criado até que cada símbolo
de biblioteca dinâmica tenha sido resolvido. A lista completa de bibliotecas dependentes é pesquisada até que a última referência de símbolo seja resolvida.
No Linux, no entanto, esta regra é um pouco distorcida por padrão ao construir as bibliotecas dinâmicas, pois é permitido que a vinculação da biblioteca dinâmica
seja concluída (e o arquivo binário criado) mesmo que nem todos os símbolos tenham sido resolvidos.
A razão por trás de permitir esse desvio da rigidez da regra sólida de outra forma é que é implicitamente assumido que os símbolos ausentes durante o estágio de
vinculação eventualmente aparecerão de alguma forma no mapa de memória do processo, muito provavelmente como resultado de alguma outra biblioteca
dinâmica sendo carregada em tempo de execução. Os símbolos necessários não fornecidos pela biblioteca dinâmica são marcados como indefinidos ("U").
Como regra, se o símbolo esperado por qualquer motivo não aparecer no mapa de memória do processo, o sistema operacional tende a relatar a causa de maneira
organizada, imprimindo a mensagem de texto no fluxo stderr, especificando o símbolo ausente.
Esta flexibilidade nas regras do Linux de vinculação de bibliotecas dinâmicas foi comprovada em várias ocasiões como um fator positivo, permitindo que certas
limitações de vinculação muito complexas sejam efetivamente superadas.
Apesar do fato de que a vinculação das bibliotecas dinâmicas é muito mais relaxada no Linux por padrão, o vinculador GCC oferece suporte à opção de
estabelecer os critérios de rigidez de vinculação que correspondem aos seguidos pelo vinculador do Windows.
Passar o sinalizador --no-undefined para o vinculador gcc resultará em uma construção malsucedida se cada símbolo não for resolvido no momento da
construção. Dessa forma, o padrão do Linux de tolerar a presença de símbolos não resolvidos é efetivamente revertido para os critérios estritos semelhantes aos do
Windows.
Observe que, ao invocar o vinculador por meio do gcc, os sinalizadores do vinculador devem ser precedidos pelo prefixo -Wl, como:
$ gcc -fPIC <arquivos de origem> -l <bibliotecas> -Wl, - no-undefined -o <nome do arquivo de saída shlib>
Em todas as discussões até agora, assumi implicitamente esse cenário particular. Na verdade, acontece com muita frequência que a necessidade de uma
funcionalidade de biblioteca dinâmica específica é necessária desde o momento em que o programa é iniciado até o seu encerramento, e esse fato é conhecido de
antemão. Neste cenário, o procedimento de construção requer os seguintes itens. Em tempo de compilação:
No momento do link:
• Os caminhos para os binários da biblioteca dinâmica necessários ao binário do cliente para configurar a lista de símbolos de biblioteca esperados.
Para obter mais detalhes sobre como os caminhos podem ser especificados, consulte a seção "Regras de localização da biblioteca em tempo de construção".
Toda a beleza do recurso de vinculação dinâmica é a capacidade do programador determinar em tempo de execução se a necessidade de uma determinada
biblioteca dinâmica realmente existe e / ou qual biblioteca específica precisa ser carregada.
Muitas vezes, o design requer que existam várias bibliotecas dinâmicas, cada uma das quais suporta a ABI idêntica, e que apenas uma delas seja carregada
dependendo da escolha do usuário. Um exemplo típico de tal cenário é o suporte a vários idiomas onde, com base nas preferências do usuário, o aplicativo carrega
a biblioteca dinâmica que contém todos os recursos (strings, itens de menu, arquivos de ajuda) escritos no idioma de escolha do usuário. Neste cenário, o
procedimento de construção requer os seguintes itens. Em tempo de compilação:
• O arquivo de cabeçalho de exportação da biblioteca dinâmica, especificando tudo o que pertence à interface ABI da biblioteca
No momento do link:
• Pelo menos o nome do arquivo da biblioteca dinâmica a ser carregada. O caminho exato do nome do arquivo da biblioteca dinâmica normalmente é resolvido
implicitamente, contando com o conjunto de regras de prioridade que regem a escolha dos caminhos nos quais o binário da biblioteca deve ser encontrado no
tempo de execução.
Todos os principais sistemas operacionais fornecem um conjunto simples de funções API que permitem ao programador explorar totalmente esse recurso precioso
(Tabela 6-1).
Independentemente do sistema operacional e / ou ambiente de programação, o paradigma típico de uso dessas funções pode ser descrito pela seguinte sequência
de pseudocódigo:
Reportar erro();
if (NULL == pFunction) {
As listagens 6-4 e 6-5 fornecem ilustrações simples do carregamento dinâmico do tempo de execução.
# define PI (3.1415926536)
void * pHandle;
dlclose (pHandle);
pHandle = NULL;
return -1;
dZcZose (pHandle);
pHandle = NULL;
return 0;
A Listagem 6-5 ilustra o carregamento dinâmico do tempo de execução do Windows, no qual tentamos carregar a DLL, localizar os símbolos das funções
DllRegisterServer () e / ou DllUnregisterServer () e executá-los.
#ifdef __cplusplus
extern "C" {
#endif // __cplusplus
#ifdef __cplusplus}
#endif // __cplusplus
enum {
if (NULL == dllHandle) {
if (NUMBER_OF_SUPPORTED_CMD_LINE_ARGUMENTS> argc) {
PDLL_REGISTER_SERVER pDllRegisterServer =
printf ("Falha ao encontrar o símbolo V'DllRegisterServerV '"); :: FreeLibrary (dllHandle); dllHandle = NULL; return -1;
pDllRegisterServer ();
outro {
PDLL_UNREGISTER_SERVER pDllUnregisterServer =
if (NULL == pDllUnregisterServer) {
printf ("Falha ao encontrar o símbolo \" DllUnregisterServer \ ""); :: FreeLibrary (dllHandle); dllHandle = NULL; return -1;
pDllUnregisterServer ();
Existem muito poucas diferenças substanciais entre os dois modos de vinculação dinâmica. Mesmo que o momento em que o link dinâmico acontece seja
diferente, o mecanismo real de link dinâmico é completamente idêntico em ambos os casos.
Além disso, a biblioteca dinâmica que pode ser carregada estaticamente ciente também pode ser carregada dinamicamente no tempo de execução. Não há
elementos do design dinâmico da biblioteca que qualificariam a biblioteca estritamente para uso em um ou outro cenário.
A única diferença substancial é que, no cenário com reconhecimento estático, há um requisito extra que precisa ser satisfeito: você precisa fornecer o local da
biblioteca em tempo de construção. Como será mostrado no próximo capítulo, essa tarefa requer algumas sutilezas que um bom desenvolvedor de software
precisa estar ciente em ambientes Linux e Windows.
CAPÍTULO 7
Localizando as Bibliotecas
A ideia de compartilhamento de código binário está no cerne do conceito de bibliotecas. Um pouco menos óbvio é que normalmente significa que a única cópia do
arquivo binário da biblioteca residirá em um local fixo em uma determinada máquina, enquanto muitos binários de cliente diferentes precisarão localizar a
biblioteca necessária (tanto em tempo de construção quanto em tempo de execução) . A fim de abordar a questão da localização das bibliotecas, uma variedade de
convenções foram concebidas e implementadas. Neste capítulo, discutirei os detalhes dessas convenções e diretrizes.
A prática de usar as bibliotecas acontece por meio de dois cenários de casos de uso distintos. O primeiro cenário de caso de uso ocorre quando os desenvolvedores
tentam integrar as bibliotecas de terceiros (estáticas ou dinâmicas) em seu produto. Outro cenário ocorre quando as bibliotecas (neste caso, especificamente as
bibliotecas dinâmicas) precisam ser localizadas no tempo de execução para que o aplicativo instalado na máquina do cliente seja executado corretamente.
Ambos os cenários de caso de uso apresentam o problema de localização dos arquivos binários da biblioteca. A maneira como esses problemas foram
estruturalmente resolvidos será descrita neste capítulo.
Normalmente, o pacote de terceiros que contém a biblioteca, cabeçalhos de exportação e, possivelmente, alguns extras (como documentação, ajuda online, ícones
de pacotes, aplicativos utilitários, código e amostras de mídia, etc.) é instalado em um caminho predeterminado na máquina do desenvolvedor . Imediatamente
depois, o desenvolvedor pode criar uma infinidade de projetos residindo em muitos caminhos diferentes em sua máquina.
Copiar a biblioteca de terceiros para todo e qualquer projeto que um desenvolvedor possa criar é definitivamente uma possibilidade, embora seja uma escolha
muito ruim. Obviamente, ter cópias da biblioteca na pasta de cada projeto que possa precisar delas vai contra a ideia original de reutilização de código que está
por trás do conceito de bibliotecas.
A alternativa aceitável seria ter apenas uma cópia dos binários da biblioteca e um conjunto de regras ajudando os projetos binários do cliente a localizá-la. Esse
conjunto de regras, comumente referido como regras de localização da biblioteca de tempo de construção, é normalmente suportado pelo vinculador da plataforma de
desenvolvimento. Essas regras estipulam basicamente como as informações sobre o caminho das bibliotecas necessárias para concluir a vinculação binária do
cliente podem ser passadas para o vinculador.
As regras de localização da biblioteca de tempo de construção são bastante elaboradas e tendem a vir em uma variedade de opções. Cada uma das principais
plataformas de desenvolvimento geralmente fornece um conjunto muito sofisticado de opções de como essas regras podem ser aplicadas.
É muito importante entender que as regras de localização da biblioteca de tempo de construção são pertinentes às bibliotecas estáticas e dinâmicas. Independentemente das
diferenças reais entre vincular bibliotecas estáticas e dinâmicas, o vinculador deve saber a localização dos binários de biblioteca necessários.
Depois que os desenvolvedores integram as bibliotecas de terceiros, seus produtos estão prontos para serem entregues aos clientes finais. Com base na vasta
variedade de critérios de design e considerações da vida real, a estrutura do produto entregue pode vir em uma ampla variedade de opções:
• No caso mais simples possível, o pacote do produto contém apenas um único arquivo de aplicativo. O uso pretendido é que o cliente simplesmente execute o
aplicativo.
Este caso é bastante trivial. Para acessar e executar o aplicativo, tudo o que o usuário precisa fazer é adicionar seu caminho à variável de ambiente PATH geral.
Qualquer pessoa que não seja completamente analfabeta em informática é capaz de completar esta tarefa simples.
• Nos cenários mais complexos, o pacote do produto contém uma combinação de bibliotecas dinâmicas e um ou mais aplicativos utilitários. As bibliotecas
dinâmicas podem ser bibliotecas de terceiros encaminhadas diretamente, ou podem ser criadas pelo fornecedor do pacote, ou podem ser uma combinação de
ambas.
O uso pretendido é que uma variedade de aplicativos se vinculem dinamicamente às bibliotecas dinâmicas fornecidas. Os exemplos típicos de tal cenário no
domínio da multimídia são os frameworks multimídia, como DirectX ou GStreamer, já que cada um deles fornece (ou conta com estar disponível em tempo de
execução) um elaborado conjunto de bibliotecas dinâmicas, cada uma fornecendo um certo conjunto de funcionalidades.
Muito parecido com o cenário de caso de uso de desenvolvimento, a abordagem significativa para o problema pressupõe que haverá apenas uma cópia das
bibliotecas dinâmicas necessárias, residindo no caminho onde o procedimento de instalação as implementou. Por outro lado, essas bibliotecas podem ser
necessárias para uma infinidade de binários de cliente (outras bibliotecas dinâmicas ou os aplicativos) que residem em uma infinidade de caminhos diferentes.
Para estruturar o processo de localização dos binários das bibliotecas dinâmicas no tempo de execução (ou um pouco antes, no tempo de carregamento), um
conjunto de regras de localização da biblioteca em tempo de execução precisa ser estabelecido. As regras de localização da biblioteca de tempo de execução geralmente são
bastante elaboradas. Cada uma das plataformas de desenvolvimento fornece seu próprio sabor de opções sofisticadas de como essas regras podem ser aplicadas.
Finalmente - correndo o risco de reiterar o óbvio - as regras de localização da biblioteca em tempo de execução pertencem apenas às bibliotecas dinâmicas. A integração de
bibliotecas estáticas é sempre concluída muito antes do tempo de execução (ou seja, no estágio de vinculação do processo de construção do binário do cliente) e
nunca há necessidade de localizar as bibliotecas estáticas no tempo de execução.
A parte importante da receita de como as regras de localização da biblioteca de tempo de construção são implementadas no Linux pertence às convenções de
nomenclatura das bibliotecas do Linux.
Os nomes de arquivo da biblioteca estática do Linux são criados de acordo com o seguinte padrão: nome do arquivo da biblioteca estática = lib +
<nome da biblioteca> + .a
A parte do meio do nome do arquivo da biblioteca é o nome real da biblioteca, que é usado para enviar a biblioteca ao vinculador.
Os nomes de arquivo da biblioteca dinâmica do Linux são criados de acordo com o seguinte padrão:
nome de arquivo da biblioteca dinâmica = lib + <nome da biblioteca> + .so + <informações da versão da biblioteca>
A parte intermediária do nome do arquivo da biblioteca é o nome real da biblioteca, que é usado para enviar a biblioteca ao vinculador e, posteriormente, à
pesquisa da biblioteca em tempo de construção, bem como aos procedimentos de pesquisa da biblioteca em tempo de execução.
As informações da versão da biblioteca transportadas pela última parte do nome do arquivo da biblioteca seguem a seguinte convenção: informações da
versão da biblioteca dinâmica = <M>. <m>. <p>
• M: versão principal
• m: versão secundária
A importância das informações de controle de versão da biblioteca dinâmica será discutida em detalhes no Capítulo 11.
biblioteca soname = lib + < nome da biblioteca > + .so + < dígito (s) da versão principal da biblioteca >
O fato de que apenas os dígitos da versão principal desempenham um papel no soname da biblioteca implica que as bibliotecas cujas versões secundárias diferem
ainda serão descritas pelo mesmo valor de soname . Exatamente como esse recurso é usado, será discutido na seção Capítulo 11 dedicada ao tópico de manipulação
de versões de biblioteca dinâmica.
O soname da biblioteca é normalmente incorporado pelo vinculador no campo ELF dedicado do arquivo binário da biblioteca. A string que especifica o soname da
biblioteca é normalmente passada para o vinculador por meio do sinalizador do vinculador dedicado, assim:
Os programas utilitários para examinar o conteúdo dos arquivos binários geralmente fornecem a opção de recuperar o valor do soname (Figura 7-1).
Observe que o nome da biblioteca conforme descrito por essas convenções não é necessariamente usado em conversas humanas para denotar a biblioteca. Por
exemplo, a biblioteca que fornece a funcionalidade de compactação em uma determinada máquina pode residir no nome de arquivo libz.so.1.2.3.4. De acordo com
a convenção de nomenclatura da biblioteca, o nome dessa biblioteca é simplesmente "z", que será usado em todas as negociações com o linker e o carregador. Do
ponto de vista da comunicação humana, a biblioteca pode ser referida como "libz", como por exemplo na seguinte descrição de bug em um sistema de
rastreamento de bugs: "Problema 3142: problema com o binário libz ausente '! Para evitar a confusão, às vezes a biblioteca nome também é conhecido como o nome
do vinculador da biblioteca.
A especificação do caminho da biblioteca de tempo de construção é implementada no Linux na forma da chamada opção -L -l . A maneira realmente correta de
usar essas duas opções pode ser descrita pelo seguinte conjunto de diretrizes:
• Divida o caminho completo da biblioteca em duas partes: o caminho da pasta e o nome do arquivo da biblioteca.
Por exemplo, a linha de comando para criar a demonstração do aplicativo compilando o arquivo main.cpp e vinculando-a à biblioteca dinâmica
libworkingdemo.so localizada na pasta ../sharedLib pode ter a seguinte aparência:
A A
Eu eu
Nos casos em que a linha gcc combina a compilação com a vinculação, esses sinalizadores do linker devem ser prefixados com o sinalizador -Wl, assim:
• O caminho completo para uma biblioteca dinâmica é passado para a opção -l (a parte -L não está sendo usada).
• A parte do caminho é passada pela opção -L , e o resto do caminho, incluindo o nome do arquivo, é passada pela opção -l .
O vinculador geralmente aceita formalmente essas variações de especificação dos caminhos da biblioteca de tempo de construção. No caso em que o caminho para
bibliotecas estáticas é fornecido, esses tipos de "liberdade criativa" não causam problemas no futuro.
No entanto, quando o caminho para a biblioteca dinâmica é passado, os problemas introduzidos pelo desvio da maneira realmente correta de passar o caminho da
biblioteca começam a aparecer no tempo de execução. Por exemplo, digamos que uma demonstração de aplicativo cliente dependa da biblioteca libmilan.so, que
reside na máquina do desenvolvedor na seguinte pasta:
/home/milan/mylibs/case_a/libmilan.so.
O aplicativo cliente é construído com sucesso pela seguinte linha de comando do vinculador: $ gcc main.o -l / home / milan / mylibs / case_a /
libmilan.so -o demo
Vamos supor agora que o projeto seja implantado em uma máquina diferente e fornecido a um usuário cujo nome é "john". Quando esse usuário tentar executar o
aplicativo, nada acontecerá. Uma investigação cuidadosa (técnicas das quais serão discutidas nos Capítulos 13 e 14) revelará que o aplicativo requer em tempo de
execução a biblioteca dinâmica libmilan.so (que está OK), mas espera encontrá-la no caminho / home / milan / mylibs / case_a /.
Especificar caminhos relativos em vez de caminhos absolutos pode aliviar apenas parcialmente o problema. Se, por exemplo, o caminho da biblioteca for
especificado como relativo à pasta atual (ou seja, ../mylibs/case_a/libmilan.so), o aplicativo na máquina de john será executado apenas se o binário do
cliente e a biblioteca dinâmica necessária forem implantado na máquina de john na estrutura de pastas que mantém as posições relativas exatas entre a biblioteca
executável e dinâmica. Mas, se john se atrever a copiar o aplicativo para uma pasta diferente e tentar executá-lo de lá, o problema original reaparecerá.
Não só isso, mas o aplicativo pode parar de funcionar até mesmo na máquina do desenvolvedor, onde costumava funcionar perfeitamente. Se você decidir copiar
o binário do aplicativo em um caminho diferente na máquina do desenvolvedor, o carregador começará a pesquisar a biblioteca nos caminhos relativos ao ponto
onde o binário do aplicativo reside. Muito provavelmente esse caminho não existirá (a menos que você se preocupe em recriá-lo)!
A chave para entender a causa subjacente do problema é saber que o vinculador e o carregador não valorizam igualmente os caminhos da biblioteca passados na opção -L e
na opção -l .
Na verdade, o vinculador dá muito mais significado ao que você passou na opção -l . Mais especificamente, a parte do caminho que passou pela opção -L
encontra seu uso apenas durante o estágio de vinculação, mas não desempenha nenhuma função depois disso.
A parte especificada na opção -l , entretanto, é impressa no binário da biblioteca e continua desempenhando um papel importante durante o tempo de execução.
Na verdade, ao tentar encontrar as bibliotecas necessárias no tempo de execução, o carregador primeiro lê o arquivo binário do cliente tentando localizar essas
informações específicas.
Se você se atrever a desviar da regra estrita e passar qualquer coisa além do nome do arquivo da biblioteca por meio da opção -l , o aplicativo construído na
máquina de milan quando implantado e executado na máquina de john irá procurar a biblioteca dinâmica no caminho codificado, que a maioria provavelmente
existe apenas na máquina do desenvolvedor (milan), mas não na máquina do usuário (john). Uma ilustração desse conceito é fornecida na Figura 7-2.
-primeiro
Cliente binário
Figura 7-2. A convenção -L desempenha um papel apenas durante a construção da biblioteca. O impacto da convenção -l, no entanto, permanece importante no tempo de execução
também
A biblioteca necessária está localizada com êxito na máquina de tempo de execução, embora resida em uma estrutura de pastas completamente diferente.
A opção padrão é fornecer as informações sobre a DLL exigida pelo vinculador da seguinte maneira: • Especifique o arquivo de biblioteca de importação da DLL
(.lib) na lista de entradas do vinculador (Figura 7-3).
Gerenciador de configuração...
> Propriedades Comuns 4 Propriedades de Configuração Depuração Geral Diretórios VC ++ t> C / C ++ a Linker
Entrada Geral
Depuração do arquivo de manifesto Otimização do sistema IDL incorporado Linha de comando avançada t> Ferramenta de manifesto t> Gerador de documento XML 0 Navegar nas informações t> Eventos de
compilação 0 Etapa de compilação personalizada
uaf xc wd Jib; libc mtd. Ii b; Ii bmf xl ïb; dxva2.l ib; d 3d 9.lîb; d wm z uaf xc wd. lib; libc mt J ib;
Dependências Adicionais
uafxcwdJib »
libcmtd.lib SI
1
libmfx.lib
_
dxva2.tib
d3d9.ltb
4 >
Valores herdados:
Dependências Adicionais
Macros >>
Cancelar
OK
• Adicione o caminho da biblioteca de importação ao conjunto de diretórios de caminho da biblioteca (Figura 7-4).
#pragma Comentário
O requisito da biblioteca pode ser especificado adicionando uma linha como esta a um arquivo de origem: #pragma comment (lib, "<nome da biblioteca de
importação, caminho completo ou caminho relativo>");
Ao encontrar esta diretiva, o compilador irá inserir um registro de pesquisa da biblioteca no arquivo objeto, que será eventualmente obtido pelo linker. Se apenas
o nome do arquivo da biblioteca for fornecido entre aspas duplas, a pesquisa pela biblioteca seguirá as regras de pesquisa da biblioteca do Windows.
Normalmente, esta opção é usada para exercer mais precisão durante o processo de pesquisa de bibliotecas e, portanto, é mais comum especificar o caminho
completo e a versão da biblioteca do que o contrário.
#ifdef CUSTOMER_XYZ
#ifdef CUSTOMER_ABC
#ifdef CUSTOMER_MPQ
#pragma comment (lib, "<clientMPQ-specific library>"); #endif // CUSTOMER_MPQ #endif // CUSTOMER_ABC #endif // CUSTOMER_XYZ
Esta opção pode ser usada apenas em casos especiais, quando o projeto de biblioteca dinâmica e seu projeto executável do cliente são partes da mesma solução do
Visual Studio. Se o projeto DLL for adicionado à lista de referências do projeto do aplicativo cliente, o ambiente do Visual Studio fornecerá tudo (automática e
quase invisivelmente para o programador) necessário para construir e executar o aplicativo.
Para começar, ele passará o caminho completo da DLL para a linha de comando do vinculador do aplicativo. Por fim, fará a cópia da DLL para a pasta runtime do
aplicativo (normalmente Debug para a versão de depuração e Release para a versão de lançamento), atendendo da maneira mais fácil possível as regras de
localização da biblioteca runtime.
As Figuras 7-5 a 7-8 ilustram a receita de como fazê-lo, usando o exemplo da solução (SystemExamination) composta pelos dois projetos relacionados: uma DLL
SystemExaminer que está estaticamente ciente vinculada pelo aplicativo SystemExaminerDemoApp.
Não especificarei a dependência da DLL contando com o primeiro método descrito anteriormente (ou seja, especificando o arquivo de biblioteca de importação
(.lib) da DLL na lista de entradas do vinculador). Esse detalhe aparentemente peculiar e um pouco contra-intuitivo é ilustrado pela Figura 7-5.
Figura 7-5. Neste método, você não precisa especificar a dependência da biblioteca diretamente
Em vez disso, será suficiente definir o projeto binário do cliente para fazer referência ao projeto de biblioteca de dependência. Visite a guia Common Properties ^
Frameworks and References (Figura 7-6).
O resultado final será que o caminho de tempo de construção da DLL necessária é passado para a linha de comando do vinculador, conforme mostrado na Figura
7-8.
Figura 7-8. O resultado da referência implícita: o caminho exato para a biblioteca é passado para o vinculador
Da perspectiva do programador, a codificação dos caminhos de cada caminho de biblioteca dinâmica parece totalmente errada. Faria muito mais sentido se um
programador pudesse apenas fornecer o nome do arquivo da biblioteca dinâmica, e o sistema operacional saberia de alguma forma onde procurar a biblioteca.
Todos os principais sistemas operacionais reconheceram a necessidade de implementar tal mecanismo, que seria capaz de pesquisar e encontrar a biblioteca
dinâmica em tempo de execução com base no nome do arquivo da biblioteca fornecido pelo programa. Não apenas um conjunto de locais de biblioteca
predeterminados foi definido, mas também a ordem de pesquisa foi definida, especificando onde o sistema operacional procurará primeiro.
Finalmente, saber a localização do tempo de execução da biblioteca dinâmica é igualmente importante, independentemente de a biblioteca dinâmica ser carregada
com reconhecimento estático ou no tempo de execução.
O algoritmo de busca da biblioteca dinâmica em tempo de execução é governado pelo seguinte conjunto de regras, listadas na ordem de prioridade mais alta.
Bibliotecas pré-carregadas
A prioridade mais alta inquestionável acima de qualquer pesquisa de biblioteca é reservada para as bibliotecas especificadas para pré-carregamento, pois o
carregador primeiro carrega essas bibliotecas e, em seguida, começa a pesquisar as outras. Existem duas maneiras de especificar as bibliotecas pré-carregadas:
Este arquivo contém uma lista separada por espaços em branco de bibliotecas compartilhadas ELF a serem carregadas antes do programa.
Especificar as bibliotecas pré-carregadas não é a norma de design padrão. Em vez disso, ele é usado em cenários especiais, como teste de estresse de design,
diagnósticos e correção de emergência do código original.
Nos cenários de diagnóstico, você pode criar rapidamente uma versão personalizada de uma função padrão, vinculá-la às impressões de depuração e construir
uma biblioteca compartilhada cujo pré-carregamento substituirá efetivamente a biblioteca dinâmica que fornece essa função de maneira padrão.
Após a conclusão do carregamento das bibliotecas indicadas para pré-carregamento, inicia-se a busca por outras bibliotecas listadas como dependências. Ele segue
um conjunto elaborado de regras, cuja lista completa (organizada do método de prioridade mais alta para baixo) é explicada nas seções a seguir.
rpath
Desde os primeiros dias, o formato ELF apresentava o campo DT_RPATH usado para armazenar a string ASCII contendo os detalhes do caminho de pesquisa
relevantes para o binário. Por exemplo, se o executável XYZ depende da presença de tempo de execução da biblioteca dinâmica ABC, então XYZ pode carregar em
seu DT_RPATH a string especificando o caminho no qual a biblioteca ABC pode ser encontrada em tempo de execução.
Esse recurso claramente representou um bom passo à frente ao permitir que o programador estabeleça um controle mais rígido sobre os problemas de
implantação, principalmente para evitar a ampla escala de possíveis incompatibilidades entre as versões das bibliotecas pretendidas e as disponíveis.
As informações transportadas pelo campo DT_RPATH do executável XYZ seriam lidas em tempo de execução pelo carregador. Um detalhe importante a lembrar é
que o caminho a partir do qual o carregador é iniciado desempenha um papel na interpretação das informações DT_RPATH . Mais notavelmente, nos casos em que
DT_RPATH carrega um caminho relativo, ele será interpretado não em relação à localização da biblioteca XYZ, mas sim em relação ao caminho a partir do qual o
carregador (ou seja, o aplicativo) é iniciado. Por melhor que seja, o conceito de rpath sofreu algumas modificações.
De acordo com fontes da web, por volta de 1999, quando a versão 6 da biblioteca de tempo de execução C estava em processo de substituição da versão 5, certas
desvantagens do rpath foram notadas, e ele foi substituído por um campo muito semelhante chamado runpath (DT_RUNPATH) do Formato de arquivo binário
ELF.
Hoje em dia, tanto o rpath quanto o runpath estão disponíveis, mas o runpath recebe maior consideração na lista de prioridades de pesquisa em tempo de
execução. Apenas na ausência de seu runpath irmão mais novo ( campo DT_RUNPATH ), o rpath ( campo DT_RPATH ) permanece a informação do caminho de
busca de maior prioridade para o carregador Linux. Se, entretanto, o campo runpath (DT_RUNPATH) do binário ELF não estiver vazio, o rpath será ignorado.
O rpath é normalmente definido passando ao vinculador o sinalizador -R ou -rpath imediatamente seguido pelo caminho que você deseja atribuir como
runpath. Além disso, por convenção, sempre que o vinculador é invocado indiretamente (ou seja, chamando gcc ou g ++), os sinalizadores do vinculador
precisam ser prefixados pelo prefixo -Wl (ou seja, "menos vírgula Wl"):
A A A
III
II
-Wl, prefixo necessário ao invocar o linker indiretamente, por meio do gcc, em vez de invocar diretamente o ld
Finalmente, o rpath do binário pode ser modificado após o fato, executando o programa utilitário chrpath . Uma desvantagem notável do chrpath é que ele não
pode modificar o rpath além de seu comprimento de string já existente. Mais precisamente, chrpath pode modificar e excluir / esvaziar o campo DT_RPATH ,
mas não pode inseri-lo ou estendê-lo para uma string mais longa.
A maneira de examinar o arquivo binário para o valor do campo DT_RPATH é examinar o cabeçalho ELF do binário (como executar readelf -d ou objdump -
f).
Foi muito cedo durante o desenvolvimento do conceito de caminho de pesquisa de biblioteca quando foi reconhecida a necessidade de um tipo de mecanismo
temporário, rápido e sujo, mas eficaz, que poderia ser usado por desenvolvedores para experimentar e testar seus projetos. A necessidade foi abordada fornecendo
o mecanismo no qual uma variável de ambiente específica (LD_LIBRARY_PATH) seria usada para satisfazer essas necessidades.
Quando o valor rpath (DT_RPATH) não é definido, este caminho fornecido desta forma é usado como a informação de caminho de pesquisa de maior prioridade.
■ Observe que neste esquema de prioridade, há uma batalha desigual entre o valor embutido no arquivo binário e a variável de ambiente. se as coisas
permanecessem iguais, a presença de rpath no binário tornaria a solução de problemas com um produto de software de terceiros impossível. Felizmente, o
novo esquema de prioridade tratou desse problema reconhecendo que o rpath é muito ditatorial e fornecendo uma maneira de substituir temporariamente
suas configurações. O runpath irmão mais novo do rpath tem o poder de silenciar o rpath desonesto e autoritário , neste caso ld_library_path tem uma
chance de obter temporariamente o tratamento de prioridade mais alta.
A sintaxe de configuração de LD_LIBRARY_PATH é idêntica à sintaxe de configuração de qualquer tipo de variável de caminho. Isso pode ser feito na instância
específica do shell, digitando, por exemplo, algo assim:
Mais uma vez, o uso deste mecanismo deve ser reservado para fins de experimentação. As versões de produção dos produtos de software não devem depender
desse mecanismo.
caminho de corrida
O conceito de runpath segue o mesmo princípio do rpath. É o campo (DT_RUNPATH) do formato binário ELF, que pode ser definido no momento da
construção para apontar para o caminho onde a biblioteca dinâmica deve procurar. Ao contrário do rpath, cuja autoridade é inquestionável, o runpath é
projetado para ser tolerante com as necessidades urgentes do mecanismo LD_LIBRARY_PATH .
O caminho de execução é definido de maneira muito semelhante a como o caminho de execução é definido. Além de passar o linkerflag -R ou - rpath ,
um sinalizador de linker --enable-new-dtags adicional precisa ser usado. Como já explicado no caso de rpath, sempre que o vinculador é chamado
indiretamente, por meio da chamada de gcc (ou g ++) em vez de invocar diretamente ld, por convenção, os sinalizadores do vinculador precisam ser
precedidos pelo prefixo -Wl :
A A A A
I II I
-Wl, prefixo necessário ao invocar o linker indiretamente, por meio do gcc, em vez de invocar diretamente o ld
Como regra, sempre que o runpath é especificado, o vinculador configura rpath e runpath com o mesmo valor.
A maneira de examinar o arquivo binário para o valor do campo DT_RUNPATH é examinar o cabeçalho ELF do binário (como executar readelf -h ou objdump -
f).
Do ponto de vista da prioridade, sempre que DT_RUNPATH contém uma string não vazia, o campo DT_RPATH é ignorado pelo carregador. Desta forma, o poder
ditatorial de rpath é subjugado e a vontade de LD_LIBRARY_PATH tem a chance de ser respeitada quando realmente é necessária.
O útil programa utilitário patchelf é capaz de modificar o campo do caminho de execução do arquivo binário. No momento, não faz parte do repositório
oficial, mas seu código-fonte e o manual simples podem ser encontrados em http://nixos.org/patchelf.html . Compilar o binário é bastante simples. O
exemplo a seguir ilustra o uso do patchelf :
A |
vários caminhos podem ser definidos, separados por dois pontos (:)
■ Nota Embora a documentação do patchelf mencione rpath, o patchelf de fato atua no campo do runpath .
Cache Idconfig
Um dos procedimentos de implantação de código padrão é baseado na execução do utilitário Linux ldconfig ( http://linux.die.net/ man / 8 /
ldconfig). A execução do utilitário ldconfig é geralmente uma das últimas etapas durante o procedimento de instalação do pacote padrão, que normalmente
requer a passagem do caminho para uma pasta contendo bibliotecas como argumento de entrada. O resultado é que o ldconfig insere o caminho da pasta
especificada na lista de pastas de pesquisa da biblioteca dinâmica mantida no arquivo /etc/ld.so.conf . No mesmo token, o caminho da pasta recém-
adicionado é verificado em busca de bibliotecas dinâmicas, o resultado é que os nomes de arquivos das bibliotecas encontradas são adicionados à lista de nomes
de arquivos das bibliotecas mantida noArquivo /etc/ld.so.cache . Por exemplo, o exame de minha máquina Ubuntu de desenvolvimento revela o conteúdo do
arquivo /etc/ld.so.conf na Figura 7-9.
drwxr-xr-x 2 root 4096 17 de agosto drwxr-xr-x 131 root 12288 Fev S Irwxrwxrwx 1 root 40 Fev S es / 1386-linux-gnu_gl_conf -rw-r -
r- 1 root 108 Abr 19 -rw-r --r-- 1 root 44 abr 19 nllan @ nilan-ub-1204-32-lts: - $ cat /etc/Id.so.conf.d/* / usr / lib / \ 386-
linux-gnu / mesa V Suporte multiarch / Ub / i386-linux-gnu / us r / lib / i.386-linux-gnu / lib / i686-linux-gnu / usr / lib /
1686-llnux-gnu ff llbc configuração padrão / usr / local / lib nilan @ milanS
2012 16:69 ..
Quando ldconfig examina todos os diretórios listados no arquivo /etc/ld.so.conf , ele encontra zilhões de bibliotecas dinâmicas cujos nomes de arquivo ele
mantém no arquivo /etc/ld.so.cache (apenas uma pequena parte é mostrada na Figura 7-10).
■ Observe que algumas das bibliotecas referenciadas pelo arquivo /etc/ld.so.conf podem residir nos chamados caminhos de biblioteca confiáveis. se o
sinalizador do vinculador -z nodeflib foi usado ao construir o executável, as bibliotecas encontradas nos caminhos da biblioteca confiável do sistema
operacional serão ignoradas durante a pesquisa da biblioteca.
Por favor note que o caminho / usr / local / lib faz não pertencem a esta categoria. Claro, nada impede que você adicione à lista de prioridades usando um
dos mecanismos descritos anteriormente.
■ Observe que se o executável foi vinculado ao sinalizador de vinculador -z nodeflib , todas as bibliotecas encontradas nos caminhos de bibliotecas
confiáveis do sistema operacional serão ignoradas durante a pesquisa da biblioteca.
1. LD_LIBRARY_PATH
3. ld.so.cache
2. LD_LIBRARY_PATH
3. ld.so.cache
Para obter mais detalhes sobre este tópico específico, verifique a página do manual do carregador do Linux ( http://linux.die.net/man/1/ld ).
No conhecimento mais simples, popular e difundido sobre o tópico, os dois locais a seguir são mais usados como os caminhos favoritos para implantar a DLL
necessária no tempo de execução:
• Uma das pastas DLL do sistema (como C: \ Windows \ System ou C: \ Windows \ System32)
No entanto, não é aqui que a história termina. Os esquemas de prioridade de pesquisa da biblioteca dinâmica de tempo de execução do Windows são muito mais
sofisticados, pois os seguintes fatores desempenham um papel no esquema de prioridade:
• Os aplicativos da Windows Store (Windows 8) têm um conjunto de regras diferente dos aplicativos da área de trabalho do Windows.
• Se a DLL pertence ao grupo de DLLs conhecidas para a versão fornecida do sistema operacional Windows.
Para obter informações mais precisas e atualizadas, faz mais sentido verificar a documentação oficial da Microsoft sobre este tópico, atualmente localizada em
http://msdn.microsoft.com/en-us/library/windows/ desktop / ms682586 (v = vs. 85) .aspx.
Ambos os aplicativos também contam com a opção rpath para especificar o local do tempo de execução da biblioteca dinâmica necessária. A pasta do projeto da
biblioteca dinâmica e a pasta do projeto dos aplicativos são estruturadas como a Figura 7-11.
Mlnus_L_lnvestlgatton
denoNoMinusL deploy
1
- llbdynanicllnklngdeno.so UlustratlngMlnusLInportance, png Makefile sharedLib
1
- exportações - sharedLibExports.h
- Itbdynanicllnklngdeno.so
No momento da construção, a dependência está na biblioteca que reside na pasta localizada um nível acima, (ou seja, pasta ../ implantar), que é diferente do caminho do tempo de
execução
1
- testDynanicLinking.c
testApp_wi.thMT.nusL
- demoMtnusL
- Makefile
1
- src - naln.c
- dernoNoMinusL
- Makefile
- src
1
- naln.c 9 diretórios, 15 arquivos
Figura 7-11. A estrutura de pastas do projeto projetada para ilustrar os benefícios de seguir estritamente as convenções -L -l
CAPÍTULO 7 LOCALIZANDO AS LiBRARiES O Makefile do aplicativo que não depende da convenção -L é semelhante à Listagem 7-1.
# Importação inclui
# Bibliotecas
SYSLIBRARIES = \
-lpthread \ -lm \
-ldl
DEMOLIB_PATH = ../deploy
# especificar o caminho completo ou parcial pode sair pela culatra em tempo de execução !!! DEMO_LIBRARY =
../deploy/libdynamiclinkingdemo.so
# Saídas
EXECUTABLE = demoNoMinusL
# Compilador
INCLUDES = $ (COMMON_INCLUDES)
ifeq ($ (DEBUG), 1)
COMPILAR = g ++ $ (CFLAGS)
# Linker
LINK = g ++
% .o:% .c
rm $ (OBJETOS) $ (EXECUTÁVEL)
implantar:
= ./src
= $ (SRC_PATH) /main.o
# Bibliotecas SYSLIBRARIES
-lpthread \ -lm \
-ldl
SHLIB_BUILD_PATH = ../sharedLib
# Saídas EXECUTÁVEIS
demoMinusL
ifeq ($ (DEBUG), 1)
COMPILAR
# Linker DEMOLIB_PATH
% .o:% .c
limpar:
rm $ (OBJETOS) $ (EXECUTÁVEL)
implantar:
IH nimtHinat-iMAi
21:30 21:34 21:33 21:33 21:30 21:15 21:33 21:33 21133 / MlnusJ / Minusl / MinusJ / Mlnusj / Minusl
A biblioteca especificada como -L <path> -l <name> pode ser tratada perfeitamente tanto na vinculação quanto no tempo de execução (onde seu
nome pode ser combinado com rpath).
A biblioteca especificada sem requer a preocupação com a manutenção dos caminhos relativos.
Quando o processo de construção da biblioteca dinâmica é concluído, seu binário é implantado na pasta de implantação , que reside nos dois níveis de
profundidade acima da pasta na qual o aplicativo Makefile reside. Portanto, o caminho de tempo de construção precisa ser especificado como
../deploy/libdynamiclinkingdemo.so.
A Figura 7-12 ilustra a vantagem de aderir à convenção -L : a imunidade do programa à mudança dos caminhos da biblioteca em tempo de execução.
/ Minus_L_investlgatlonS Is -alg
.L 1'-. litígios
ZA
Investigations idd denoNoMinusL llnux-gate.so.l => (0xb77dl000) ../deploy/llbdynamlclinkingdeno.so (0xb77cc000) llbc.so.6 =:>
/Ilb/l386-llnux-gnu/llbc.so,öO ) /llb/ld-llnux.so.2 (Oxb77d20O0) / Mlnus_L_uTv; 'i-iQatlonS
Figura 7-12. A vantagem de seguir cuidadosamente as convenções -L -l. Seguir a convenção normalmente significa estar livre de preocupações em tempo de execução
Quando o caminho da biblioteca de tempo de construção foi especificado com a opção -L , o nome da biblioteca é efetivamente separado do caminho e, como tal,
impresso no arquivo binário do cliente. Quando chega a hora de conduzir a pesquisa em tempo de execução, o nome impresso (ou seja, não o caminho mais o
nome, mas apenas o nome da biblioteca!) Se encaixa muito bem com a implementação do algoritmo de pesquisa em tempo de execução.
CAPÍTULO 8
A regra importante da vinculação dinâmica é que processos diferentes compartilham o segmento de código da biblioteca dinâmica, mas não compartilham os
segmentos de dados. Espera-se que cada um dos processos que carregam a biblioteca dinâmica forneça sua própria cópia dos dados nos quais o código da
biblioteca dinâmica opera (ou seja, o segmento de dados da biblioteca). Seguindo a analogia culinária, vários chefs em vários restaurantes podem usar
simultaneamente o mesmo livro de receitas (instruções). É muito provável, entretanto, que chefs diferentes usem receitas diferentes do mesmo livro. Além disso,
presume-se que os pratos elaborados a partir das receitas de um mesmo livro de receitas serão servidos a diferentes clientes. Obviamente, apesar de os chefs lerem
o mesmo livro de receitas, cada um deve usar seu próprio conjunto de pratos e utensílios de cozinha. Caso contrário, seria uma grande confusão.
Por melhor e simples que a história toda pareça agora, vários problemas técnicos precisaram ser resolvidos ao longo do caminho. Vamos olhar mais de perto.
Ou seja, certos grupos de instruções esperam que o endereço do operando na memória seja conhecido em tempo de execução. Em geral, os dois grupos de
instruções a seguir exigem estritamente os endereços calculados com precisão:
• As instruções de acesso a dados (mov, etc.) requerem o endereço do operando na memória. Para
Por exemplo, para acessar uma variável de dados, a instrução mov assembler da arquitetura X86 requer o endereço de memória absoluto da variável para
transferir os dados entre a memória e um registro de CPU.
A seguinte sequência de instruções de montagem é usada para incrementar uma variável armazenada na memória:
mov eax, ds: 0xBFD10000; carregue a variável do endereço 0xBFD10000 para registrar eax add eax, 0x1; incrementar o valor carregado
• Chamadas de subrotina (call, jmp, etc.) requerem o endereço da função no segmento de código. Por exemplo, para chamar uma função, a instrução de
chamada deve ser fornecida com o endereço de memória do segmento de código do ponto de entrada da função.
ligue para 0x0A120034; função de chamada cujo ponto de entrada reside no endereço 0x0A120034
que é equivalente a
pressione eip + 2; o endereço de retorno é o endereço atual + tamanho de duas instruções jmp 0x0A120034; saltando para o endereço
de my_function
Para que as coisas sejam um pouco mais fáceis, existem cenários em que apenas um deslocamento relativo desempenha um papel. Os endereços de variáveis
estáticas, bem como os pontos de entrada de funções de escopo local (ambos declarados usando a palavra-chave static no sentido da linguagem de
programação C) podem ser resolvidos conhecendo-se apenas o deslocamento relativo das instruções que os referenciam. Tanto o acesso a dados quanto as
instruções do assembler de chamada de sub-rotina têm os tipos que exigem o deslocamento relativo em vez do endereço absoluto. Isso, no entanto, não remove o
problema geral; apenas o diminui até certo ponto.
• Uma parte fixa e predeterminada do blueprint do mapa de memória do processo é fornecida pelo binário executável.
• Depois que o carregamento dinâmico é concluído, a biblioteca dinâmica se torna uma parte legítima do processo.
• A conexão entre o executável e a biblioteca dinâmica ocorre naturalmente pelo executável chamando uma ou mais funções implementadas e devidamente
exportadas por uma biblioteca dinâmica.
O processo de carregamento da biblioteca no mapa de memória do processo começa traduzindo o intervalo de endereços do segmento da biblioteca para um novo
local. Em geral, a faixa de endereços onde a biblioteca dinâmica será carregada não é conhecida com antecedência. Em vez disso, é determinado no momento do
carregamento pelo algoritmo interno do módulo do carregador.
O nível de indeterminismo neste cenário é apenas ligeiramente diminuído pelo fato de que o formato executável estipula o intervalo de endereços onde a
biblioteca dinâmica pode ser carregada. No entanto, o intervalo estipulado de endereços permitidos é bastante amplo, pois foi projetado para acomodar muitas
bibliotecas dinâmicas carregadas ao mesmo tempo. Isso claramente não ajuda muito a adivinhar onde exatamente a biblioteca dinâmica será carregada.
O processo de tradução de endereços (ilustrado na Figura 8-1) que acontece durante o carregamento dinâmico da biblioteca é o problema crucial da ligação
dinâmica, o que torna todo o conceito bastante complexo.
gj
0xA0120000
0x00000000
Figura 8-1. A tradução de endereços acontece inevitavelmente quando o carregador tenta encontrar um lugar para a biblioteca dinâmica no mapa de memória do processo
Mais especificamente, existem diferenças substanciais no cenário em que o vinculador executa a conversão de endereço do cenário em que o carregador faz a mesma
coisa.
• Ao realizar a tradução de endereços, o vinculador em geral tem uma situação de "quadro limpo / neve virgem". Nenhum dos arquivos-objeto obtidos pelo
vinculador durante o processo de tiling tem qualquer uma das referências resolvidas. Isso dá ao vinculador um grande grau de liberdade para manipular os
arquivos de objeto ao tentar encontrar o lugar certo para eles. Ao concluir a colocação inicial dos arquivos de objeto, o vinculador examina a lista de referências
não resolvidas, as resolve e marca os endereços corretos nas instruções de montagem.
• A carregadeira, por outro lado, opera em circunstâncias significativamente diferentes. Toma como entrada o binário da biblioteca dinâmica, que já passou no
processo completo de construção e resolveu todas as referências. Em outras palavras, todas as instruções do montador são carimbadas com os endereços corretos.
Nos casos particulares em que o linker imprimiu os endereços absolutos nas instruções do montador, a tradução de endereço realizada pelo carregador torna os endereços
impressos completamente sem sentido. Executar essas instruções fundamentalmente interrompidas fornece, na melhor das hipóteses, resultados falsos e pode ser
muito perigoso. Obviamente, a tradução de endereços realizada durante o tipo de carregamento dinâmico cai na ampla categoria do paradigma "elefante na loja
de porcelana".
Em resumo, a tradução de endereços do loader não pode ser evitada, pois é inerente à ideia de carregamento dinâmico. No entanto, isso impõe imediatamente um
tipo de problema muito sério. Felizmente, embora isso não possa ser evitado, algumas maneiras de dançar em torno dele foram implementadas com sucesso.
É quase óbvio que as funções e variáveis declaradas estáticas (no sentido da linguagem C, como relevantes apenas para o arquivo em que residem) estão fora de
perigo. Na verdade, uma vez que apenas as instruções próximas precisam acessar esses símbolos, todos os acessos podem ser implementados fornecendo os
deslocamentos de endereço relativos. Qual é a situação com as funções e variáveis que não são declaradas estáticas?
Como se constatou, não ser declarado estático ainda não significa que tal função ou variável estará inevitavelmente condenada a sofrer com a tradução do
endereço.
Na verdade, apenas as funções e variáveis cujos símbolos são exportados pela biblioteca dinâmica têm garantia de sofrer os efeitos negativos da tradução de endereços.
Na verdade, quando o vinculador sabe que um determinado símbolo é exportado, ele implementa todos os acessos por meio dos endereços absolutos. A tradução
do endereço torna essas instruções inutilizáveis.
O exemplo de código analisado no Apêndice A ilustra esse ponto, no qual duas variáveis não estáticas são apresentadas no código, das quais apenas uma é
exportada pela biblioteca dinâmica. Como mostra a análise, a variável exportada é aquela que é afetada pela tradução de endereço de carregamento dinâmico.
Cenário 1: O cliente binário precisa saber o endereço dos símbolos da biblioteca dinâmica
Este é o cenário mais básico, que acontece quando o binário do cliente (um executável ou uma biblioteca dinâmica) conta com os símbolos de uma biblioteca
dinâmica carregada disponíveis em tempo de execução, mas não sabe qual será o endereço final, conforme ilustrado na Figura 8 -2.
Figura 8-2. Cenário 1: o binário do cliente deve resolver os símbolos da biblioteca dinâmica
Se você assumir a abordagem usual em que a tarefa de resolver os endereços de símbolo tradicionalmente pertence ao vinculador (e apenas ao vinculador), você
está em uma situação problemática. Ou seja, o vinculador já concluiu sua tarefa de construir tanto o binário do cliente quanto a biblioteca, que está sendo
carregada dinamicamente.
Rapidamente se torna óbvio que certos pensamentos "fora da caixa" precisam ser aplicados para resolver esse tipo de situação. A solução leva à concessão de parte
das responsabilidades do vinculador de resolver os símbolos para o carregador.
No novo esquema das coisas, a nova capacidade do carregador de obter algumas das habilidades do vinculador é normalmente implementada como um módulo
comumente referido como vinculador dinâmico.
Cenário 2: a biblioteca carregada não conhece mais os endereços de seus próprios símbolos
Normalmente, as funções ABI exportadas pelas bibliotecas dinâmicas são os pontos de entrada bem encapsulados na funcionalidade interna da biblioteca. A
sequência típica que acontece em tempo de execução é que o binário do cliente normalmente chama um dos métodos ABI, que por sua vez chama as funções
internas da biblioteca, que não são de interesse particular para o binário do cliente e, portanto, não são exportadas.
Um possível cenário diferente (embora um pouco menos frequentemente encontrado) é quando uma função ABI de biblioteca dinâmica chama internamente a
outra função ABI.
Vamos supor, por exemplo, que uma biblioteca dinâmica hospede um módulo que exporta as duas funções de interface:
• Inicializar ()
• Desinicializar ()
O fluxo de execução interna de cada uma das duas funções muito provavelmente assumirá a sequência de chamadas das funções internas da biblioteca, declaradas
com escopo estático. Chamar os métodos internos é normalmente executado pela família de instruções de chamada do assembler que apresenta endereços
relativos. A tradução de endereço não afeta negativamente a implementação das funções de chamada, conforme ilustrado na Figura 8-3.
Independentemente do fato de que o endereço absoluto da função ABI não é conhecido, os deslocamentos relativos às funções estáticas internas da biblioteca são suficientes para
implementar as instruções de chamada.
A tradução do endereço não causou problemas, pelo menos nesta parte do quadro geral.
deslocamento de endereço
Figura 8-3. Independentemente da tradução de endereço, as chamadas para funções locais (que podem ser implementadas como saltos relativos) podem ser facilmente resolvidas
É perfeitamente possível, entretanto, que os projetistas da biblioteca decidam fornecer a função de interface Reinitialize () . Não seria surpreendente nem
errado que essa função primeiro chame internamente a função de interface Uninitialize () , seguida imediatamente pela chamada para a função de interface
Initialize () .
Por ser a função de interface ABI, o ponto de entrada da função Reinitialize () deve pertencer ao conjunto de símbolos exportados da biblioteca dinâmica. As
instruções de salto que se referem a esta função não podem ser implementadas como saltos relativos. Em vez disso, o vinculador deve implementar as instruções
de salto / chamada como saltos para os endereços absolutos.
Obviamente, agora você tem um tipo de situação interessante. A parte danificada neste cenário não é mais apenas o binário do cliente, mas também a biblioteca
carregada. Depois que a tradução da memória é realizada pelo carregador, os endereços da função não são mais aplicáveis. As instruções de chamada do
assembler para que o linker tenha uma boa impressão com os endereços absolutos não só não têm sentido, mas são potencialmente perigosas, já que seu alvo de
salto não está mais onde foi planejado originalmente, como mostrado na Figura 8-4.
Figura 8-4. Cenário 2: uma função ABI que chama internamente outra sofre de problemas de referências não resolvidos. Ambos os pontos de entrada da função são designados para
exportação, o que estimula o compilador a implementar chamadas como saltos absolutos. Resolver os endereços absolutos não é possível até que o carregador conclua a tradução do
endereço
o
Novamente, o problema idêntico que você enfrenta com as funções ABI existe com as variáveis de escopo global da biblioteca dinâmica.
Coordenação Linker-Loader
Foi reconhecido no início que no cenário de vinculação dinâmica, o vinculador não pode resolver completamente tudo o que normalmente resolve durante a
construção do executável monolítico.
Durante o estágio inicial de vinculação dinâmica, o carregador carrega o segmento de código da biblioteca dinâmica no novo intervalo de endereços. Mesmo que o
vinculador tenha concluído legitimamente a tarefa de resolver as referências ao construir a biblioteca dinâmica, isso simplesmente não é suficiente; o processo de
tradução de endereços tornou inválidos os endereços absolutos impressos nas instruções de chamada do assembler.
Estratégia Geral
Conhecendo todas as restrições descritas anteriormente, a cooperação entre o vinculador e o carregador foi estabelecida de acordo com o seguinte conjunto de
diretrizes gerais:
• O vinculador estima precisamente o dano, prepara as diretivas para consertá-lo e incorpora as diretivas ao arquivo binário.
Ao criar uma biblioteca dinâmica, além de ser inteligente em descobrir as relações entre as várias partes do quebra-cabeça, o vinculador também deve ser
inteligente o suficiente para reconhecer o que será interrompido como resultado do carregamento do segmento de código em diferentes intervalos de endereços .
Primeiro, o intervalo de endereço de código do mapa de memória da biblioteca dinâmica é baseado em zero, ao contrário dos executáveis, caso em que o
vinculador lida com os intervalos de endereço não baseados em zero mais específicos.
Em segundo lugar, ao reconhecer que os endereços de certos símbolos não podem ser resolvidos antes do tempo de carregamento, o vinculador para de tentar; em
vez disso, ele preenche os símbolos não resolvidos com valores temporários (normalmente sendo alguns valores obviamente errados, como todos os zeros ou algo
assim).
É completamente possível classificar todos os cenários nos quais a tradução do endereço do carregador tornará ineficazes as referências resolvidas anteriormente.
Esses casos acontecem sempre que as instruções do assembler exigem endereços absolutos. Ao concluir o estágio de vinculação da construção da biblioteca
dinâmica, o vinculador pode identificar tais ocorrências e, de alguma forma, avisar o carregador sobre elas.
A fim de fornecer suporte para a coordenação do linker-carregador, as especificações do formato binário oferecem suporte a novas seções cujo propósito é
unicamente fornecer o lugar para o linker deixar as diretivas para o carregador de como consertar os danos causados pela tradução de endereço ocorrida durante
o carregamento dinâmico. Além disso, uma sintaxe simples específica foi desenvolvida para que o vinculador possa especificar com precisão para o carregador o
curso de ação a ser executado. Essas seções são chamadas de seções de realocação no binário, das quais a seção .rel.dyn é a mais antiga.
Em geral, as diretivas de realocação são gravadas no binário pelo vinculador, para serem lidas posteriormente pelo carregador. Eles especificam
• Em quais endereços o carregador precisa aplicar algum patch depois de definir o mapa de memória final de todo o processo.
• O que exatamente o carregador precisa fazer para corrigir corretamente os endereços não resolvidos.
A última fase pertence ao carregador. Ele lê na biblioteca dinâmica criada pelo vinculador, lê nos segmentos do carregador (cada um carregando uma variedade
de seções do vinculador) e os coloca no mapa de memória do processo, junto com o código pertencente ao executável original.
Finalmente, ele localiza a seção .rel.dyn , lê as diretivas que o vinculador deixou e, de acordo com essas diretivas, executa o patching da biblioteca dinâmica
original. Quando o patch for concluído, o mapa de memória estará pronto para iniciar o processo.
Obviamente, a tarefa de lidar com o carregamento dinâmico da biblioteca requer que um pouco mais de inteligência seja concedido ao carregador do que o
necessário para suas tarefas básicas.
Táticas
Em geral, a troca de informações entre o vinculador e o carregador acontece por meio da seção .rel.dyn específica inserida pelo vinculador no corpo do binário.
A única questão é em qual dos arquivos binários o vinculador inserirá a seção .rel.dyn ?
A resposta é simples: é a roda que faz barulho que obtém o óleo. O binário cuja seção de código precisa de reparo geralmente carregará a seção .rel.dyn .
Concretamente, no Cenário 1, o vinculador incorpora a seção .rel.dyn ao binário do cliente (a biblioteca executável ou dinâmica cujas instruções foram
"danificadas" pelo carregamento de uma nova biblioteca dinâmica), pois é aqui que a tradução do endereço de carregada bibliotecas causaram problemas. A
Figura 8-5 ilustra a ideia.
Depois que a biblioteca é »carregada e o intervalo de endereço de sua seção de código é determinado, certas instruções na seção de código do executável que sofreu com a tradução
do intervalo de endereço precisam ser corrigidas.
seção rel.dyn
Cliente binário
Figura 8-5. No Cenário 1, as diretivas do vinculador são incorporadas ao arquivo binário do cliente
No Cenário 2, entretanto, o vinculador incorpora a seção .rel.dyn ao binário da biblioteca carregada, pois ela precisa de ajuda para reconstruir a coerência entre
os endereços e as instruções que apontam para eles (Figura 8-6).
Depois que a biblioteca é carregada e o intervalo de endereço da seção de código é determinado, certas instruções na seção de código da biblioteca carregada que sofreram com a
tradução do intervalo de endereço precisam ser corrigidas.
1
seção rel.dyn
function_xyz ()
Neste exemplo específico, você tem o cenário mais simples possível, no qual um executável carrega uma biblioteca dinâmica. Um caso muito mais realista é que a
própria biblioteca dinâmica pode carregar outra biblioteca dinâmica, que por sua vez pode carregar outra biblioteca dinâmica, etc. Qualquer uma das bibliotecas
dinâmicas no meio da cadeia de carregamento dinâmico pode ter dupla função. O Cenário 1, bem como o Cenário 2, podem ser aplicáveis ao mesmo binário.
As especificações do formato binário normalmente especificam em detalhes as regras de sintaxe de comunicação entre o vinculador e o carregador. As diretivas do
linker para o carregador geralmente tendem a ser muito simples, mas muito precisas e diretas (Figura 8-7). Conseqüentemente, a estruturação das informações
transportadas pelas diretivas do vinculador não exige um grande esforço de implementação e compreensão.
OO0OOOO0 príntf
00000000 shlib_abi_function
00OOOO00 _gmon_start__
00000000 dl_lterate_phdr
00000000 _libc_start_nain
00000000 putchar
O0OOlfe8 00000106 R_386_GLOB_DAT 0O001fec 00000206 R_386_CL0B_DAT OOOOlffO 00000306 R 386 GLOB DAT 0000201c shlibNonStaticAccessed
0000201c shltbNonstatlcAccessed 0000200c shUbNon
000OOO00 _gnon_start__
OOOOOOOO _3v_RegisterClasses
Em particular, o formato de arquivo ELF carrega as definições detalhadas de como o vinculador especifica as diretivas para o carregador. As diretivas são
armazenadas principalmente na seção .rel.dyn , bem como em algumas outras seções especializadas (rel.plt, got, got.plt). As ferramentas como
readelf ou objdump podem ser usadas para exibir o conteúdo das diretivas. A Figura 8-7 mostra alguns dos exemplos.
• Offset especifica o deslocamento de byte da seção de código para o operando da instrução assembler, que é tornado sem sentido pela tradução do endereço e
precisa de reparo.
Onde
• ELFxx_R_SYM denota o índice da tabela de símbolos em relação ao qual a realocação deve ser feita:
Uma das seções do arquivo binário contém a lista de símbolos. Este valor representa simplesmente o índice do item da tabela de símbolos que representa este
símbolo específico. O readelf e objdump podem fornecer a lista completa de símbolos contidos na tabela de símbolos do binário.
• ELFxx_R_TYPE denota o tipo de realocação a ser aplicada. Uma descrição detalhada dos tipos de realocação disponíveis é mostrada abaixo.
Nome
Valor
file:///C:/Users/noels/Desktop/Advanced C and C++ Compiling ( PDFDrive ).html 101/235
21/09/2021 22:07 Advanced C and C++ Compiling ( PDFDrive ).html
Campo
Cálculo
tabela para a entrada da tabela de deslocamento global do símbolo. Além disso, instrui o editor de links a construir uma tabela de deslocamento global.
Este tipo de realocação calcula o endereço da entrada tabica de ligação de procedimento do símbolo e, adicionalmente, instrui o editor de link a construir uma tabela de
ligação de procedimento.
O editor de links cria esse tipo de realocação para links dinâmicos. Seu membro deslocado refere-se a um local em um segmento gravável. O índice da tabela de símbolos
especifica um símbolo que deve existir no arquivo objeto atual e em um objeto compartilhado. Durante a execução, o vinculador dinâmico copia os dados associados ao
símbolo do objeto compartilhado para o local especificado pelo deslocamento.
Este tipo de realocação é usado para definir uma entrada da tabela de deslocamento global para o endereço do símbolo especificado. O tipo de realocação especial permite
determinar a correspondência entre os símbolos e as entradas tabicas de deslocamento global.
O editor de links cria esse tipo de realocação para links dinâmicos. Seu membro de deslocamento fornece a localização de uma entrada da tabela de ligação de
procedimento. O vinculador dinâmico modifica a entrada da tabela de ligação de procedimento para transferir o controle para o endereço do símbolo designado [ver 'Tabela
de ligação de procedimento "na Parte 2],
O editor de links cria esse tipo de realocação para links dinâmicos. Seu membro deslocado fornece uma localização dentro de um objeto compartilhado que contém um valor
que representa um endereço relativo. O I inker dinâmico calcula o endereço virtual correspondente adicionando o endereço virtual no qual o objeto compartilhado foi
carregado ao endereço relativo. As entradas de realocação para este tipo devem especificar 0 para o índice da tabela de símbolos.
Este tipo de realocação calcula a diferença entre o valor de um símbolo e o endereço da tabela de deslocamento global. Além disso, instrui o editor de links a construir a
tabela de deslocamento global.
Este tipo de realocação se assemelha a R_38 6_PC32, exceto que usa o endereço da tabela de deslocamento global em seu cálculo. O símbolo referenciado nesta realocação
normalmente é _GLOBAL_OFFSET_TABLE_. que adicionalmente instrui o editor de links a construir a tabela de deslocamento global.
R_336_PLT32
R 336 CÓPIA
R_386_RELATIVE
R 38 6 GOTOFF
R 38 6 GOTPC
Figura 8-8. Visão geral dos tipos de diretiva do vinculador (da especificação do formato ELF)
• Sym.Value especifica o deslocamento provisório e temporário dentro da seção de código (no caso de funções) ou dentro do segmento de dados (no caso de
variáveis) onde o símbolo reside atualmente no arquivo binário original. Presume-se que a tradução do endereço afetará esses valores.
Cronologicamente, a primeira implementação do conceito de vínculo dinâmico veio na forma do chamado Load Time Relocation. Em termos gerais, essa técnica
foi a primeira técnica de carregamento dinâmico que realmente funcionou. Seu benefício imediato foi a capacidade de liberar os binários do aplicativo da
necessidade de carregar "bagagem" desnecessária (o código que lida com as tarefas habituais específicas do sistema operacional).
Os benefícios imediatos que o conceito LTR trouxe foi que não apenas o tamanho dos bytes dos binários dos aplicativos tornou-se substancialmente menor, mas
também a maneira como certas tarefas específicas do sistema operacional foram executadas tornou-se unificada em uma ampla variedade de aplicativos.
Apesar dos benefícios óbvios que esse conceito trouxe, ele tinha várias desvantagens importantes. Primeiro, essa técnica modifica (corrige) o código da biblioteca
dinâmica com os valores literais de endereços de variáveis e funções, significativos apenas no contexto do aplicativo que o carregou primeiro. No contexto de
qualquer outro aplicativo (que muito possivelmente apresentaria o layout do mapa de memória do processo diferente), as modificações de código muito
provavelmente seriam inúteis, sem sentido e simplesmente não aplicáveis.
Como consequência, se vários aplicativos precisassem dos serviços de uma biblioteca dinâmica ao mesmo tempo, isso significaria que você teria exatamente esse
número de cópias da mesma biblioteca dinâmica na memória.
A segunda desvantagem era que uma quantidade proporcionalmente grande de modificações de código seria necessária. Com essa técnica em vigor, o carregador
precisa modificar / corrigir exatamente tantos lugares no código que fazem referência a uma determinada variável ou chamam uma determinada função. Nos
casos em que o aplicativo carrega muitas bibliotecas dinâmicas, o tempo de carregamento aumenta para uma latência inicial significativa e perceptível durante o
início do aplicativo.
A terceira desvantagem é que o segmento de código gravável (.texto) representa uma ameaça potencial à segurança. Com essa técnica em vigor, o sonho de
carregar a biblioteca dinâmica na memória física apenas uma vez e mapeá-la na infinidade de diferentes endereços de mapas de memória de aplicativos não foi
alcançável. A Figura 8-9 ilustra a ideia por trás do conceito de realocação de tempo de carga.
Biblioteca dinâmica
• ♦
Após a conclusão do carregamento, os endereços finais dos símbolos são conhecidos. Agora é a hora de conectar a instrução com os símbolos aos quais fazem referência.
A abordagem de realocação de tempo de carregamento modifica a fonte da biblioteca dinâmica (seção .text) carregando os operandos de endereço de instruções para apontar para os
endereços do mapa de memória do processo atual.
No entanto, codificar os endereços torna a biblioteca dinâmica inutilizável para qualquer outro mapa de memória de processo no qual os símbolos residam em endereços diferentes.
Todas as desvantagens foram abordadas com o design da abordagem mais recente e, em muitos aspectos, superior do Código Independente de Posição (PIC), que
rapidamente se tornou a escolha predominante de técnicas de vinculação.
As limitações do esquema Load Time Relocation foram abordadas na próxima implementação do link dinâmico, a técnica conhecida como Código Independente
de Posição (Figura 8-10). As modificações diretas indesejadas das instruções do segmento de código da biblioteca dinâmica foram evitadas com uma etapa extra de
indireção. No jargão das linguagens de programação, a abordagem pode ser descrita como o uso de ponteiro a ponteiro em vez de ponteiro.
Biblioteca dinâmica
A tabela de deslocamento global (GOT) mantém os slots de armazenamento que transportam os endereços de cada um dos símbolos não resolvidos.
O deslocamento GOT é constante e conhecido no momento do link. Conseqüentemente, as instruções da CPU que fazem referência ao slot GOT são independentes do layout de mapa
de memória de processo específico.
O conteúdo dos slots de armazenamento, entretanto, depende do layout do mapa de memória do processo específico. Depois que tudo é carregado e os endereços dos símbolos são
conhecidos, o carregador visita o GOT e atualiza os slots de armazenamento com os valores corretos dos endereços dos símbolos.
Biblioteca dinâmica
r eu
eu ■ M
<
n r?
(
1
o eu
Q
Tabela de deslocamento global
(GOT) que mantém os slots de armazenamento para cada um dos endereços de símbolo necessários
E+X
file:///C:/Users/noels/Desktop/Advanced C and C++ Compiling ( PDFDrive ).html 104/235
21/09/2021 22:07 Advanced C and C++ Compiling ( PDFDrive ).html
Fixo
Morada
Deslocamento
Deslocamento de endereço
Basicamente, os endereços dos símbolos são fornecidos às instruções necessitadas em duas etapas. Para obter o endereço do símbolo, primeiro uma instrução mov
acessa a localização do endereço do endereço e carrega seu conteúdo (um endereço de símbolo necessário) em um registro de CPU disponível. Imediatamente
depois, o endereço de símbolo recuperado agora armazenado em um registro pode ser usado como o operando nas instruções subsequentes (mov para dados,
chamada para chamadas de função).
A diferença especial na solução é que os endereços dos símbolos são mantidos em uma chamada tabela de deslocamento global (GOT), para a qual o vinculador
reserva uma seção .got dedicada . A distância entre a seção .text e a seção .got é constante e conhecida no momento do link. Para cada um dos símbolos que
precisam ser resolvidos, a tabela de deslocamento global mantém um slot dedicado no deslocamento conhecido e fixo desde o início da tabela.
Dada a distância GOT fixa e deslocamento de slot fixo (ambos conhecidos no tempo de link), torna-se possível para um compilador implementar as instruções de
código para fazer referência aos locais fixos. Mais importante ainda, o código implementado não depende dos endereços de símbolo reais e pode ser usado sem
quaisquer alterações mapeadas diretamente em uma infinidade de outros processos.
O ajuste final às peculiaridades de um layout de mapa de memória específico é concluído pelo carregador. Neste esquema, entretanto, o carregador não modifica
irreversivelmente o código ( seção .text ). Em vez disso, uma vez que os endereços dos símbolos são conhecidos, o carregador corrige a seção .got , que é
(muito parecida com as seções de dados) sempre implementada por processo.
■ Observe que , para implementar este esquema, foi necessário um esforço substancial de projeto, que se espalhou além dos limites do linker-carregador.
na verdade, para implementar o conceito PiC, a história deve começar no nível do compilador. em particular, o sinalizador -fPic deve ser passado para o
compilador. O mnemônico "fPiC" ou simplesmente "PiC" eventualmente se tornou um sinônimo de link dinâmico.
Vinculação preguiçosa
O fato de que a referência dos símbolos na abordagem PIC passa pelo nível extra de indireção fornece o potencial para alcançar os benefícios de desempenho
adicionais em tempo de execução. A estratégia de implementação do pontapé desempenho extra é baseada no fato de que o carregador não desperdiça tempo
precioso configurar o conteúdo dos .got e .got.plt seções até que o programa é iniciado.
As instruções do montador que fazem referência aos símbolos são definidas para apontar para o ponto intermediário de qualquer maneira, e não há nada
terrivelmente errado com a forma geral do código que interromperia o carregamento do programa.
Na verdade, o carregador normalmente não se incomodam mesmo para completar a configuração do conteúdo dos .got e .got.plt seções até que seja
absolutamente necessário. Esses momentos acontecem depois que o programa já começou, e somente quando o fluxo de execução vem com as instruções que
fazem referência os símbolos cujos endereços são mantidos no .got e .got.plt seções.
O benefício óbvio da procrastinação do carregador (comumente referido como ligação lenta) é que o processo de carregamento é concluído mais rápido, o que
torna o início do aplicativo mais rápido. Uma pequena penalidade de desempenho única ocorre quando o carregador rapidamente compensa seu descuido inicial
(embora premeditado). Isso acontece apenas conforme a necessidade e apenas uma vez, na primeira ocorrência de referência de símbolo. Quanto menos símbolos
da biblioteca dinâmica forem realmente referenciados no tempo de execução, mais economia de desempenho o carregador será capaz de obter.
O conceito de ligação lenta é um recurso extra da abordagem PIC, que obviamente adiciona outra boa razão para os desenvolvedores escolherem o PIC em vez da
implementação LTR. Na verdade, a abordagem PIC é uma implementação favorita do tipo de problema Cenário 1 quando o binário do cliente é o arquivo
executável (ou seja, aplicativo).
Na realidade, a estrutura de um programa típico pode ser descrita como a cadeia recursiva de ligação dinâmica, na qual cada uma das bibliotecas dinâmicas da
cadeia carrega várias outras bibliotecas dinâmicas. Visualmente, a cadeia recursiva de carregamento pode ser representada como a elaborada estrutura de árvore
com muitas conexões laterais entre os ramos. O comprimento de ramos individuais, em alguns casos, pode acabar sendo muito grande. Por mais interessante que
seja a complexidade da cadeia recursiva de ligação dinâmica, e por mais impressionante que pareça o comprimento de seus ramos, esses não são os detalhes mais
poderosos de toda a história.
Muito mais importante é o fato emergente de que na cadeia de carregamento dinâmico cada uma das bibliotecas dinâmicas participantes pode se ver desempenhando os
papéis do Cenário 1 e do Cenário 2. A Figura 8-11 ilustra o ponto.
Em outras palavras, uma biblioteca dinâmica na cadeia de carregamento pode precisar resolver as referências da biblioteca que carrega, bem como resolver
novamente a referência de seus próprios símbolos. Isso torna toda a história um pouco mais interessante.
Um certo conjunto de fortes preferências de implementação que residem neste nível molecular estipula os detalhes de implementação, que revisarei brevemente
na próxima seção.
Independentemente do cenário, sempre há duas maneiras de como a coordenação linker-carregador pode ser implementada: a abordagem LTR ou PIC pode ser
aplicada. A escolha da técnica de coordenação linker-loader não é totalmente gratuita. Além da escolha do designer com base nos prós e contras de cada técnica,
existem algumas outras limitações que precisam ser explicitamente apontadas:
• Código Independente de Posição (PIC) é a técnica fortemente preferida de executável para resolver as referências do primeiro nível de bibliotecas carregadas (o
cenário marcado com a letra A dentro de um círculo na Figura 8-11).
Em termos de escolha entre LTR ou PIC, as bibliotecas dinâmicas na cadeia de carregamento podem apresentar uma variedade de combinações. Uma biblioteca
dinâmica que implementa LTR pode, por sua vez, carregar dinamicamente a próxima biblioteca dinâmica, que implementa o PIC, que por sua vez pode carregar
dinamicamente a biblioteca que implementa ... você escolher - seja qual for sua escolha, é permitido.
Cenário 1 Cenário 2
CAPÍTULO 8 ■ DESENVOLVENDO LIBRAS DINÂMICAS: TÓPICOS AVANÇADOS A Figura 8-12 ilustra as regras descritas.
/ Pi
LTR
LTR
xu.
LTR
FOTO
y
Cenário 1 Cenário 2
Figura 8-12. Fortes preferências de implementação (no nível molecular) que regem a implementação da cadeia recursiva de ligação dinâmica
CAPÍTULO 9
Da mesma forma, a complexidade de como as bibliotecas dinâmicas funcionam internamente trouxe vários desafios distintos para o domínio da cadeia de
ferramentas de software (compiladores, vinculadores, carregadores). A necessidade inicialmente reconhecida de o vinculador e o carregador cooperarem mais
estreitamente e as técnicas de implementação foram discutidas no capítulo anterior.
Outro paradigma interessante intimamente associado ao domínio das bibliotecas dinâmicas é a questão do tratamento de símbolos duplicados. Mais
especificamente, quando as bibliotecas dinâmicas são ingredientes de entrada no processo de vinculação, o vinculador se desvia da abordagem usual de senso
comum normalmente seguida em casos em que arquivos de objetos individuais e / ou bibliotecas estáticas são combinados no arquivo binário.
Como uma observação lateral, o algoritmo do vinculador para seus próprios fins internos normalmente aplica modificações dos nomes dos símbolos originais.
Como consequência direta, os problemas duplicados relatados impressos pelo vinculador podem se referir a nomes um tanto diferentes dos nomes originais. As
modificações do nome do símbolo podem variar de simples decorações de nome (por exemplo, antes do sublinhado) até o tratamento sistemático de questões de
afiliação de função C ++. Felizmente, as modificações são normalmente realizadas de maneira estritamente uniforme e previsível.
As causas de símbolos duplicados podem variar. No caso mais simples possível, aconteceu que os diferentes designers escolheram o nome mais óbvio para suas
classes de módulos, funções, estruturas (por exemplo, classe Timer, função getLength () ou variáveis lastError ou libVersion). Tentar combinar os
módulos desses designers inevitavelmente leva à descoberta da existência de símbolos duplicados.
As outras possibilidades cobrem os casos típicos quando a instância do tipo de dados (de uma classe, estrutura ou tipo de dados simples) é definida em um
arquivo de cabeçalho. Mais de uma inclusão do arquivo de cabeçalho cria inevitavelmente o cenário de símbolos duplicados.
Símbolos C duplicados
A linguagem C impõe critérios bastante simples para que dois ou mais símbolos sejam considerados duplicatas um do outro. Desde que os nomes das funções,
estruturas ou tipos de dados sejam idênticos, os símbolos são considerados idênticos. Por exemplo, construir o seguinte código falhará:
arquivo: main.c
#include <stdio.h>
return 0;
return 0;
main.c: 9: 5: erro: tipos conflitantes para 'function_with_duplicated_name' main.c: 3: 5: nota: a definição anterior de
'function_with_duplicated_name' estava aqui main.c: Na função 'main':
compilação terminada.
Símbolos C ++ duplicados
Sendo uma linguagem de programação orientada a objetos, C ++ impõe critérios de símbolos duplicados mais relaxados. Em termos de namespaces, classes /
estruturas e tipos de dados simples, o uso de nomes idênticos ainda permanece como o único critério de símbolos duplicados. No entanto, no domínio das
funções, os critérios de símbolos duplicados não se limitam mais apenas aos nomes das funções, mas também levam em consideração a lista de argumentos.
Os princípios de sobrecarga de função (métodos) permitem usar o mesmo nome para diferentes métodos da mesma classe com diferentes listas de argumentos de
entrada, desde que o tipo de valor de retorno não seja diferente.
O mesmo princípio se aplica aos casos secundários de duas ou mais funções pertencentes ao mesmo namespace, não sendo membros de nenhuma classe. Mesmo
que essas funções não sejam afiliadas a nenhuma classe, os critérios de duplicado C ++ mais elásticos se aplicam - eles são considerados duplicados apenas se seus
nomes forem idênticos e sua lista de argumentos de entrada idêntica.
CAPÍTULO 9 ■ SÍMBOLOS HANDUNG DUPUCATE QUANDO DESCONHECIDO EM UBRÁRIOS DINÂMICOS A construção do código a seguir será concluída com sucesso:
arquivo: main.cpp
classe CTest {
público:
};
return 0;
return 0;
arquivo: build.sh
return 0.0f;
violará as regras básicas de sobrecarga de função C ++, o que resultará na seguinte falha de compilação:
compilação terminada.
Quando o vinculador detecta os símbolos duplicados, ele imprime uma mensagem de erro especificando os arquivos / linhas de código onde ocorrem as
ocorrências dos símbolos duplicados e a vinculação é declarada uma falha. Isso basicamente significa que os desenvolvedores precisam voltar à prancheta e tentar
resolver o problema, o que muito provavelmente significa que o código precisa ser recompilado.
O exemplo a seguir ilustra o que acontece quando você tenta vincular ao mesmo binário cliente as duas bibliotecas estáticas que apresentam símbolos duplicados.
O projeto é composto por duas bibliotecas estáticas muito simples, apresentando símbolos duplicados, bem como o aplicativo cliente que tenta vincular os dois:
arquivo: staticlibfirstexports.h
arquivo: staticlib.c
#include <stdio.h>
arquivo: build.sh
arquivo: staticlibsecondexports.h
arquivo: staticlib.c
#include <stdio.h>
retorno (x + 1);
arquivo: build.sh
ClientApplication:
arquivo: main.c
#include <stdio.h>
#include "staticlibfirstexports.h"
#include "staticlibsecondexports.h"
int nRetValue = 0;
Devido à presença de símbolos duplicados em ambas as bibliotecas estáticas, tente construir os resultados do aplicativo cliente com o erro do vinculador:
../libFirst/libfirst.a(staticlib.o):/home/milan/Desktop/duplicateSymbolsHandlingResearch/01_dupl icateSymbolsCriteria /
02_duplicatesInTwoStaticLibs / 01_plainAndSimple / libFirst / statib.c: 10: primeiro definido aqui
Comentar a chamada à função duplicada não ajuda a evitar a falha do vinculador. Obviamente, o vinculador primeiro tenta agrupar tudo que vem das bibliotecas
estáticas de entrada e arquivos de objetos individuais (main.c). Se os símbolos duplicados acontecerem tão cedo no jogo de vinculação, o vinculador declarará
uma falha, independentemente do fato de ninguém ter tentado fazer referência aos símbolos duplicados.
Curiosamente, as funções locais declaradas com a palavra-chave estática no significado da linguagem C dessa palavra-chave (ou seja, escopo de visibilidade
limitado apenas às funções que residem no mesmo arquivo de origem) não são registradas como duplicatas. Modifique os arquivos de origem das bibliotecas
estáticas em seu exemplo com o seguinte código:
ClientApplication:
arquivo: main.c
#include <stdio.h>
#include "staticlibfirstexports.h"
#include "staticlibsecondexports.h"
O aplicativo cliente agora será construído com sucesso e produzirá a seguinte saída: staticlibfirst_function
Obviamente, o vinculador mantém os compartimentos separados para as funções locais. Mesmo que seus nomes de símbolo sejam completamente idênticos, a
colisão não acontece.
Para ilustrar a abordagem totalmente diferente do vinculador para este cenário específico, o projeto de demonstração simples é criado. É composto por duas
bibliotecas dinâmicas que apresentam os símbolos duplicados e o aplicativo cliente que os vincula:
arquivo: shlibfirstexports.h
arquivo: shlib.c
#include <stdio.h>
return 0;
arquivo: build.sh
arquivo: shlibsecondexports.h
arquivo: shlib.c
#include <stdio.h>
arquivo: build.sh
arquivo: main.c
#include <stdio.h>
#include "shlibfirstexports.h"
#include "shlibsecondexports.h"
int nRetValue = 0;
nRetValue + = shlibfirst_function (1); nRetValue + = shlibsecond_function (2); nRetValue + = shlib_duplicate_function (3); return
nRetValue;
arquivo: build.sh
gcc -Wall -g -O0 -I ../ libFirst -I ../ libSecond -c main.c gcc main.o -Wl, -L ../ libFirst -Wl, -lfirst \ -Wl, -L .. / libSecond
-Wl, -lsecond \ -Wl, -R ../ libFirst \
-o clientApp
Mesmo que as duas bibliotecas compartilhadas apresentem as duplicatas e até mesmo uma das duplicatas (shlib_duplicate_function) não seja uma função
local, a construção do aplicativo cliente é concluída com sucesso. A execução do aplicativo cliente, no entanto, traz um pouco de surpresa:
Obviamente, o vinculador encontrou alguma maneira de resolver os símbolos duplicados. Ele resolveu escolhendo uma das ocorrências de símbolo (aquela em
shlibfirst.so) e direcionou todas as referências a shlib_duplicate_function para aquela ocorrência de símbolo particular.
A decisão desse linker é claramente uma etapa muito controversa. Em cenários do mundo real, as funções nomeadas de forma idêntica de diferentes bibliotecas
dinâmicas podem transportar funcionalidades substancialmente diferentes. Imagine, por exemplo, que cada uma das bibliotecas dinâmicas
libcryptography.so, libnetworkaccess.so e libaudioport.so apresentam o método Initialize () . Imagine agora que o vinculador decidiu que a
chamada para Initialize () sempre significa apenas inicializar uma das bibliotecas (e nunca inicializar as outras duas).
Obviamente, esses tipos de cenários devem ser evitados com cuidado. Para fazer isso direito, a maneira de como o vinculador "pensa" deve ser completamente
entendida primeiro.
Os detalhes do algoritmo interno do vinculador para lidar com símbolos duplicados da biblioteca dinâmica serão discutidos posteriormente neste capítulo.
Em geral, a melhor abordagem para resolver os símbolos duplicados é reforçar as afiliações dos símbolos aos seus módulos particulares, visto que isso geralmente
elimina a grande maioria dos problemas potenciais de símbolos duplicados.
Em particular, recorrer ao uso de namespaces é a técnica mais recomendada, pois está comprovado que funciona em muitos cenários diferentes,
independentemente da forma em que o código é disponibilizado para a comunidade de software (biblioteca estática vs. biblioteca compartilhada ) Este recurso
está confinado ao domínio da linguagem C ++ e requer o uso do compilador C ++.
Alternativamente, se por qualquer razão o uso de um compilador estritamente C for fortemente preferido, adicionar os nomes das funções com o prefixo exclusivo
pode ser usado como uma alternativa viável, embora um pouco menos poderosa e menos flexível.
O carregamento dinâmico de bibliotecas dinâmicas em tempo de execução (por meio das chamadas dlopen () ou LoadLibrary () ) não impõe praticamente
nenhum risco de ter símbolos duplicados. Os símbolos recuperados da biblioteca dinâmica são normalmente atribuídos (por meio das chamadas dlsym () ou
GetProcAddress () ) à variável cujo nome provavelmente já foi escolhido para não duplicar nenhum dos símbolos existentes no binário do cliente.
Ao contrário, é a vinculação estaticamente ciente de bibliotecas dinâmicas que representa o cenário típico em que ocorrem as ocorrências de símbolos duplicados.
A razão genuína para decidir vincular em uma biblioteca dinâmica é o interesse no conjunto de símbolos ABI da biblioteca dinâmica ou seu subconjunto. Muito
frequentemente, no entanto, a biblioteca dinâmica pode carregar muito mais símbolos remotos ou sem importância para o projeto binário do cliente, e o
desconhecimento de sua presença pode levar a uma escolha não intencional de uma função nomeada duplicada ou dados provenientes de diferentes bibliotecas
dinâmicas.
Existe um limite de precaução que os desenvolvedores de bibliotecas dinâmicas podem tomar para tornar as coisas melhores. Reduzir a exportação dos símbolos
da biblioteca dinâmica para apenas o conjunto essencial de símbolos é definitivamente uma medida que pode reduzir significativamente a probabilidade de
colisão de nomes de símbolo. No entanto, essa prática de design altamente recomendada não atua diretamente contra a raiz do problema. Independentemente de
quão frugal você seja ao exportar seus símbolos de biblioteca dinâmica, ainda é possível que diferentes desenvolvedores escolham os nomes mais simples para os
símbolos, o que resulta em dois ou mais arquivos binários disputando o direito de usar o nome do símbolo.
Por fim, é importante ressaltar que você não está lidando com a peculiaridade de um vinculador específico em uma plataforma específica; o vinculador do
Windows (certamente Visual Studio 2010) segue quase completamente o mesmo conjunto de regras para determinar como lidar com os símbolos duplicados
encontrados durante o processo de vinculação dinâmica.
Na busca pelo melhor candidato para representar o nome do símbolo duplicado, o vinculador toma a decisão com base nas seguintes circunstâncias:
• Localização dos símbolos duplicados: O vinculador atribui diferentes níveis de importância aos símbolos localizados em diferentes partes do mapa de memória do
processo. Uma explicação mais detalhada segue imediatamente.
• Ordem de ligação especificada das bibliotecas dinâmicas: Se dois ou mais símbolos residem nas partes do código de prioridades iguais, o símbolo que reside na
biblioteca dinâmica que foi passado para o vinculador anteriormente na lista de bibliotecas dinâmicas especificadas vencerá a luta para representar o símbolo
duplicado sobre o símbolo que reside na biblioteca dinâmica declarada posteriormente na lista.
A variedade de símbolos de vinculação que participam da construção do binário do cliente pode residir em uma variedade de locais. O primeiro critério que o
vinculador aplica para resolver colisões de nomes entre símbolos é baseado na comparação entre o seguinte esquema de prioridade de símbolos.
O ingrediente inicial da construção do arquivo binário é a coleção de seus arquivos de objeto, que são originários do projeto ou vêm na forma de biblioteca
estática. No caso do Linux, as seções provenientes desses ingredientes normalmente ocupam a parte inferior do mapa de memória do processo.
Os símbolos exportados da biblioteca dinâmica (que residem na seção dinâmica das bibliotecas dinâmicas) são considerados pelo vinculador como o próximo
nível de prioridade no esquema de prioridade.
Os símbolos declarados como estáticos normalmente nunca são o assunto dos conflitos de nome de símbolo duplicado, independentemente de pertencerem ao
binário do cliente ou à biblioteca dinâmica vinculada estaticamente.
Ao mesmo grupo pertencem os símbolos despojados da biblioteca dinâmica, que obviamente não participam da etapa de vinculação do binário cliente. A Figura 9-
1 ilustra a abordagem de zoneamento de prioridade de símbolos.
>
Símbolos locais visíveis;
símbolos invisíveis / retirados
n
.dynsym: ^^ 0
exportado
dinâmico
símbolos
n
.dynsym: 0
exportado
dinâmico
símbolos y
k
Arquivos de objeto e
bibliotecas estáticas
Caso 1: o símbolo binário do cliente colide com a função ABI da biblioteca dinâmica
Este cenário pode ser basicamente descrito como o símbolo pertencente à zona prioritária 1 colidindo com o símbolo pertencente à zona prioritária 2 (Figura 9-2).
1"
Figura 9-2. Caso 1: o símbolo binário do cliente colide com o símbolo ABI da biblioteca dinâmica
Como regra geral, o símbolo relacionado à zona de código de prioridade mais alta sempre vence; em outras palavras, ele é escolhido pelo vinculador como o
destino de todas as referências ao símbolo nomeado duplicado.
O projeto a seguir é criado para demonstrar este cenário específico. Ele consiste em uma biblioteca estática, uma biblioteca dinâmica e o aplicativo cliente que
vincula as duas (a biblioteca dinâmica é vinculada estaticamente). As bibliotecas apresentam um símbolo de nome duplicado:
arquivo: staticlibexports.h
arquivo: staticlib.c
retorno (x + 2);
return 0;
arquivo: build.sh
arquivo: shlibexports.h
arquivo: shlib.c
return 0;
return 0;
arquivo: build.sh
ln -s libshlib.so.1 libshlib.so
ClientApplication:
arquivo: main.c
int nRetValue = 0;
shlib_function ();
arquivo: build.sh
gcc -Wall -g -O0 -I ../ staticLib -I ../ sharedLib -c main.c gcc main.o -Wl, -L ../ staticLib -lstaticlib \ -Wl, -L ../ sharedLib
- lshlib \ -Wl, -R ../ sharedLib \
-o clientApp
staticlib_first_function
staticlib_second_function
sharedLib: shlib_function
staticlib: shared_static_duplicate_function
nRetValue = 6
Obviamente, o vinculador escolhe o símbolo da biblioteca estática, pois ele pertence à zona de código de prioridade mais alta. Altere a ordem de construção,
conforme mostrado aqui:
arquivo: buildDifferentLinkingOrder.sh
gcc -Wall -g -O0 -I ../ staticLib -I ../ sharedLib -c main.c gcc main.o -Wl, -L ../ sharedLib -lshlib \ -Wl, -L ../ staticLib -
lstaticlib \ -Wl, -R ../ sharedLib \
-o clientAppDifferentLinkingOrder
$ ./clientAppDifferentLinkingOrder
staticlib_first_function
staticlib_second_function
sharedLib: shlib_function
staticlib: shared_static_duplicate_function
nRetValue = 6
O vinculador do Visual Studio tem uma maneira ligeiramente diferente de implementar essa regra neste caso específico (ou seja, quando a biblioteca estática
apresenta o símbolo do mesmo nome com o símbolo ABI da biblioteca dinâmica).
Quando a biblioteca estática aparece como a primeira na lista de bibliotecas, os símbolos da DLL são silenciosamente ignorados, o que é exatamente o esperado.
No entanto, se a DLL for especificada como a primeira na lista de bibliotecas, o que acontece não é o que você esperava (ou seja, o símbolo da biblioteca estática
sempre prevalece). Em vez disso, o link falha com uma mensagem dizendo algo como
em SharedLib.lib (SharedLib.dll) ClientApp.exe: erro fatal LNK1169: um ou mais símbolos definidos de multiplicação encontrados
FALHA DE CONSTRUÇÃO.
Este cenário pode ser basicamente descrito como dois símbolos, ambos pertencentes à zona de prioridade 2, colidindo entre si (Figura 9-3).
Claramente, uma vez que nenhum dos símbolos tem a vantagem de zoneamento, o fator decisivo neste caso será a ordem de ligação. Para demonstrar este cenário
específico, o seguinte projeto de demonstração é criado, que consiste em duas bibliotecas compartilhadas apresentando os símbolos ABI duplicados e o aplicativo
cliente que vincula estaticamente as duas bibliotecas dinâmicas. Para fornecer mais alguns detalhes importantes, uma das funções ABI da biblioteca compartilhada
chama internamente a função ABI duplicada:
arquivo: shlibfirstexports.h
arquivo: shlib.c
#include <stdio.h>
return 0;
arquivo: build.sh
arquivo: shlibsecondexports.h
arquivo: shlib.c
#include <stdio.h>
return 0;
arquivo: build.sh
arquivo: main.c
#include <stdio.h>
#include "shlibfirstexports.h"
#include "shlibsecondexports.h"
shlibfirst_function ();
shlibsecond_function ();
arquivo: build.sh
gcc -Wall -g -00 -I ../ libFirst -I ../ libSecond -c main.c gcc main.o -Wl, -L ../ libFirst -Wl, -lfirst \ -Wl, -L .. / libSecond
-Wl, -lsecond \ -Wl, -R ../ libFirst \
-o clientApp
Mesmo que as duas bibliotecas compartilhadas apresentem as duplicatas e até mesmo uma das duplicatas (shlib_duplicate_function) não seja uma função
local, a construção do aplicativo cliente é concluída com sucesso. A execução do aplicativo cliente resulta na seguinte saída:
$ ./clientApp
Obviamente, o vinculador escolheu a versão do shlibFirst do símbolo duplicado para representar exclusivamente o nome do símbolo duplicado. Além disso,
embora shlibsecond_another_function () chame internamente a shlib_function () duplicada , isso não afeta o resultado final do estágio de vinculação.
Sendo o símbolo ABI (a parte da seção .dynsym ), o símbolo de função duplicado sempre é resolvido da mesma maneira, independentemente do fato de residir
no mesmo arquivo de origem com as funções ABI restantes.
Como parte da investigação, o impacto da ordem de chamada de função invertida é examinado (consulte a Listagem 9-1).
#include <stdio.h>
#include "shlibfirstexports.h"
shlibsecond_function ();
shlibsecond_another_function ();
shlibfirst_function ();
return 0;
Essa mudança específica não afetou o resultado final de forma alguma. Obviamente, os momentos importantes do estágio de vinculação que impactam
criticamente o processo de resolução de símbolo duplicado acontecem durante um estágio anterior de vinculação.
gcc -Wall -g -O0 -I ../ shlibFirst -I ../ shlibSecond -c main.c gcc main.o -Wl, -L ../ shlibSecond -lsecond \ -Wl, -L ../
shlibFirst - lfirst \ -Wl, -R ../ shlibFirst \
-o clientAppDifferentLinkingOrder
Obviamente, a ordem de vinculação reversa especificada afetou a decisão do vinculador. O shlibSecond versão 's de duplicada shlib_function agora é
escolhido para representar o símbolo duplicado.
Caso 3: o símbolo ABI da biblioteca dinâmica colide com outro símbolo local da biblioteca dinâmica
Este cenário pode ser basicamente descrito como um símbolo pertencente à zona prioritária 2 colidindo com um símbolo pertencente à zona prioritária 3 (Figura 9-
4).
- \
Biblioteca dinâmica
Figura 9-4. Caso 3: o símbolo ABI da biblioteca dinâmica colide com outro símbolo local da biblioteca dinâmica
Para ilustrar este cenário específico, o seguinte projeto de demonstração é criado; ele consiste em duas bibliotecas compartilhadas (apresentando os símbolos
duplicados) e o aplicativo cliente que vincula estaticamente as duas bibliotecas:
arquivo: shlibfirstexports.h
arquivo: shlib.c
#include <stdio.h>
return 0;
return 0;
arquivo: build.sh
arquivo: shlibsecondexports.h
arquivo: shlib.c
#include <stdio.h>
return 0;
arquivo: build.sh
CAPÍTULO 9 ■ MANUSEIO DE SÍMBOLOS DUPLICADOS QUANDO LIGADO EM LÍBRICAS DINÂMICAS Aplicação do cliente :
arquivo: main.c
#include <stdio.h>
#include "shlibfirstexports.h"
#include "shlibsecondexports.h"
arquivo: build.sh
gcc -Wall -g -O0 -I ../ shlibFirst -I ../ shlibSecond -c main.c gcc main.o -Wl, -L ../ shlibFirst -lfirst \ -Wl, -L ../
shlibSecond - lsecond \ -Wl, -R ../ shlibFirst \
-o clientApp
A construção do aplicativo cliente foi concluída com êxito. A execução dos resultados do aplicativo cliente com a seguinte saída:
$ ./clientApp
Primeiro, quando o binário do cliente invoca a duplicata chamada shlib_function, o vinculador não tem dúvidas de que esse símbolo deve ser representado
pelo método da biblioteca shlibFirst , simplesmente porque ele reside na zona de código de maior prioridade. A primeira linha da saída do aplicativo cliente
testemunha esse fato.
No entanto, muito antes de a deliberação do vinculador acontecer, durante a construção da própria biblioteca dinâmica, as chamadas internas de
shlibsecond_function () para seu shlib_function () local já foram resolvidas, simplesmente porque os dois símbolos são locais entre si. Esta é a razão
pela qual a chamada interna de uma função shlibSecond para outra função shlibSecond não é afetada pelo processo de construção do binário do cliente.
Como esperado, quando a decisão do vinculador é determinada pelas diferenças nas prioridades da zona de código, a reversão da ordem de vinculação não tem
impacto no resultado final.
Caso 4: o símbolo não exportado da biblioteca dinâmica colide com outro símbolo não exportado da
biblioteca dinâmica
Este cenário pode ser basicamente descrito como dois símbolos, ambos pertencentes à zona de prioridade 3, colidem um com o outro (Figura 9-5).
- \
Biblioteca dinâmica
Os símbolos pertencentes à zona de código 3 são geralmente invisíveis para o processo de construção do binário do cliente. Esses símbolos são declarados de
escopo local (e completamente não interessantes para o vinculador) ou removidos (invisíveis para o vinculador).
Mesmo que os nomes dos símbolos possam ser duplicados, esses símbolos não acabam na lista de símbolos do vinculador e não causam conflitos. Sua importância
está estritamente confinada ao domínio das bibliotecas dinâmicas das quais fazem parte.
Para ilustrar este cenário específico, o seguinte projeto de demonstração é criado; ele consiste em uma biblioteca estática, uma biblioteca compartilhada e o
aplicativo cliente que vincula as duas bibliotecas. A biblioteca dinâmica está vinculada estaticamente.
Cada um dos binários apresenta funções locais cujos nomes são duplicatas dos nomes das funções locais encontradas nos módulos restantes. Além disso, o
aplicativo cliente tem a função local identicamente nomeada como a função da biblioteca compartilhada cujos símbolos são intencionalmente removidos.
arquivo: staticlibexports.h
arquivo: staticlib.c
função_local (x);
retorno (x + 1);
arquivo: build.sh
arquivo: shlib.c
arquivo: build.sh
gcc -shared shlib.o -Wl, -soname, libshlib.so.1 -o libshlib.so.1.0.0 strip -N local_function_strippedoff libshlib.so.1.0.0
ldconfig -n.
ln -s libshlib.so.1 libshlib.so
return 0;
return 0;
arquivo: build.sh
gcc -Wall -g -O0 -I ../ staticLib -I ../ sharedLib -c main.c gcc main.o -Wl, -L ../ staticLib -lstaticlib \ -Wl, -L ../ sharedLib
- lshlib \ -Wl, -R ../ sharedLib \
-o clientApp
Como esperado, o aplicativo cliente foi criado com sucesso e produziu a seguinte saída:
sharedLib: shlib_function
sharedLib: local_function
sharedLib: local_function_strippedoff
staticlib_function
staticLib: local_function
clientApp: local_function
clientApp: local_function_strippedoff
Obviamente, o vinculador não percebeu nenhum problema de símbolo duplicado. Todos os símbolos locais / removidos foram resolvidos em seus módulos
específicos e não entraram em conflito com nenhum dos símbolos locais / removidos com nomes idênticos nos outros módulos.
Imagine por um momento o seguinte cenário do mundo real: digamos que você precise projetar uma classe de utilitário de registro em todo o processo exclusivo.
Deve existir em uma instância e deve ser visível para todos os diferentes módulos funcionais.
O paradigma de implementação seria normalmente baseado no padrão de design singleton. Vamos supor por um momento que a origem de sua classe singleton
seja uma biblioteca estática dedicada.
Uma vez que o processo é iniciado e todas as bibliotecas dinâmicas são carregadas, você acaba tendo uma situação em que várias bibliotecas dinâmicas vivem no
mesmo processo, cada uma delas tendo uma classe única em seus próprios "quintais privados". E eis que, devido à natureza não competitiva da zona de símbolos
locais das bibliotecas dinâmicas, de repente você acaba tendo várias instâncias (bem coexistentes) de sua classe de utilitário de registro único.
O único problema é que você queria uma única instância de classe singleton única, não muitas delas !!!
Para ilustrar este cenário específico, o próximo projeto de demonstração é criado com os seguintes componentes:
• Duas bibliotecas compartilhadas, cada uma conectando-se à biblioteca estática. Cada uma das bibliotecas compartilhadas exporta apenas um símbolo: uma
função que chama internamente os métodos do objeto singleton. Os símbolos de classe singleton vindos da biblioteca estática vinculada não são exportados.
• Um aplicativo cliente que se vincula à biblioteca estática para acessar a própria classe singleton. Ele também possui links estaticamente compatíveis em ambas as
bibliotecas compartilhadas.
O aplicativo cliente e ambas as bibliotecas compartilhadas fazem suas próprias chamadas para a classe singleton. Como você verá em breve, o aplicativo
apresentará três instâncias diferentes da classe singleton:
arquivo: singleton.h
class Singleton {
público:
público:
~ Singleton () {};
privado:
Singleton () {};
Singleton (Const & Singleton); // propositalmente não implementado void operator = (Singleton const &); // propositalmente não
implementado
privado:
};
arquivo: singleton.cpp
if (NULL == m_pInstance)
cout << "endereço de instância singleton =" << this << endl; return 0;
# para SO de 64 bits também deve passar -mcmodel = sinalizador de compilador grande g ++ -Wall -g -O0 -c singleton.cpp ar -rcs
libsingleton.a singleton.o
arquivo: shlibfirstexports.h
#ifdef __cplusplus
extern "C" {
#endif // __cplusplus
#ifdef __cplusplus}
#endif // __cplusplus
arquivo: shlib.c
#ifdef __cplusplus
extern "C" {
#endif // __cplusplus
singleton.DoSomething ();
return 0;
#ifdef __cplusplus}
#endif // _cplusplus
arquivo: build.sh
rm -rf * .o lib *
g ++ -Wall -g -00 -fPIC -I ../ staticLib -c shlib.cpp g ++ -shared shlib.o -L ../ staticLib -lsingleton \ -Wl, - version-script =
versionScript \
ln -s libfirst.so.1 libfirst.so
arquivo: versionScript
global:
shlibfirst_function; local:
};
#ifdef __cplusplus
extern "C" {
#endif // __cplusplus
#ifdef __cplusplus}
#endif // __cplusplus
arquivo: shlib.c
#ifdef _cplusplus
extern "C" {
#endif // _cplusplus
singleton.DoSomething ();
return 0;
#ifdef __cplusplus}
#endif // __cplusplus
arquivo: build.sh
rm -rf * .o lib *
g ++ -Wall -g -00 -fPIC -I ../ shlibFirst -I ../ staticLib -c shlib.cpp g ++ -shared shlib.o -L ../ staticLib -lsingleton \
ln -s libsecond.so.1 libsecond.so
arquivo: versionScript
global:
shlibsecond_function; local:
};
ClientApplication:
arquivo: main.c
cout << "Acessando o singleton diretamente do aplicativo cliente" << endl; Singleton & singleton = Singleton :: GetInstance ();
singleton.DoSomething (); return 0;
g ++ -Wall -g -O0 -I ../ staticLib -I ../ shlibFirst -I ../ shlibSecond -c main.cpp g ++ main.o -L ../ staticLib -lsingleton \ -L
../ shlibFirst - lfirst \ -L ../ shlibSecond -lsecond \ -Wl, -R ../ shlibFirst \
-o clientApp
shlibfirst_function:
shlibsecond_function:
■ Observe que é deixado para o leitor diligente descobrir que o carregamento dinâmico do tempo de execução (dlopen) não mudaria nada a esse respeito.
Como uma nota final sobre este tópico, foi tentada uma versão de singleton thread-safe em que a instância singleton seria uma variável estática de função em vez
de uma variável estática de classe:
Essa abordagem resultou apenas com resultados um pouco melhores, em que ambas as bibliotecas compartilhadas imprimem o valor de endereço de instância
singleton idêntico, embora o aplicativo cliente imprima valores de endereço de instância singleton substancialmente diferentes.
Resolvendo o problema
Para não ser totalmente pessimista, existem várias maneiras de resolver esse tipo de problema.
Uma das possibilidades é baseada no relaxamento dos critérios de exportação de símbolo um pouco, permitindo que as bibliotecas dinâmicas exportem
adicionalmente os símbolos de classe singleton. Depois de exportados, os símbolos singleton não pertencerão mais à categoria de símbolos não prioritários / não
concorrentes cuja existência é permitida em zilhões de instâncias. Em vez disso, eles serão promovidos à categoria de "símbolos ABI concorrentes". De acordo com
as regras elaboradas, o vinculador escolheria apenas um dos símbolos e direcionaria todas as referências a esse símbolo de classe de singleton específico.
A solução definitiva para o problema seria hospedar a classe singleton em uma biblioteca dinâmica. Dessa forma, a grande maioria dos possíveis cenários
indesejados seria completamente eliminada. Nenhuma das regras de design da ABI seria violada, e o design de novos módulos não enfrentaria os absurdos
requisitos extras de design.
Independentemente do fato de que uma biblioteca compartilhada pode vincular outra biblioteca compartilhada, que pode vincular ainda outra biblioteca
compartilhada, que eventualmente pode vincular a biblioteca estática, protegendo a singularidade dos símbolos transportados por uma biblioteca situada em
algum lugar no meio da cadeia de vinculação requer que exatamente o código daquela biblioteca em particular seja encapsulado em seu próprio namespace
proprietário.
Esperar que o namespace da biblioteca superior protegerá a exclusividade dos símbolos da biblioteca entre possíveis conflitos com as outras bibliotecas dinâmicas
é simplesmente errado.
O único plano sólido, aquele que realmente funciona, é que cada biblioteca, estática ou dinâmica, deve apresentar seu próprio namespace dedicado.
CAPÍTULO 10
Dado o fato de que as bibliotecas dinâmicas fornecem funcionalidade que normalmente é usada por muito mais de um binário cliente, a precisão de rastrear as
versões da biblioteca e a disciplina em respeitar as informações de versão indicadas requerem um nível extra de rigidez. Deixar de perceber e reagir às
discrepâncias entre a funcionalidade fornecida pelas diferentes versões de uma biblioteca dinâmica pode significar não apenas o mau funcionamento de um único
aplicativo, mas às vezes o caos na funcionalidade mais ampla do sistema operacional (sistema de arquivos, rede, sistema de janelas , etc).
Como regra, as alterações no código da biblioteca dinâmica que quebram a funcionalidade suportada anteriormente devem resultar no aumento do número da
versão principal . Os sintomas de interrupção da funcionalidade anterior abrangem uma ampla gama de possibilidades, incluindo o seguinte:
• Uma mudança substancial na funcionalidade de tempo de execução fornecida, como a eliminação completa de um recurso suportado anteriormente, mudança
substancial de requisitos para um recurso a ser suportado, etc.
• Incapacidade do binário do cliente de se vincular à biblioteca dinâmica devido a uma ABI alterada, como funções removidas ou interfaces inteiras, assinaturas
de função exportadas alteradas, estrutura reordenada ou layout de classe, etc.
• Paradigmas completamente alterados na manutenção do processo em execução ou mudanças que exigem grandes mudanças na infraestrutura (como mudar
para um tipo de banco de dados completamente diferente, passar a contar com diferentes formas de criptografia, passar a exigir diferentes tipos de hardware, etc.).
Alterações no código da biblioteca dinâmica que introduzem nova funcionalidade sem quebrar a funcionalidade existente normalmente resultam no número de
versão secundária incrementado . As alterações de código que se qualificam para o incremento de números de versão secundária da biblioteca dinâmica normalmente
não impõem recompilar / revincular os binários do cliente, nem causam alterações substanciais no comportamento do tempo de execução. Os recursos adicionados
normalmente não representam curvas radicais, mas sim um leve aprimoramento da variedade existente de opções disponíveis.
As modificações das interfaces ABI não são excluídas automaticamente no caso de alterações de código de incremento de versão secundária. As modificações ABI
no caso de alterações de versão menores, no entanto, geralmente significam adições de novas funções, constantes e estruturas ou classes - em outras palavras,
alterações que não afetam a definição e o uso das interfaces existentes anteriormente. Mais importante ainda, os binários do cliente que dependiam da versão
anterior não requerem reconstrução para usar a nova versão secundária da biblioteca dinâmica.
Versão Patch
As alterações de código que são principalmente de escopo interno, que não causam nenhuma alteração na interface ABI nem trazem uma alteração substancial de
funcionalidade, normalmente se qualificam para o status de "patch".
Esquema de controle de versão baseado em Soname do Linux Nome do arquivo da biblioteca do Linux contém as
informações da versão
Conforme mencionado no capítulo 7, a última parte do nome do arquivo de uma biblioteca dinâmica Linux representa as informações de versão da biblioteca:
nome do arquivo da biblioteca = lib + <nome da biblioteca> + .so + <informações da versão da biblioteca>
As informações da versão da biblioteca normalmente usam o formato informações da versão da biblioteca dinâmica = <M>. <m>. <p>
onde o M representa um ou mais dígitos indicando a versão principal da biblioteca, om representa um ou mais dígitos indicando a versão secundária da biblioteca
e op representa um ou mais dígitos indicando o número do patch da biblioteca (isto é, alteração muito secundária).
file:///C:/Users/noels/Desktop/Advanced C and C++ Compiling ( PDFDrive ).html 129/235
21/09/2021 22:07 Advanced C and C++ Compiling ( PDFDrive ).html
Em cenários típicos do mundo real, as chegadas de novas versões secundárias de bibliotecas dinâmicas tendem a acontecer com bastante frequência. As
expectativas de uma atualização de versão secundária causando problemas são geralmente muito baixas, especialmente se o fornecedor seguir procedimentos de
teste sólidos antes de publicar o novo código.
Na maioria das vezes, a instalação da nova versão secundária da biblioteca dinâmica deve ser um procedimento bastante simples e tranquilo, como uma cópia
simples de um novo arquivo.
No entanto, independentemente de quão pequenas sejam as chances de que uma nova versão secundária quebre a funcionalidade existente, ainda existe a
possibilidade de que isso aconteça. Para poder voltar e restaurar elegantemente a versão anterior da biblioteca dinâmica que funcionou perfeitamente, a cópia
simples de arquivos precisa ser substituída por uma abordagem um pouco mais sutil.
Por definição, um softlink é o elemento do sistema de arquivos que carrega uma string contendo o caminho para outro arquivo. Na verdade, podemos dizer que o
softlink aponta para um arquivo existente. Na maioria dos aspectos, o sistema operacional trata o softlink como o arquivo para o qual ele aponta. O acesso a um
softlink e o redirecionamento ao arquivo que ele representa impõe penalidades de desempenho insignificantes.
Ele também pode ser redirecionado para apontar para outro arquivo. $ ln -s -f <outro arquivo> <softlink existente>
Finalmente, o softlink pode ser destruído quando não for mais necessário. $ rm -rf <caminho do softlink>
Conforme mencionado na discussão do Capítulo 7 sobre as convenções de nomenclatura da biblioteca do Linux, o nome do arquivo da biblioteca deve seguir o
seguinte esquema:
nome do arquivo da biblioteca = lib + <nome da biblioteca> + .so + <informações da versão completa da biblioteca>
biblioteca soname = lib + < nome da biblioteca > + .so + <(apenas os) dígitos da versão principal da biblioteca >
Obviamente, o soname é quase idêntico ao nome do arquivo da biblioteca, a única diferença é que ele não carrega as informações completas de controle de versão,
mas carrega apenas a versão principal da biblioteca dinâmica . Como você verá, esse fato desempenha um papel particularmente importante nos esquemas de
controle de versão de bibliotecas dinâmicas.
A flexibilidade do softlink se adapta muito bem a cenários de atualização de bibliotecas dinâmicas. As seguintes diretrizes descrevem o procedimento:
• Na mesma pasta onde reside o nome do arquivo da biblioteca dinâmica real, é mantido um softlink que aponta para o arquivo da biblioteca real.
• Seu nome corresponde exatamente ao soname da biblioteca para a qual aponta. Dessa forma, o softlink de fato carrega o nome da biblioteca no qual as
informações de versão são um pouco relaxadas (ou seja, carrega não mais do que as informações da versão principal).
• Como regra, os binários do cliente nunca são (ou seja, apenas excepcionalmente raramente) vinculados ao nome do arquivo da biblioteca dinâmica que contém
as informações de versão totalmente detalhadas. Em vez disso, como você verá em detalhes em breve, o procedimento de construção do binário do cliente é
propositalmente definido para resultar com o binário do cliente sendo vinculado ao soname da biblioteca.
• O raciocínio por trás desta decisão é bastante simples: especificar as informações completas e exatas de versão da biblioteca dinâmica imporia muitas restrições
desnecessárias, pois impediria diretamente a vinculação com qualquer versão mais recente da mesma biblioteca.
Ao construir o binário do cliente, você precisa determinar a localização do tempo de construção da biblioteca dinâmica, durante a qual se espera que você siga as
regras da convenção "-L -l". Mesmo que seja possível passar o nome do arquivo da biblioteca dinâmica exata (ou softlink / soname) para o vinculador, adicionando
o caractere de dois pontos entre o "-l" e o nome do arquivo (-l: <nome_do_arquivo>), como
é uma convenção informal, mas bem estabelecida, de passar apenas o nome da biblioteca privado de qualquer informação de controle de versão. Por exemplo,
indica que o binário do cliente requer vinculação com bibliotecas cujos nomes são libm, libdl, libpthread, libxml2 e libxyz, respectivamente.
Por esse motivo, além do softlink que carrega o soname da biblioteca, é típico fornecer o softlink que carrega apenas o nome da biblioteca mais a extensão de
arquivo .so , conforme ilustrado na Figura 10-2.
Figura 10-2. O uso de softlinks durante o tempo de construção vs. durante o tempo de execução
Existem várias maneiras de fornecer o softlink extra. A maneira mais estruturada de fazer isso é por meio da configuração de implantação do pacote (pkg-config).
Uma maneira um pouco menos estruturada é fazê-lo no destino de implantação do makefile que governa a construção da biblioteca dinâmica. Finalmente, sempre
é possível criar o softlink manualmente a partir da linha de comando ou configurando um script simples para fazê-lo.
O esquema descrito obviamente combina duas flexibilidades: a flexibilidade inerente de um softlink com a flexibilidade de versionamento do soname. Aqui está
como as duas flexibilidades atuam juntas no esquema geral das coisas.
O papel do Softlink
Como o sistema operacional trata o softlink como o arquivo para o qual ele aponta e fornece mecanismos de desreferenciação eficientes, o carregador não tem
nenhum problema particular ao conectar o binário do cliente por meio do softlink ao arquivo de biblioteca real disponível no tempo de execução.
Quando uma nova versão de uma biblioteca dinâmica chega, leva muito pouco esforço e tempo para copiar seu arquivo na mesma pasta onde a versão mais antiga
já reside e para modificar o link de software para apontar para o arquivo de versão mais recente.
• Não há necessidade de apagar ou sobrescrever a versão atual do arquivo de biblioteca dinâmica. Ambos os arquivos podem coexistir na mesma pasta.
• Configuração fácil, elegante e rápida do binário do cliente para usar a versão mais recente da biblioteca dinâmica.
• A capacidade de restaurar com elegância a conexão do binário do cliente com a versão mais antiga da biblioteca dinâmica nos casos em que a atualização resulta
com uma funcionalidade inesperada.
Conforme mencionado na seção anterior, nem todos os tipos de alterações no código da biblioteca dinâmica terão um impacto prejudicial na funcionalidade
binária do cliente. É razoável esperar que os incrementos da versão secundária não causem problemas maiores (como a incapacidade de vincular ou executar
dinamicamente, ou alterações graves de tempo de execução indesejadas e inesperadas). As atualizações requerem incrementos de versão principais; por outro
lado, são proposições extremamente arriscadas e devem ser tomadas com extrema cautela.
Não é necessário pensar muito para concluir que o soname é de fato projetado para agir como uma espécie de salvaguarda bastante elástica.
O carregador é projetado para ser inteligente o suficiente para reconhecer a tentativa de atualizar a biblioteca dinâmica para uma versão principal diferente da que
o soname sugere e evitar que isso aconteça.
• Ao omitir propositalmente os detalhes sobre a versão secundária e o número do patch, você permite implicitamente que as alterações na versão secundária
aconteçam sem muitos problemas.
Por melhor que tudo isso pareça, este esquema é bastante seguro apenas nos cenários em que você tem boas razões para esperar que as mudanças trazidas pela
nova versão da biblioteca não quebrem a funcionalidade geral, o que é o caso quando, no máximo, a versão secundária alterar. A Figura 10-3 ilustra a função de
proteção da versão do soname.
libxyz.so.
Versão principal
Versões menores
Figura 10-3. O Soname protege contra vinculação com versões principais incompatíveis da biblioteca compartilhada, mas não interfere nas atualizações de versões secundárias
Em situações em que a nova biblioteca dinâmica apresenta uma versão principal atualizada, esse esquema foi projetado para impedir a execução. Explicar como
exatamente as medidas de limitação funcionam neste caso exige que nos aprofundemos um pouco mais nos detalhes da implementação.
Por mais fundamentalmente sólido que pareça, o esquema baseado no uso de soname não seria tão poderoso, a menos que sua implementação apresentasse uma
faceta muito importante. Mais especificamente, o soname é incorporado aos binários. O formato ELF reserva os campos dedicados da seção dinâmica que são usados
(dependendo da finalidade) para transportar as informações do soname. Durante o estágio de vinculação, o vinculador pega a string de soname especificada e a
insere no campo de formato ELF de sua escolha.
A "vida secreta" do soname começa quando o vinculador o imprime na biblioteca dinâmica, com o objetivo de declarar a versão principal da biblioteca. No
entanto, não termina aí. Sempre que um binário do cliente se vincula à biblioteca dinâmica, o vinculador extrai o soname da biblioteca dinâmica e o insere no
arquivo do binário do cliente também, embora desta vez com um propósito um pouco diferente - indicar os requisitos de versão do binário do cliente.
Ao construir uma biblioteca dinâmica, você pode usar o sinalizador do vinculador dedicado para especificar o soname da biblioteca. $ gcc -shared <lista de
entradas do linker> -Wl, -soname, <soname> -o <nome do arquivo da biblioteca>
O vinculador incorpora a string de soname especificada no campo DT_SONAME do binário, conforme mostrado na Figura 10-4.
nilan @ nilaii $ total 12 drwxrwxr-x 2 drwxr-xr-x 7 -rw-rw-r-- l -rw-rw-r-- 1 nilan @ nilan $ nilan @ nilan $ nilan @ nilan $ total 24 drwxrwxr- x drwxr-xr-x -rwxrwxr-x -rw-rw-r --- rw-rw-r --- rw-rw-r--
ls -alg
nilan 4096 11 de dezembro 22:41. nilan 4096 10 de dezembro 00:10 .. nilan 43 11 de dezembro 22:40 test.c nilan 41 11 de dezembro 23:01 test.h gcc -fPIC -c test.c -o test.o
nilan 6864 11 de dezembro 22:42 libtest.so.1.0.0 nilan 43 11 de dezembro 22:40 test.c nilan 41 11 de dezembro 23:01 test.h
nilan 864 11 de dezembro 22:41 test.o nilan @ nilan $ readelf -d libtest. tão. 1.0.0
0X478
OxOOOOOOOd (FINI)
OOOO
Quando o binário do cliente é vinculado (diretamente ou por meio do softlink) à biblioteca dinâmica, o vinculador obtém o soname da biblioteca dinâmica e o
insere no campo DT_NEEDED do binário do cliente, conforme mostrado na Figura 10-5.
piilan @ milão: cllentBlnaryS gcc -I ../ -c fiain.c -o roaln.o rotlan @ mtlan: cllentBlnary $ gcc -shared -L., / -Itest naln.o -o cltentBtnary
Dessa forma, as informações de versão transportadas pelo soname são propagadas ainda mais, estabelecendo regras de versão firmes entre todas as partes
envolvidas (o vinculador, o arquivo de biblioteca dinâmica, o arquivo binário do cliente e o carregador).
Ao contrário dos nomes de arquivos da biblioteca, que podem ser facilmente modificados por todos (desde um irmão mais novo com muitos dedos por célula
cerebral e muito tempo até hackers maliciosos), alterar o valor do soname não é uma tarefa simples nem prática , pois requer não apenas modificações do arquivo
binário, mas também total familiaridade com o formato ELF.
Além de serem suportadas por todos os jogadores necessários no cenário de vinculação dinâmica (ou seja, o vinculador, os arquivos binários, o carregador), as
outras ferramentas tendem a oferecer suporte ao conceito de soname. O programa utilitário ldconfig é um exemplo notável a esse respeito. Além de seu escopo
original de responsabilidades, esta ferramenta tem um recurso extra de "canivete suíço".
Quando -n <diretório> argumentos de linha de comando são passados, o ldconfig abre todos os arquivos de biblioteca dinâmica (cujos nomes estão em
conformidade com a convenção de nomenclatura de biblioteca!), Extrai seu soname e para cada um deles cria um softlink cujo nome é igual a o soname extraído.
A opção -l <arquivo de biblioteca específico> é ainda mais flexível, pois neste caso o nome do arquivo da biblioteca dinâmica pode ser absolutamente
qualquer nome de arquivo válido. Não importa a aparência do nome do arquivo (seja o nome completo da biblioteca original com as informações de versão
completa ou um nome de arquivo severamente alterado), o soname embutido no arquivo especificado é extraído e o softlink correto é criado apontando
inequivocamente para o arquivo da biblioteca .
Para demonstrar isso, um pequeno experimento foi executado no qual o nome da biblioteca original foi alterado propositalmente. Ainda assim, o ldconfig
conseguiu criar o softlink correto, conforme mostrado na Figura 10-6.
-rwxrwxr-x 1 nilan 6662 11 de dezembro 22:42 libtest.so.1.0.a nilan (0piilan $ civ libtest.so. 1.0.0 propositadamenteChangedNane
Irwxrwxrwx 1 nilan 23 dez 11 23:02 libtest.so.1 -> propositadamenteChangedNane -rwxrwxr-x 1 milão 6864 dez 11 22:42 propositadamenteChangedNane
Além de controlar as informações de versão de toda a biblioteca dinâmica, o vinculador GNU oferece suporte a um nível extra de controle sobre a versão, no qual
as informações de versão podem ser atribuídas a símbolos individuais. Neste esquema, os arquivos de texto conhecidos como scripts de versão com uma sintaxe
bastante simples são passados para o vinculador durante o estágio de vinculação, que o vinculador insere nas seções ELF (.gnu.version e similares)
especializadas em transportar as informações de versão de símbolo .
Para comparação, quando o método de versão baseado em soname é usado, a fim de oferecer suporte a várias versões principais da mesma biblioteca, você precisa
exatamente que muitos binários diferentes (cada um carregando um valor diferente de soname) estejam fisicamente presentes na máquina de destino. A Figura 10-
7 ilustra a diferença entre os esquemas de controle de versão.
Figura 10-7. Comparação de esquemas de versão com base em soname e com base em símbolo
Como um bônus adicional, devido à riqueza de recursos suportados pela sintaxe do arquivo de script, também é possível controlar a visibilidade do símbolo (ou
seja, quais símbolos são exportados pela biblioteca e quais permanecem ocultos), da maneira cuja elegância e a simplicidade ultrapassa todos os métodos de
visibilidade de símbolo descritos até agora.
Para entender completamente o mecanismo de controle de versão de símbolo, é importante definir os cenários de caso de uso usuais em que ele é usado.
No início, digamos que uma primeira versão publicada da biblioteca dinâmica fique felizmente ligada ao binário cliente "A" e tudo corra bem. A Figura 10-8
descreve essa fase inicial do ciclo de desenvolvimento.
Cliente binário
s
Biblioteca dinâmica versão 1.0.0
Figura 10-8. Cronologicamente, as primeiras linhas binárias "A" do cliente na versão 1.0.0 da biblioteca. Este é, no entanto, apenas o começo da história.
A cada dia que passa, o progresso do desenvolvimento dinâmico da biblioteca inevitavelmente traz mudanças. Ainda mais importante, não apenas a biblioteca
dinâmica é alterada, mas também uma nova série de binários de cliente ("B," "C" etc.) emergem, que não existiam no momento em que a ligação da biblioteca
Cliente binário
Figura 10-9. Linlcs binários "B" do cliente um pouco mais recentes na versão de biblioteca mais recente (1.1.0)
Cliente binário
■ '///////,
UMA /■
■ Si
Biblioteca dinâmica versão 1.0.0
Versão da
biblioteca dinâmica Novos recursos
Algumas das alterações da biblioteca dinâmica podem não ter implicações na funcionalidade dos binários de cliente já existentes. Tais mudanças são corretamente
consideradas as mudanças da versão secundária .
Ocasionalmente, as alterações do código da biblioteca dinâmica acontecem para trazer diferenças que são muito radicais e significam uma ruptura completa com o
que as versões anteriores da biblioteca forneciam. Os novos binários do cliente ("C") criados no momento dessas novas mudanças normalmente não têm
problemas em se relacionar com o novo paradigma.
Os binários do cliente mais velhos ("A" e "B"), no entanto, podem acabar na situação ilustrada pela Figura 10-10, que é semelhante a um casal de idosos em uma
recepção de casamento rock'n'roll esperando eternamente pela banda para tocar sua música favorita de Glenn Miller.
Cliente binário
Figura 10-10. Os melhores e mais recentes linlcs binários "C" do cliente na versão mais recente da biblioteca dinâmica (2.0.0), que é incompatível para uso pelos binários clientes
mais antigos "A" e "B"
Cliente binário
O esquema de controle de versão de símbolo é implementado combinando o script de versão do vinculador com a diretiva assembler .symver , ambas as quais
serão elaboradas em detalhes a seguir.
A implementação mais básica do mecanismo de controle de visibilidade do símbolo é baseada na leitura do vinculador GNU nas informações da versão do
símbolo fornecidas na forma do arquivo de texto do script de versão .
Vamos começar uma demonstração simples com o exemplo de uma biblioteca dinâmica simples (libsimple.so) que apresenta as três funções mostradas na
Listagem 10-1.
retorno (x + 1).
retorno (x + 2).
retorno (x + 3).
Digamos agora que você deseja que as duas primeiras funções de biblioteca (mas não a terceira!) Carreguem as informações de controle de versão. A maneira de
especificar a versão do símbolo é criar um arquivo de script de versão bastante simples, que pode ser parecido com o código da Listagem 10-2.
LIBSIMPLE_1.0 {global:
Finalmente, vamos construir a biblioteca dinâmica. O nome do arquivo do script de versão pode ser convenientemente passado para o vinculador usando o
sinalizador de vinculador dedicado, assim:
O vinculador extrai as informações do arquivo de script e as incorpora à seção de formato ELF dedicada ao controle de versão. Mais informações sobre como as
informações de versão de símbolo são incorporadas aos arquivos binários ELF serão disponibilizadas em breve.
Ao contrário do arquivo de script de versão que representa o "pão com manteiga" do conceito de controle de versão de símbolo, usado em todas as fases e todos os
cenários, o paradigma de controle de versão de símbolo depende de outro ingrediente - a diretiva assembler .symver - para resolver os casos difíceis.
Vamos supor um cenário de mudanças de versão principais em que uma assinatura de função não mudou entre as versões, mas a funcionalidade subjacente
mudou um pouco. Além disso, há uma função que originalmente costumava retornar uma série de elementos vinculados, mas na versão mais recente foi
reprojetada para retornar o número total de bytes ocupados pela lista vinculada (ou vice-versa). Veja a Listagem 10-3.
Listagem 10-3. Exemplo de implementações substancialmente diferentes da mesma função que se qualificam para diferentes versões principais
// VERSÃO 1.0:
// VERSÃO 2.0:
// aqui nós escaneamos a lista, mas agora retornamos o número total de bytes return nElements * sizeof (struct List);
Obviamente, os clientes da primeira versão da biblioteca terão problemas, pois o valor retornado pela função não corresponderá mais ao esperado.
Conforme declarado anteriormente, o credo dessa técnica de controle de versão é fornecer as diferentes versões do mesmo símbolo no mesmo arquivo binário.
Muito bem dito, mas como fazer? Uma tentativa de construir as duas versões de função resultará no linker relatando os símbolos duplicados. Felizmente, o
compilador GCC suporta a diretiva montadora .symver customizada , que ajuda a aliviar o problema (consulte a Listagem 10-4).
Listagem 10-4. O mesmo par de versões diferentes da função apresentada na Listagem 10-3, desta vez com controle de versão de símbolo aplicado corretamente
// EU
// aqui nós escaneamos a lista, mas agora retornamos o número total de bytes return nElements * sizeof (struct List);
Para eliminar o vinculador que enfrenta o problema de símbolos duplicados, você pode criar nomes diferentes para versões diferentes da mesma função, que serão
usados apenas para fins internos (ou seja, não serão exportados). Essas duas funções são list_occupancy_1_0 e list_occupancy_2_0.
file:///C:/Users/noels/Desktop/Advanced C and C++ Compiling ( PDFDrive ).html 137/235
21/09/2021 22:07 Advanced C and C++ Compiling ( PDFDrive ).html
Da perspectiva do mundo externo, no entanto, o vinculador criará o símbolo apresentando o nome da função esperada (ou seja, list_occupancy ()), embora
decorado com as informações de versão do símbolo apropriadas, aparecendo nas duas versões diferentes: list_occupancy@MYLIBVERSION_1.0 e
list_occupancy @ MYLIBVERSION_2.0.
Como consequência, tanto o binário do cliente mais antigo quanto o mais novo serão capazes de identificar o símbolo que esperam. O binário do cliente mais
antigo ficará feliz em ver que o símbolo list_occupancy@MYLIBVERSION_1.0 existe. Suas chamadas para este símbolo de função intermediária serão roteadas
internamente para o lugar certo - para a função de biblioteca dinâmica list_occupancy_1_0 () , que é o símbolo real .
Finalmente, os novos binários do cliente, que não se preocupam particularmente com o histórico de versões anteriores, escolherão o símbolo padrão, indicado pelo
caractere @ extra no nome (neste caso, list_occupancy @@ MYLIBVERSION_2.0).
primeira_função int (int x); segunda_função int (int x); terceira_função int (int x);
retorno (x + 1);
retorno (x + 2);
retorno (x + 3);
LIBSIMPLE_1.0 {global:
Agora que a biblioteca foi construída, vamos examinar mais de perto como o formato ELF oferece suporte ao conceito de controle de versão de símbolo.
A análise da seção do arquivo de biblioteca indica que existem três seções com nomes bastante semelhantes que são usadas para transportar as informações da
versão, conforme mostrado na Figura 10-12.
L SJ .gnu.version VERSYM
[4] .dynstr
Invocar o utilitário readelf com o argumento de linha de comando -V fornece um relatório sobre o conteúdo dessas seções de uma maneira particularmente
interessante, conforme mostrado na Figura 10-13.
1 dos
A seção símbolos de versão .gnu.version 'contém 8 entradas: Addr: O00OOO000Q00QZ7C Offset: 0x00027c Link: 3 (.dynsym)
A seção de definição de versão '.gnu, verslon_d' contém 2 entradas: Addr: 0x000000000000028c Offset: 0x00028c Link: 4 (.dynstr) 000900: Rev: 1 Sinalizadores: Índice BASE: 1 Cnt: 1 Nome: libsimple.so.1.0.0
0x081c : Rev: 1 Sinalizadores: nenhum Índice: 2 Cnt: 1 Nane: LIBSIMPLE_1.0
A seção de necessidades de versão '.gnu.version_r' contém 1 entradas: Addr: 0X0O0OO00O00O002C4 Offset: 0xOO02c4 Link: 4 (.dynstr)
Figura 10-13. Usando readelf para listar o conteúdo das seções relacionadas à versão
• A seção .gnu.version_d descreve as informações de versão definidas nesta biblioteca particular (daí o apêndice "_d" no nome da seção).
• A seção .gnu.version_r descreve as informações de versão das outras bibliotecas, que são referenciadas por esta biblioteca (daí o apêndice "_r" no nome da
seção).
• A seção .gnu_version fornece uma lista resumida de todas as informações de versão relacionadas à biblioteca.
É interessante, neste ponto, verificar se as informações de versão foram associadas aos símbolos especificados no script de versão.
De todas as maneiras disponíveis (nm, objdump, readelf) para examinar os símbolos do arquivo binário, é novamente o utilitário readelf que fornece a
resposta na forma mais agradável em que a associação de símbolos com as informações de versão especificada torna-se aparente, conforme ilustrado em Figura 10-
14.
nilan @ nilan $
Como uma observação interessante, a desmontagem do arquivo binário, no entanto, mostra que não existe first_function @@ LIBVERSIONDEMO_1.0. Tudo
que você pode encontrar é o símbolo da primeira função real . A desmontagem em tempo de execução (executando gdb) mostra a mesma coisa.
Obviamente, o símbolo exportado decorado com a informação de versão do símbolo é um tipo de ficção (útil, mas ainda uma ficção), enquanto a única coisa que
conta no final é o símbolo da função real existente.
Outra rodada de descobertas interessantes acontece quando você examina os binários do cliente vinculados à sua biblioteca dinâmica com versão de símbolo. Para
explorar o controle de versão do símbolo nessa direção específica, vamos criar um aplicativo de demonstração simples que faz referência aos símbolos com versão;
consulte a Listagem 10-9.
int nPrimeiro = primeira_função (l); int nSegundo = segunda_função (2); int nRetValue = nPrimeiro + nSegundo; printf ("primeiro
(l) + segundo (2) =% d \ n", nRetValue); return nRetValue;
$ gcc -g -00 -c -I ../ sharedLib main.c $ gcc main.o -Wl, -L ../ sharedLib -lsimple \
Observe que, para exercer apenas o mecanismo de controle de versão de símbolos, a especificação da biblioteca soname foi propositalmente omitida.
Não é uma grande surpresa que o aplicativo demo, sendo um arquivo binário ELF, também carregue as seções relacionadas à versão (conforme mostrado pela
seção de inspeção ilustrada na Figura 10-15).
Cabeçalhos de seção
É muito mais importante que as informações da versão do símbolo da biblioteca dinâmica de demonstração tenham sido ingeridas pelo binário do cliente por
meio do processo de vinculação, conforme mostrado na Figura 10-16.
a seção de símbolos de versão '.gnu.version' contém 8 entradas: Addr: O000O00008O482f4 Offset: 0xOOO2f4 Link: 5 (.dynsyn) 000: 0 (»local4 ) 2 (GLIBC_2.0) 3 (LIBSIMPLE_1.0) 3 (LIBSIMPLE_1.0)
1
A versão precisa da seção .gnu.version_r 'contém 2 entradas: Addr: 0x0000000008048394 Offset: OxO0O3O4 Link: 6 (.dynstr)
Figura 10-16. O binário do cliente "ingere" as informações de versão de símbolo da biblioteca vinculada
Exatamente como acontece no cenário de versão com base em soname descrito anteriormente, o mecanismo de versão de símbolo também é passado da biblioteca
dinâmica para seu binário cliente. Desta forma, foi estabelecida uma forma de contrato entre o binário cliente e o controle de versão da biblioteca dinâmica.
Por que isso é importante? A partir do momento em que a vinculação do binário do cliente com a biblioteca dinâmica aconteceu, o código da biblioteca dinâmica
pode passar por uma infinidade de mudanças e, conseqüentemente, por uma infinidade de versões secundárias e principais.
Antes de avançar, vamos nos certificar de que seu esquema de controle de versão não impeça a execução do aplicativo. O experimento simples é mostrado na
Figura 10-17.
Depois de entender os fundamentos de como o esquema de controle de versão de símbolo opera, é hora de simular o cenário no qual o desenvolvimento da
biblioteca dinâmica resulta com as mudanças não disruptivas (ou seja, a versão secundária). Na tentativa de simular os cenários da vida real, serão realizadas as
seguintes etapas:
• Você modificará a biblioteca dinâmica adicionando mais algumas funções. Apenas uma das funções adicionadas recentemente será exportada. O script de
controle de versão será enriquecido pelo item extra anunciando a atualização da versão secundária LIBSIMPLE_1.1.
• O novo binário do cliente (outro aplicativo de demonstração simples) será criado e vinculado à biblioteca dinâmica atualizada. Dessa forma, ele servirá como um
exemplo de um novo binário cliente, criado na época da última e maior biblioteca dinâmica versão 1.1, desconhecendo qualquer uma das versões anteriores da
biblioteca.
• Para simplificar a demonstração, seu código não será significativamente diferente do aplicativo de demonstração simples original. A diferença mais notável é
que ele chamará a nova função ABI, que não existia antes da última versão 1.1.
As Listagens 10-10 e 10-11 mostram como o arquivo de origem da biblioteca dinâmica modificada se parece agora.
primeira_função int (int x); segunda_função int (int x); terceira_função int (int x);
retorno (x + 1);
retorno (x + 2);
retorno (x + 3);
retorno (x + 4);
LIBSIMPLE_1.1 {
global:
quarta função ;
local:
retornar nRetValue.
$ gcc -g -00 -c -I ../ sharedLib main.c $ gcc main.o -Wl, -L ../ sharedLib -lsimple \ -Wl, -R ../ sharedLib -o newerApp
Vamos agora dar uma olhada mais de perto nos efeitos dessa pequena aventura de versionamento, que imita perfeitamente o cenário da vida real que acontece
quando versões menores de bibliotecas dinâmicas são atualizadas.
Primeiro, conforme mostrado na Figura 10-18, as informações de versão agora apresentam não apenas a versão original (1.0), mas também a versão mais recente
(1.1)
A seção de símbolos de versão ', gnu.version' contém entradas IS: Addr: O0O0000000O0O2c2 Offset: OxO002c2 Link: 3 (.dynsyn) 000: 0 (noeal *) 4 (GLIBC_2.1.3) 5 (GLIBC_2.0) 0 (* local * )
A seção de definição de versão '.gnu.version_d' contém 3 entradas: Addr: 0xOO0O00OO0OO002d8 Offset: OxO0O2d8 Link: 4 (.dynstr) 090000: Rev: 1 Sinalizadores: Índice BASE: 1 Cnt: 1 Nane:
libsinple.so.1.0.0 0x001c : Rev: 1 Sinalizadores: nenhum Índice: 2 Cnt: 1 Nane: LIBSIMPLE_1.0 8x6838: Rev: 1 Sinalizadores: nenhum Índice: 3 Cnt: 1 Nome: LIBSIMPLE 1.1
A versão precisa da seção '.gnu.version_r' contém 1 entradas: Addr: 0x000000000000032c Offset: 0x00032c Link: 4 (.dynstr) 00O0OO: Versão: 1 Arquivo: libc.so.6 Cnt: 2 0x0010: Nome: GLI8t_2.0
Sinalizadores: nenhum Versão: S 0x0020: Nane: GLIBC 2.1.3 Sinalizadores: nenhum Versão: 4 nilan @ nilan $
O conjunto de símbolos exportados agora é composto pelos símbolos da versão 1.0 e da versão 1.1, conforme mostrado na Figura 10-19.
Valor Tamanho 00000000 O 00600000 00000000 00000000 00000000 00000550 00000000 0000O4f8 000OO4CC O0ROOO0O
contém 10 ent
file:///C:/Users/noels/Desktop/Advanced C and C++ Compiling ( PDFDrive ).html 142/235
21/09/2021 22:07 Advanced C and C++ Compiling ( PDFDrive ).html
Tipo Bind
NOTYPE LOCAL
FUNC FRACO
FUNC GLOBAL
NOTYPE WEAK
NOTYPE WEAK
FUNC GLOBAL
0B3ECT GLOBAL
FUNC GLOBAL
FUNC GLOBAL
OBJETO GLOBAL
ries: Vis
DEFAULT DEFAULT DEFAULT DEFAULT DEFAULT DEFAULT DEFAULT DEFAULT DEFAULT DEFAULT
Freira
0 12
8 9
UND _gnon_start__
UND _3v_RegisterClasses
12 quarta função (a @ LIBSIMPLE 1.1 ABS LIBSIMPLEl.0 12 segundos_função @@ LIBSIMPLE_l.0 12 primeira_functton @@ LIBSIMPLE_l, 0 ABS LIBSIMPLE 1.1
nilan @ nilan $
Vamos ver agora como as coisas ficam com o binário de cliente mais novo e moderno (newerApp) criado pela primeira vez após o lançamento da versão 1.1.
Conforme ilustrado na Figura 10-20, o vinculador leu as informações sobre todas as versões suportadas pela biblioteca dinâmica e as inseriu no binário cliente do
aplicativo mais recente.
A seção de synbols de versão '.gnu.version' contém 9 entradas: Addr: 0000000008048322 Offset: 0x000322 Link: S (.dynsyn) 000: 0 (* local *) 2 (GLIBC2.0) 3 (LIBSIMPLEl.0} 4 (LIBSIMPLE 1.1 )
008: 1 (* global *)
A seção de necessidades de versão '.gnu.verslonr' contém 2 entradas: Addr; 0x0000000003048334 Deslocamento: 0x000334 Link: 6 (.dynstr) OOOOOO: Versão: 1 Arquivo: libs triplo .so Cnt: 2 0x9010: Nome:
libsimple ll Sinalizadores: nenhuma versão: 4 0x0020: Nome: " libsimple _1.0 Sinalizadores: nenhum Versão : 3 0x0030: Versão: 1 Arquivo: ltbc.so.6 Cnt: 1 0x0040: Nome: glibc _2.0 Sinalizadores:
nenhum Versão: 2 ntlan @ mtlan $
Figura 10-20. O binário do cliente mais recente ingeriu informações completas de controle de versão (versões de símbolo antigas e mais recentes)
00000000
mi.lan@ntlan$
Agora, para verificar se a adição da nova funcionalidade e as informações de versão modificadas funcionam conforme o esperado, você pode tentar executar o
aplicativo antigo e o novo. Conforme mostrado na Figura 10-22, a execução do aplicativo antigo provará que a nova versão secundária da biblioteca dinâmica não
trouxe nenhuma surpresa desagradável.
ntlan @ nulan $ ./newerApp lib: ftrst_function lib: second_function lib: quarta_função flrst (l) + second (2) + quarta (4) = 14 ntl3n @ nllan $, / flrstDemoApp lib: flrst_function lib: second_function ftrst (l) + segundo
(2) = 6 ntlan (0ntlan $
Figura 10-22. Os aplicativos mais antigos e mais recentes vinculam a mesma biblioteca, mas usam os símbolos de versões diferentes
Não tentarei cobrir situações muito mais dramáticas, nas quais as mudanças de código prejudicam seriamente a maneira como os clientes usavam o código, caindo
claramente na categoria de incrementos de versão principais.
Potencialmente, as alterações de código mais desagradáveis acontecem quando aparentemente nada acontece com os símbolos da biblioteca dinâmica (ou seja, as
funções não têm seus protótipos alterados e / ou as estruturas não têm seu layout alterado), mas o significado subjacente do que as funções fazem com os dados - e
o mais importante, os valores que eles retornam - muda.
Imagine por um momento que você tem uma função que costumava retornar o valor do tempo em milissegundos. Um belo dia, os desenvolvedores perceberam
que o milissegundo como medida não era preciso o suficiente e decidiram retornar o valor em nanossegundos (que é 1.000 vezes maior).
Esse cenário é o que usaremos como tópico do próximo exemplo; Mostrarei como problemas dessa natureza podem ser resolvidos pelo uso inteligente do
mecanismo de controle de versão de símbolo. (Concordo que o exemplo é um pouco infantil / fantástico / ingênuo. Na verdade, há cerca de um milhão de
maneiras de evitar o caos resultante dessa mudança. Por exemplo, você poderia introduzir uma nova função ABI com a palavra "nanossegundos" no nome, que
retornaria o tempo em nanossegundos. Mesmo assim, um exemplo como este é bom o suficiente para fins de demonstração.)
De volta ao tópico, vamos supor que o cabeçalho de exportação da biblioteca dinâmica de demonstração não mudou nada, portanto, os protótipos de função
permanecem inalterados. No entanto, os requisitos de design mais recentes ditam que first_function () a partir de agora precisa retornar um valor diferente
do que costumava retornar.
Desnecessário dizer que esse tipo de mudança está fadado a causar estragos nos binários do cliente existentes. Sua infraestrutura de código existente simplesmente
não espera um valor dessa ordem de magnitude. É possível que sair dos limites das matrizes cause uma exceção. Em cenários de plotagem de gráfico, o valor
estaria fora dos limites, etc.
Portanto, agora você precisa de uma maneira de garantir que os clientes antigos recebam o tratamento normal (ou seja, as chamadas dos binários do cliente
existente para first_function () retornam o valor que costumava ser), enquanto os novos clientes obtêm o benefício de um novo design.
O único problema é que você precisa resolver o conflito; o mesmo nome de função deve ser usado em dois cenários substancialmente diferentes. Felizmente, o
mecanismo de controle de versão de símbolo prova que é capaz de lidar com problemas desse tipo.
Como uma primeira etapa, você modificará o script da versão para indicar o suporte para a nova versão principal; consulte a Listagem 10-14.
LIBSIMPLE_1.0 {global:
};
LIBSIMPLE_1.1 {global:
quarta_função;
local: *.
LIBSIMPLE_2.0 {
global:
first_function;
local: *.
Em seguida, você aplicará a receita com base no uso da diretiva assembler .symver, conforme mostrado na Listagem 10-15. Listagem 10-15. simple.c (apenas as
alterações mostradas aqui)
1
A seção de símbolos de versão '.gnu.version contém 12 entradas: Addr: aooooBOOOBoaozfe offset: ox6002fe Link: 3 (.dynsyn) 006: 6 (* local *) 5 (GLIBC_2.0) 6 (GLIBC_2.1.3) 0 (* local *)
A seção de definição de versão ', gnu, version_d' contém 4 entradas: Addr: 0X0O00O00000O0O318 Offset: 0x000318 Link: 4 (.dynstr) O0O000: Rev: 1 Sinalizadores: Índice BASE: 1 Cnt: 1 Nome:
llbslmple.so.1.0.0 0x001c : Rev: 1 Sinalizadores: nenhum Índice: 2 Cnt: 1 Nane: LIBSIMPLE_1.0 0x6038: Rev: 1 Sinalizadores: nenhum Índice: 3 Cnt: 1 Nane: LIB5IMPLE_1.1 6x6054: Rev: 1 Sinalizadores:
nenhum Índice: 4 Cnt: 1 Nane: LIB5IMPLE_2.6
A versão precisa da seção '.gnu.version ^ r' contém 1 entradas: Addr: 0x0000000000000388 Offset: 0x000388 Link: 4 (.dynstr) 000000: Versão: 1 Arquivo: llbc.so.6 Cnt: 2 0x0010: Nane: GLIBC_2.1.3
Sinalizadores: nenhum Versão: 6 0x6020: Nane: GIIBC_2.0 Sinalizadores: nenhum Versão: 5 i <itlan @ mllanS
Figura 10-23. A última e melhor versão da biblioteca contém todas as versões de símbolos
Curiosamente, conforme mostrado na Figura 10-24, parece que a diretiva .symver realmente fez sua mágica.
milan @ milan $ readelf --dyn-syms llbslnple.so A tabela Synbol '.dynsyn' contém 12 entradas:
1,1
00000630 t flfth_function
00000552 T flrst_functiong@LIBSIMPLE_2.0
0000051c T first_functlon0LIBSIMPLE_l.0
0000051c t flrst_function_l_0
00000552 t flrst_functton_2_0
OOOOOSfa T quarta_função
0000058e T second_function
nllan @ rnilanS
O efeito final de todo o esquema .symver é a magia de exportar duas versões do símbolo first_function () , apesar do fato de que uma função com tal nome
não existe mais porque foi substituída por first_function_1_0 () e first_function_2_0 ().
Para mostrar claramente as diferenças de implementação, você criará o novo aplicativo cuja fonte não difere da versão anterior (consulte a Listagem 10-16).
retornar nRetValue.
$ gcc -g -00 -c -I ../ sharedLib main.c $ gcc main.o -Wl, -L ../ sharedLib -lsimple \
A comparação do tempo de execução mostrará claramente que os clientes antigos não terão sua funcionalidade afetada pelas principais alterações de versão. O
app contemporâneo, porém, contará com a nova funcionalidade trazida pela versão 2.0. A Figura 10-25 resume o ponto.
Figura 10-25. Três aplicativos (cada um dos quais depende de diferentes versões de símbolos da mesma biblioteca dinâmica) são executados conforme pretendido
O caso descrito anteriormente é um pouco bizarro. Devido às inúmeras maneiras de evitar isso, as chances de isso acontecer na vida real são bastante baixas. Do
ponto de vista da educação, entretanto, é precioso, pois o procedimento para corrigir esse problema é o mais simples possível.
Um caso muito mais comum que se enquadra nas principais alterações do código de versão é quando a assinatura de uma função precisa ser alterada. Por
exemplo, vamos supor que para os novos cenários de caso de uso, a first_function () precisa aceitar um argumento de entrada adicional.
Obviamente, agora você precisa oferecer suporte às funções com o mesmo nome, mas com assinaturas diferentes. Para demonstrar esse problema, vamos criar
outra versão, mostrada na Listagem 10-17.
LIBSIMPLE_1.0 {global:
LIBSIMPLE_1.1 {
global:
quarta_função;
local: *.
LIBSIMPLE_2.0 {
global:
first_function;
local: *.
LIBSIMPLE_3.0 {
global:
first_function;
Em geral, a solução para este problema não difere substancialmente do caso anterior, pois a receita baseada
na diretiva assembler .symver será usada da mesma maneira que no exemplo anterior (consulte a Listagem 10-18).
A diferença mais substancial, entretanto, é que o cabeçalho de exportação deve ser modificado, conforme mostrado na Listagem 10-19.
// definido ao construir o binário do cliente mais recente #ifdef SIMPLELIB_VERSION_3_0 int first_function (int x, int
normfactor); #outro
Apenas o binário cliente construído com a constante de pré-processador SIMPLELIB_VERSI0N_3_0 passada para o compilador incluirá o novo protótipo
first_function () .
$ gcc -g -00 -c -DSIMPLELIB_VERSI0N_3_0 -I ../ sharedLib main.c $ gcc main.o -Wl, -L ../ sharedLib -lsimple \ -Wl, -R ../
sharedLib -o ver3PeerApp
Será um pequeno exercício agradável para o leitor verificar se em todos os outros aspectos (informações de versão, presença de símbolos, resultados de tempo de
execução) o exemplo atende às suas expectativas.
Os scripts de versão mostrados nos exemplos de código até agora apresentam apenas um subconjunto da ampla gama de recursos de sintaxe suportados. O
objetivo desta seção é fornecer uma breve visão geral das opções com suporte.
Nó de versão
A entidade básica do script de versão é o nó de versão, a construção nomeada encapsulada entre as chaves que descrevem certa versão, como
LIBXYZ_1.0.6 {
};
O nome do nó é normalmente escolhido para descrever com precisão a versão completa descrita pelo nó. Normalmente, o nome termina com dígitos separados
por pontos ou sublinhados. É uma prática de bom senso que os nós que representam as versões posteriores venham após os nós que representam as versões
anteriores.
Porém, esta é apenas uma prática que facilita a vida dos humanos. O vinculador não se preocupa particularmente como você nomeia seus nós de versão, nem se
importa com a ordem em que eles aparecem no arquivo. Tudo isso realmente requer que os nomes sejam diferentes.
Uma situação semelhante ocorre com as bibliotecas dinâmicas e seus binários de cliente. O que realmente importa para eles é a cronologia em que os nós de
versão foram adicionados aos arquivos de versão - qual versão específica estava presente no momento em que foram construídos.
Os modificadores globais e locais de um nó de versão controlam diretamente a exportação do símbolo. A lista de símbolos separados por ponto e vírgula
declarada no rótulo global será exportada, ao contrário dos símbolos declarados no rótulo local.
LIBXYZ_1.0.6 {global:
};
Embora não seja o tópico principal do esquema de controle de versão, esse mecanismo de exportação dos símbolos é, na verdade, uma forma completamente
legítima (e em muitos aspectos a mais elegante) de especificar a lista de símbolos exportados. Um exemplo de como esse mecanismo funciona será fornecido nas
seções subsequentes.
Suporte a curinga
O script de versão suporta o mesmo conjunto de curingas que os shells suportam para operações de correspondência de expressão. Por exemplo, o seguinte script
de versão declara como globais todas as funções cujo nome começa com "primeiro" ou "segundo:"
LIBXYZ_1.0.6 {global:
};
Além disso, o asterisco sob o rótulo local especifica todas as outras funções como sendo do escopo local (aquelas que não devem ser exportadas). Os nomes de
arquivo especificados entre aspas duplas devem ser interpretados literalmente, independentemente de quaisquer caracteres curinga que possam conter.
O script de versão pode ser usado para especificar o especificador de ligação externo "C" (sem alteração do nome) ou externo "C ++" .
LIBXYZ_1.0.6 {global:
extern "C" {
first_function;
local: *.
};
Suporte a namespace
Os scripts de versão também suportam o uso de namespace na especificação da afiliação dos símbolos versionados e / ou exportados.
LIBXYZ_1.0.6 {global:
namespace libxyz :: *
local: *.
};
Nó Sem Nome
Um nó sem nome pode ser usado para especificar os símbolos não versionados. Além disso, sua finalidade pode ser hospedar os especificadores de exportação de
símbolos (globais e / ou locais).
Na verdade, quando o controle sobre a exportação do símbolo é seu único motivo para usar o mecanismo de script de controle de versão, é muito comum ter um
script de versão contendo apenas um nó sem nome.
Um recurso secundário do mecanismo de script de versão é que ele também fornece controle sobre a visibilidade do símbolo. Os símbolos listados no nó do script
de versão na guia global acabam sendo exportados, enquanto os símbolos listados na guia local não são exportados.
É perfeitamente legal usar o mecanismo de script de versão apenas com o propósito de especificar os símbolos a serem exportados. No entanto, é altamente
recomendável, nesse caso, usar os nós de versão de script sem nome, conforme demonstrado na demonstração simples ilustrada pela Figura 10-26.
Figura 10-26. O script de versão pode ser usado como a forma mais elegante de controlar a visibilidade do símbolo, pois não requer nenhuma modificação do código-fonte
As alterações de código que afetam principalmente os detalhes da funcionalidade interna são referidas no Linux como patches e no Windows como versões de
compilação. Além das diferenças óbvias de nomenclatura, não há diferenças substanciais entre os dois conceitos.
Como acontece com as bibliotecas dinâmicas do Linux, as informações de versão das bibliotecas dinâmicas do Windows (DLL) são opcionais. A menos que um
esforço de design consciente seja feito para especificar essas informações, elas não aparecerão como parte da DLL. Como uma boa regra de design, no entanto,
todos os principais fornecedores de DLL (começando com a Microsoft, é claro) garantem que as bibliotecas dinâmicas que fornecem carreguem as informações de
versão. Quando disponíveis, as informações da versão DLL são fornecidas como uma guia dedicada nas páginas de propriedades do arquivo, que podem ser
recuperadas clicando com o botão direito do mouse no ícone do arquivo no painel Explorador de Arquivos, conforme mostrado na Figura 10-27.
Nome
® msvcrll0_cli0400.dll
Modelo
Tamanho
836 KB
msvcrt.dll
msvfw32.dll msvidc32.dll MSVidCtl.dll mswmdm.dll mswsock.dll & msxml3.dll ^ m sxm I3r.dll 1% msxmlfi.dll 1%, rnsxml6r.dll msyuv.dll MTril + ÇS MTnl + 64 Ç! MTrigger2 Mtriggeri ■ J mtstocom mbrelu.dll
mtxdm.dll mtxex.dll% mtxoci.dli 1%, mgifontsetup.dll MUtLanguageCleanup.
13 MuiUnattend
G3
file:///C:/Users/noels/Desktop/Advanced C and C++ Compiling ( PDFDrive ).html 150/235
21/09/2021 22:07 Advanced C and C++ Compiling ( PDFDrive ).html
s: «
grepWin ...
7-Zip KDiff3
Abrir com..
TextPad
WinMerge
Enviar para
Cortar copiar
Criar atalho
Excluir
Renomear
Propriedades
dtl
13/07 / 20C 11/20 / 2C 13/07 / 2W 13/07 / 2W 11/20 / 2a 13/07 / 20C 11/20 / 2a 11/20 / 2C 6 / 1T / 2K 11/20/21
620 KB
T ^ T
OK
ippty
HI KB 38 KB 3.565 KB
Propriedades msvcrtdll
Dotais
Geral (Segurança
Versões prévias
Propriedade Valor
Descrição
Caned
Para ilustrar os aspectos mais importantes do controle de versão de DLL do Windows, uma solução de demonstração do Visual Studio é criada com dois projetos:
• Projeto VersionedDLL, que cria a DLL cujas informações de versão são fornecidas
• Projeto VersionedDLLClientApp, que cria o aplicativo cliente que carrega a DLL com versão e tenta recuperar suas informações de versão
A maneira usual de fornecer as informações de versão ao projeto DLL é adicionar o elemento de recurso de versão dedicado ao arquivo de recurso de biblioteca,
conforme mostrado na Figura 10-28.
à LJi Sou
«B m
fj. *
Versão CjRea 33
a IQ A
^ Solução 'DLLVersioningDemc' (2 projetos) a . "J] VersionedDLL
-Eu- eu
Adicionar
Ctfl + Shift +
Ctrl + X
Ctrf + C
Ctrl + V
Cortar
cópia de
Colar De
Re i Prd
Adicionar Recurso
Tipo de recurso:
r:
l_J Resr ----- '-
(Âmbito global)
El // VersionedDLL.cpp: define o
U
I
VersionedDLL.cpp X
1 Recurso...
Novo
Diálogo ffl-SI
Ill HTML [Ícone Ml § Menu m Ribbon ii® String Table Toolba r
Importar.,,
Personalizado..
Cancelar
Ajuda
@r
Versão
Depois que o recurso de versão é adicionado ao arquivo de recurso de projeto DLL, ele pode ser exibido e modificado por meio do editor de recursos do Visual
Studio.
Como indica a Figura 10-29, as informações de versão fornecem dois componentes distintos de versão, FILEVERSION e PRODUCTVERSION. Apesar do fato de
que na grande maioria dos cenários da vida real esses dois componentes têm valores idênticos, existem certas diferenças na forma como os valores desses
componentes são definidos. Se uma DLL for usada em mais de um projeto, o número da versão do arquivo provavelmente será notavelmente maior que o número
da versão do produto.
VersionedDLLcpp
Chave _ lvalue _
V If X
Explorador de Soluções
> i ^ l Dependências externas s L? Arquivos de cabeçalho resource.h ^ stdafx.h • i "] targetver.h, _h] VersionedDLLh j Arquivos de recursos
0x3fL OxOL
OriginalFilename VersionedDLL.dll
ProductName VersionedDLL.dll
FILEVERSION
VERSÃO DO PRODUTO
Figura 10-29. Usando o editor do Visual Studio para definir a versão do arquivo e as informações da versão do produto
Normalmente, quando a DLL acaba de ser criada, a versão (versão principal, versão secundária, número de compilação) é normalmente definida para valores
razoavelmente pequenos e arredondados, como 1.0.0. Neste exemplo, no entanto, eu escolhi propositalmente por causa de uma demonstração convincente não
apenas definir as informações de versão para valores numéricos razoavelmente grandes, mas também para que os valores FILEVERSION difiram dos valores
PRODUCTVERSION.
Quando a biblioteca é construída, as informações de versão especificadas pela edição do arquivo de recurso de versão podem ser visualizadas clicando com o
botão direito do mouse no ícone do arquivo no painel File Explorer e escolhendo o item de menu Propriedades (Figura 10-30).
VersionedDLL.rc - 3 VersionedDLL.cpp
Chave Valor
FILEVERSION 16.21.7123,1
FILEFLAGSMASK Qx3fL
FILEFLAGS (MIL
FILEOS VOS_NT_WINDOWS32
TIPO DE ARQUIVO VFT_UN KNOWN
FILESUBTYPE VFT2_UNKNOWN
Bloco de Cabeçalho Inglês (Estados Unidos) (040904b0)
CompanyNarme C o rp de Pesquisa de Versão do Wi n dowsD LL
Descrição do arquivo DLL que demonstra Yersioning
FileVersion 16.24.7123.1
Nome Interno DLL com versão simples
legalCopyright Copyright (C) 2013
Nome do Arquivo Original VeriionedDLl.dll
Nome do Produto VersionedOLL.dll
Versão do produto 4.1.71
Saída
H,
neletiniJ filp nphijp \ Vprsiori ^ rini I. ur ^ ur rpsc-ful hui
Área de Trabalho
OK
Aplicar
ISi VersionedDLLdll
Propriedade Valor
Descrição
Cancelar
Figura 10-30. Defina os valores que aparecem nas propriedades do arquivo binário DLL construído
As informações sobre a versão do DLL podem ser de particular importância para várias partes interessadas e em diversos cenários. Os binários do cliente cuja
funcionalidade depende criticamente da versão da DLL podem querer examinar programaticamente os detalhes da versão da DLL a fim de realizar um curso
apropriado de ações adicionais. Os pacotes de instalação / implementação podem primeiro recuperar as informações de versão das DLLs existentes para decidir se
devem ou não substituir / sobrescrever as DLLs existentes por versões mais novas do mesmo arquivo. Finalmente, as pessoas que executam as funções de
manutenção de administração do sistema ou solução de problemas podem desejar dar uma olhada mais de perto na versão DLL.
Nesta seção, enfocarei principalmente as maneiras programáticas pelas quais as informações da versão da DLL podem ser recuperadas.
Estrutura VERSIONINFO
A estrutura DLLVERSIONINFO, declarada no arquivo de cabeçalho <shlwapi.h> , é normalmente usada para passar as informações de versão. A Figura 10-31
mostra seus detalhes de layout.
Requisitos de ligação
As maneiras pelas quais as informações da versão da DLL podem ser recuperadas serão discutidas a seguir.
DLLs bem projetadas normalmente exportam a implementação da função DllGetVersion () , cuja assinatura segue a seguinte especificação:
Não é complicado para as DLLs de design personalizado implementá-lo também. Aqui está o esboço da receita: o protótipo da função deve ser devidamente
declarado e exportado, ilustrado pela Listagem 10-20, bem como pela Figura 10-33.
// O seguinte bloco ifdef é a maneira padrão de criar macros que tornam a exportação // de uma DLL mais simples. Todos os arquivos
dentro dessa DLL são compilados com o símbolo VERSIONEDDLL_EXPORTS // definido na linha de comando. Este símbolo não deve ser
definido em nenhum projeto // que use esta DLL. Desta forma, qualquer outro projeto cujos arquivos de origem incluam este arquivo
vê as funções // VERSIONEDDLL_API como sendo importadas de uma DLL, enquanto esta DLL vê os símbolos // definidos com esta macro
como sendo exportados.
#ifdef VERSIONEDDLL_EXPORTS
#outro
#include <Shlwapi.h>
? Os membros da estrutura DLLVERSIONINFO podem ser definidos para o conjunto predeterminado de valores.
É preferível ter os valores da versão na forma de constantes parametrizadas (em vez de constantes literais).
• A estrutura DLLVERSIONINFO pode ser preenchida carregando os recursos DLL, extraindo as cadeias de informações de versão e analisando os detalhes sobre a
versão principal, secundária e de compilação.
A Listagem 10-21 ilustra a combinação de ambos os métodos. Se a recuperação do recurso de versão falhou, os valores predeterminados podem ser retornados.
(Para simplificar, as constantes literais são usadas nesta lista. Todos nós sabemos que isso pode ser realizado de uma forma mais estruturada).
return E_INVALIDARG;
// não deve acontecer que acabemos aqui, // mas apenas no caso - tente salvar o dia // aderindo aos números de versão reais //
TBD: use valor parametrizado em vez de literais pdvi-> dwMajorVersion = 4; pdvi-> dwMinorVersion = 1; pdvi-> dwBuildNumber = 7;
return S_OK;
Os detalhes da função que extrai as informações da versão dos recursos DLL seguem imediatamente na Listagem 10-22.
wsprintf (fileVersion, L "\\ StringFileInfo \\% 02X% 02X% 02X% 02X \\ ProductVersion", (dwLanguageID & 0xff00) >> 8, dwLanguageID
& 0xff, (dwLanguageID & 0xff000000) >> 24, (dwLanguageID & 0xff0000) ) >> 16);
outro
fileVersion,
retorna falso;
pwstrSubstring = wcstok_s (lpwstrVersion, L ".", & pContext); pDLLVersionInfo-> dwMajorVersion = _wtoi (pwstrSubstring);
pwstrSubstring = wcstok_s (NULL, L ".", & pContext); pDLLVersionInfo-> dwMinorVersion = _wtoi (pwstrSubstring);
pwstrSubstring = wcstok_s (NULL, L ".", & pContext); pDLLVersionInfo-> dwBuildNumber = _wtoi (pwstrSubstring);
pwstrSubstring = wcstok_s (NULL, L ".", & pContext); pDLLVersionInfo-> dwPlatformID = _wtoi (pwstrSubstring);
return TRUE;
A parte importante da receita é que o bom momento para capturar o valor do identificador do módulo da DLL é quando a função DllMain () é chamada,
conforme mostrado na Listagem 10-23.
case DLL_PROCESS_ATTACH:
g_hModule = hModule;
return TRUE;
Finalmente, a Listagem 10-24 mostra como o binário do cliente recupera as informações de versão. Listagem 10-24. main.cpp (aplicativo cliente)
DLLGETVERSIONPROC pDllGetVersion;
return TRUE;
Se acontecer de a DLL não exportar a função DllGetVersion () , você pode recorrer à medida mais brutal de extrair as informações de versão incorporadas nos
recursos do arquivo. O esforço completo de implementação dessa abordagem reside no lado binário do cliente. Como pode ser facilmente concluído comparando
o código a seguir com o código apresentado na descrição da abordagem anterior, a mesma metodologia é aplicada, com base no carregamento dos recursos do
arquivo, extraindo a sequência de versão e extraindo os números de versão dela (consulte a Listagem 10 -25).
DLLVERSIONINFO * pDLLVersionInfo)
arquivo WCHAR estáticoVersion [256]; LPWSTR lpwstrVersion = NULL; UINT nVersionLen = 0; DWORD dwLanguageID = 0; BOOL retVal;
wsprintf (fileVersion, L "\\ StringFileInfo \\% 02X% 02X% 02X% 02X \\ FileVersion", (dwLanguageID & 0xff00) >> 8, dwLanguageID &
0xff, (dwLanguageID & 0xff000000) >> 24, (dwLanguageID & 0xff0000) ) >> 16);
outro
fileVersion,
retorna falso;
pwstrSubstring = wcstok_s (lpwstrVersion, L ".", & pContext); pDLLVersionInfo-> dwMajorVersion = _wtoi (pwstrSubstring);
pwstrSubstring = wcstok_s (NULL, L ".", & pContext); pDLLVersionInfo-> dwMinorVersion = _wtoi (pwstrSubstring);
pwstrSubstring = wcstok_s (NULL, L ".", & pContext); pDLLVersionInfo-> dwBuildNumber = _wtoi (pwstrSubstring);
pwstrSubstring = wcstok_s (NULL, L ".", & pContext); pDLLVersionInfo-> dwPlatformID = _wtoi (pwstrSubstring);
DWORD dwVersionHandle = 0;
dwVersionHandle,
dwVersionInfoSize,
lpstrFileVersionInfo);
if (bRetValue) {
//
printf ("Versão do arquivo DLL (principal, secundária, build, platformID) =% d.% d.% d.% d \ n", dvi.dwMajorVersion,
dvi.dwMinorVersion, dvi.dwBuildNumber, dvi.dwPlatformID);
outro
printf ("Falha na extração da versão do arquivo DLL \ n"); FreeLibrary (hDll); return 0;
Finalmente, o resultado da execução do aplicativo demo que demonstra ambas as abordagens (consulta de DLL e "força bruta") é mostrado na Figura 10-34.
Figura 10-34. Extraindo programaticamente a versão do produto DLL, bem como a versão do arquivo
CAPÍTULO 11
Conceito de Plug-in
Provavelmente, o conceito mais importante possibilitado pelo avanço da vinculação dinâmica é o conceito de plug-ins. Não há nada substancialmente difícil de
entender no conceito em si, já que o encontramos em uma infinidade de cenários do dia-a-dia, a maioria dos quais não requer nenhum conhecimento técnico. Um
bom exemplo do conceito de plug-in é a broca e a variedade de brocas que podem ser trocadas de acordo com as necessidades da situação particular e a decisão do
usuário final (Figura 11-1).
O conceito de software de plug-ins segue o mesmo princípio. Basicamente, há um aplicativo principal (ou ambiente de execução) que executa uma determinada
ação em um determinado assunto de processamento (por exemplo, o aplicativo de processamento de fotos que modifica as propriedades de uma imagem) e há um
conjunto de módulos especializados em realizar uma ação específica sobre o assunto de processamento (por exemplo, filtro de desfoque, filtro de nitidez, filtro
sépia, filtro de contraste de cor, filtro passa-alto, filtro de média, etc.), um conceito muito fácil de compreender.
Nem todos os sistemas que compõem o aplicativo principal e os módulos associados merecem ser chamados de "arquitetura de plug-in". Para que a arquitetura
suporte o modelo de plug-in, os seguintes requisitos também precisam ser atendidos:
• Os módulos devem exportar sua funcionalidade por meio de algum tipo de mecanismo carregável em tempo de execução.
Na realidade, os requisitos acima são normalmente suportados por meio das seguintes decisões de design:
• Os plug-ins são implementados como bibliotecas dinâmicas. Independentemente da funcionalidade interna, todas as bibliotecas dinâmicas de plug-in exportam
a interface padronizada (um conjunto de funções que permite ao aplicativo controlar a execução do plug-in).
• O aplicativo carrega os plug-ins por meio do processo de carregamento dinâmico da biblioteca. As duas opções a seguir são normalmente suportadas:
• O aplicativo examina a pasta predefinida e tenta carregar todas as bibliotecas dinâmicas que encontrar lá em tempo de execução. Ao carregar, ele tenta encontrar
os símbolos correspondentes
para a interface que se espera que os plug-ins exportem. Se os símbolos não forem encontrados (ou apenas alguns deles), a biblioteca de plug-ins é descarregada.
• O usuário, por meio de uma opção GUI dedicada em tempo de execução, especifica o local do plug-in e diz ao aplicativo para carregar o plug-in e começar a
fornecer sua funcionalidade.
Regras de Exportação
Não existem regras rigorosamente estritas para cada arquitetura de plug-in. No entanto, existe um conjunto de diretrizes de bom senso. De acordo com o
parágrafo que explica o impacto da linguagem C ++ nos problemas do linker, a maioria das arquiteturas de plug-in tende a seguir o esquema mais simples
possível, no qual um plug-in exporta um ponteiro para a interface composta de funções C-linkage.
Mesmo que a funcionalidade interna dos plug-ins possa ser implementada como uma classe C ++, essa classe geralmente implementa a interface exportada por seu
contêiner de biblioteca dinâmica e passando um ponteiro para a instância da classe (lançado como o ponteiro para a interface) para o a aplicação é prática usual.
Há uma grande variedade de programas populares que oferecem suporte à arquitetura de plug-in, como (mas não se limitando a):
• Processamento de som (arquitetura de plug-in Steinberg VST, com suporte universal em todos os principais editores de áudio)
• Editores de texto (um grande número dos quais possui plug-ins fornecendo certas funcionalidades)
• Ambientes de desenvolvimento integrado de desenvolvimento de software (IDEs) que suportam uma variedade de recursos por meio dos plug-ins
• Etc.
Para cada uma dessas arquiteturas de plug-in, normalmente há um documento de interface de plug-in publicado estipulando em detalhes a interação entre o
aplicativo e o plug-in.
Dicas e truques
A última etapa em sua jornada para compreender totalmente o conceito de bibliotecas dinâmicas requer que você dê um pequeno passo para trás e organize tudo
o que aprendeu até agora em outro conjunto de fatos simples. Formular as coisas de uma maneira diferente às vezes pode significar muita diferença no domínio
das práticas de design do dia a dia.
Depois que todos os detalhes sobre as bibliotecas dinâmicas foram examinados, a verdade mais potente sobre elas é que vincular a bibliotecas dinâmicas é uma
espécie de vinculação na promessa. De fato, durante o estágio de construção, tudo o que o executável do cliente se preocupa são os símbolos das bibliotecas
dinâmicas. É apenas no estágio de carregamento em tempo de execução que o conteúdo das seções da biblioteca dinâmica (código, dados, etc.) começa a ser
reproduzido. Existem várias implicações na vida real decorrentes do conjunto de circunstâncias descrito.
Este simples fato tem um impacto tremendo nas rotinas diárias de programação, pois tende a reduzir muito o tempo de compilação desnecessário. Em vez de ter
que recompilar todo o corpo do código sempre que uma mudança minúscula de código acontece, usando as bibliotecas dinâmicas, os programadores podem
reduzir a necessidade de reconstruir o código para a própria biblioteca dinâmica. Não é de se admirar que os programadores frequentemente decidam hospedar o
código em desenvolvimento na biblioteca dinâmica, pelo menos até que o desenvolvimento seja concluído.
No momento da construção, o binário do cliente não precisa da biblioteca dinâmica totalmente desenvolvida, com todos os recursos adequados. Em vez disso,
tudo o que o binário do cliente realmente precisa em tempo de construção é o conjunto de símbolos da biblioteca dinâmica - nada mais e nada menos do que isso.
Isso é realmente interessante. Por favor, respire fundo e vamos dar uma olhada no que essa afirmação realmente significa. O arquivo binário da biblioteca
dinâmica que você usa no tempo de construção e o arquivo da biblioteca dinâmica que é carregado no tempo de execução podem ser substancialmente diferentes
em todos os aspectos, exceto em um: os símbolos devem corresponder.
Em outras palavras (e sim, isso é verdade e exatamente como deveria ser), para fins de construção com reconhecimento estático, você pode usar uma biblioteca
dinâmica cujo código (flash e sangue) ainda está para ser implementado, mas seus símbolos ( o esqueleto) já estão em sua forma final.
Ou você pode usar uma biblioteca cujo código você sabe que será alterado, contanto que tenha certeza de que o conjunto de símbolos exportados não será
alterado.
Ou você pode usar a biblioteca dinâmica adequada para um tipo específico (como pacote de idioma) no momento da construção, mas vincular no tempo de
execução a outra biblioteca dinâmica - desde que os binários da biblioteca dinâmica exportem o mesmo conjunto de símbolos.
Isso é muito, muito interessante. Um exemplo extremo de como podemos nos beneficiar dessa importante descoberta ocorre no domínio da programação nativa
do Android. Durante o esforço para desenvolver um módulo (biblioteca dinâmica ou aplicativo nativo), não é completamente incomum para equipes inteiras de
desenvolvedores desnecessária e imprudentemente tomar o caminho demorado de adicionar seu código-fonte na gigantesca árvore de origem do Android cuja
construção pode levar algumas horas.
Alternativamente, um procedimento muito mais eficaz é desenvolver um módulo como um projeto Android autônomo, não relacionado à árvore de origem do
Android. Em questão de minutos, as bibliotecas dinâmicas nativas do Android necessárias para completar a fase de compilação podem ser copiadas ("adb
puxado" no jargão do Android) de qualquer dispositivo / telefone Android funcional e adicionadas à estrutura de compilação do projeto. Em vez de demorar
várias horas, o procedimento de construção agora leva vários minutos, no máximo.
Mesmo que o código (a seção .text ) da biblioteca dinâmica extraída do telefone Android disponível mais próximo possa diferir significativamente do código
encontrado na árvore de origem do Android, a lista de símbolos é muito provavelmente idêntica em ambas as bibliotecas dinâmicas. Obviamente, a biblioteca de
substituição rápida retirada do dispositivo Android pode satisfazer os requisitos de construção, enquanto em tempo de execução "a certa" do binário da biblioteca
dinâmica será carregada.
Dicas Diversas
Dadas tantas semelhanças e tão poucas diferenças, é possível converter a biblioteca dinâmica em executável? A resposta a esta pergunta é positiva. É certamente
possível no Linux (ainda estou tentando confirmar a afirmação no Windows também). Na verdade, a biblioteca que implementa a biblioteca de tempo de execução
C (libc.so) é realmente executável. Quando invocado digitando seu nome de arquivo na janela do shell, você obtém a resposta mostrada na Figura 11-2.
GNU C Library (Ubuntu EGLIBC 2.15-0ubuntul0) versão estável versão 2.15, de Roland McGrath et al.
PROPÓSITO PARTICULAR.
Extensões disponíveis:
Crypt add-on versão 2.1 por Michael Glad e outros GNU Libidn por Simon Josefsson
Biblioteca de Threads POSIX nativa de Ulrlch Drepper et al BIND-8.2.3-T5B llbc ABIs: UNIQUE IFUNC
A questão que surge naturalmente a seguir é como implementar a biblioteca para torná-la executável? A seguinte receita torna isso possível:
• Implemente a função principal dentro da biblioteca dinâmica - a função cujo protótipo é int main (int argc, char * argv [];
• Declare a função main () padrão como o ponto de entrada da biblioteca. Passar o sinalizador de vinculador -e é como você realiza essa tarefa.
• Transforme a função main () em uma função sem retorno. Isso pode ser feito inserindo a chamada _exit (0) como a última linha da função main () .
• Especifique o intérprete para ser o vinculador dinâmico. A seguinte linha de código faria isso: #ifdef _LP64_
"/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2";
#outro
"/lib/ld-linux.so.2";
#fim se
Um projeto de demonstração simples é feito para ilustrar a ideia. A fim de provar a natureza verdadeiramente dual da biblioteca dinâmica (ou seja, embora agora
possa ser executada como executável, ainda permanece capaz de funcionar como uma biblioteca dinâmica regular), o projeto de demonstração contém não apenas
a biblioteca dinâmica de demonstração, mas também o executável que o carrega dinamicamente e chama sua função printMessage () . A Listagem 11-1 ilustra
os detalhes do projeto de biblioteca compartilhada executável:
Listagem 11-1.
arquivo: executableSharedLib.c
#include "sharedLibExports.h"
"/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2"; #outro
"/lib/ld-linux.so.2"; #fim se
// deve fazer com que a função de ponto de entrada seja uma função 'sem retorno' tipo _exit (0);
arquivo: build.sh
g ++ -Wall -00 -fPIC -I./exports/ -c src / executableSharedLib.c -o src / executableSharedLib.o g ++ -shared -Wl, -e, main
./src/executableSharedLib.o -pthread -lm -ldl -o ../deploy/libexecutablesharedlib.so
A Listagem 11-2 ilustra os detalhes do aplicativo demo cujo objetivo é provar que, ao se tornar executável, nossa biblioteca compartilhada não perdeu sua
funcionalidade original:
Listagem 11-2.
arquivo: main.c
arquivo: build.sh
g ++ ./src/main.o -lpthread -lm -ldl -L ../ deploy -lexecutablesharedlib -Wl, -Bdynamic -Wl, -R ../deploy -o demoApp
árvore milarnaroilan $
- demoApp
- implantar
1
- llbexecutablesharedltb.so
- Makefile
- Notas
1
- README.txt
- sharedLlb
- exportações
1
- sharedLtbExports.h
- llbexecutablesharedltb.so
- Makefile - src
1
I— executableSharedLlb.c - executableSharedLtb.o
- testApp
E denoApp
Makefile
src
7 diretórios, 13 arquivos
milarignUanS, / deploy / Ubexecutablesharedlib.so Biblioteca compartilhada matn (} função nilan @ nilan $ ./demoApp
No entanto, há uma reviravolta especial em toda a história que precisa ser examinada com cuidado.
Normalmente, independentemente de quantas bibliotecas dinâmicas são carregadas no processo, todas elas se vinculam à mesma instância da biblioteca de tempo
de execução C, que fornece a infraestrutura de alocação de memória - malloc e free (ou no caso de C ++, new e delete), bem como a implementação da lista
mantendo o controle dos buffers de memória alocados. Se essa infraestrutura for única por processo, não há realmente nenhuma razão para que o esquema
descrito, no qual qualquer pessoa possa desalocar a memória alocada por outra pessoa, não funcione.
O caso interessante, entretanto, pode acontecer no domínio da programação do Windows. O Visual Studio fornece (pelo menos) duas DLLs básicas sobre as quais
todos os executáveis (aplicativos / bibliotecas dinâmicas) são construídos - a biblioteca de tempo de execução C usual (msvcrt.dll) , bem como a biblioteca
Microsoft Foundation Classes (MFC) (mfx42.dll ) Às vezes, os requisitos de projetos podem ditar a combinação e correspondência das DLLs construídas em
DLLs de base diferentes, o que pode causar imediatamente desvios muito desagradáveis das regras esperadas.
Digamos que, para fins de clareza, no mesmo projeto você tenha as duas DLLs a seguir carregadas no tempo de execução: DLL "A", criada em msvcrt.dll, e
DLL "B", criada em DLL MFC. Vamos agora supor que a DLL "A" aloque buffers de memória e os transmita para a DLL "B", que os usa e depois os desaloca. Nesse
caso, a tentativa de desalocar a memória resultará em um travamento (a exceção se parece com a da Figura 11-4).
int lockrium)
J *
eu
ou em
Isso pode ser devido a uma interrupção do heap, o que indica um bug no exe qualquer uma das DLLs carregadas.
Isso também pode ser devido ao usuário pressionar F12 enquanto | tem foco.
#ifdef #pragm
#endif j ***
*_trancar *
* Purpo
Prosseguir
Pausa
Ignorar
Figura 11-4. Caixa de diálogo de mensagem de erro típica para problemas de memória em conflito entre DLLs
A causa do problema é que existem duas autoridades de contabilidade em torno do pool de memória heap disponível; tanto a DLL de tempo de execução C
quanto a DLL MFC mantêm suas próprias listas separadas de buffers alocados (consulte a Figura 11-5).
Figura 11-5. O mecanismo de problemas de tempo de execução causados por registros de alocação de memória não relacionados mantidos por diferentes DLLs
Normalmente, ao enviar o buffer para desalocação, a infraestrutura de alocação de memória pesquisa a lista de endereços de memória alocados e, se o buffer
passado para desalocação for encontrado na lista, a desalocação pode ser concluída com êxito. Se, no entanto, o buffer alocado for mantido em uma lista (digamos,
mantido pela DLL de tempo de execução C) e passado para desalocação para a outra lista (digamos, mantido pela DLL MFC), o endereço de memória do buffer
não será encontrado no lista, e a chamada de desalocação lançará uma exceção. Mesmo que você lide com a exceção silenciosamente, é questionável se o aplicativo
será capaz de enviar o buffer para a DLL correta para desalocação, causando vazamentos de memória.
Para piorar as coisas, virtualmente nenhuma das ferramentas usuais de verificação de limite de memória foi capaz de detectar e relatar algo errado. Na defesa das
ferramentas, você pode notar que de fato nenhuma das violações de memória típicas acontecem neste caso particular (como escrever além dos limites do buffer,
sobrescrever o endereço do buffer, etc). Tudo isso torna o problema desagradável de lidar e, a menos que você tenha uma ideia inicial sobre os problemas em
potencial, pode ser muito difícil identificar a causa, quanto mais a solução para o problema.
A solução para o problema é excepcionalmente simples: os buffers de memória alocados em uma DLL devem, em última instância, ser passados de volta para a
mesma DLL para serem desalocados. O único problema é que para aplicar esta solução simples é necessário ter acesso ao código-fonte de ambas as DLLs, o que
nem sempre é possível.
A ideia de um símbolo de linker fraco é, em sua essência, semelhante ao recurso predominante das linguagens orientadas a objetos (que é uma das manifestações
do princípio do polimorfismo). Quando aplicada ao domínio da vinculação, a ideia de símbolos fracos significa praticamente o seguinte:
• Compiladores (mais notavelmente, gcc) suportam a construção da linguagem, permitindo que você declare um símbolo (uma função e / ou uma variável
global ou estática de função) como fraco.
O exemplo a seguir demonstra como declarar uma função C como um símbolo fraco: int _ attribute __ ((fraco)) someFunction (int, float);
• O vinculador obtém essas informações para lidar com esse símbolo de uma maneira única.
• Se outro símbolo com o mesmo nome aparecer durante a vinculação e não for declarado fraco, esse outro símbolo substituirá o símbolo fraco.
• Se outro símbolo com nome idêntico aparecer durante a vinculação e for declarado fraco, o vinculador é livre para decidir qual dos dois será realmente
implementado.
• A presença de dois símbolos não fracos (ou seja, fortes) com o mesmo nome é considerada um erro (o símbolo já está definido).
• Se durante a vinculação nenhum outro símbolo com nome idêntico aparecer, o vinculador pode não implementar tal símbolo. Se o símbolo for um ponteiro de
função, a proteção do código é obrigatória (na verdade, é altamente recomendável fazê-lo sempre).
Uma excelente ilustração do conceito de símbolos fracos é encontrada na postagem do blog de Winfred CH Lu em http://winfred-
lu.blogspot.com/2009/11/understand-weak-symbols-by-examples.html . O cenário da vida real de quando esses recursos podem ser úteis é descrito na
postagem do blog de Andrew Murray em www.embedded-bits.co.uk/2008/gcc-weak-symbols/ .
CAPÍTULO 12
Linux Toolbox
O objetivo deste capítulo é apresentar ao leitor um conjunto de ferramentas (programas utilitários, bem como outros métodos) para analisar o conteúdo dos
arquivos binários do Linux.
$ file /usr/bin/gst-inspect-0.10
SV), dinamicamente vinculado (usa bibliotecas compartilhadas), para GNLi / Linux 2.6.24, BuildID [shai] = ex4ib8f8a4
14SO3SbO9099222Oee852afe2f9dO0c2, despojado $
$ file /usr/llb/i386-linux-gnu/xen/libpthread.a
$ file /lib/i386-linux-gnu/ltbc-2.15.so
/lib/i386-linux-gnu/libc-2.15.so: ELF objeto compartilhado LSB de 32 bits, Intel 80386, versão 1 (SY SV), vinculado dinamicamente (usa libs compartilhadas), BuildID [shal] =
0xe4aOe031bf28aaf48f716bee471e36
O utilitário de linha de comando denominado size ( http://linux.die.net/man/lZsize ) pode ser usado para obter instantaneamente uma visão dos
comprimentos de byte das seções ELF (Figura 12-2).
5 tamanho /usr/bin/gst-i.nspect-0.10
dados de texto bss dec hex nome de arquivo 29056 836 20 29912 74d8 /usr/bln/gst-tnspect-0.10
$$
$ size /lib/i386-linux-gnu/libc-2.15.so
$ size /usr/lib/i386-linux-gnu/xen/libdl.a
ldd
Ao analisar as dependências de tempo de carregamento, o ldd primeiro examina o arquivo binário tentando localizar o campo de formato ELF no qual a lista das
dependências mais imediatas foi impressa pelo vinculador (conforme sugerido pela linha de comando do vinculador durante o processo de construção).
Para cada uma das bibliotecas dinâmicas cujos nomes foram encontrados embutidos no arquivo binário do cliente, o ldd tenta localizar seus arquivos binários
reais de acordo com as regras de pesquisa de localização da biblioteca em tempo de execução (conforme descrito em detalhes no Capítulo 7). Uma vez que os
binários das dependências mais imediatas tenham sido localizados, o ldd executa o próximo nível de seu procedimento recursivo, tentando encontrar suas
dependências. Em cada uma das dependências de "segunda geração", o ldd executa outra rodada de investigação e assim por diante.
Depois que a pesquisa recursiva descrita é concluída, o ldd reúne a lista de dependências relatadas, elimina as duplicatas e imprime o resultado (conforme
mostrado na Figura 12-3).
"llbgstreamer-0.10 .so .0 => /usr/ltb/1386-llnux-gnu/"libgstreaner-0.10.so.0 (0xb7633O00) li.bgobject-2.0, então, 0 => / usr / lib / i386- Unux-gnu / li.bgobject-2.0.so.O (0xb75e4000) libglib-2.0.so.0 == ■ /li.b/l386-
"Llnux-gnu/libgl\b-2.0.so.0 (0xb74ea000) "libpthread.so.0 => / lib / 1386-Unux-gnu / llbpthread. so.0 (0xb74cf000) VLbc.so.6 => /Ub/t386-lT.nux-gnu/llbc.so,6 (0xb732500e)
/lib/ld-linux.so.2 (0xb7730O98)
• O ldd não pode identificar as bibliotecas carregadas dinamicamente em tempo de execução chamando a função dlopen () . Para obter este tipo de
informação, diferentes abordagens devem ser aplicadas. Para obter mais detalhes, visite o Capítulo 13.
• De acordo com sua página de manual, a execução de certas versões do ldd pode na verdade representar uma ameaça à segurança.
Esteja ciente, entretanto, que em algumas circunstâncias, algumas versões do ldd podem tentar obter as informações de dependência executando diretamente o programa. Portanto,
você nunca deve empregar o ldd em um executável não confiável, pois isso pode resultar na execução de código arbitrário. Uma alternativa mais segura ao lidar com executáveis não
confiáveis é a seguinte (e também mostrada na Figura 12-4):
mi aterro sanitário e Q
Figura 12-4. Usando objdump para (apenas parcialmente) substituir o utilitário ldd
O mesmo resultado pode ser alcançado usando o utilitário readelf (Figura 12-5):
Figura 12-5. Usando readelf para (apenas parcialmente) substituir o utilitário ldd
Obviamente, na análise de dependências, ambas as ferramentas não vão além da simples leitura da lista das dependências mais imediatas do arquivo binário. Do
ponto de vista da segurança, esse é definitivamente um método mais seguro de encontrar a resposta.
No entanto, a lista fornecida está longe de ser tão exaustivamente completa quanto normalmente fornecida pelo ldd. Para fazer a correspondência, você
provavelmente precisará conduzir a pesquisa recursiva por conta própria.
nm
O utilitário nm ( http://linux.die.net/man/1/nm ) é usado para listar os símbolos de um arquivo binário (Figura 12-6). A linha de saída que imprime o
símbolo também indica o tipo de símbolo. Se o binário contiver código C ++, os símbolos serão impressos por padrão na forma fragmentada. Aqui estão algumas
das combinações de argumento de entrada mais usadas:
• $ nm <path-to-binary> lista todos os símbolos de um arquivo binário. No caso de bibliotecas compartilhadas, isso significa não apenas o exportado (da
seção .dynamic ), mas todos os outros símbolos também. Se a biblioteca foi removida (usando o comando strip ), nm sem argumentos relatará nenhum símbolo
encontrado.
• $ nm -D <path-to-binary> lista apenas os símbolos na seção dinâmica (ou seja, símbolos exportados / visíveis de uma biblioteca compartilhada).
09084ac9 T pspell_aspell_dumny () 00094040 T aconmon :: BetterList :: set_cur_rank () O9O93ed0 T acomnon :: BetterList :: set_best_fron_cur () 09094000 T aconmon :: BetterLlst :: init ()
09094Sf9 T acommon :: BetterList :: BetterLlst () 09096C20 w acoromon :: BetterList :: - BetterList {) 09096af9 W aconmon: rBetterList :: ~ BetterLlst ()
09082900 W aconmon :: BlockSList <aspeller :: Conds const * »:: add_block (unsigned int)
Figura 12-6. Usando o utilitário nm para listar símbolos não mutilados 246
• $ nm -D --no-demangle <path-to-binary> imprime os símbolos dinâmicos da biblioteca compartilhada e exige estritamente que os símbolos não sejam
demangled (Figura 12-7).
_Z19pspell_aspell_dunnyv
_ZN7aconnonl0BetterListl2set_cur_rankEv
_ZN7aconnonl0Betterl_i.stl7set_best_f ron_curEv
,
_ZN7acomi ioril0BetterList4lnitE \ /
_ZN7aconnonl0BetterListClEv
_ZN7aconnonl0BetterListC2Ev
i
_ZN7acoi iPionl0BetterL1.stD0Ev
_ZN7aconnonl0Betterl_i.stD2Ev
_ZN7acommonl0BetterSi.zel2set_cur_rankEv
Esta opção é extremamente útil na detecção do bug mais comum ao projetar a biblioteca compartilhada - o caso em que o designer esquece o especificador
externo "C" na declaração / definição da função ABI (que é exatamente o que o binário do cliente espera encontrar) .
• $ nm -A <library-folder-path> /5 | grep symbol-nameé útil quando você procura um símbolo em vários binários localizados na mesma pasta, como-
Umaopção imprime o nome de cada biblioteca na qual um símbolo é encontrado (Figura 12-8).
objdump
O programa utilitário objdump ( http://linux.die.net/man/1/objdump ) é provavelmente a ferramenta de análise binária mais versátil. Cronologicamente,
é mais velho do que pronto, o que é paralelo às suas habilidades em muitos casos. A vantagem do objdump é que, além do ELF, ele suporta cerca de 50 outros
formatos binários. Além disso, suas capacidades de desmontagem são melhores do que as do readelf.
A opção de linha de comando objdump -f é usada para obter uma visão do cabeçalho do arquivo de objeto. O cabeçalho fornece muitas informações úteis. Em
particular, o tipo binário (arquivo-objeto / biblioteca estática x biblioteca dinâmica x executável), bem como as informações sobre o ponto de entrada (início da
seção de texto ), podem ser obtidas rapidamente (Figura 12-9).
./driverApp/driver: arquivo da arquitetura elf32-l386: 1386, sinalizadores 0x00000112: EXECP, HASSYMS, endereço inicial DPACED 0x080484c0
./sharedLlb/llbnreloc.so: arquivo fornat elf32-i386 arquitetura: 1386, sinalizadores 0x00000150: HAS_SYMS, DYNAMIC, D_PACED endereço inicial 0x00000390
./ml_nalnreloc.o: formato de arquivo elf32-1386 arquitetura: i386, sinalizadores 0x00000011: HAS_REL0C, endereço inicial HAS_SYMS 0x00000000
Figura 12-9. Usando objdump para analisar o cabeçalho ELF de vários tipos de arquivos binários
Ao examinar a biblioteca estática, objdump -f imprime o cabeçalho de cada arquivo de objeto encontrado na biblioteca.
00084ac0 00094040 00093ed0 00094000 000945f0 000945f0 00096C20 00096af0 W 00096af0 W 00093f30 00093f10 00093efO 00096aa0 00O96a70 W 00096a70 W 0002dd50 W 0002df50 W 0005da00 W
00082900 W O00Sd740 W 00082900 W O00Sd740
Idx Nome Tamanho VMA Arquivo LMA desligado Algn 9 .note.gnu.build-id 00000024 00009114 00009114 09009114 2 ** 2
4 .gnu.version 00000016 000002a4 O00002a4 000002a4 2 ** 1 CONTEÚDO, ALLOC, LOAD, READONLY, DATA
Ú Ú
file:///C:/Users/noels/Desktop/Advanced C and C++ Compiling ( PDFDrive ).html 171/235
21/09/2021 22:07 Advanced C and C++ Compiling ( PDFDrive ).html
5 .gnu.verston_r 0O00002O 000002bc 00O002bc 0O0002bc 2 ** 2 CONTEÚDOS, ALLOC, LOAD, READONLY, DATA 00000038 000002dc O00002dc O00002dc 2 ** 2 CONTEÚDOS, ALLOC, LOAD,
READONLY, DATA300010 00000314 000, CONTEÚDO300010 00000314 0003 , CARREGAR, APENAS LEITURA, DADOS O000002e 00000324 00000324 00000324 2 ** 2 CONTEÚDOS, ATRIBUIR,
CARREGAR, APENAS LEITURA, CÓDIGO 00000030 00000360 00000360 00000360 2 ** 4 CONTEÚDOS, ATRIBUIR, CARREGAR, READONLY, CÓDIGO 00000118 00000390 00000390 00000390 000
ALLOC, CARREGAR, APENAS LEITURA, CÓDIGO 0000001a O000O4aB 00OO04aB 00OO04aB 2 ** 2
OQ
oo
1 .gnu.hash
2 .dynsyn
3 .dynstr
6 .rel.dyn
7 .rel.plt
8 .tntt
9 .pit
10 .texto
11 .ftnt
21 .bss
22 .connent
2 ** 0
,
nilan @ r itlan $
Quando se trata de exames de seção, objdump fornece opções de comando dedicadas para as seções que são o tópico de interesse mais frequente para os
programadores. Nas seções a seguir, examinarei alguns dos exemplos notáveis.
A execução de objdump -t <path-to-binary> fornece uma saída que é o equivalente completo da execução de nm <path-to-binary> (Figura 12-11).
d .data oooeosee
d, bS5 00600000
d. comentário 86006608
d .debug_aranges 99000000
d .debug_tnfo 00006600
d .debug_abbrev 98006688
d .debug_line 96000000
.conseguiu
.data .bss
.dinâmico
• got.pit
IIII
66088000 I 00088080 I 0O0000O0 I 00000006 I 00000900 I 06088006 I 86081f8C 0O001fl4 eeeeific 0oo003ao 0000200c 00082016 00000420 60000000 8009 If10 I 9O090S7O 0000 Ifle I 80099489
06000006 000004S7 I
00082088 0000200c
_3CR_LIS1_
completado.6159 dtor_tdx.6iai
frd'e Jjnny
crtstuff.c
_CTOR_EHD_
_3CR _END_
_do_global_ctors_aux
sharedLlblFunctions.c
_1686.get_pc_thunk.bx
_DTOR_EN [> _ ~
_dso_handle
_DYNAHIC
prlntfg (! GLlBC_2.8
_edata
_fini
_cxa_ftnallzeg0C.LIBe_2.1.3
__gnon_start_
_fim
_bss_start
00001ff4 00090000 8609200c 060004bS 06698608 06099000 86602614 8809200c 00000009 886064Sc 0009933c mtlan ^ milanS
eu df *ABDÔMEN* 60660066
eu □ .ctors anaafluan UODuOODU
eu 0 .dtors 00600000
eu 0 ■ Jcr 00660860
eu F .texto 00600800
eu 0 .bss 60660001
eu 0 . bss 00808864
eu r .texto 00609000
eu df *ABDÔMEN* 00600800
eu 0 .ctors nflnnAnnn DwOuuvJvJO
eu 0 , eh_frame 1
eu 0 .jcr 00000000
eu F • texto 60900000
eu df *ABDÔMEN* 60060060
eu F .texto 60080086
eu 0 -dors 60900000
eu 0 .dados 00000000
eu 0 *ABDÔMEN* 00080080
eu 0 00000000
F * UND * 0GO00O00
g * A8S * 00000000
C * UND * 00000000
g F • texto 0O000O1C
g F • inlt 00000066
A execução de objdump -T <path-to-binary> fornece uma saída que é o equivalente completo da execução de nm -D <path-to-binary> (Figura 12-12).
prlntf
_cxa_finalize
_gnon_start_
_Jv_RegisterClasses
_edata
_fim
_bss_start
_init _flni
sharedLiblFunction
DINÂMICA 00000000 00000000 0OO0O0O0 00000000 0000200c 00002014 0000200c 0000033c OOO0O4b8 0000045c
A execução de objdump -p <path-to-binary> examina a seção dinâmica (útil para localizar as configurações DT_RPATH e / ou DT_RUNPATH ). Observe que
neste cenário você se preocupa com a parte final da saída exibida (Figura 12-13).
ooo
Seção dinâmica:
PRECISAVA libpthread.so.0
PRECISAVA libdl.so.2
PRECISAVA libdynaciiclinkingdemo.so
PRECISAVA libstdc ++. so.6
PRECISAVA libci.so.6
Referências de versão:
mtlan @ mtlan $
milan ^ nllan $ objdunp -R sharedLib / libnreloc.so sharedLib / libnreloc.so: arquivo fornat elf32-i386
REGISTOS DE REALOCAÇÃO DINÂMICA OFFSET TYPE 00002008 R_386_RELATIVE 00000450 R_386_32 00000458 R_386_32 000O045d R_386_32 OOOOlfeS R_386_CL0B_DAT OOOOlfec
R_386_CL0B0038_GL2000_DATB_Olff6 R_386 000038_GL2000_DATB_Olff6 RLO6 000038_GL2000 JLOT_LOT RLOT82000 RLOT3000 RLO6000 RLO6 0000382000_GL2000_Olff6 RLO6003800_GL2000
JLOT3000_Olff6 R_386 0000380038_GL2000 JLOT_Olfec R_386 000038_GL2000_DATB_Oloff6
Executar objdump -s -j <nome da seção> <path-to-binary> fornece o dump hexadecimal dos valores transportados pela seção. Na Figura 12-15, é a seção
.got .
Executar objdump -p <path-to-binary> exibe informações sobre os segmentos binários ELF. Observe que apenas a primeira parte da saída exibida pertence a
esta tarefa específica (Figura 12-16).
VALOR
*ABDÔMEN*
nyglob
nyglob
_cxa_flnallze
__gnon_start_
_Jv_RegisterClasses
_cxa_finalize
_gnon_start_
Cabeçalho do programa:
PHDR desativado 0x0000000060000040 arquivo vaddrSZ 0x0000000000000230 memSZ STACK desativado 0X00000000OO0010O0 arquivos vaddr; 0x0000000060000000 memsz carga fora
0x0000000000000000 vaddr filesz 0x0000000000001000 memsz INTERP off 0x0000000000000510 vaddr filesz 0x000000000000001c memsz carga fora 0x0000000000001000 vaddr filesz
Ox0800000060O008dc memsz NOTA off 0x0000000060001254 vaddr filesz 0x0000000060000044 memsz EH_FRAME off 0x000000000000181c vaddr filesz 0x0000000000000024 memsz carga fora
OxOO0000000000Idas vaddr filesz 0x0000000000000280 memsz RELRo off oxo000oo000000ida8 vaddr filesz 0x0000000060000258 memsz DYNAMIC desligado 0x0O0000O000e01dd0 vaddr
0x00000600003ff040 paddr 0x0000000000000230 sinalizadores 0x0000000000000900 paddr 0x0000000000000000 sinalizadores Ox0O0O0OOO003ff000 paddr 0x0000000000001000 sinalizadores
0x0000O000003ff510 paddr 0x000000000000001c sinalizadores 0x0000000000400000 paddr
Sinalizadores 0x000000000O00O8de
0x00000000004002 54 paddr 0x0000000000000044 sinalizadores 0x000000000040081c paddr 0X0000000000000024 sinalizadores Ox0O0O000000600da8 paddr 0x0000000000000290 sinalizadores
0x0000O0000O600da8 paddr 0x0000000000000258 sinalizadores 0xO00OO00000600dd0 paddr 0x000000000210 sinalizadores
Ox0O00O00OO03ff040 rx
0X0000000000000000 rw-
OxOO0OOOO0OO3ffO0O rw-
0X0000O000003ff510
r- -
0x0000000000400000
rx
0x0000000000400254
r- -
0x00000000004008lc r- -
0xOO00OO0000600da8 rw-
0x0O0O60O0O0600da8
r--
0x00000000006o0dd0 rw-
alinhar 2 ** 3 alinhar 2 ** 3 alinhar 2 ** 12 alinhar 2 ** 0 alinhar 2 ** 12 alinhar 2 ** 2 alinhar 2 ** 2 alinhar 2 ** 12 alinhar 2 ** 0 alinhar 2 ** 3
Desmontando o Código
Aqui estão alguns exemplos de como objdump pode ser usado para desmontar o código:
• Desmontar e especificar o tipo de notação assembler (estilo Intel neste caso), conforme mostrado na Figura 12-17.
465 C3 ret
466 90 nop
mlan @ nilen5
55 push ebp
B3 e4 fo e esp, 0xfffffff0
00
08O48646 <maln>: 8048646: 8048647: 8048649: 804864c: 804864f: 8048656; 8048657: 804865e: 8048663: 8B48666: mllan @ nllan5
nllangnllanS cbldurop -d -5 - m -Intel, / llbdenol.so | grep - a 26 0606045c <sharedLtblFunctton>: »Inclui" sharedLlbiFunctlons.h "
<sharedLiblFunction> *
sharedLiblFunction (lnt x) 55
89 e5 83 ec 18
printf ("sharedLlblFunct be d4 04 OS 00 8b 55 08 89 54 24 04
89 04 24
e8 fC ff ff ff
C9 c3
90 90 90 90 90 90 90 90
vazio {
EU
476: 477:
478
Empurre vazante
nov ebp, esp
sub esp, 0xl8
em (Sd) é chamado de \ n *, x);
ret
nop
nop
nop
nop
nop
nop
nop
nop
Figura 12-18. Usando objdump para desmontar o arquivo binário (sintaxe Intel)
Esta opção funciona apenas se o binário for construído para depuração (ou seja, com a opção -g ).
Além da seção .text que contém o código, o binário pode conter outras seções (.plt, por exemplo) que também contêm o código. Por padrão, objdump
desmonta todas as seções que contêm o código. No entanto, pode haver cenários nos quais você esteja interessado em examinar o código realizado estritamente
por uma determinada seção (Figura 12-19).
8048486: 68 08 06
804848b: e9 £ 0 ff
8048496: 68 08 00
804849b: e9 d8 ff
80484ab: e9 cs ff
Empurre
jnp
adicionar
jnp
Empurre
jnp
push jnp
jnp
jnp
Empurre
jnp
80484e6: 68 38 08
80484eb: e9 88 ff
(ntlan? nllan $
a8 04 08 jnp
88 00 push
ff ff jnp
como 04 88 jnp
80 O0 push
ff ff jnp
aO 04 88 jnp
88 push OO
ff ff jnp
aO 04 08 jnp
88 push OO
ff ff jnp
□ HORB PTR ds; 0X8049ff8 DWORD PTR tJs: 0X8049ffC byte PTR [eax], al
0x28
equivalentes nm objdump
• $ nm <path-to-binary> equivalente é
$ objdump -t <path-to-binary>
• $ nm -D <path-to-binary>
equivalente é
$ objdump -T <path-to-binary>
• $ nm -C <path-to-binary>
equivalente é
$ objdump -C <path-to-binary>
readelf
O utilitário de linha de comando readelf ( http://linux.die.net/man/lZreadelf ) fornece funcionalidade quase completamente duplicada encontrada no
utilitário objdump . As diferenças mais notáveis entre readelf e objdump são
• readelf suporta apenas o formato binário ELF. Por outro lado, o objdump pode analisar cerca de 50 formatos binários diferentes, incluindo o formato
Windows PE / COFF.
• readelf não depende da biblioteca Binary File Descriptor ( http://en.wikipedia.org/ wiki / Binary_File_Descriptor_library), da qual todas as
ferramentas de análise de arquivo de objeto GNU dependem, fornecendo assim uma visão independente do conteúdo do formato ELF
As duas seções a seguir fornecem uma visão geral das tarefas mais comuns que usam objdump.
A opção de linha de comando readelf -h é usada para obter uma visão do cabeçalho do arquivo de objeto. O cabeçalho fornece muitas informações úteis. Em
particular, o tipo binário (arquivo-objeto / biblioteca estática vs. biblioteca dinâmica vs. executável), bem como as informações sobre o ponto de entrada (o início
da seção .text ) podem ser obtidos rapidamente (Figura 12-20).
Magia: 7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00 00
Classe: ELF32
Versão: 1 (atual)
Versão ABI: 0
0X80484C0
52 (bytes)
32 (bytes) 9
40 (bytes) 36
33
Tamanho deste cabeçalho: Tamanho dos cabeçalhos do programa: Número de cabeçalhos do programa: Tamanho dos cabeçalhos da seção: Número dos
cabeçalhos da seção: String do cabeçalho da seção índice da tabela: milan @ milan $
Magic: 7f 45 4c 46 01 01 01 00 Classe: Dados: Versão: OS / ABI: ABI Versão: Tipo: Máquina: Versão:
Endereço do ponto de entrada: Início dos cabeçalhos do programa: Início dos cabeçalhos da seção: Sinalizadores:
00 00 00 00 00 00 00 00 ELF32
1
2 complemento de s, 1 (atual) Unix - System v 0
Intel 80386
Oxl
0x390
52 (bytes)
32 (bytes) 7
40 (bytes)
33 30
pequeno endian
Tamanho deste cabeçalho: Tamanho dos cabeçalhos do programa: Número de cabeçalhos do programa: Tamanho dos cabeçalhos da seção: Número dos
cabeçalhos da seção: String do cabeçalho da seção índice da tabela: mllan @ mllan $
Magia: 7f 45 4c 46 01 01 01 00 00
Classe:
Dados:
Versão:
OS / ABI:
Versão ABI:
Modelo:
Máquina:
Endereço do ponto de entrada: Início dos cabeçalhos do programa: Início dos cabeçalhos da seção: Sinalizadores:
Tamanho deste cabeçalho: Tamanho dos cabeçalhos do programa: Número de cabeçalhos do programa: Tamanho dos cabeçalhos da seção: Número dos
cabeçalhos da seção: String do cabeçalho da seção índice da tabela: mllan @ nilan $
00 00 00 oo oo 00 00 ELF32
Intel 80386
0x1
0x0
52 (bytes) 0 (bytes) 0
40 (bytes) 21 18
Figura 12-20. Exemplos de uso de readelf para examinar o cabeçalho ELF de executável, biblioteca compartilhada e arquivo de objeto / biblioteca estática
Ao examinar a biblioteca estática, readelf -h imprime o cabeçalho de cada arquivo de objeto encontrado na biblioteca.
Cabeçalhos de seção
y para Bandeiras:
I (informações), L (ordem do link), G (grupo), T (TLS), E (excluir), x (desconhecido) 0 (processamento de SO extra necessário) o (específico do SO), p (específico do processador)
Quando se trata de exames de seção, readelf fornece opções de comando dedicadas para as seções que são os tópicos de interesse mais frequentes para
programadores, como as seções .symtab, .dynsym e .dynamic .
Executando readelf --symbols fornece uma saída que é o equivalente completo da execução de nm <path-to-binary> (Figura 12-22).
9 * nd Vis hdx
global oefault 9
global predefinição 12
global predefinição 11
_edata _end
_bss start
_tnlt ftnt
ntlan @ ntlan $ readelf --synbols llbdemol.so A tabela Synbol '.dynsyiV contém 11 entradas:
FUNC FUNC
FUNC FUNC
0: 09000000 0
1: 89000111 O
2: 09000138 0
3; 69000174 O
5: esssejhí 8
4: 89090Jet O
7: 69090? FC O
s: 6900032c o
9: 0909033C O
1 «: essas 370 9
11: 69896330 6
12: 696964ba 8
13: 090904d4 O
14: 090904fa O
IS: 09090S14 o
16: 8989lf0C 8
17: 6969lf14 6
19: 69891flc 8
19: 69891f20 8
28: 0909ifea o
11: 69091ff4 O
22: 09882BBS 8
26: 09090909 O
27: 69898000 6
29: 60090000 8
29: 60666000 6
36: 69000000 0
32: 6900lf0C 0
33: 090®lfl4 0
34: eoeoific e
35: 000003a0 0
36: 0000200c 1
37: 60002016 4
38: 69000420 6
39: 60000000 8
46: 8089lf16 8
41: 80890576 8
42: 69661flc 6
43: 69690466 6
44: 69890000 6
46: 6066lf19 8
47: 60662669 8
46: 6066lf26 6
49: 66661ff4 O
56: 69090006 6
51: 8080200c 8
5S: 66662614 8
56: 6906286c 6
59: 8686845c 28
contém M entradas:
8 9
16 11 12
13
14
15
16
19 26
21 22
26
71 28
crtstuff.c
_CT0R_LIST_
_DTOB_LrST_
_: CR_LIST_
_CTOR_ENB_
_JCR_ £ MD_
sharedL.lblFLinctlons.c
_1989.get_pc.Chunk.bx
_DTOR_END_
__dso_handle
_DYNAÍlC
_gnon_start_
_fim
_bss_start
Executar readelf --dyn-syms fornece uma saída que é o equivalente completo da execução de nm -D <path-to-binary> (Figura 12-23).
A tabela de símbolos '.dynsym' contém o tipo NOTYPE FUNC FUNC NOTYPE NOTYPE NOTYPE NOTYPE NOTYPE FUNC FUNC FUNC
Edata End
_bss_start
_init _fini
sharedLlblFunctton
A execução de readelf -d examina a seção dinâmica (útil para localizar as configurações DT_RPATH e / ou DT_RUNPATH ), conforme mostrado na Figura 12-
24.
A seção dinâmica no deslocamento OxlddO contém 28 entradas Nome / valor do tipo de tag
OxO00000006fffffff (VERNEEDNUM) 2
nilan @ nilan $
Modelo
0000200c myglob
6000200c myglob
0000200c myglob
00000000 _cxafinalize
00000000 _gmon_start_
0O0O0O0O JvRegisterClasses
A seção de realocação '.rel.pit' no deslocamento 6x314 contém 2 entradas: Offset Info Type Sym.Value Sym. Nane
A execução de readelf -x fornece o dump hexadecimal dos valores transportados pela seção. Na Figura 12-26, é a seção .got . Figura 12-26. Usando readelf para
fornecer um dump hexadecimal de uma seção (a seção .got neste exemplo)
Executar readelf --segments exibe informações sobre os segmentos binários ELF (Figura 12-27).
Cabeçalhos do Progran
00 .note.gnu.build-id .gnu.hash .dynsyn .dynstr .gnu.version .gnu.version_r .rel.dyn .rel.plt .init .pit .text .fini .eh_frame_hdr .eh_frame
02 .dynamic
03 .note.gnu.build-id
04 .eh_frame_hdr
05
milan @ riilan $
O comando readelf tem um suporte muito bom para exibir todos os tipos de informações específicas de depuração contidas no binário (Figura 12-28).
[-a | --all] [-hj - file-header] [-1 | --program-headers | --segments] [-S | --section-headers | --sections] [-g | - -section-groups] [-t j - section-details] [-e | --headers] [-s | --sypis | - -symbols] [- -dyn-syns] [-n | --notes] [-r
| --relocs] [-u | - -unwind] [-d | --dynamic] [-V | --version-info] [-Aj - específico do arco] [-D | --use-dynamic]
[-x <número ou nome> | --hex-dump = -; número ou nane>] [-p «número ou nomes | --string-dump = <número ou nome>] [-R <número ou nome> | --relocated-dump = <número ou nome>] [-c | --archive-index] [-w
[lLlaprmfFsoRt] | --debug-dump [^ rawline ^ decodedline ^ info. ^ abrev ^ pubnames ^ aran ges, = macro, = fraries, = frames-interp, = str, = loc, = Ranges, = pubtypes, = trace_info, = trace _abbrev , =
trace_aranges, = gdb_index]]
Figura 12-28. Readelf oferece a opção de examinar as informações de depuração do arquivo binário
Para determinar rapidamente se o binário foi construído para depuração ou não, no caso de uma compilação de depuração, a saída da execução de readelf --
debug-dump com qualquer uma das opções disponíveis será composta de um número de linhas impressas em stdout. Ao contrário, se o binário não foi criado
para depuração, a saída será uma linha vazia. Uma das maneiras rápidas e práticas de limitar o spew de saída no caso em que o binário contém as informações de
depuração é canalizar a saída readelf para o comando wc :
Alternativamente, o seguinte script simples pode ser usado para exibir o readelf 's descobertas em forma de texto puro e simples. Requer que o caminho para o
arquivo binário seja passado como um argumento de entrada.
arquivo: isDebugVersion.sh
if readelf --debug-dump = linha $ 1> / dev / null; então echo "$ 1 é construído para depuração"; fi
readelf
readelf
chrpath
O programa utilitário de linha de comando chrpath ( http://linux.die.net/man/1Zchrpath ) é usado para modificar o rpath ( campo DT_RPATH ) dos
binários ELF. O conceito básico por trás do campo runpath é descrito no Capítulo 7, na seção "Regras de localização da biblioteca de tempo de execução Linux".
Os detalhes a seguir ilustram o uso (Figura 12-29), bem como algumas das limitações (Figura 12-30) do chrpath:
• Pode ser usado para modificar DT_RPATH dentro de seu comprimento de string original.
• Pode ser usado para excluir o campo DT_RPATH existente . No entanto, tenha cuidado!
• Se a string DT_RPATH estiver inicialmente vazia, ela não pode ser substituída por uma nova string não vazia.
• Não pode substituir a string DT_RPATH existente pela string mais longa.
ntlan @ r »iilan $ chrpath -r / hone / john / Desktop / Test ./demo_rpath ./demo_rpath: RPATH = / hone / itilan / Desktop / Test
. / demo_rpath: novo RPATH: / hone / john / Desktop / Test 1) pode modificar o RPATH existente dentro do
OxOOOOOeOf (RPATH) nilan (5nilan $ chrpath -c ./deno_rpath ./derco_rpath: RPATH convertido em RUNPATH ./deno_rpath: RUNPATH = / hone / john / Desktop / Teste nllan (j] nilan $ readelf -d demo_rpath |
grep PATH
nilan @ nllan $ chrpath -r / excepttonally / long / new / rpath / para / deno / finalidades, / deno_rpath, / deno_rpath: RUNPATH = / hone / john / Desktop / Test
novo rpath '/ exceptlonaily / long / new / rpath / para / deno / finalidades' muito grande; comprimento naxinun 24 nilan (0ntlan $
drwxrwxr-x 2 nilan 4096 28 de abril 12:30. drwxr-xr-x 4 nilan 4096 28 de abril 12h14 .. -rw-rw-r-- 1 nilan 95 28 de abril 12h15 nain.cpp nilan @ nilan $ gcc nain.cpp -o deno_no_rpath_set_inltially nllan ^ nllanj
readelf - d ./dei6 io_no_rpath_set_lnltially
ntlan @ milan $ chrpath -c / hone / nilan / Desktop / ./deno_no_rpath_set_lnitially open: É um diretório elfopen: É um diretório nilan @ nilan $
remendar
O programa utilitário de linha de comando patchelf ( http://nixos.org/patchelf.html ) útil atualmente não faz parte dos repositórios padrão, mas é
possível construí-lo a partir do tarball de origem. Documentação simples e básica também está disponível.
Este utilitário pode ser usado para definir e modificar o caminho de execução ( campo DT_RUNPATH ) dos binários ELF. O conceito básico por trás do campo
runpath é descrito no Capítulo 7, na seção "Regras de localização da biblioteca de tempo de execução Linux". A maneira mais simples de configurar o caminho de
execução é emitir um comando como este:
UMA
Os recursos do patchelf de modificar o campo DT_RUNPATH excedem em muito os recursos do chrpath de modificar o campo DT_RPATH , pois ele pode
modificar o valor da string de DT_RUNPATH de qualquer maneira imaginável (substituindo por uma string mais curta ou mais longa, inserindo vários caminhos,
apagando, etc. )
faixa
O programa utilitário de linha de comando strip ( http://linux.die.net/man/1/strip ) pode ser usado para eliminar todos os símbolos da biblioteca que
não são necessários no processo de carregamento dinâmico. Uma ilustração dos efeitos de faixa foi demonstrada no Capítulo 7, na seção "Exportando os símbolos
da biblioteca dinâmica do Linux".
ldconfig
No Capítulo 7 (dedicado às regras de localização de bibliotecas em tempo de execução do Linux), indiquei que uma das maneiras (embora não seja a prioridade
mais alta) para especificar os caminhos onde o carregador deve procurar bibliotecas em tempo de execução é através do uso de cache ldconfig .
O programa utilitário de linha de comando ldconfig ( http://linux.die.net/man/8Zldconfig ) é normalmente executado como a última etapa de um
procedimento de instalação de pacote. Quando um caminho contendo a biblioteca compartilhada é passado como argumento de entrada para ldconfig, ele
pesquisa o caminho para as bibliotecas compartilhadas e atualiza o conjunto de arquivos que usa para contabilidade:
• O arquivo /etc/ld.so.cache , que contém a lista textual ASCII de todas as bibliotecas encontradas nas varreduras de vários caminhos que foram passados
como argumento de entrada
strace
O programa utilitário de linha de comando strace ( http://linux.die.net/man/1/strace ) rastreia as chamadas do sistema feitas pelo processo, bem
como os sinais recebidos pelo processo. Pode ser útil para descobrir as dependências de tempo de execução (ou seja, não apenas dependências de tempo de
carregamento para as quais o comando ldd é adequado). A Figura 12-31 ilustra a saída típica do strace.
accessf / etc / ld.so.rohwcap ", F_OK) = -1 ENOENT ( Não existe esse arquivo ou diretório)
accessf /etc/ld.so.preload ", R_OK) = -1 ENOENT (Não existe tal arquivo ou diretório)
openf ../ sharedLlb / tls / l686 / sse2 / criov / llbiireloc.so ", O_RDONLY | O_CL0EXEC) = -1 ENOENT (nenhum arquivo ou diretório)
abra {",. / sharedLib / tls / L686 / sse2 / lU> i" ireloc. então ", OJtDONlY | 0_CL0EXEC) openf. ./sharedLtb/tls/lese/crnov/llbfireloc.so",
ORDONLY | OCLOEXEC) enoent (Nenhum arquivo ou diretório) enoent (Nenhum arquivo ou diretório)
= -1 = -1
openf. ./sharedLlb/tls / l & 86 / VLbnreloc. so ", o_rdonly | o_cloexec) = -l enoent (nenhum arquivo ou diretório) openf .. / sharedLib / tls /
sse2 / cnov / libnreloc.so", 0_RD0NLY | 0_CL0EXEO = -1 ENOENT (Nenhum arquivo ou diretório) openf . ./sharedLib/tlS/$Se2/UbPirelOC.SO ",
0_RDONLY | 0_CLOEXEC) = -1 ENOENT (Não existe esse arquivo ou diretório) aberto {" ../ sharedLlb / tls / cmov / llbi "ireloc.so" 0
p
RDONLYIO ^ CLOEXEC) = -1 ENOENT (Nenhum arquivo ou diretório)., / SharedLib / tls / libnreloc.so ", 0_RD0NLY | 0_CL0EXEO = -1 ENOENT
(Nenhum arquivo ou diretório) ../sharedLib/i686/sse2/cmov /llbmreloc.so ", oroonly | o ^ cloexec) = -1 enoent (Nenhum arquivo ou diretório)
../sharedLlb/l686/sse2/llbnreloc.so", 0_roonly | 0_cl0exec) = -1 enoent(Não existe esse arquivo ou diretório)
../sharedLtb/i686/cinov/libnreloc.so ", O ~ rd0nly | O ~ CL0EXEC) - -1 ENOENT (Não existe esse arquivo ou diretório)
,. / sharedLib / i686 / libnreloc.so ", 0_RD0NLY | 0_CL0EXEC) = -1 ENOENT (nenhum arquivo ou diretório)
../sharedLib/sse2/cnov/llbnreloc.so ", 0_RDDNLY | Q_CLOEXEC) = -1 ENOENT (Não existe esse arquivo ou diretório) openf.
./sharedLlb/sse2/Ubmreloc.so'*, O_RD0NLY | O_CL0EXEC) = t - 1 ENOENT (Nenhum arquivo ou diretório) openf. ./SharedLib/cnov/Ubmreloc.so ",
O_RD0NLY | O_CL0EXEC) = -1 ENOENT (Nenhum arquivo ou diretório) openf. ./sharedLlb/llbmreloc.so ", 0_FtDONLY | o_CLOEXEC) = 3
leia {3, "\ 177ELF \ l \ l \ l \ O \ 0 \ a \ O \ 0 \ 0 \ 0 \ e \ e \ 3 \ O \ 3 \ 0 \ l \ O \ 0 \ 0 \ 260 \ 3 \ 0 \ aO04 \ 0 \ O \ e "... , 512) = 512 fstat64 {3,
{st_mode = S_IFR EG10775, st_size = 7727, ...}) - 0
getcwd ("/ horie / nilaii / Desktop / Test / loadTti'ief! elocatlon / example2 / driverApp", 128) = 63 nnap2 (NULL, 8216, PR0T_READ |
PR0T_EXEC, NAP_PRIVATE | MAP_DENYWRITE, 3, 0) = 0xb7737000
rinap2 (exb77380O0, 8192, PROTREAOIPROTWRITE, MAPPRIVATE | MAPFIXEO | NAP_DENYWRITE, 3, 0) = Oxb7738O0O fechar (3) = O
,,
open < ../sharedLlb/tls/l686/sse2/cnov/T.lbc.so.6 ", 0_RDONLY | O_CLOEXEC) = -l ENOENT (Não existe esse arquivo ou diretório)
abrir {"../ sharedLib / tIs / i686 / sse2 / libc. so. 6", 0_RD0NLY | 0_CL0EXEC) openf ../ sharedLib / tls / l686 / cnov / llbc.so.6 ",
0_RD0NLY | 0 CLOEXEC)
- -1 = -1
ENOENT (Sem esse arquivo ou diretório) ENOENT (Sem esse arquivo ou diretório)
openf ../ sharedLib / tls / l686 / libc.so. 6 ", o_rdonly | o_cloexec) = -l enoent (nenhum arquivo ou diretório)
../sharedLib/tls/sse2/cnov/Ubc.so,6 ", 0_RD0NLY | 0_CL0EXEC) - -1 ENOENT (Não existe esse arquivo ou diretório),. / sharedLib / tls / sse2
/ libc.so, &", 0_RD0NLY | 0_CL0EXEC) = -1 ENOENT (Nenhum arquivo ou diretório) ../sharedLlb/tls/cmov/llbc.so.6 ", 0_RD0NLY] 0_CL0EXEC) = -l
ENOENT (Nenhum arquivo ou diretório) ../sharedLib/ tls / libc.so.e ", 0_RD0NLY | 0_CL0EXEC) = -1 ENOENT (Nenhum arquivo ou diretório)
../shared Lib / 1686 / sse2 / cciov / libc. então, 6 ", 0_RD0NLY | Q_CL0EXEC) = -1 ENOENT (nenhum arquivo ou diretório)
../sharedLib/i686/libc-so.6 ", 0_RD0NLY | 0_CL0EXEC) - -1 ENOENT (Não existe esse arquivo ou diretório)
., / sharedLib / sse2 / cnov / llbc, so.6 ", OROONLY | OCLOEXEC) ../sharedLlb/sse2/llbc.so,6. ./sharednb/cmov/llbc.so.6
../sharedLib/llbc.so.fl ", 0_rd0nty | 0_cl0exec) = -1 enoent (nenhum arquivo ou diretório) aberto {" / etc / Id.so.cache ", 0 _rd0nly | 0 _cl0exec)
= 3 fstat64 { 3 {st_mode = s_ifr EG 1 0644, st_size = 70505, ...}) - 0 Fimap2 (nulo, 70595, pr0t_read, nap_private 3, O) = Oxb7725O0O
1 1
openf openf openC openC openC openC openC openf openf openC openC openC
enoent (sem esse arquivo ou diretório) ENOENT {Sem esse arquivo ou diretório)
= -1 = -1
= -1 ENOENT {Nenhum tal lima ou diretório) 0_RD0NLY | 0_CL0EXEC) «-1 ENOENT (Nenhum tal lima ou diretório) 0I rD 0 NLY | 0_ CL 0 EXEC) =
-1 ENOENT (Nenhum arquivo ou diretório)
addr2line
file:///C:/Users/noels/Desktop/Advanced C and C++ Compiling ( PDFDrive ).html 194/235
21/09/2021 22:07 Advanced C and C++ Compiling ( PDFDrive ).html
O programa utilitário de linha de comando addr2line ( http://linux.die.net/man/1/addr2line ) pode ser usado para converter o endereço de tempo de
execução em informações sobre o arquivo de origem e o número da linha correspondente ao endereço.
Se (e se apenas) o binário for construído para depuração (passando os sinalizadores de compilador -g -O0 ), usar este comando pode ser muito útil ao
analisar informações de travamento em que o endereço do contador do programa onde o travamento ocorreu é impresso no terminal tela como algo assim:
# 00 pc 0000d8cc6 /usr/mylibs/libxyz.so
A execução de addr2line nessa saída de console $ addr2line -C -f -e /usr/mylibs/libxyz.so 0000d8cc6 resultará em uma saída que pode ser
semelhante a esta: /projects/mylib/src/mylib.c: 45
A lendária ferramenta de depuração GNU conhecida como gdb pode ser usada para realizar a desmontagem do código em tempo de execução. A vantagem de
desmontar o código em tempo de execução é que todos os endereços já foram resolvidos pelo carregador e os endereços são em sua maioria finais.
Os seguintes comandos gdb podem ser úteis durante a desmontagem do código de tempo de execução:
• O sinalizador / r requer que as instruções do montador sejam mostradas adicionalmente em notação hexadecimal (Figura 12-32).
117 {
118 int t = 0;
119 dl_lterate_phdr (header_handler, NULL); 0x08048886 <+17>; mov DWORD PTR [esp + 0x4], 0x0 0x0804888e <+25>: mov DWORD PTR [esp], 0x804875f 0XO8O4889S <+32>: chamar 0x8048540
<dl_Uerate_phdr @ plt>
120
121 int primeiro = shllbNonStatlcAccessedAsExternvarlable 4 l; 0x0804889a <+37>: mov eax, ds: 6x804a030
122 t = Inltlallze (primeiro, argc); 0xO8O488a6 <+49>: mov eax, DWORD PTR [ebp + 0x8]
Para combinar esses dois sinalizadores, digite-os juntos (ou seja, / rm) em vez de separadamente (ou seja, / r / m), conforme mostrado na Figura 12-34.
ar
O exemplo simples a seguir ilustra os estágios usuais de uso da ferramenta AR . O projeto de demonstração é composto por quatro arquivos de origem
(primeiro.c, segundo.c, terceiro.c e quarto.c) e um arquivo de cabeçalho de exportação que pode ser usado pelos binários do cliente (mostrado nos
cinco exemplos a seguir).
first.c
#include "mystaticlibexports.h"
retorno (x + 1);
segundo.c
#include "mystaticlibexports.h"
retorno (x + 4);
terceiro.c
#include "mystaticlibexports.h"
retorno (x + 2);
quarto.c
#include "mystaticlibexports.h"
retorno (x + 3);
} mystaticlibexports.h
primeira_função int (int x); segunda_função int (int x); terceira_função int (int x); int quarta_função (int x);
Vamos supor que você tenha os arquivos de objeto criados compilando cada um dos arquivos de origem:
Os instantâneos de tela a seguir ilustram os vários estágios de como lidar com a biblioteca estática.
A execução de ar -rcs <nome da biblioteca> <lista de arquivos-objeto> combina os arquivos-objeto especificados na biblioteca estática (Figura 12-35).
nilan @ milan $ ar -res libnystaticlib.a primeiro.o segundo.o terceiro.o quarto, o piilan @ piilan $ Is -alg total 48
drwxrwxr-x 2 nilan drwxrwxr-x 5 nilan -rw-rw-r-- 1 milan -rw-rw-r— 1 nilan -rw-rw-r --- rw-rw-r- --rw-rw- r --- rw-rw-r- --rw-rw-r --- rw-rw-r- --rw-rw-r- * -rw-rw-r--
1 nilan 1 nilan! 1 nilan 3) 1 nilan 1 nilan 1 nilan! 1 nilan 1 nilan i milan @ nllan $ file libnystaticlib.a libmystaticlib.a: arquivo ar atual
Figura 12-35. Usando ar para combinar arquivos de objeto para biblioteca estática
A execução de ar -t <nome da biblioteca> imprime a lista dos arquivos-objeto transportados pela biblioteca estática (Figura 12-36).
primeiro.o
segundo.o
terceiro.
quarto.
Dez 25 11: : 37
Dez 25 10: : 48
nilan @ milan $
Figura 12-36. Usando ar para imprimir a lista de arquivos de objeto da biblioteca estática
Digamos que você queira modificar o arquivo first.c (para corrigir um bug ou simplesmente para adicionar um recurso extra) e por enquanto não deseja que
sua biblioteca estática carregue o arquivo de objeto first.o. A maneira de excluir o arquivo-objeto da biblioteca estática é executar ar -d <nome da
biblioteca> <arquivo-objeto a ser removido> (Figura 12-37).
ntlan @ rit'lan $ ar -d libnystaticlib.a first.o Figura 12-37. Usando ar para excluir um arquivo de objeto da biblioteca estática
Digamos que você esteja satisfeito com as alterações feitas no arquivo primeiro.c e que o tenha recompilado. Agora você deseja colocar o arquivo de objeto
recém-criado primeiro.o de volta na biblioteca estática. A execução de ar -r <nome da biblioteca> <arquivo-objeto a ser anexado> basicamente
anexa seu novo arquivo-objeto à biblioteca estática (Figura 12-38).
milão @ milão $ gcc -Wall -I ../ staticLib -c first.c milão @ milão $ ar -r libmystaticlib.a first.o
Observe que a ordem em que os arquivos-objeto residem na biblioteca estática foi alterada. O novo arquivo foi efetivamente anexado ao arquivo.
Se você insiste em que seus arquivos de objeto apareçam na ordem original que existia antes das alterações de código, você pode corrigi-lo. A execução de ar -m
-b <arquivo-objeto antes de> <nome da biblioteca> <arquivo-objeto a ser movido> realiza a tarefa (Figura 12-39).
terceiro.
quarto.
primeiro.o
segundo.o
terceiro.
quarto.
Figura 12-39. Usando ar para restaurar a ordem dos arquivos de objeto na biblioteca estática
CAPÍTULO 13
Normalmente, há mais de uma maneira de concluir uma tarefa de análise. Para cada uma das tarefas descritas neste capítulo, maneiras alternativas de concluir a
tarefa serão fornecidas.
Depurando a vinculação
Provavelmente, a ajuda mais poderosa na depuração do estágio de vinculação é o uso da variável de ambiente LD_DEBUG (Figura 13-1). É adequado para testar
não apenas o processo de construção, mas também o carregamento dinâmico da biblioteca em tempo de execução.
Para direcionar a saída de depuração para um arquivo em vez da saída padrão, um nome de arquivo pode ser especificado usando a variável de ambiente
LD_DEBUG_OUTPUT. milan @ riilan $
O sistema operacional suporta um conjunto predeterminado de valores para os quais LD_DEBUG pode ser definido antes de executar a operação desejada
(construção ou execução). A maneira de listá-los é digitar
Como com qualquer outra variável de ambiente, existem várias maneiras de definir o valor de LD_DEBUG:
• De dentro do arquivo de perfil de shell (como .bashrc) , configurando-o para cada sessão de terminal. A menos que sua tarefa diária seja testar o processo de
vinculação, essa opção provavelmente não é a mais adequada.
• O utilitário de arquivo (entre a ampla variedade de tipos de arquivo que ele pode manipular) fornece provavelmente a maneira mais simples, rápida e elegante
de determinar a natureza do arquivo binário.
• a análise do cabeçalho ELF readelf fornece, entre outros detalhes, informações sobre o tipo de arquivo binário. Correndo
No caso de bibliotecas estáticas, a saída REL aparecerá uma vez para cada um dos arquivos-objeto transportados pela biblioteca.
O ponto de entrada do executável (ou seja, o endereço da primeira instrução no mapa de memória do programa) pode ser determinado por qualquer
• análise de cabeçalho ELF readelf , que fornece, entre outros detalhes, informações sobre o tipo de arquivo binário. Correndo
$ readelf -h <path-of-binary> | grep Entry exibirá uma linha parecida com esta: Endereço do ponto de entrada: 0x <address>
• análise de cabeçalho EFL objdump , que pode fornecer uma análise semelhante com um relatório um pouco menos detalhado. A saída deste comando
Provavelmente, a maneira mais simples é rodar o executável que carrega a biblioteca dinâmica no depurador GNU. Se a variável de ambiente LD_DEBUG for
definida, as informações sobre a biblioteca carregada serão impressas. Tudo o que você precisa fazer é definir o ponto de interrupção na função main () . É muito
provável que este símbolo exista independentemente de o executável ter sido criado para depuração ou não.
Nos casos em que a biblioteca dinâmica está vinculada de forma estática, no momento em que a execução do programa atinge o ponto de interrupção, o processo
de carregamento já estará concluído.
Em casos de carregamento dinâmico em tempo de execução, provavelmente a abordagem mais fácil é redirecionar a impressão da tela massiva para o arquivo
para inspeção visual mais tarde.
arquivo = libreadline.so.6 [8]; necessário para gdb [O] file = libreadline.so.6 [0]; gerando mapa de link dinâmico: Oxb775bb8S base: 0xb772600O tamanho: entrada: Oxb7730efO phdr: 0xb7726G34 phnun:
1) antes de executar o depurador, ative a depuração do vinculador definindo a variável de ambiente LD_DEBUG (escolha
0xO0039de4 7
2) Inicie o depurador
3229:
4) Execute o processo uma vez que carregará todas as bibliotecas necessárias (assumindo que as regras de localização do tempo de execução foram satisfeitas).
i
arquivo = libi ireloc.so [8]; arquivo = libpireloc.so [0]; dynamic: 6xb7fd9f20 entry: 0xb7fd8390
necessário para / home / nilan / driverApp / driver [0] gerando link nap base: 0xb7fd800O | tamanho: 0x00002018 phnun: 7
ir: | exb
(
phdr: 10xb7fd8034
Observe que ° entry - base = 0x390, o que é o valor lido por readelf a partir do binário da biblioteca
Ponto de interrupção 1, nain (argc = l, argv = 0xbffff344) em driver.c: 28 28 dl_iterate_phdr (header_handler, NULL);
6) quando o ponto de interrupção for atingido, tente desmontar o código ao redor do endereço que está sendo relatado como ru n -time
Empurre
ROV
vazante
ebp.esp
esi
ebx
0xb7fd8447
<i686.qet pc thunk.bx>
Se tudo estiver OK, o gdb relatará que este endereço também é um ponto de entrada de função.
Símbolos de lista
As seguintes abordagens podem ser seguidas ao tentar listar os símbolos de executáveis e bibliotecas:
• utilitário nm
• Uma lista de todos os símbolos visíveis pode ser obtida executando $ readelf --symbols <path-to-binary>
• Uma lista apenas dos símbolos exportados para fins de link dinâmico pode ser obtida executando
• Uma lista de todos os símbolos visíveis pode ser obtida executando $ objdump -t <path-to-binary>
• Uma lista apenas dos símbolos exportados para fins de link dinâmico pode ser obtida executando
$ objdump -T <path-to-binary>
A lista de seções do arquivo binário ELF pode ser obtida por um dos seguintes métodos:
• utilitário readelf
$ readelf -S <path-to-binary>
• utilitário objdump
$ objdump -t <path-to-binary>
De longe, as seções examinadas com mais frequência são as que contêm os símbolos do linker. Por esse motivo, uma ampla variedade de ferramentas foi
desenvolvida para atender a essa necessidade específica. Pela mesma razão, embora pertença à ampla categoria de examinar as seções, o parágrafo que descreve a
extração de símbolos foi apresentado primeiro como um tópico separado.
A seção dinâmica do binário (a biblioteca dinâmica em particular) contém muitas informações interessantes. A listagem do conteúdo desta seção específica pode
ser realizada com base em um dos seguintes:
$ readelf -d <path-to-binary>
• utilitário objdump
$ objdump -p <path-to-binary>
Entre as informações úteis que podem ser extraídas da seção dinâmica, aqui estão as que são extremamente valiosas:
Se a biblioteca dinâmica for construída sem o sinalizador do compilador -fPIC , sua seção dinâmica apresenta o campo TEXTREL , que de outra forma não estaria
presente. O seguinte script simples (pic_or_ltr.sh) pode ajudá-lo a determinar se a biblioteca dinâmica foi construída com o sinalizador -fPIC ou não:
• utilitário readelf
$ readelf -r <path-to-binary>
• utilitário objdump
$ objdump -R <path-to-binary>
• utilitário readelf
• utilitário objdump
• utilitário readelf
• utilitário objdump
$ objdump -p <path-to-binary>
Desmontando o Código
Nesta seção, você examinará diferentes abordagens para desmontar o código.
$ objdump -d <path-to-binary>
Além disso, você pode especificar o tipo de impressão (AT&T vs. Intel).
Se você quiser ver o código-fonte (se disponível) intercalado com as instruções de montagem, você pode executar o seguinte:
Finalmente, você pode querer analisar o código em uma determinada seção. Além da seção .text , que é famosa por conter código, algumas outras seções
(.plt, por exemplo) podem conter código-fonte.
Por padrão, objdump desmonta todas as seções que contêm código. Para especificar a seção individual a ser desmontada, use a opção -j :
A melhor maneira é confiar no depurador gdb. Consulte a seção do capítulo anterior dedicada a esta ferramenta maravilhosa.
$ ldd <path-to-binary>
Alternativamente, confiar em objdump ou readelf para examinar a seção dinâmica dos binários é uma proposição um pouco mais segura, que tem o custo de
fornecer apenas o primeiro nível de dependências.
$ objdump -p / caminho / para / programa | grep NEEDED $ readelf -d / path / to / program | grep NECESSÁRIO
$ ldconfig -p
irá imprimir a lista completa de bibliotecas conhecidas pelo carregador (isto é, atualmente presentes no arquivo /etc/ld.so.cache ) junto com seus respectivos
caminhos.
Consequentemente, a busca por uma determinada biblioteca em toda a lista de bibliotecas disponíveis para o carregador pode ser realizada executando
Os métodos a seguir fornecerão a lista completa de bibliotecas dinâmicas carregadas. A lista inclui tanto as bibliotecas vinculadas dinamicamente como
estaticamente cientes quanto as bibliotecas vinculadas dinamicamente no tempo de execução.
Strace Utility
Chamar strace <linha de comando do programa> é um método útil para listar a sequência de chamadas do sistema entre as quais open () e mmap () são
as mais interessantes para nós. Este método revela a lista completa de bibliotecas compartilhadas carregadas. Sempre que uma biblioteca compartilhada é
mencionada, normalmente as poucas linhas de saída abaixo da chamada mmap () revelam o endereço de carregamento.
Dada a sua flexibilidade e grande variedade de opções, esta opção está sempre na lista de ferramentas para rastrear tudo relacionado ao processo de vinculação /
carregamento. Para este problema específico, a opção LD_DEBUG = files pode fornecer muitas impressões contendo informações excessivas sobre as bibliotecas
carregadas dinamicamente no tempo de execução (seus nomes, caminhos de tempo de execução, endereços de pontos de entrada, etc.).
Sempre que um processo é executado, o sistema operacional Linux mantém um conjunto de arquivos na pasta / proc , controlando os detalhes importantes
relacionados ao processo. Em particular, para o processo cujo PID é NNNN, o arquivo em location / proc / <NNNN> / maps contém a lista de bibliotecas e seus
respectivos endereços de carregamento. Por exemplo, a Figura 13-3 mostra o que esse método informa para o navegador Firefox.
m_lan @ nilan $ ps -ef | grep firefox nilan 15536 14480 8 22:57 pts / 0 nilan 15596 14480 0 22:58 pts / O m.lan@nilan$ cat / proc / 15536 / niaps a2cO0000-a2dO0O00 rw-p 0OOO00O0 00:00 0 rw-p 09000000
00:00 O —p OOOO0000 00:00 O rw-p 00000000 00:00 0
oooo o
—P 00038000 08:
r - p 00038000 08:
r - p 00036000 08:
01 7868984 /usr/lib/i386•linux-gnu/libcroco-0.6.so.3.O.1
01 7868984 /usr/Iib/i386-linux-gnu/libcroco-0.6.so.3.O.1
01 7869354 /usr/lib/i386-linux-gnu/librsvg-2.so.2.36.1
01 7869354 /usr/lib/i386-linux-gnu/librsvg-2.so.2.36.1
01 7869354 /usr/lib/i386-linux-gnu/librsvg-2.so.2.36.1
oi 9S68812 /usr/share/xul-ext/ubufox/chroroe/ubufox.jar
Figura 13-3. Examinando o arquivo / proc / <PID> / maps para examinar o mapa de memória do processo
OBSERVAÇÃO 1:
Um pequeno problema potencial pode ser que certos aplicativos sejam concluídos rapidamente, não deixando tempo suficiente para examinar o mapa de memória
do processo. A solução mais simples e rápida neste caso seria iniciar o processo através do depurador gdb e colocar um ponto de interrupção na função principal.
OBSERVAÇÃO 2:
Se você tiver certeza de que apenas uma instância do programa está sendo executada atualmente, você pode eliminar a necessidade de procurar o PID do
processo, contando com o comando pgrep (process grep). No caso do navegador Firefox, você digitaria
lsof Utility
O utilitário lsof analisa o processo em execução e imprime no fluxo de saída padrão a lista de todos os arquivos abertos por um processo. Conforme declarado
em sua página de manual ( http://linux.die.net/man/8/lsof ), um arquivo aberto pode ser um arquivo normal, um diretório, um arquivo especial de
bloco, um arquivo especial de caractere, uma referência de texto em execução , uma biblioteca, um fluxo ou um arquivo de rede (soquete de Internet, arquivo NFS
ou soquete de domínio UNIX).
Dentre a ampla seleção de tipos de arquivo que relata estar aberto, ele também relata a lista de bibliotecas dinâmicas carregadas pelo processo,
independentemente se o carregamento foi executado estaticamente ou dinamicamente (executando dlopen em tempo de execução).
O recorte a seguir ilustra como obter a lista de todas as bibliotecas compartilhadas abertas pelo navegador Firefox mostrado na Figura 13-4:
,
mlan | ilmi Lan: - / Desktop $ ps -ef | grep
Raposa de fogo
8,1 239248 7868934 / usr / llb / 1386- Unux -gnu / Ubcroco-0.6. tão. 3.Ô. 1
8,1 227972 7869354 / usr / llb / 1386 ■ llnux-gnu / Ubrsvg-2, portanto. 2,36.1
8, , 1 905712 7869397
8, , 1 30684 7344054
8, , 1 13940 7344062
uma, , 1 124663 7344052
8, , 1 5408 7865724
8, , 1 9624 7867962
8, , 1 13604 7867057
8, , 1 17700 7867631
8, , 1 134344 7344053
/lib/1386-llnux-gnu/llbdl■2.IS, então
/lib/1386-llnux-gru/llbptbread-2.15 so
/usr/llb/firefox/libplds4.so
Figura 13-4. Usando o utilitário lsof para examinar o mapa de memória do processo
Observe que lsof fornece a opção de linha de comando para executar o exame do processo periodicamente. Ao especificar o período de exame, você pode
capturar os momentos em que ocorre o carregamento e descarregamento dinâmico em tempo de execução.
Ao executar lsof com a opção -r , o exame periódico do processo continua em um loop infinito, exigindo que o usuário pressione Ctrl-C para encerrar. Executar
lsof com a opção + r tem o efeito de encerrar o lsof quando nenhum outro arquivo aberto é detectado.
Forma programática
Também é possível escrever código para que ele imprima as bibliotecas que estão sendo carregadas pelo processo. Quando o código do aplicativo incorpora
chamadas à função dl_iterate_phdr () , suas impressões em tempo de execução podem ajudá-lo a determinar a lista completa de bibliotecas compartilhadas
que carrega, bem como os dados extras associados a cada biblioteca (como o endereço inicial da biblioteca carregada).
Para ilustrar o conceito, foi criado um código de demonstração composto por um aplicativo de driver e duas bibliotecas dinâmicas simples. O arquivo de origem
do aplicativo é mostrado no exemplo a seguir. Uma das bibliotecas dinâmicas é vinculada dinamicamente com reconhecimento estático, enquanto a outra
biblioteca é carregada dinamicamente invocando a função dlopen () :
#define _GNU_SOURCE
#include <link.h>
#include <stdio.h>
#include <dlfcn.h>
#include "sharedLib1Functions.h"
#include "sharedLib2Functions.h"
interruptor (tipo)
caso PT_NULL: // 0
case PT_LOAD: // 1
case PT_INTERP: // 3
caso PT_NOTE: // 4
case PT_SHLIB: // 5
case PT_PHDR: // 6
case PT_TLS: // 7
predefinição:
Retorna "???";
switch (sinalizadores) {
caso 1:
predefinição:
static int header_handler (struct dl_phdr_info * info, size_t size, void * data) {
int j;
info-> dlpi_name, info-> dlpi_phnum, (void *) info-> dlpi_addr); para (j = 0; j <info-> dlpi_phnum; j ++) {
(void *) (info-> dlpi_addr + info-> dlpi_phdr [j] .p_vaddr)); printf ("\ t \ t \ t tipo = 0x% X (% s), \ n \ t \ t \ t
sinalizadores = 0x% X (% s) \ n", info-> dlpi_phdr [j] .p_type,
if (NULL == pLibHandle) {
printf ("Falha ao carregar libdemo2.so, erro =% s \ n", dlerror ()); return -1;
if (NULL == pFunc) {
dlclose (pLibHandle);
pLibHandle = NULL;
return -1;
O local central neste exemplo de código pertence à chamada à função dl_iterate_phdr () . Essa função extrai essencialmente as informações de mapeamento
de processo relevantes em tempo de execução e as passa para o responsável pela chamada. O chamador é responsável por fornecer a implementação
personalizada da função de retorno de chamada (header_handler () neste exemplo). A Figura 13-5 mostra como pode ser a impressão da tela produzida.
nane = .. / sharedLlbl / ltbdercoi.so (7 segmentos) endereço = 0xb77adO00 nome = .. / sharedLi.b2 / ltbdeno2.so (7 segmentos) endereço = 0xb77c3000
Figura 13-5. A maneira programática (baseada na chamada dl_iterate_phdr ()) de examinar os locais de carregamento da biblioteca dinâmica no mapa de memória do processo
CAPÍTULO 14
Configurando o ambiente para usar as ferramentas do Microsoft Visual Studio 2010 x86.
C = sProgram PilessMicrosoft Uisual Studio 10.0 \ UOlib.exe rlicrosoft (R> Library Manager Uersioo IB.00.40219.01 Copyright CO Microsoft Corporation. Todos os direitos reservados.
opções:
^ DEPC ^ filcname]
/ ERRORREPORT: iNONE \ PROMPT! SUEDE iSENM / EXPORT: símbolo / EXTRACT-membeinsme / INCLUIR: símbolo / LIBPAÏH: dii * / LISTA I: nome do arquivo] / LI CG
SH41THUMB IK641HB £> / NOME: fllsnane / N0DEPAULTL1B [: biblioteca] / N0L0G0 / OUT = nome do arquivo / REMOU E r me mbo i * nome
NRIIUEIPOSlKiWINOOWSiUlNDOUSCEJUBt-tinn
Microsoft Si fertig ht Microsoft SiEverlight 5 SDK Microsoft Si (verlight4 SDK Microsoft SQL Server 2008. Microsoft Sync Framework. Microsoft Visual Studio 2010 <0; Microsoft Visual Studio2010 Doc um oo
Microsoft Visual Studio 2010 Ferramentas do Microsoft Windows SDK Team Foundation Server Ferramentas J * Visual Toots de estúdio
Ci Dotfusealor Software Services ^ Gerenciar configurações de ajuda - Ferramenta EMU M FC-ATI Tract
eu. Espião-
Visual Studio 2010 Remote Debu SB Visual Studio Prompt de comando SB Visual Studio x64 Cross Tools Co
Saco
pl
Este programa utilitário não apenas lida com as bibliotecas estáticas da mesma maneira que sua contraparte do Linux (arquivador), mas também desempenha um
papel no domínio das bibliotecas dinâmicas como a ferramenta que pode criar bibliotecas de importação (a coleção de símbolos DLL, arquivo extensão .lib) ,
bem como os arquivos de exportação (capaz de resolver as dependências circulares, extensão de arquivo .exp). A documentação detalhada sobre lib.exe pode
ser encontrada no site do MSDN ( http://msdn.microsoft.com/en-us/library/7ykb2k5f.aspx ).
Nesta seção, ilustrarei as funções típicas nas quais a ferramenta lib.exe pode ser realmente útil.
Quando o Visual Studio é usado para criar um projeto de biblioteca estática C / C ++, lib.exe é definido como a ferramenta de arquivador / bibliotecário padrão e
a guia Bibliotecário das configurações do projeto é usada para especificar as opções de linha de comando para ele (Figura 14-2) .
Por padrão, a construção do projeto de biblioteca estática invoca lib.exe após o estágio de compilação, que ocorre sem nenhuma ação exigida pelo
desenvolvedor. No entanto, não é necessariamente aí que o uso de lib.exe deve terminar. É possível executar lib.exe a partir do prompt de comando do
Visual Studio da mesma forma que o arquivador de ar do Linux é usado para executar os mesmos tipos de tarefas.
Para ilustrar o uso de lib.exe, você criará uma biblioteca estática do Windows que corresponde totalmente à funcionalidade da biblioteca estática do Linux
usada para demonstrar o uso de ar no Capítulo 10. O projeto demo é composto de quatro arquivos de origem (first.c , second.c, third.c e quarter.c) e
um arquivo de cabeçalho de exportação que pode ser usado pelos binários do cliente. Esses arquivos são mostrados nos cinco exemplos a seguir.
arquivo: first.c
#include "mystaticlibexports.h"
retorno (x + 1);
arquivo: second.c
#include "mystaticlibexports.h"
retorno (x + 4);
arquivo: third.c
#include "mystaticlibexports.h"
arquivo: quarto.c
#include "mystaticlibexports.h"
retorno (x + 3);
arquivo: mystaticlibexports.h
primeira_função int (int x); segunda_função int (int x); terceira_função int (int x); int quarta_função (int x);
Vamos supor que você compilou todos os quatro arquivos de origem e que tem quatro arquivos de objeto disponíveis (primeiro.obj, segundo.obj,
terceiro.obj e quarto.obj). Passar o nome da biblioteca desejada para lib.exe (após o sinalizador / OUT ) seguido pela lista de arquivos de objetos
participantes terá o efeito de criar a biblioteca estática, conforme mostrado na Figura 14-3.
c: \ U se rs \ ni lan \ myst em iclib \ riy static lib \ De bug> dir * .ohj Uolume na unidade C não tem rótulo. O número de série do Uolume é F4F7-CFD4
c: \ Usuários \ nilan \ mystaticlib \ riystaticlib \ Debug> lib.exe /OUT:mystaticlib.lib / NOLOGO primeiro.obj segundo.obj terceiro.obj quarto.obj
c: \ Users \ milan \ mystaticlib \ nystaticlib \ Dehutf> dir ».lib Uolume no driue C não tem rótulo. Número de série do Uolume é F4F7-CFD4
Figura 14-3. Usando lib.exe para combinar arquivos de objeto em uma biblioteca estática
Para imitar completamente as configurações padrão fornecidas pelo Visual Studio ao criar o projeto de biblioteca estática, adicionei o argumento / NOLOGO .
Quando o sinalizador / LIST é passado para lib.exe, ele imprime a lista de arquivos-objeto atualmente contidos pela biblioteca estática, conforme mostrado
na Figura 14-4.
c: \ Usuários \ nilan \ mystaticlih \ mystat iclib \ Debug> lib-exe / LI SI mystat iclib. lib Microsoft (R> Library Manager Uersion 10.00.40219.01 Copyright (C) Microsoft Corporation. preencher direitos reservados,
c: SUsers \ milan \ mystaticlibNriystat ic lihNDebug> Figura 14-4. Usando lib.exe para listar os arquivos objeto da biblioteca estática
Os arquivos de objetos individuais podem ser removidos da biblioteca estática passando o sinalizador / REMOVE para lib.exe (Figura 14-5).
Microsoft <R> Library Manager Uersion 10.00.40219.01 Copyright (C> Microsoft Corporation. Todos os direitos reservados.
c: \ llsers \ milan \ mystat iclib \ mystat iclib \ Debug> lib.exe / LIST nystat iclih. lib Microsoft <R> Library Manager Uersion 10.00.40219.01 Copyright (C> Microsoft Corporation. Todos os direitos reservados.
Figura 14-5. Usando lib.exe para remover arquivo de objeto individual da biblioteca estática
O novo arquivo objeto pode ser adicionado à biblioteca estática existente, passando o nome do arquivo da biblioteca seguido pela lista de arquivos objeto a serem
adicionados. Essa sintaxe é muito semelhante ao cenário de criação da biblioteca estática, exceto que o sinalizador / OUT pode ser omitido (Figura 14-6).
c : \ Users \ milan \ nystaticlib \ nystaticlib \ DehLigJlib.exe / LISI mystatic lib.lib Microsoft <R> Library Manager Uersion 10.00.40219.01 Copyright <C> Microsoft Corporation. Todos os direitos
reservados.
c: \ Usei's \ niltin \ inystatie lib \ nystat ic lib ^ Depurar> lib.exe ciystat ic lib. lib f irst -obj Microsoft <R> Library Manager Uersion 10.00.40219.01 Copyright <C> Microsoft Corporation. Todos os direitos reservados.
c: \ Usei's \ nilan \ nystaticlib \ nystat iclib \ Dehug> lib.exe / LIST nystat iclib.lib Microsoft <R> Library Manager Uersion 10.00.40219.01 Copyright <C> Microsoft Corporation. Todos os direitos reservados.
Figura 14-6. Usando lib.exe para inserir arquivo de objeto na biblioteca estática
Finalmente, os arquivos de objetos individuais podem ser extraídos da biblioteca estática. Para demonstrar isso, primeiro excluí propositadamente o arquivo
objeto original (first.obj), cuja extração da biblioteca estática está planejada para acontecer (Figura 14-7).
c: SUsers \ piilanSnystat iclibSmystat iclib \ Debug> dir * .obj Uolune na unidade C não tem rótulo. O número de série do Uolume é F4F7-CFD4
c: \ Users \ miltU »Srriystt» t iclib \ i <i.ysttiticlib \ Debiig> dir «.obj Uolune na unidade C não tem rótulo. O número de série do Uolume é F4F7-CFD4
c: \ Users \ milan \ mystaticlib \ mystatic1ib \ Debug> lib.exe / LIST mystaticlib.lib Microsoft <FD Library Manager Uersion 10.00.40219.01 Copyright <C> Microsoft Corporation. preencher direitos reservados.
c: \ Users \ milan \ rnystat iclib \ mystaticlib \ Debug> lib.exe / EXTRACT: primeiro .obj mystat iclib-lib
Microsoft <P> Library Manager Uersion 10.00.40219.01 Copyright (C> Microsoft Corporation. Preencher direitos reservados.
Figura 14-7. Usando lib.exe para extrair um arquivo de objeto individual da biblioteca estática
lib.exe também é usado para criar o arquivo de biblioteca de importação DLL (.lib) e o arquivo de exportação (.exp) com base no arquivo de definição de
exportação disponível (.def). Ao trabalhar estritamente no ambiente do Visual Studio, essa tarefa normalmente é atribuída automaticamente ao lib.exe. Um
cenário muito mais interessante ocorre quando a DLL é criada por um compilador de terceiros que não cria a biblioteca de importação e o arquivo de exportação
correspondentes. Nesses casos, lib.exe deve ser executado a partir da linha de comando (ou seja, o prompt de comando do Visual Studio).
O exemplo a seguir ilustra como lib.exe pode ser usado para criar as bibliotecas de importação ausentes após a sessão de compilação cruzada na qual o
compilador MinGW executado no Linux produziu os binários do Windows, mas não forneceu as bibliotecas de importação necessárias (Figura 14-8).
Microsoft <R> library Manager Versão 10.00.40219.01 Copyright <C> Microsoft Corporation- preencher direitos reservados-
Microsoft <R> Library Manager Uersion 10.00.40219.01 Copyright <C> Microsoft Corporation. Todos os direitos reservados.
Microsoft (R) Library Manager Uersion 10.00.40219.01 Copyright <C> Microsoft Corporation. preencher direitos reservados.
Microsoft <R> Library Manager Uersion 10,00.40219.01 Copyright CO Microsoft Corporation. Todos os direitos reservados.
<: \ MilanFFMpegWin32Build> lib / machine: X86 /def:avutil-51.def /out:avutil.lib Microsoft <R) Library Manager Uersion 10.00.40219.01 Copyright <C> Microsoft
Corporation - Todos os direitos reservados.
Microsoft <R> Library Manager Uersion 10.00.40219.01 Copyright <C> Microsoft Corporation. Todos os direitos reservados-
Microsoft <R> Library Manager Uersion 10.00.40219.01 Copyright <C> Microsoft Corporation- Todos os direitos reservados-
X: \ MilanFFMpegUin32Build> lib / machine: X86 /def:swscale—2.def /out:swscale.lib Microsoft <R> Library Manager Uersion 10.00.40219.01 Copyright <C> Microsoft
Corporation- Todos os direitos reservados.
Figura 14-8 . Usando lib.exe para criar uma biblioteca de importação baseada em DLL e sua definição (.DEF) _ arquivo
utilitário dumpbin
O utilitário dumpbin do Visual Studio ( http://support.microsoft.com/kb/177429 ) é, em sua maior parte, o equivalente do Windows ao utilitário
objdump do Linux , pois executa o exame e a análise de detalhes importantes do executável, como símbolos exportados, seções, desmontagem das seções de
código (.text) , lista de arquivos-objeto na biblioteca estática, etc.
Essa ferramenta também é uma parte padrão do pacote do Visual Studio. Semelhante à ferramenta lib descrita anteriormente , ela é executada normalmente a
partir do Prompt de Comando do Visual Studio (Figura 14-9).
Ambiente de configuração para uso das ferramentas Microsoft Uisual Studio 2010 x86. ,
opções:
/TUDO
/ EXPORTAÇÕES
/ FPO
/ HEfiDERS
/NÚMEROS DE LINHA
/ LOADCONFIG
/ FDATA
/ PDBPAIH [: UERBOSE]
/ RELOCATIONS
/Nome da Seção
/RESUMO
/ SYMBOLS
/ ILS
/ UNWINDINFO
Quando executado sem os sinalizadores extras, o dumpbin relata o tipo de arquivo binário (Figura 14-10).
c: \ Users \ milan \ DLLUersioningDemo \ Uers ionedDLL \ Debug> dunpbin d Una em .abj Microsoft <R> COFF / PE Dumper Uersion
10.00.40219.01 Copyright <C> Microsoft Corporat ion. Todos os direitos reservados.
1
c: \ Users \ milan \ DLHJersioningDemo \ Debusf> dumpbin UersionedDLL.dll Microsoft <R> COFF / PE Dumper Uersion
10.00.40219.01 Copyright <C> Microsoft Corporation. Todos os direitos reservados.
Resumo
1000 .data 1000 .idata 2000 .rdata 1000 .reloc 1000 .rsrc 4000 .text 10000 .textbss
c: \ Users \ milan \ DLHJersioningDemo \ Debug> dumpbin UersionedDLLClientApp.exe Microsoft <R> COFF / PE Dumper Uersion
10.00.40219.01 Copyright <C> Mic rosoft Corporation. Todos os direitos reservados.
Resumo
1000 .data 1000 .idata 2000 .rda ta 1000 .reloc 1000 .rsrc 4000 .text 10000 .textbss
Figura 14-10. Usando o utilitário dumpbin para identificar tipos de arquivos binários
Executar dumpbin / EXPORTS <dll path> fornece a lista de símbolos exportados (Figura 14-11).
c: \ Users \ niilan \ DH.UersioningDen »o \ Debug> durtpbin / EXPORTS UersionedDLL.dll Microsoft <R> COFF / PE Dumper Uersion
10.00.40219.01 Copyright <C> Microsoft Corporation. Todos os direitos reservados.
A seção contém as seguintes exportações para UERSIONEDDLL.dll 52B625A0 carimbo de data Sábado 21 de dezembro 15:34:56 de 2013
8 00011087
SIONINFOee @ Z> DllGetUersion = PIU + 130 <? DllGetUersion (? <? VGJPAUJt> U.UER c: \ Users \ milan \ BLLUers ion
ingDemo \ Debug>
Figura 14-11. Usando o utilitário dumpbin para listar os símbolos exportados do arquivo DLL
Executar dumpbin / HEADERS <caminho do arquivo binário> imprime a lista completa das seções presentes no arquivo (Figura 14-12).
c: \ Users \ milan \ DLLUersionincfDemo \ Debug> dumpbin / HEADERS UersionedDLL.dll Microsoft <R> COFF / PE Dumper Uersion
10.00.40219.01 Copyright <C> Microsoft Corporation. preencher direitos reservados.
Assinatura FE encontrada
ooo
10000 tamanho virtual 1000 endereço virtual <10001000 a 10010FFF> 0 tamanho dos dados brutos 0 apontador de arquivo para dados
brutos 0 apontador de arquivo para tabela de realocação 0 apontador de arquivo para número de linha 0 número de relocações 0 número
de números de linha E00000A0 sinalizadores Código
nome
tamanho virtual
número de realocações
Leitura
ooo
Depois que os nomes das seções são listados, as informações das seções individuais podem ser obtidas executando dumpbin / SECTION: <nome da seção>
<caminho do arquivo binário> (Figura 14-13).
c: \ Users \ milan \ DLLUersioningDemo \ Debug> diinpbin /SECTION:.text UersionedDLL.dll Microsaft <R> COFF / PE Dumper Uersion
10.00.40219.01 Copyright <C> Microsoft Corporation. Todos os direitos reservados.
CABEÇALHO DA SEÇÃO »2 .nome do texto 3CA3 tamanho virtual 11000 endereço virtual <10011000 a 10014CA2> 3E00 tamanho dos
dados brutos 400 apontador de arquivo para dados brutos <00000400 a 000041FF> 0 apontador de arquivo para tabela de realocação 0
apontador de arquivo para números de linha 0 número de deslocamentos 0 número de números de linha 60000020 sinalizadores Código
Executar leitura
Resumo
4000 .text
c: \ Users \ milan \ DLLUersioningDemo \ Debug> dumpbin /SECTION:.data UersionedDLL.dll Microsoft <R> COFF / PE Dumper Uersion
10.00.40219.01 Copyright <C> Microsoft Corporation. Todos os direitos reservados.
7C0 tamanho virtual 17000 endereço virtual <10017000 a 100177BF> 200 tamanho de dados brutos 5E00 ponteiro de arquivo para
dados brutos <00005E00 a 00005FFF> 0 ponteiro de arquivo para tabela de realocação 0 arquivo po inter para números de linha 0
número de relocações 0 número de números de linha C0000040 bandeiras
Resumo
Figura 14-13. Usando o dumpbin para obter informações detalhadas sobre uma seção específica
Desmontando o Código
A execução de dumpbin / DISASM <caminho do arquivo binário> fornece a lista desmontada do arquivo binário completo (Figura 14-14).
c: \ Users \ nilan \ DLLUers ion ingDemo \ Debug> diinpbin ^ DISflSM Uers ionedDLL.dll Microsoft <R> COFF / PE Dunper Uersion
10.00.40219.01 Copyright <C> Microsoft Corporation. Todos os direitos reservados.
CC CC CC CC CC
eiLT + 0 <_ucstok_s>:
10011005: E9 04 00 00 00 jnp
PI LT + 5 <_utoi>:
1001100F: E9 2C 13 00 00 jnp
10011014: E9 E7 IE 00 00 jnp
PI LT + 35 (_BTC_Terninate>:
jnp
ligar
não voce
não voce
não voce
Empurre
ligar
adicionar
enp
ligar
não voce
não voce
nov
agora
não voce
Empurre
ligar
enp
01 10
01 10
JOP
ooo
FF FF
100116B4 E8 91 Ffl
100116B9 89 45 ft4
100116BC SB F4
100116BE SB 45 A4
10011SC1 50
100116C2 FF 15 38
100116C8 83 C4 04
100116CB 3B F4
100116CD ES 78 Ffl
100116D2 8B 4D 08
100116D5 89 ■ 41 10
100116D8 8B 45 08
100316DB C7 00 14
100116E1 8B F4
100116E3 8B 45 AC
100116E6 50
100116E7 FF 15 28
100116ED 3B F4
_wcstûk_s _litoi
_RT C_Ge t Er r De sc
_nalloc_dbg
P__security_check_cookieP4
sDebuggerPresentP0 „GetUserDefaultLangiDP0
._RTC_Terninate
JlideCharToMultiByte032 _DllMain @ 12
PI LT + 325Î_RTC_CheckEsp>
eax
esp, 4 esi.esp
PI LT + 325 <_RTC_CheckEsp>
esi, esp
oo Ö
O utilitário dumpbin é usado para identificar a versão de depuração de um arquivo binário. Os indicadores de uma compilação de depuração variam dependendo
do tipo de arquivo binário real.
Arquivos de objeto
Executar dumpbin / SYMBOLS <caminho do arquivo binário> nos arquivos objeto (* .obj) relatará o arquivo objeto construído para depuração como um
arquivo do tipo COFF OBJECT (Figura 14-15).
c: \ U5er & * M * lilan \ DLLUers ion inciDemo \ UersionedDLL \ Debuj}} dimpbin / SVMEOLS dllmain -obJ
Microsoft (R> COFF / PE Duniper llersion 10.00.40219.01 Copyright <C> Microsoft Corporation, direitos de preenchimento reservados-
TABELA DE SÍMBOLOS DE COFF 0UH HHfiEVDIB flBS 901 00000001 ftBS 002 00000000 SECT1 Comprimento da seção Relocação CRC 00000000 005 00000000 SECT2
notype
Comprimento da seção 1E24, 8relocs Relocation CRC 8E00PI6DS 008 00000000 SECT3 notype
Comprimento da seção 4, 8relocs Relocation CRC 00000000 00B 00000000 SECT3 notype
Comprimento da seção 12C, íon de Brelocs 5 <selecionar seção associativa 0x4> Relocação CFC 638E05RE
01C 00000000 Notype UNDEF O Externo 01D 00000000 SECTS notype Estático
Resumo
Estático 2, 81
Estático 0. 81
Externo
! . bss
inenuns 0, checksum
Eu. Texto
Estático]
2 * 81inenuns
E estático, 81
0, selecione
0, selecione
_RTC_Shutdoun.rtciTrtZ
_RTC_Shutdown
.rtcSIMZ
. dehuc | $ T
inenuns 0, checksum 0
Figura 14-15. Usando dumpbin para detectar a versão de depuração do arquivo objeto
A versão de lançamento do mesmo arquivo será relatada como tipo de arquivo ANONYMOUS OBJECT (Figura 14-16).
c: sUsers \ nilan \ DLLUers ion intfDemo \ Uers ionedDLL \ Re lease> dumpbin / SYMBOLS d lima em .o
Microsoft <R> COFF / PE Dumper Uersion 10.00.40219.01 Copyright (C> Microsoft Corporation. (Il direitos reservados.
c: \ Users \ milan \ DLLUersion incfDemoNUersionedDLL \ Release> Figura 14-16. Indicação da liberação construída do arquivo objeto
DLLs e executáveis
O indicador certo de que uma DLL ou arquivo executável foi criado para depuração é a presença da seção .idata na saída da execução da opção dumpbin /
HEADERS . O objetivo desta seção é oferecer suporte ao recurso "editar e continuar" que está disponível apenas no modo de depuração. Mais especificamente, para
habilitar essa opção, o sinalizador de vinculador / INCREMENTAL é necessário e normalmente definido para Depurar e desabilitado para configuração de versão
(Figura 14-17).
c: \ DsersSmilan \ DLLUers ion ingDemoNDebug> dumpbin / HEADERS Uers ionedDLL. dll Microsoft <JD COFF / PE Dumper Versão
10.00.40219.01 Copyright <C> Microsoft Corporation, direitos reservados rtIX.
PE s ignature £ out
7 número de seções 52B697A6 carimbo de data sabado 21 de dezembro 23:41:26 2013 0 ponteiro de arquivo para a tabela de símbolos 0
número de símbolos E0 tamanho do cabeçalho opcional 2102 característico ics Executável de máquina de palavras de 32 bits DLL
oo o
961 tamanho virtual 18000 endereço virtual <10018000 a 10018960> A00 tamanho de dados brutos 6000 apontador de arquivo para
dados brutos <00006000 a 000069FF) 0 apontador de arquivo para relocação tabic 0 apontador de arquivo para números de linha 0
número de relocações 0 número de números de linha C0000040 sinalizadores
oo o
A lista completa das bibliotecas de dependências e dos símbolos importados delas pode ser obtida executando dumpbin / IMPORTS <caminho do arquivo
binário> (Figura 14-18).
c: \ Users \ milan \ DLLUersioningDemo \ Dehug> ctunipbin / IMPORTS Oer s ionedDLL-dll Microsoft <R> COFF / PE Dumper Uersion 10.00.40219.01 Copyright (C)
Microsoft Corporation. Todos os direitos reservados.
UERSION.dll
10018 Tabela de endereço de importação 3AC 100181F0 Tabela de nome de importação 0 carimbo de data e hora
KERHEL32.dll
10018220 Tabela de endereços de importação 1001S664 Tabela de nomes de importação 0 carimbo de data e hora
4AS SetllnhandledExceptionFiIter 4D3 UnhandledExceptionFilter 165 FreePesource 29C GetUserDefaultLanglD 354 LockResource 341 LoadResource 14E FindFesourcelJ 1C0
GetCurrentFrocess
oo o
245 GetProcAddress S4D IstrlenA 3E1 RaiseException 367 MultiByteToUideChar 300 IsDebuggerFresent 511 UideCharToMultiByte 162 FreeLibrary 2E9
InterlockedCoropareExchange 4B2 Sleep
USER32.dll
1001837C Tabela de endereços de importação 100181C0 Tabela de nomes de importação 0 carimbo de data e hora
Dependency Walker
Índice
■A
Programa utilitário de linha de comando addr2line, interface binária de aplicativo 270 (ABI), ferramenta 64, 87 ar, 273
■B
Reutilização de código binário
analogia culinária, 71
analogia da expedição, 72
analogia legal, 71
design de software, 72
bibliotecas estáticas
arquivos de objeto, 53, 55 método trivial, 54 bibliotecas estáticas vs. dinâmicas (consulte Bibliotecas estáticas vs. dinâmicas) Ponto de entrada do arquivo binário
ponto de entrada da biblioteca dinâmica, 279 ponto de entrada executável, 279 regras de localização da biblioteca em tempo de construção
Arquivo de biblioteca de importação de DLL (.lib), 121 referência implícita, caminho de biblioteca 123, 121
percepção do linker vs. humano, 118 biblioteca de tempo de construção do Linux, 118 biblioteca dinâmica do Linux
nome da biblioteca, 117 biblioteca soname incorporada, 118 biblioteca x informações, 117 biblioteca estática do Linux, 116 -L x opção -l, 119 # comentário pragma,
122
■C
Formato AT&T, 15 projetos de demonstração, 15 formato intel, 17 conteúdos binários, 20 emissão de código, 19 limitações do processo de compilação, variáveis
externas, 28 chamadas de função, 28 grandes quebra-cabeças, 28 ligações, 27
mapa de memória do programa, formato 27 ELF, 19 função de arquivo .c, 19 definições introdutórias, 11 análise linguística, 14 comando objdump, 20-21
propriedades de arquivo de objeto, 26 otimização, 18 pré-processamento, 13 definições relacionadas, 12 tela de terminal, 21, 23
■D
Ferramenta de arquivamento padrão, 292 Dependency Walker, 307 Projetando a interface binária do aplicativo de bibliotecas dinâmicas
Projetando bibliotecas dinâmicas ( cont.) Funções de estilo C, 92 namespaces, 94 palavras-chave C padrão, 92 símbolos, 94 interface binária ABI, 88 problemas de C
++, 88 modelos, inicialização de 91 variáveis, 89 requisitos de conclusão de vinculação, visibilidade de 109 símbolos
símbolos da biblioteca) Símbolos do Windows (consulte os símbolos da biblioteca dinâmica do Windows) Projetando a biblioteca dinâmica
processo de tradução de endereço, binário de cliente 138-139, 141 vinculador dinâmico, 141 funções e variáveis, 140 função de interface Initialize (), função de
interface 142 Reinitialize (), função de interface 142 Uninitialize (), 142 instruções de montagem, 137 instruções de acesso a dados, 137 coordenação linker-loader
(consulte coordenação Linker-loader) função dl_iterate_phdr (), 287, 289 sinalizadores de compilador DLL, 86 sinalizadores de linker DLL, 87 função dlopen (),
284, 287 utilitário dumpbin
DLLs e executáveis, 305 arquivos de objeto, 304 desmontagem, 302 símbolos exportados de DLL, 299 listando e examinando, 300 dependência de tempo de
carregamento, pacote do Visual Studio 306, 297 classes de símbolos duplicados, funções
e estruturas, 155 símbolos C, 156 símbolos C ++, 156 main.cpp, 158 erro de linker de manipulação padrão, 160 funções locais, biblioteca estática 160-161, definição
158-159, 155
aplicativo cliente, 172, 176 projeto demo, 174-176 ordem de chamada de função, 173 ordem de vinculação, 171, 173 zona de prioridade, 170, 174 shlib_function (),
173, 176 arquivo binário, 61 processo de construção, 59 vinculação de tempo de construção, 60 cliente aplicativo, símbolo binário de 163 cliente
aplicativo cliente, 169 símbolo de nome duplicado, zona de prioridade 167-168, símbolo de biblioteca estática 166, vinculador do Visual Studio 169, 170
compartimentado,
no Linux, 81 no Windows, 83 projetando ( consulte Projetando bibliotecas dinâmicas) símbolos duplicados (consulte Símbolos duplicados) Biblioteca vinculada
dinamicamente (.dll), 63 vinculação dinâmica, 59 comparação, 113 tempo de execução, 110-113 estaticamente ciente (tempo de carga) , 110 arquivo executável
código-fonte da biblioteca de demonstração, 238 libc.so, 237 função main (), saída 238, arquivo de exportação 238-239 (.exp), biblioteca de importação 63 (.lib),
método 63 Initialize (), símbolo fraco do ligador 163-164, 241 modo de vinculação, 164 ordem de vinculação, 165 estágio de vinculação, 161-162 processo de tempo
de carregamento, 61 vinculação de tempo de carregamento, 62 namespace (s), 164 herança de namespace, 185 aplicativo cliente de símbolo não exportado, 179
componentes, 180 paradigma de implementação, 180 zona prioritária, 176 biblioteca compartilhada, 177-178 única classe, 180- 183
zona de código não priorizada / não competitiva, sistema operacional 180, técnica de PIC 56, abordagem de reprodução por confiança 58, modelo de plug-in 60
arquitetura, 235 brocas e bits, 233-234 exportando, 235 requisitos, 234-235 regras de zoneamento de prioridade, 165 funções de API de vínculo dinâmico de tempo
de execução, procedimento de construção 111, carregamento dinâmico de tempo de execução 110 Linux, sequência de pseudocódigos 111, carregamento dinâmico
de tempo de execução 111 Windows, 112 manipulação de memória de tempo de execução, 239 capacidade de substituição rápida de tempo de execução, 236 contra
bibliotecas compartilhadas, 56 shlib_duplicate_function, 163 símbolos, 61, 236 natureza única, 64 versionamento
(ver esquemas de controle de versão da biblioteca dinâmica do Linux) alterações de código de versão principal, 187 alterações de código de versão secundária,
versão de patch 188 , 188
Controle de versão das bibliotecas dinâmicas do Windows (consulte Controle de versão das bibliotecas dinâmicas do Windows)
Vinculação dinâmica, 61 tipos de arquivos binários, 62 processos de construção, 59 tempo de construção vs. tempo de execução, 62
■E
■F
compilador bandeira fPIC, 81- 82
■ G, H
Tabela de deslocamento global (GOT), 151
■ I, J, K
Função de interface Initialize (), 142
■L
Programa utilitário de linha de comando ldconfig, 269 variável de ambiente LD_DEBUG, 277 lib.exe
Biblioteca de importação de DLL, ferramenta de biblioteca estática 296 (consulte Ferramenta de biblioteca estática) Ferramentas de desenvolvimento do Visual
Studio, 291 Localização da biblioteca
regras de localização de biblioteca) biblioteca de tempo de execução do usuário final, convenções 116 -L e -R, 132, 135 regras de localização de biblioteca dinâmica
de tempo de execução Linux, caminhos de biblioteca padrão, 131 ldconfig Cache, 129
Variável de ambiente LD_LIBRARY_PATH, 128 versões operacionais, 131 biblioteca pré-carregada, 127 rpath, 127 runpath, 129 bibliotecas estáticas e dinâmicas,
115 regras de localização da biblioteca dinâmica de tempo de execução do Windows, 131 erro do Linker, 83 limitações de coordenação do Linker-loader, 144
diretivas do linker
Formato de arquivo ELF, 147 ferramentas readelf / objdump, 147 seção .rel.dyn, 144-145 tipos de realocação, 148 LTR, 149 PIC
GOT, 151
implementação, 154 ligação lenta, esquema de realocação de tempo de carregamento 152, abordagem 150 ponteiro a ponteiro, cadeia recursiva 150 de ligação
dinâmica, 152 seções de realocação, estágio de ligação 144
abordagem tudo de uma vez, 33 .bss desmontar, 35 definição, 29 saída desmontada, 34 ponto de vista do vinculador, 35 ferramenta objdump, 35 resolução de
referência
Sinalizador do compilador fvisibility, 97 libdefaultvisibility.so, 96 símbolos da biblioteca, 96-97 atributos de visibilidade, 98 biblioteca dinâmica do Linux
esquemas de controle de versão com base em Soname softlink de esquema de versão, 189-191 salvaguardas Soname, 192 aspectos técnicos, 192 esquema de versão
de símbolo ( ver esquema de versão de símbolo do Linux) Vantagens do esquema de versão de símbolo do Linux, 195 versão inicial build.sh, 202 binários de
cliente, 204 suporte ao formato ELF , 202 simple.c, 201 simple.h, 201 simpleVersionScript, 202 linker version script, 199 versão principal
Comportamento da função ABI, protótipo de função 210 ABI, versão 214 secundária
aplicativo mais antigo e mais recente, 209 biblioteca compartilhada, 208 simple.c, 206-207 simple.h, 206 simpleVersionScript, 207 mecanismo de controle de versão
de símbolo, 196 diretiva assembler symver, 200 scripts de versão
suporte para especificador de ligação, suporte para namespace 217, controle de exportação de 217 símbolos, controle de visibilidade de símbolo 216, 218 nó não
nomeado, 217 nó de versão, 216 regras de nomenclatura de nó de versão, 216 suporte curinga, 217 tarefas Linux
tipo de arquivo binário, 278 seção de dados, 282 compilação de depuração, 284 vinculação de depuração, 277 código de desmontagem, 283 ponto de entrada da
biblioteca dinâmica, 279
carregamento de biblioteca dinâmica, 284 LD_DEBUG, 285 utilitário lsof, arquivo 286 / proc / <ID> / maps, 285 via programática, 287 utilitário strace, 285 seção
dinâmica, 282 arquivo binário ELF, 281 ponto de entrada executável, 279 executáveis e símbolos de bibliotecas, Carregador 280, 284
deploying stage chrpath, 267 ldconfig, 269 patchelf, 268 strip, 269 file utility program, 243 ldd
limitações, 245 objdump, 245 readelf, 246 recursive search, 244 nm utility
símbolos mutilados, 247 $ nm -D <path-to-binary>, 246 $ nm <path-to-binary>, 246 pesquisa recursiva, 247 tipo de símbolo, 246 símbolos não mutilados, 246
objdump
seções de arquivo binário, 249 desmontar código, 254 examinar segmentos, 253 examinar seção de dados, 253 seção dinâmica de biblioteca, 252 listar todos os
símbolos, 250 listar e examinar seção, 248 listar símbolo dinâmico, 251 nm equivalente, 257 analisar cabeçalho ELF, 248 seção de realocação , 252 pronto
informações de depuração, exibição de 265 seção dinâmica, 263 símbolos dinâmicos, 262 examinar segmentos, 265 despejo hexadecimal, 264
listar e examinar as seções, 259 análise do cabeçalho ELF, 257 seção de realocação, 263
runtime analysis tools addr2line, 270 gdb, 271 strace, 269 size utility program, 243 static library tools add object file, 275 ar tool, 273 creation, 274 delete object file,
275 object file list, 274 restore object file, 276 load_elf_binary function, 45 Dependência do tempo de carregamento, 244 Relocação do tempo de carregamento
(LTR), 57, 149
■ M, N
chamada mmap (), 285 abstrações de sistemas operacionais multitarefa, 1
binários, compilador, vinculador e carregador, 7 hierarquia de memória e sistemas de computador em cache, 2 princípios, 3 analogias da vida real, 3 processos de
esquema de divisão de memória, 6 endereçamento virtual, 5 diretrizes de memória virtual, 3-4 implementação, 4
■O
objdump EFL header analysis, 278-279 open () call, 285
P, Q
linha de comando patchelf
implementação, 154
cadeia recursiva de
vínculo dinâmico, pasta 152 / proc, 285 ponto de entrada dos estágios de execução do programa
função _start (), função do kernel 50, tamanho do byte da função do carregador 45, carregamento dinâmico 48, mapa da memória do programa 47, 48-49 seções vs.
segmentos, construção estática 45, shell 48
processo filho, 44
novo mapa de memória de processo, ambiente 43-44 PATH, 43 sh, bash e tcsh, 43 chamada de sistema (), 45 escrita de código de estágios de vida do programa, 10
estágio de compilação (consulte Estágio de compilação) projeto de demonstração, função principal de 10 propriedades de arquivo executáveis, 37 código de objeto,
37 tipos de seção, 38 estrutura, 37 tipos de símbolo, 40 suposições iniciais, 9 estágio de vinculação (consulte Estágio de vinculação)
■R
readelf ELF header analysis, 278-279 Reinitialize () função de interface, 142 Runtime dynamic linking, 110
■ S, T
Estático (tempo de carga)
vinculação dinâmica, procedimento de construção de 110 bibliotecas estáticas vs. dinâmicas, 69 cenários de dilema de implantação, 68 facilidade de combinação,
70 facilidade de conversão, 70 impacto no tamanho do executável, 69 critérios de seletividade de importação, 65 integração com executável, 69 misc / outro, 70
natureza de binário, 69 portabilidade, 69 prós e contras, 68 adequado para desenvolvimento, 70 cenário de arquivo completo, 67
Bibliotecas estáticas, 53 ferramentas de arquivamento, 75 Linux de 64 bits, implementação de código 79-80, 76 contra-indicação, 78 vs. biblioteca dinâmica, 79
vinculação, 78 criação Linux, 75 modularidade, 77 domínio multimídia, 77 visibilidade e exclusividade de símbolos, 77 janelas criação, 75 ferramenta de biblioteca
estática
remover arquivos de objetos individuais, 294 tipos, 292 linha de comando strace
programa utilitário, programa utilitário de linha de comando de 269 strip, 269 função sys_execve, 45
■ U, V
Função de interface Uninitialize (), 142
■ W, X, Y, Z
Arquivo de recursos de projeto de versão de bibliotecas dinâmicas do Windows, item de menu de 220 propriedades, 221 consulta e recuperação de alternativa
brutal, 229 função DllGetVersion, 224 requisitos de vinculação, 223 estrutura VERSIONINFO, 223 editor Visual Studio, 221
Símbolos da biblioteca dinâmica do Windows dumpbin.exe, 104 opção "Exportar símbolos", 100 extern "C" 105
Dependency Walker, 307 utilitário dumpbin (consulte o utilitário dumpbin) Library Manager (consulte lib.exe)
Compilação C e C ++ Avançada
Milan Stevanovic
Apress
Compilação C e C ++ Avançada
Este trabalho está sujeito a direitos autorais. Todos os direitos são reservados pela Editora, seja a totalidade ou parte do material em causa, especificamente os
direitos de tradução, reimpressão, reutilização de ilustrações, recitação, transmissão, reprodução em microfilmes ou de qualquer outra forma física, e transmissão
ou armazenamento de informações e recuperação, adaptação eletrônica, software de computador ou por metodologia semelhante ou diferente agora conhecida ou
desenvolvida no futuro. Estão dispensados desta reserva legal breves trechos relativos a resenhas ou análises acadêmicas ou materiais fornecidos especificamente
para fins de inserção e execução em sistema de computador, para uso exclusivo do adquirente da obra. A duplicação desta publicação ou de partes dela é
permitida apenas de acordo com as disposições da Lei de Direitos Autorais do local do Editor, em sua versão atual, e a permissão de uso deve sempre ser obtida
da Springer. As permissões de uso podem ser obtidas através do RightsLink no Copyright Clearance Center. As violações podem ser processadas de acordo com a
respectiva Lei de Direitos Autorais.
Nomes de marcas registradas, logotipos e imagens podem aparecer neste livro. Em vez de usar um símbolo de marca registrada em todas as ocorrências de um
nome, logotipo ou imagem de marca registrada, usamos os nomes, logotipos e imagens apenas de forma editorial e para o benefício do proprietário da marca
registrada, sem intenção de violação da marca registrada.
O uso nesta publicação de nomes comerciais, marcas registradas, marcas de serviço e termos semelhantes, mesmo que não sejam identificados como tal, não deve
ser interpretado como uma expressão de opinião sobre se estão ou não sujeitos a direitos de propriedade.
Presidente e editor: Paul Manning Editor principal: Michelle Lowman Editor de desenvolvimento: James Markham
Conselho Editorial: Steve Anglin, Mark Beckner, Ewan Buckingham, Gary Cornell, Louise Corrigan, Jim DeWolf, Jonathan Gennick, Jonathan Hassell, Robert
Hutchinson, Michelle Lowman, James Markham, Matthew Moodie, Jeff Olson, Jeffrey Pepper, Douglas Pundick, Ben Renow -Clarke, Dominic Shakeshaft,
Gwenan Spearing, Matt Wade, Steve Weiss Editor Coordenador: Jill Balzano Editor de texto: Mary Behr Compositor: SPi Global Indexer: SPi Global Artista: SPi
Global Cover Designer: Anna Ishchenko
Distribuído para o mercado mundial de livros pela Springer Science + Business Media New York, 233 Spring Street, 6th Floor, New York, NY 10013. Telefone 1-
800-SPRINGER, fax (201) 348-4505, e-mail orders-ny @ springer-sbm.com , ou visite www.springeronline.com . Apress Media, LLC é uma LLC da
Califórnia e o único membro (proprietário) é a Springer Science + Business Media Finance Inc (SSBM Finance Inc). SSBM Finance Inc é uma empresa de Delaware.
Para obter informações sobre traduções, por favor, e-mail rights@apress.com , ou visita www.apress.com .
Os livros da Apress e amigos de ED podem ser adquiridos a granel para uso acadêmico, corporativo ou promocional. Versões e licenças de e-books também estão
disponíveis para a maioria dos títulos. Para obter mais informações, consulte nossa página da web Licenciamento de e-books em massa especial em
www.apress.com/bulk-sales .
Qualquer código-fonte ou outro material suplementar citado pelo autor neste texto está disponível para os leitores em www.apress.com . Para obter
informações detalhadas sobre como localizar o código-fonte do seu livro, vá para www.apress.com/source-code/ .
Conteúdo
Sobre o autor............................................... .................................................. ............... xv
Visualização específica do carregador de um arquivo binário (seções vs. segmentos) .................................... ....................................... 45
Vinculação dinâmica estaticamente ciente (tempo de carregamento) ......................................... .................................................. ...... 110
Cenário de caso de uso de tempo de execução do usuário final ............................................ .................................................. ................. 116
Regras de localização da biblioteca de tempo de construção do Linux ............................................ .................................................. .............. 116
Regras de localização da biblioteca do Windows Build Time ............................................ .................................................. ........ 120
Por que endereços de memória resolvidos são obrigatórios ........................................... ............................... 137
Quais símbolos provavelmente sofrerão com a tradução de endereços? ........................................ ................................ 140
Cenário 1: O cliente binário precisa saber o endereço dos símbolos da biblioteca dinâmica .................................... ..... 140
Cenário 2: a biblioteca carregada não conhece mais os endereços de seus próprios símbolos .................................... ....... 141
Critérios de Linker no Algoritmo Aproximado de Resolução de Símbolos Duplicados de Bibliotecas Dinâmicas ................ 165
Caso 1: o símbolo binário do cliente colide com a função ABI da biblioteca dinâmica ...................................... ................ 166
Caso 2: Símbolos ABI de diferentes bibliotecas dinâmicas colidem ........................................ ..................................... 170
Caso 3: O símbolo ABI da biblioteca dinâmica colide com outro símbolo local da biblioteca dinâmica ............................. 174
Caso 4: o símbolo não exportado da biblioteca dinâmica colide com outra biblioteca dinâmica
Observação final: a vinculação não fornece nenhum tipo de herança de namespace ......................... 185
Gradação de versões e seu impacto na compatibilidade com versões anteriores ........................................ 187
Esquema de controle de versão baseado em Soname do Linux ............................................ .................................................. ........... 188
lib.exe como uma ferramenta de biblioteca estática .......................................... .................................................. ............................... 292
lib.exe no Reino de Bibliotecas Dinâmicas (Ferramenta de Importação de Biblioteca) .................................... .................................... 296
Sobre o autor
Milan Stevanovic é consultor sênior de software de multimídia baseado na área da baía de São Francisco. A extensão de sua experiência em engenharia cobre uma
infinidade de disciplinas, que vão desde o design de hardware digital e analógico em nível de placa e programação de montagem, até o design C / C ++ e
arquitetura de software. Seu foco profissional tem sido no domínio da análise de uma variedade de formatos de multimídia compactados e design relacionado a
uma variedade de estruturas de multimídia (GStreamer, DirectX, OpenMAX, ffmpeg) no Linux (desktop, incorporado, Android nativo), bem como no Windows.
Ele projetou software multimídia para várias empresas (C-Cube, Philips, Harman, Thomson, Gracenote, Palm, Logitech, Panasonic, Netflix), criando uma
variedade de produtos de tecnologia de ponta (chip decodificador de DVD ZiVA-1 e ZiVA-3, Processador de mídia Philips TriMedia, telefones celulares Palm Treo
e Palm Pre, aplicativo Netflix para Android).
Ele é um membro original da dupla de desenvolvedores (junto com David Ronca) que criou o projeto de código aberto avxsynth (o primeiro port de avisynth para
Linux).
Ele possui um diploma de MSEE pela Purdue University (1994) e um diploma de graduação em EE (1987) e Música - performance de flauta (1990) pela University
of Belgrado.
Ben Combee é um desenvolvedor líder na estrutura Enyo JS. Em uma vida anterior, ele trabalhou nas ferramentas CodeWarrior para Palm OS e CodeWarrior para
Win32 na Metrowerks, além de muitos projetos na Palm, incluindo o netbook Foleo e na arquitetura do Palm webOS.
Miroslav Ristic, por uma cadeia de eventos, ao invés de se tornar um piloto de caça a jato, formou-se e recebeu seu MSc em Engenharia da Computação na
Universidade de Novi Sad, Sérvia. Atualmente ele mora na área da Baía de São Francisco, buscando seus objetivos de carreira. Seus interesses abrangem uma
ampla variedade de tópicos. Ele é apaixonado por criar coisas do zero. Ele tem um casamento feliz com a extraordinária e surpreendente senhora dos seus sonhos,
Iva.
Agradecimentos
Muitas pessoas causaram um impacto duradouro na maneira como penso e raciocino, e como vejo o mundo, especialmente o mundo da tecnologia. Como este é
meu primeiro livro publicado, que imagino ser uma ocasião especial para todos os autores, terei a liberdade de expressar minha gratidão a uma longa lista de
pessoas.
Se eu não tivesse encontrado um grupo de professores superestrelas ensinando no meu 12º Ginásio de Belgrado, toda a minha trajetória de vida provavelmente
teria tomado uma direção significativamente diferente (não, eu não teria acabado no lado errado da lei; provavelmente teria tornar-se um músico / arranjador /
compositor profissional, um Quincy Jones ou Dave Grusin de algum tipo). As habilidades matemáticas que ganhei como aluno do professor Stevan Sijacki foram
Sem o Dr. George Wodicka, cujo assistente de pesquisa eu estava na Purdue University, provavelmente não estaria nos Estados Unidos hoje, então mais de 20 anos
de minha carreira no Vale do Silício não teriam acontecido, e muito provavelmente este livro nunca teria sido escrito.
O incentivo do Dr. George Adams, da Purdue University, para fazer seu curso básico de Modelos e Métodos Computacionais foi um primeiro passo decisivo na
longa jornada de transformação da minha carreira de um jovem engenheiro de design de hardware para um profissional de software experiente com mais de 20
anos de experiência.
A forte crença de David Berkowitz, meu gerente na Palm, de que minhas habilidades em design de multimídia podiam e deveriam se expandir para o território do
Linux foi um dos momentos decisivos de minha carreira. Suas habilidades pessoais únicas e sua capacidade de criar uma equipe coesa fizeram do meu tempo em
sua equipe do Palm Multimedia Group uma experiência memorável. A atmosfera na cafeteria do Palm depois de assistir a transmissão do vídeo da apresentação
do Palm Pre que tirou o mundo da tecnologia de suas meias no show CES em Las Vegas fez daquele 8 de janeiro de 2009 o dia mais memorável da minha carreira
profissional. Meu conjunto de habilidades de engenharia foi enriquecido por muitas experiências e orientações significativas, algumas das quais me levaram
diretamente ao material apresentado neste livro.
Trabalhar com Saldy Antony foi outra experiência verdadeiramente inspiradora. Após os anos que passamos juntos na equipe Philips TriMedia, nossas carreiras
seguiram diferentes direções. Enquanto eu permanecia profundamente imerso nos detalhes imediatos da multimídia, ele espalhou tremendamente suas
habilidades no domínio da arquitetura e gerenciamento de software. Quando nossos caminhos se cruzaram novamente na 2-Wire e mais tarde na Netflix, a era do
código aberto já entrou na vida de um desenvolvimento profissional de multimídia. A cada dia que passa, o trabalho diário do profissional de software multimídia
significa muito menos escrita de código e mais integração existente de terceiros / código-fonte aberto. As conversas com Saldy nas quais ele tentou me convencer
de que eu deveria aprimorar meu conjunto de habilidades com as habilidades de um engenheiro de desenvolvimento de software definitivamente tiveram algum
efeito em mim. Contudo, vê-lo em ação, arregaçar as mangas no tempo livre entre as reuniões de nível gerencial, juntar-se à equipe nos cubículos e resolver
rapidamente qualquer problema relacionado ao compilador, linker, bibliotecas e problemas de implantação de código definitivamente teve uma impressão
duradoura no mim. Foi quando decidi "abraçar o monstro" e aprender o que originalmente não considerava vital para o meu papel pessoal na indústria.
O convite de David Ronca, gerente do Netflix Encoding Technology Group, para trabalhar em algo realmente interessante é outra pedra angular na jornada de
criação deste livro. O "algo realmente interessante" foi o projeto de código aberto de conversão da popular ferramenta de pós-produção de vídeo avisynth do
Windows para Linux,
o projeto conhecido como avxsynth. Sua visão excepcionalmente clara e requisitos arquitetônicos firmemente definidos, combinados com a liberdade que ele me
deu para investigar os detalhes de implementação, levaram a um tremendo sucesso. O projeto, realizado em um período de apenas 2,5 meses, também foi um
imenso aprendizado para mim. Encontrar e superar as dificuldades ao longo do caminho exigiu passar horas pesquisando tópicos e formando meu tesouro
pessoal de dicas e truques relacionados. Conversas diárias com os membros do meu grupo (Dra. Anne Aaron, Pradip Gajjar e especialmente Brian Feinberg, o
arquivo ambulante de peças raras de conhecimento) sobre os lattes de David me ajudaram a ter uma ideia de quanto mais ainda tenho que aprender.
O encontro com o Diretor Editorial da Apress, Steve Anglin, foi uma experiência semelhante à de um filme. A última vez que vi alguém remotamente parecido
com ele foi quando assistia a uma série de detetives na TV nos anos 1970, quando era criança, em Belgrado, na Sérvia. Inteligente, comunicativo, de raciocínio
rápido, reconhecendo imediatamente as coisas certas, reagindo a um palpite na hora certa e direto ao ponto, ele era o tipo de profissional que eu quase parei de
acreditar que existia. A colaboração com ele tornou o processo de publicação deste livro uma experiência memorável.
A editora de aquisição da Apress, Michelle Lowman, realizou o esforço decisivo para revisar e apresentar os materiais do livro por meio de várias rodadas de
discussões da equipe da Apress, pelas quais sou profundamente grato.
Meus agradecimentos especiais também à minha equipe editorial da Apress (Ewan Buckingham, Kevin Shea, Jill Balzano, James Markham e o exército de outras
pessoas com quem não tive contato direto). O caractere 'senhor britânico' que uso no livro para apontar a natureza do arquivo executável não foi, de fato,
inspirado por Ewan Buckingham, mas não seria um erro se fosse. Seu controle sobre o fluxo principal do esforço editorial foi soberano e direto ao ponto em muitas
ocasiões.
Sem a intervenção de última hora da minha talentosa sobrinha Jovana Stefanovic e seu algoritmo DSP Brosnan-Clooney, minha foto da capa seria igual a mim, ou
talvez até pior.
Finalmente, a contribuição da minha equipe de revisores (Nemanja Trifunovic, Miroslav Ristic, Ben Combee), o apoio de amigos (David Moffat, Pradip Gajjar) e
do grupo misterioso conhecido coletivamente como "Arques 811 ex-grupo Philips" (especialmente Daniel Ash e Thierry Seegers) provou ser muito valioso. Sou
profundamente grato por seus esforços e tempo gasto em suas vidas profissionais ocupadas para fornecer feedback sobre o conteúdo do livro.
Por fim, sem o amor e o apoio de minha esposa Milena, meu filho Pavle e sua filha Selina, e sua paciência durante muitos fins de semana e noites trabalhando no
material do livro, todo o projeto não teria acontecido.
xx
1 Um programa é normalmente composto de muitas unidades de tradução. Embora seja perfeitamente possível e legal manter todo o código-fonte do projeto
em um único arquivo, existem boas razões (explicadas na seção anterior) para que isso não aconteça.
Dotfuscator Software Services ^ Gerenciar configurações de ajuda ■ ENU nh MFC-ATI Trace Tool jf Spy * -
t
it Visual Studio 2010 Remote Debu ^^^ EU Visual Studio Command Prompt QZ Visual Studio x64 Cross Tools Co
3 Tipo especifica o tipo de ação que o carregador precisa executar no operando da instrução montador para reparar os problemas causados pela tradução do
endereço. O formato binário ELF mostrado na Figura 8-8 (Figura 1-22 da especificação ELF) especifica os seguintes tipos de realocação.
Uma única biblioteca dinâmica utiliza estritamente uma das técnicas de coordenação do linker-carregador para resolver tanto o Cenário 1 quanto o Cenário 2 (se
necessário). Não pode acontecer que a mesma biblioteca dinâmica resolva o Cenário 1 pela abordagem LRT e os problemas do Cenário 2 pela abordagem PIC (ou
vice-versa).
4 O binário cliente original (ou seja, o aplicativo de demonstração simples inicial) será deixado intencionalmente intocado. Ao não reconstruí-lo, ele irá imitar
perfeitamente o aplicativo legado, construído no momento em que a biblioteca dinâmica apresentava a versão 1.0 inicial.
5 $ nm -u <path-to-binary> é útil quando você deseja listar os símbolos indefinidos da biblioteca (ou seja, os símbolos que a própria biblioteca não contém,
mas conta para serem fornecidos em tempo de execução, possivelmente por algum outro biblioteca dinâmica).
6
vários caminhos podem ser definidos, separados por dois pontos (:)
7 O sinalizador / m intercala as instruções do montador com as linhas de código C / C ++ (se disponíveis), conforme mostrado na Figura 12-33.
117 {
118 int t = 0; Ox0804887e <+9>: C7 44 24 14 00 00 00 00 00 mov DWORD PTR [esp + 0Xl4], 0X0
119 dl_iterate_phdr (header_handler, NULL); 0X08048886 <+17>: C7 44 24 04 00 00 09 00 mov DWORD PTR [esp + 0x4] , 0x0 Ox0804888e <+25>: C7 04 24 5f 87 04 08 mov DWORD PTR
[esp], 0x804875f 0X08048895 <+32> : e8 a6 fc ff ff chamada 0x8048540 <dl_iterate_phdr @ plt>
120
0x0804889a c + 37>: al 30 a0 04 08 mov eax, ds: 0x804a030 0x0804889f <+42>: 83 cO 01 add eax, 0xl
0x080488a2 <+45>: 89 44 24 18 mov DWORD PTR [esp + 0xl8], eax Figura 12-34. Combinando sinalizadores de desmontagem / r e / m