Escolar Documentos
Profissional Documentos
Cultura Documentos
Engenharia informática
Compiladores
Discentes:
Denzel Anselmo
O presente trabalho foi o culminar de uma serie de estudo levado a cabo pelos colegas através
de debates pesquisas em manuais , sobre o tema Geração do código intermediário.
De salientar que para a computação tem sido expressamente dita geração de código como
fase preponderante para funcionamento normal de um compilador.
A tradução do código de alto nível para o código do processador está associada a traduzir
para a linguagem-alvo a representação da árvore gramatical obtida para as diversas
expressões do programa. Embora tal atividade possa ser realizada para a árvore completa
após a conclusão da análise sintática, em geral ela é efetivada através das ações semânticas
associadas à aplicação das regras de reconhecimento do analisador sintático. Este
procedimento é denominado tradução dirigida pela sintaxe.
Várias técnicas e várias tarefas se reúnem sob o nome de Otimização. Alguns autores da
literatura consideram que, para qualquer critério de qualidade razoável, é impossível construir
um programa “optimizador”, isto é, um programa que recebe como entrada um programa P e
constrói um programa P’ equivalente que é o melhor possível, segundo o critério
considerado. O que se pode construir são programas que melhoram outros programas, de
maneira que o programa P’ é, na maioria das vezes, melhor, segundo o critério especificado
do que o programa P original. A razão para essa impossibilidade é a mesma que se encontra
quando se estuda, por exemplo em LFA (Linguagens Formais e Autômatos), o “problema da
parada”: um programa (procedimento, máquina de Turing, etc.) não pode obter informação
suficiente sobre todas as possíveis formas de execução de outro programa (procedimento,
máquina de Turing, etc.).
Não é prático tentar otimizar um programa tentando sucessivamente eliminar todos os seus
comandos, um de cada vez. Note que as condições para cada eliminação dependem do
comando, de sua posição, e, de certa maneira, de todos os comandos restantes do programa.
Para construir um optimizador de utilidade na prática, faz-se necessário identificar
oportunidades para otimização que sejam produtivas em situações correntes.
Procedimentos/função de geração
o Exemplo – gerando P-código com base na árvore sintática procedure genCode(T: nó
árvore);
begin
if T não é nulo then
if ('+') then
genCode(t->leftchild);
genCode(t->rightchild);
write(“adi”);
else if ('id=') then
write(“lda ”+id.strval);
genCode(t->leftchild);
write(“stn”);
else if ('num') then write(“ldc
”+num.strval);
else if ('id') then write(“lod ”+id.strval);
end;
Código gerado:
lda x
lod x
ldc 3
adi
stn
ldc 4
adi
O que se faz para gerar código é criar regras para a aplicação de conjuntos de instruções que
cumpram funcionalidades declaradas nas instruções do código fonte. Isto é conhecido como
geração dirigida sintaticamente, uma vez que usamos a própria árvore de derivação sintática
para definir qual será a sequência de operações desejada. A seguir temos alguns exemplos de
como fazer isso.
Exemplo 1: Obter o resultado para 3*4+5*2:
A introdução dos valores na pilha deve ser feita através da instrução LoadCon, onde
<value> será o valor imediato das constantes da expressão. Além disso temos que executar
duas multiplicações e uma soma, através dos comandos Multiply e Add respectivamente.
Entretanto, temos ainda que alterar a expressão dada para a notação posfixa, que é a que se
adequa ao uso da pilha e a estrutura do analisador sintático. Assim, temos:
3*4+5*2 --> 34*52*+
Código:
Instrução Conteúdo da pilha (topo a esquerda)
LoadCon 3 3
LoadCon 4 4 3
Multiply 12
LoadCon 5 5 12
LoadCon 2 2 5 12
Multiply 10 12
Add 22
Exemplo 2: Uso de variáveis.
Apesar de que o conjunto de operações dado acima funciona bastante bem, temos que na
maioria das vezes os operandos são variáveis, cujos valores reais devem ser buscados na
memória. Para fazer isso lançamos mão das instruções Load e Store, que trabalham sobre
endereços indexados através da pilha. Por exemplo, se quisermos fazer a atribuição a = b
temos que fazer:
Instrução Conteúdo da pilha (topo a esquerda)
LoadCon <value> endereço da variável a
LoadCon <value> endereço de b; endereço de a
Load valor de b; endereço de a
Store (vazia)
Instruções de atribuição são aquelas nas quais o resultado de uma operação é armazenado
na variável especificada à esquerda do operador de atribuição, aqui denotado por __ . Há
três formas para esse tipo de instrução. Na primeira, a variável recebe o resultado de uma
operação binária:
x := y op z
O resultado pode ser também obtido a partir da aplicação de um operador unário:
x := op y
Na terceira forma, pode ocorrer uma simples cópia de valores de uma variável para outra:
x := y
Por exemplo, a expressão em C
a = b + c * d;
seria traduzida nesse formato para as instruções:
_t1 := c * d
a := b + _t1
As instruções de desvio podem assumir duas formas básicas. Uma instrução de desvio
incondicional tem o formato goto L onde L é um rótulo simbólico que identifica uma linha do
código. A outra forma de desvio é o desvio condicional, com o formato if x opr y goto L onde
opr é um operador relacional de comparação e L é o rótulo da linha que deve ser executada se
o resultado da aplicação do operador relacional for verdadeiro; caso contrário, a linha
seguinte é executada.
*x := z
Notação posfixa
A notação tradicional para expressões aritméticas, que representa uma operação binária na
forma x+y, ou seja, com o operador entre seus dois operandos, é conhecida como notação
infixa. Uma notação alternativa para esse tipo de expressão é a notação posfixa, também
conhecida como notação polonesa1, na qual o operador é expresso após seus operandos.
O atrativo da notação posfixa é que ela dispensa o uso de parênteses. Por exemplo, as
expressões
a*b+c;
a*(b+c);
(a+b)*c;
(a+b)*(c+d);
seriam representadas nesse tipo de notação respetivamente como
ab*c+
abc+*
ab+c*
ab+cd+*
.MODEL SMALL
Define o modelo de memória a usar em nosso programa
.STACK
Reserva espaço de memória para as instruções de programa na pilha
.CODE
Define as instruções do programa, relacionado ao segmento de código
END
Finaliza um programa assembly
Tabelas de Equivalência
Cada uma das partes numa linha de código assembly é conhecida como token, por exemplo:
MOV AX,VAR
Aqui temos três tokens, a instrução MOV, o operador AX e o operador VAR. O que o
montador faz para gerar o código OBJ é ler cada um dos tokens e procurar a equivalência em
código de máquina em tabelas correspondentes, seja de palavras reservadas, tabela de códigos
de operação, tabela de símbolos, tabela de literais, onde o significado dos mnemônicos e os
endereços dos símbolos que usamos serão encontrados.
A maioria dos montadores são de duas passagens. Em síntese na primeira passagem temos a
definição dos símbolos, ou seja, são associados endereços a todas as instruções do programa.
Seguindo este processo, o assembler lê MOV e procura-o na tabela de códigos de operação
para encontrar seu equivalente na linguagem de máquina. Da mesma forma ele lê AX e
encontra-o na tabela correspondente como sendo um registrador. O processo para Var é um
pouco diferenciado, o montador verifica que ela não é uma palavra reservada, então procura
na tabela de símbolos, lá encontrando-a ele designa o endereço correspondente, mas se não
encontrou ele a insere na tabela para que ela possa receber um endereço na segunda
passagem. Ainda na primeira passagem é executado parte do processamento das diretivas, é
importante notar que as diretivas não criam código objeto. Na passagem dois são montadas as
instruções, traduzindo os códigos de operação e procurando os endereços, e é gerado o código
objeto.
Há símbolos que o montador não consegue encontrar, uma vez que podem ser declarações
externas. Neste caso o linker entra em ação para criar a estrutura necessária a fim de ligar as
diversas possíveis partes de código, dizendo ao loader que o segmento e o token em questão
são definidos quando o programa é carregado e antes de ser executado.
Mais programas.
Outro exemplo de programa em Assembly
Primeiro passo Use qualquer editor e crie o seguinte: ;exemplo2
.model small
.stack
.code
mov ah,2h ;move o valor 2h para o registrador ah
mov dl,2ah ;move o valor 2ah para o registrador dl ;(é o valor ASCII do caractere *)
int 21h ;interrupção 21h
mov ah,4ch ;função 4ch, sai para o sistema operacional
int 21h ;interrupção 21h
end ;finaliza o programa
Segundo passo
Salvar o arquivo com o nome: exam2.asm Não esquecer de salvar em formato ASCII.
Terceiro passo
Usar o programa TASM para construir o programa objeto.
C:\>tasm exam2.asm Turbo Assembler Version 2.0 Copyright (c) 1988, 1990 Borland
International
Assembling file: exam2.asm
Error messages: None
Warning messages: None
Passes: 1 Remaining memory: 471k
Quarto passo
Usar o programa TLINK para criar o programa executável.
C:\>tlink exam2.obj Turbo Link Version 3.0 Copyright (c) 1987, 1990 Borland International
C:\>
Quinto passo
Executar o programa: C:\>exam2[enter]
*
C:\>
Este programa imprime o caracter * na tela.
Código de Maquinas
A esmagadora maioria dos programas práticos hoje são escritos em linguagens de alto
nível ou linguagem de montagem. O código de fonte é então traduzido para o código de
máquina executável por utilidades, tais como os compiladores , montadores , e ligantes , com
a excepção importante de interpretados programas, que não são traduzidas em código de
máquina. No entanto, o intérprete em si, o que pode ser visto como um executor ou
processador, que executa as instruções do código de fonte, tipicamente consiste de código
máquina directamente executável (gerado a partir de montagem ou de alto nível da linguagem
de código-fonte).
O código de máquina, por definição, é o mais baixo nível de detalhe de programação visível
para o programador, mas muitos processadores internamente usar microcódigo ou optimizar e
transformar instruções de código máquina em sequências de microinstruções , isso não é
geralmente considerado para ser um código de máquina de per si.
Conjunto de instruções
Um programa de computador é uma lista de instruções que podem ser executadas por uma
unidade de processamento central. Execução de um programa é feito para que o CPU que é
executá-lo para resolver um problema específico e, assim, conseguir um resultado específico.
Enquanto processadores simples é capaz de executar as instruções um após o
outro, superscalar processadores são capazes de executar uma variedade de diferentes
instruções de uma só vez.
O fluxo do programa podem ser influenciados por instruções especiais 'salto' que transferir a
execução para uma instrução que não seja o numericamente seguinte. Saltos condicionais são
tomadas (execução continua em outro endereço) ou não (a execução continua na próxima
instrução) dependendo de alguma condição.
Por esse motivo foi criada uma linguagem de programação chamada Assembly, composta de
códigos mnemônicos que expressam as mesmas instruções do processador, embora escritos
em acrônimos da língua inglesa, tais como mov ou rep, em vez de opcodes.
Formato da instrução
Uma instrução em código de máquina consiste em uma sequência de bytes, onde cada byte
significa algo para o processador. Instruções da arquitetura IA-32 são consistidas por[2]:
Prefixos opcionais. Onde pode-se usar nenhum prefixo ou um de cada um dos quatro
grupos existentes.
Bytes primários do opcode. Onde o opcode pode ter um, dois ou três bytes de
tamanho.
Se requerido, também pode ter o byte ModR/M. Esse byte consiste em três campos de
informações:
o O campo mod que combinado com o campo R/M pode formar 32 valores
diferentes: Oito registradores e 24 modos de endereçamento.
o O campo reg/opcode especifica o número do registrador ou mais três bits de
informação do opcode. O propósito desse campo é especificado no opcode primário.
o O campo R/M, que pode ser usado para um registrador como operando ou
pode ser combinado com o campo mod para especificar um modo de endereçamento.
Se requerido, pode ter também o byte SIB que serve para especificar três informações
sobre endereçamentos de memória. Onde essas informações ficam nos seguintes campos:
o O campo scale serve para especificar uma escala de endereçamento.
o O campo index especifica o número do registrador utilizado como índice de
acesso ao endereço.
o E o campo base especifica o número do registrador utilizado como endereço
base.
Onde ebx é o registrador index, ebp é o registrador base e 4 a escala. Onde escala pode ser os
valores 1, 2, 4 ou 8.
Prefixos
Os prefixos são bytes inseridos logo antes de um opcode, que serve para alterar a forma com
que uma instrução é executada. Os prefixos são opcionais e indiferente da ordem, isto é, não
faz diferença em que ordem eles são colocados. Além disso, só pode ser utilizado em uma
instrução apenas um prefixo de cada grupo. Não sendo possível incluir dois ou mais prefixos
pertencentes do mesmo grupo.[2]
Grupo 1
o Prefixos de bloqueio e repetição
F0h - Prefixo LOCK, utilizado para garantir o uso exclusivo da
memória compartilhada.[3]
F2h - Prefixo REPNE/REPNZ.
F3h - Prefixo REP ou REPE/REPZ.
Grupo 2
o Prefixos de sobreposição de segmentos
2Eh - Segmento CS.
36h - Segmento SS.
3Eh - Segmento DS.
26h - Segmento ES.
64h - Segmento FS.
65h - Segmento GS.
o Prefixos Branch Hints - Usados em instruções de pulo condicional para se
aproveitar da tecnologia Branch prediction.
2Eh - Caminho pouco provável. (usado somente em instruções de pulo
condicional)
3Eh - Caminho provável. (usado somente em instruções de pulo
condicional)
Grupo 3
o 66h - Prefixo para sobreposição do tamanho do operando.
Grupo 4
o 67h - Prefixo para sobreposição do tamanho do endereço.
Um programa em código de máquina é um arquivo binário. Como tal, não pode ser
visualizado em um editor de texto.
Pode-se editar o código de máquina usando editores hexadecimais, que irão exibir o código
de máquina como uma sequência de bytes em hexadecimal.
B4 03 CD 10 B0 01 B3 0A B9 0B 00 BD 13 01 B4 13
CD 10 C3 4F 69 20 6D 75 6E 64 6F 21 0D 0A
Exemplo
[ op | rs | rt | rd |shamt| funct]
0 1 2 6 0 32 decimal
000000 00001 00010 00110 00000 100000 binary
Carregar um valor no registrador 8, retirado da célula de memória 68 células após o local listado no
registro 3:
[ op | rs | rt | address/immediate]
35 3 8 68 decimal
100011 00011 01000 00000 00001 000100 binary
[ op | target address ]
2 1024 decimal
000010 00000 00000 00000 10000 000000 binary
Armazenar na memória
Conclusão
Em geral, a geração de código não se dá diretamente para a linguagem assembly do
processador-alvo, como muitos pensam, então ai que vimos a relação com o analisador
logico.
Depois disso vimos também algumas definições e conceitos como o código final e os seus
tipos.
Referências
1. ↑ Machine Code Definition
2. ↑ Intel® 64 and IA-32 Architectures Developer's Manual: Vol. 2A (PDF) (em inglês). [S.l.:
Ir para:a b
s.n.] p. 35
3. ↑ LOCK Prefix - Oracle