Alessandro Garcia LES/DI/PUC-Rio Abril 2019
Aula 10
Implementação da
Programação Modular
Especificação
•
Objetivo dessa aula
– Visão geral sobre compilação de programas modulares
– Estudar em detalhe a composição generalizada de módulos
– Estudar o uso de ponteiros
• Slides adaptados de: Staa, A.v. Notas de Aula em Programação Modular; 2008.
•
Referência básica:
– Capítulos 6, 8 e Apêndice 7 do livro texto
•
Referência complementar:
– Schildt H. C – Completo e Total, 3ª Edição. Makron Books, 1997.
Sumário
•
Pilha de execução e classes de memória
•
Ligação, pré-processamento e compilação
•
Composição de módulos e estrutura de diretórios
•
Referências, funções de acesso
•
Função como dado
Classes de memória real
• Executável
– é onde se armazena o código a ser executado e, possivelmente, algumas constantes numéricas
• Estática encapsulada
– contém todos os espaços de dados globais encapsulados (declarados
static), e todas as constantes literais definidas no interior de módulos
• Estática visível
– contém todos os espaços de dados globais externados
• Automática
– contém a pilha de execução do programa
• Dinâmica
– contém espaços de dados alocados pelo usuário (malloc, new)
• Persistente
– é onde se armazenam os dados que estarão disponíveis de uma
instância de execução do programa para outra
Ligação
•
A ligação combina
m >= 1 módulos objeto e módulos
contidos em uma ou mais bibliotecas, produzindo o
programa carregável
(.EXE, .COM)
•
No módulo objeto todos os endereços gerados
são
deslocamentos
(offsets) relativos a zero dentro da
respectiva classe de memória
•
O ligador
(linker) justapõe (concatena) os espaços de cada
uma das classes de memória (segmentos: executável e
estática) definidos nos módulos objeto, formando um único
grande espaço para cada classe de memória
•
Estes dois grandes segmentos
constituem o programa
carregável
Composição de um módulo objeto
Dados
estáticos
Código
Tab Simb
M1.OBJ
Declarados e definidos, relativos a 0
Tabela de símbolos
- referências a nomes externos
declarados e não definidos
- referências a nomes externos
declarados e definidos (externados
pelo módulo)
(importados)
Relocável, relativo a 0
Tab Reloc
Tabela de relocação
- informa os locais contendo endereços
a serem relocados
Símbolos definidos no módulo objeto
•
Cada módulo objeto contém uma tabela de símbolos
agregando os nomes globais externos, particionada em
– símbolos somente declarados
– símbolos declarados e definidos
•
Os símbolos somente declarados definem uma lista de todos
os locais
no código (ou nos dados) em que o símbolo é
referenciado
•
Um nome externo somente declarado em um determinado
módulo necessariamente deverá estar declarado e definido
em exatamente um outro módulo do programa sendo
composto
Processo de compilação simples
Compilador Compilador Compilador Ligador M3.OBJ M2.OBJ M1.OBJ L2.LIB L1.LIB PG.EXE Editor de programas M3.HPP M3.CPP M2.HPP M2.CPP M1.HPP M1.CPPPrograma
Composição de um executável
Passos
1. concatenar os módulos objeto
– código e dados estáticos
2. relocar os endereços do
módulo de modo que estejam em conformidade com a
origem na concatenação
3. resolver os nomes externos ao módulo definidos em outro
módulo p.exe
código dados estáticos
0 300 530 0 10 30 m1 m2 m3 m1 m2 m3 int a int b int c a a b c
Relocação
•
O ligador ajusta os endereços
dos elementos contidos em
cada segmento de modo que passem a ser deslocamentos
relativos à origem
dos correspondentes segmentos
do
programa
•
A relocação ocorre com relação aos segmentos
– código
– estático local e externo
•
A tabela de relocação
contida no módulo objeto informa os
pontos no código e nos dados globais do módulo que
deverão ser ajustados
– no módulo objeto os deslocamentos são relativos a zero
– para relocar basta somar a origem do segmento do módulo definida no segmento composto às referências internas ao módulo registradas na tabela de relocação
Resolução de nomes externos, sem bibliotecas
• O ligador cria uma tabela de símbolos que conterá os nomes externos. Cada símbolo informa
– o endereço no segmento composto
– a lista dos locais que referenciam o símbolo ainda não definidos
• Ao encontrar um nome externo
– adiciona-o à tabela caso ainda não figure lá
– se for um nome externo declarado e definido
• se a tabela de símbolos do ligador já define o nome, é emitido um erro de duplicação de definição
• caso contrário, percorre a lista dos locais que referenciam o símbolo e atribui o endereço definido
– se for um nome externo somente declarado
• se a tabela de símbolos do ligador já define o nome, atribui esta definição aos locais no módulo que referenciam este símbolo
• caso contrário, o ligador acrescenta a lista do módulo à lista do ligador
• Ao terminar o processamento
– para cada símbolo não definido contido na tabela do ligador, é emitido um erro de símbolo não definido
Resolução de nomes externos, com bibliotecas
•
Uma biblioteca estática
(.lib) é formado por
– uma lista de módulos
– uma tabela de símbolos contendo os símbolos externados pelos módulos e a referência para o código do respectivo módulo na lista de módulos
•
Após compor todos os módulos objeto, para cada símbolo
ainda não definido
– o ligador procura este símbolo, segundo a ordem de fornecimento das bibliotecas
• caso seja encontrado, o módulo correspondente é extraído da biblioteca e acrescentado ao programa sendo montado
– para isso segue o procedimento anterior
• caso não seja encontrado, é emitido um erro de símbolo não definido
Ligação dinâmica
•
Bibliotecas dinâmicas
(.dll) são carregadas à medida que
forem acessadas durante o processamento
– ao encontrar um símbolo externo ainda não resolvido
• utiliza a .dll, se já carregada, ou então carrega ela
• substitui a referência ao símbolo para a referência à função
– cada biblioteca é compartilhada por todos os programas que a usem
– cada programa estabelece espaços próprios para os dados
•
Vantagens
– uma biblioteca dinâmica é carregada uma única vez
considerando todos os programas em execução simultânea
– pode-se trocar uma biblioteca sem precisar recompilar ou religar todo o programa
Ligação dinâmica
•
Problemas
– precisa-se projetar com muito cuidado as bibliotecas
dinâmicas, visando explicitamente a possibilidade do seu reúso em diversos programas
– as bibliotecas são conhecidas pelo nome, portanto pode ocorrer colisão de nomes
• bibliotecas diferentes com o mesmo nome
– é necessário assegurar que a versão correta da biblioteca seja utilizada com cada um dos programas
• todos os programas utilizam a mesma versão da biblioteca, a menos que se possa armazenar as bibliotecas em locais distintos
Carga de um programa
•
Para poderem ser executados programas precisam estar em
memória real
– fragmentos de um programa executável podem estar em
qualquer um dos segmentos: executável, pilha (automático), estático, e dinâmico. A origem estará no segmento executável.
•
Ao ativar um programa é ativado o carregador
que recebe
como parâmetro o nome do arquivo
contendo o programa a
ser carregado
•
O carregador
– determina onde serão colocados os segmentos executável e estático e copia os segmentos do arquivo para a memória
– efetua as necessárias relocações de modo a ajustar os endereços contidos nesses segmentos
Pré-processamento
•
Um pré-processador
– é um processador de linguagem
– recebe um arquivo contendo texto fonte e diretivas de pré-processamento
Padrão de programação C
•
Ao desenvolver programas em C ou C++ siga o
recomendado no apêndice 1 Padrão de Composição de
Módulos C e C++.
•
Todos os módulos que podem ser incluídos devem conter
um controle de compilação única
– módulo de definição
– tabelas de definição
– tabelas de dados
#if !defined( Nome-arquivo_MOD ) #define Nome-arquivo_MOD
/* Comentário cabeçalho do arquivo */
Corpo do arquivo
#endif
Estrutura de diretórios (pastas)
•
É fortemente recomendável criar uma estrutura de
diretórios
(pastas) padrão para cada projeto
– pastas com menos arquivos são mais fáceis de manipular
– cada pasta conterá arquivos de alguns poucos tipos
– Exemplo: autotest/instrum
+ Projeto X
+ Batches contém arquivos .bat
+ Composicao contém arquivos .make, .comp, ... + ModulosFonte contém arquivos .c, .h, ...
+ ModulosObjeto contém arquivos .obj, .build, ... + Produto contém arquivos .exe, .log, ...
+ Tabelas contém arquivos .tabstr, .incstr, ... + Teste contém arquivos .script, .count, ... + ... (por exemplo os de apoio ao ambiente)
O que são referências?
•
Uma referência
é formada por um conjunto de parâmetros
a
serem fornecidas a uma função de acesso
para acessar um
determinado espaço
– exemplo: elemento de um vetor em C
< origemVetor , dimElemento , inxElemento >
– exemplo: disco físico
< unidade , cilindro , trilha , setor >
– exemplo: arquivo
< pArq , inxByte , idOrigem >
•
Cada classe de referência
possui uma função de acesso
associada a ela
– exemplo vetor: tpX vtX[ dimVtX ] ; ... vtX[ i ] ...
Dereferenciação
•
Dereferenciar
é a operação de converter uma referência em
um endereço real através de sua função de acesso,
exemplos:
(obs.: não são fragmentos de código em C)– A[j] :: &A + j * sizeof( tpA ) – tudo medido em bytes
– *pX :: [ pX ] ou por extenso: conteúdo de pX
– seja: pElemTabSimb * ObterElemTabSimb(
char * pszSimbolo)
• *( ObterElemTabSimb( "um_simbolo" ))
– é o espaço de dados associado ao símbolo "um_simbolo“
• :: operador “é definido por”
operador de dereferenciação de ponteiro
Dereferenciação
•
A dereferenciação pode ser realizada até chegar ao valor
expA = expB
RHS
right hand side LHS
left hand side
busca-se o valor referenciado pelo endereço RHS
i.e. dereferencia-se o endereço RHS atribui-se o valor RHS
Dereferenciação composta
•
Assumindo que os espaços de dados referenciados existam:
typedef struct tgElemLista {
char szSimbolo[ DIM_SIMBOLO ] ; unsigned IdSimbolo ;
struct tgElemLista * pProx; } tpElemLista ;
tpElemLista * Tabela[ DIM_TABELA ] ;
( Tabela[ ObterHash( szSimboloDado )]->pProx)->szSimbolo
ou
*(*( Tabela[ ObterHash( szSimboloDado )]).pProx ).szSimbolo
•
acessam o mesmo vetor de caracteres – o segundo
Ponteiros
•
Ponteiros são casos especiais
de referências
– função de acesso é implícita: *pX :: conteúdo de pX
•
Há quem prefira outra definição de modo que se caracterize
o controle que se pode ter quando se usa referências,
contrastando com a falta de controle quando se usa
ponteiros, exemplo
– vtA[ inxA ] ::
• if (( inxA < 0 ) || ( inxA >= dimA ))
ErroAcesso( __FILE__ , __ LINE__) ;
– pA[ inxA ] :: ??? pois não se sabe o valor de dimA
Ponteiros – Problemas comuns
•
Vazamento de Memória:
– Retornar de função sem destruir espaços de memória (não encadeados em alguma estrutura) referenciados por ponteiros locais
•
Falha de Segmentação:
– Dereferenciar ponteiro com valor NULL
– Dereferenciar ponteiro não inicializado
– Dereferenciar ponteiro cujo espaço de memória foi destruído
(free/delete)
•
Apêndice 7: regras
e recomendações
para o uso de
ponteiros
Ponteiro para função
•
Funções possuem:
– Um tipo
– Um espaço de dados que é ocupado pelo código da função
– Um nome que referencia esse espaço de dados
•
Em C é possível criar variáveis e parâmetros do tipo
“Ponteiro para função”:
int soma (int a, int b) {
return a + b; }
int main () {
int (*operacao) (int, int); operacao = soma;
operacao(1, 2); }
Função como um dado
•
O tipo de uma função é estabelecido pela sua assinatura
física
– valor retornado
– lista dos tipos de parâmetros
• os nomes dos parâmetros não fazem parte da assinatura
• o nome da função não faz parte da assinatura
– exemplo: int ( int , double )
– exemplo: tpHistórico * ( tpIdAluno )
•
Uma função A será do
mesmo tipo
que a função B caso
ambas tenham a mesma assinatura
– note que não se pergunta o que a função faz – semântica
Dado tipo ponteiro para função
•
Em C e C++ podem ser definidas variáveis do tipo ponteiro
para função
– exemplo: int (* VarF1 )( int , double )
• VarF1 é um ponteiro para função do tipo: int ( int , double )
– exemplo: tpHist * (* VarF2 )( tpIdAl )
• VarF2 é um ponteiro para função do tipo: tpHist * ( tpIdAl )
– Exemplo de atribuição
int Func( int X , double Y ) {
...
} /* Func */
VarF1 = Func ;
Func é uma constante do tipo ponteiro para uma função do tipo:
Exemplo simples: integração
/* Função de integração numérica utilizando regra dos trapézios */ double Integrar( double LimInf ,
double LimSup , int NumInt ,
double ( * Func )( double X )) {
double Integral = 0.0 ; double Intervalo ;
int i ;
assert( NumInt >= 2 ) ;
Intervalo = ( LimSup - LimInf ) / ( NumInt - 1 ) ; Integral += Func( LimInf ) ;
Integral += Func( LimSup ) ; Integral /= 2.0 ;
for ( i = 1 ; i < NumInt - 1 ; i++ ) {
Integral += Func( Intervalo * i ) ; } /* for */ Integral *= Intervalo ; return Integral ; } /* Integrar */ A B Y X 0 1 2 3 4 5 6 7 8
Exemplo simples: integração, uso
/* exemplo de uso */
double Quadrado( double X ) {
return X ** 2 ; }
double Cubo( double X ) { return X ** 3 ; } printf( ″\n F = x ** 2 de x=1 a x=10: %lf″ , Integrar( 1.0 , 10.0 , 20 , Quadrado )) ; printf( ″\n F = x ** 3 de x=1 a x=10: %lf″ , Integrar( 1.0 , 10.0 , 20 , Cubo )) ;