Você está na página 1de 26

Universidade Zambeze

Faculdade de ciência e tecnologia

Engenharia informática

Compiladores

Tema: Geração de código intermediário

Discentes:

 Asvalter de oliveira Joaquim Domingos

 Aristenio da conceição batista

 Denzel Anselmo

 Alberto António senda

Docente: Alcamate Daial Dossa

Beira, setembro de 2021


Introdução

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.

Com o presente trabalho em epigrafe visa mostrar a funcionalidade a importância que


possuem este conteudo pra o funcionamento do 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.
Geração de código

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.

Em geral, a geração de código não se dá diretamente para a linguagem assembly do


processador-alvo. Por conveniência, o analisador sintático gera código para uma máquina
abstrata, com uma linguagem próxima ao assembly, porém independente de processadores
específicos. Em uma segunda etapa da geração de código, esse código intermediário é
traduzido para a linguagem assembly desejada. Dessa forma, grande parte do compilador é
reaproveitada para trabalhar com diferentes tipos de processadores.

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.).

Normalmente, é muito fácil fazer uma otimização em um programa, ou seja, uma


transformação que o transforma em outro melhor. O difícil é sempre obter a informação
necessária que garante que a otimização pode realmente ser aplicada, sem modificar em
nenhum caso o funcionamento do programa.

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.

Outro exemplo de otimização que é citado frequentemente é a retirada de comandos de um


comando de repetição (um loop). Por exemplo, um comando cujo efeito é independente do
loop, pode valer a pena retirá-lo do loop, para que ele seja executado apenas uma vez, em vez
das muitas vezes que se presume que será executado o código de dentro do loop.

Depende muito da finalidade de um compilador o conjunto de otimizações que ele deve


oferecer. Um compilador usado em um curso introdutório de programação não precisa de
otimização, porque os programas são executados quase sempre apenas uma vez. (Em vez de
otimização, este compilador deveria dar boas mensagens de erro, que pudessem auxiliar os
principiantes na linguagem.) Por outro lado, um programa que vai ser compilado uma vez e
executado muitas vezes deve ser otimizado tanto quanto possível. Neste caso estão programas
de simulação, de previsão do tempo, e a maioria das aplicações numéricas.

Um outro problema na otimização de código para um compilador é a quantidade de


informação que se deseja manipular. Pode-se examinar otimizações locais (em trechos
pequenos de programas, por exemplo trechos sem desvios, ou seja, trechos em linha reta),
otimizações em um nível intermediário (as otimizações são consideradas apenas em funções,
módulos, ou classes, dependendo da linguagem) e otimizações globais (que consideram as
inter-relações de todas as partes de um programa). A maioria dos compiladores oferece
algumas otimizações do primeiro tipo, possivelmente combinadas com a fase de geração de
código, e quando muito algumas otimizações de nível intermediário.

A maneira de tratar a otimização pode ser extremamente pragmática. Em um programa


grande, a execução gasta 90% em 10% do código, e 10% do tempo nos 90% do código
restantes (A literatura menciona valores de 90%-10% até 70%-30%.). Isto acontece porque
em geral um programa é constituído de inicializações e finalizações, entre as quais se
encontra um conjunto de vários loops aninhados. O tempo maior de execução (“os 90%”) é
gasto no loop mais interno (“os 10%”). Existem ferramentas que registram o perfil de
execução do programa (profilers), permitindo a identificação dos trechos mais executados, e a
otimização pode se concentrar nestes trechos, ignorando para fins de otimização o restante do
programa. Por essa razão muitos compiladores oferecem opções de compilação que podem
ser ligadas e desligadas no código fonte, indicando para o compilador os trechos que o
programador considera importantes o suficiente para serem otimizados. Por estas razões,
muito do trabalho no desenvolvimento de técnicas de otimização é dedicado aos loops.

Técnicas para geração de código


o Procedimentos/funções de geração
o Baseados na árvore sintática
o Busca em pós-ordem
ou
o Ações equivalentes durante a análise sintática se a árvore não for gerada
explicitamente
o Ad hoc

o Amarrado aos procedimentos sintáticos

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

Como fazer a geração de código?

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)

Exemplo 3: Uso de endereços e códigos.


A partir do conjunto de instruções da seção 5.1.1 pode-se observar que a cada instrução
temos associado um mnemônico (que é o que temos usado até aqui) e um código. Na
realidade, o computador entende apenas o código, logo o mesmo é que vai estar presente após
a geração do executável.
Por outro lado, o exemplo anterior introduziu o problema de como localizar endereços das
variáveis presentes em cada instrução. Este problema é resolvido mais adiante, através da
tabela de símbolos, assim nos concentraremos neste exemplo a mostrar como poderia seria o
código sem o uso dos mnemônicos. Para tanto considere a resolução da atribuição a = a-1
28 a % armazena a posição de a na memória para o retorno de seu valor final
28 a % armazena a posição de a na memória para obter seu valor inicial
27 % obtém o valor de a
28 1 % coloca 1 na pilha
20 % nega o valor no topo da pilha (obtém -1)
12 % soma a + (-1)
26 % armazena o novo valor de a Destes exemplos já podemos vislumbrar algumas
rotinas a serem seguidas sempre. Uma delas é o fato de que se existir uma atribuição, temos
que construir um preâmbulo em que será armazenado na pilha o endereço de retorno e um
desfecho, fazendo de fato esta operação, deixando então a pilha vazia. A expressão cujo
resultado vai ser atribuído fica então entre estas duas instruções.
Além disso, como não existe a operação de subtração, sempre que quizermos executá-la
teremos que executar a operação de Negate sobre o subtrator, para então somá-lo ao
subtraendo. Da mesma forma, a operação de divisão não pode ser executada com apenas uma
única instrução, aliás, neste caso nem podemos fazê-la com poucas instruções, pois o único
método disponível passa a ser o das subtrações sucessivas.

Geração de código final

A linguagem utilizada para a geração de um código em formato intermediário entre a


linguagem de alto nível e a linguagem assembly deve representar, de forma independente do
processador para o qual o programa será gerado, todas as expressões do programa original.
Duas formas usuais para esse tipo de representação são a notação pós fixa e o código de três
endereços.
A fase de geração de código final é a última fase da compilação. A geração de um bom
código objeto é difícil devido aos detalhes particulares das máquinas para os quais o código é
gerado. Contudo, é uma fase importante, pois uma boa geração de código pode ser, por
exemplo, duas vezes mais rápida que um algoritmo de geração de código ineficiente. Nem
todas as técnicas de optimização são independentes da arquitetura da máquina-alvo.
Otimizações dependentes da máquina necessitam de informações tais como os limites e os
recursos especiais da máquina-alvo a fim de produzir um código mais compacto e eficiente.
O código produzido pelo compilador deve se aproveitar dos recursos especiais de cada
máquina-alvo. Segundo Aho, o código objeto pode ser uma sequência de instruções absolutas
de máquina, uma sequência de instruções de máquina relocáveis, um programa em linguagem
assembly ou um programa em outra linguagem.

Tipo de Código Final


O tipo de código final de um compilador, pode ser classificado como absoluto, relocatável ou
código assembly [ASU86].
O código absoluto é uma representação em binário que utiliza endereços relativos, mas fixos,
ou seja, as referências às posições de memória são feitas sempre através de um offset em
relação à posição inicial da memória reservada para a execução do programa. A desvantagem
principal encontra-se na utilização de referências fixas, que impede a compilação de um
programa em módulos separados. Este pode, por outro lado, ser diretamente carregado em
memória e executado.
O código relocatável também é do tipo binário, no entanto, permite que os endereços
funcionem como offsets relativos a uma referência externa, podendo ser posteriormente
recalculados de forma a determinar a sua posição correta. É este o tipo de código gerado por
linguagens que permitam desenvolver aplicações em módulos separados, onde no fim é
imprescindível realizar o processo de linkagem, de forma reunir todos os módulos num único
executável, procedendo-se nesta última fase ao ajuste dos endereços.
Este tipo de código facilita o desenvolvimento de aplicações, uma vez que permite a
reutilização de módulos pré-compilados, como é o caso das bibliotecas. No entanto, a geração
de código relocatável, obriga o compilador a gerar informação extra, essencial para que o
linker seja capaz de recalcular os endereços. Para além disso, acresce a fase de linkagem ao
processo de geração de um executável.
A geração de assembler não produz diretamente código binário, pelo que é necessário correr
um assemblador para obter o código final, o que por si só aumenta o tempo de geração do
executável. No entanto, esta representação é mais fácil de gerar, uma vez que não se depara
com problemas de endereçamento, como por exemplo, determinar o endereço das variáveis
ou a posição das labels.
Código de três endereços
O código de três endereços é composto por uma seqüência de instruções envolvendo
operações binárias ou unárias e uma atribuição. O nome “três endereços” está associado à
especificação, em uma instrução, de no máximo três variáveis: duas para os operadores
binários e uma para o resultado. Assim, expressões envolvendo diversas operações são
decompostas nesse código em uma série de instruções, eventualmente com a utilização de
variáveis temporárias introduzidas na tradução. Dessa forma, obtém-se um código mais
próximo da estrutura da linguagem assembly (Seção 10.1) e, conseqüentemente, de mais fácil
conversão para a linguagem-alvo.
Uma possível especificação de uma linguagem de três endereços envolve quatro tipos básicos
de instruções: expressões com atribuição, desvios, invocação de rotinas e acesso indexado e
indireto.

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.

Por exemplo, a seguinte iteração em C

while (i++ <= k)


x[i] = 0;
x[0] = 0;
poderia ser traduzida para
_L1: if i > k goto _L2
i := i + 1
x[i] := 0
goto _L1
_L2: x[0] := 0

A invocação de rotinas ocorre em duas etapas. Inicialmente, os argumentos do procedimento


são “registrados” com a instrução param; após a definição dos argumentos, a instrução call
completa a invocação da rotina. A instrução return indica o fim de execução de uma rotina.
Opcionalmente, esta instrução pode especificar um valor de retorno, que pode ser atribuído
na linguagem intermediária a uma variável como resultado de call.
Por exemplo, considere a chamada de uma função f que recebe três argumentos e retorna um
valor:
f(a, b, c);
Neste exemplo em C, esse valor de retorno não é utilizado. De qualquer modo, a expressão
acima seria traduzida para
parametro A
parametro B
parametro C
_t1 := call f,3
onde o número após a vírgula indica o número de argumentos utilizados pelo procedimento f.
Com o uso desse argumento adicional é possível expressar sem dificuldades as chamadas
aninhadas de procedimentos.
O último tipo de instrução para códigos de três endereços refere-se aos modos de
endereçamento indexado e indireto. Para atribuições indexadas, as duas formas básicas são
x := y[i]
x[i] := y
As atribuições associadas ao modo indireto permitem a manipulação de endereços e seus
conteúdos. As instruções em formato intermediário também utilizam um formato próximo
àquele da linguagem C:
x := &y
w := *x

*x := z

A representação interna das instruções em códigos de três endereços dá-se na forma de


armazenamento em tabelas com quatro ou três colunas. Na abordagem que utiliza
quádruplas (as tabelas com quatro colunas), cada instrução é representada por uma linha na
tabela com a especificação do operador, do primeiro argumento, do segundo argumento e do
resultado. Por exemplo, a tradução da expressão a=b+c*d; resultaria no seguinte trecho da
tabela:
operador arg 1 arg 2 resultado 1 * c d _t1
2 + b _t1 a
Para algumas instruções, como aquelas envolvendo operadores unários ou desvio
incondicional, algumas das colunas estariam vazias.
Na outra forma de representação, por triplas, evita a necessidade de manter nomes de
variáveis temporárias ao fazer referência às linhas da própria tabela no lugar dos argumentos.
Nesse caso, apenas três colunas são necessárias, uma vez que o resultado está sempre
implicitamente associado à linha da tabela. No mesmo exemplo apresentado para a
representação interna por quádruplas, a representação por triplas seria operador arg 1 arg 2
1*cd
2 + b (1)

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+*

1O criador desse tipo de notação, J. Lukasiewicz, era polonês.

Instruções de desvio em código intermediário usando a notação posfixa assumem a forma L


jump x y L jcc para desvios incondicionais e condicionais, respectivamente. No caso de um
desvio condicional, a condição a ser avaliada envolvendo x e y é expressa na parte cc da
própria instrução. Assim, jcc pode ser uma instrução entre jeq (desvio ocorre se x e y forem
iguais), jne (se diferentes), jlt (se x menor que y), jle (se x menor ou igual a y), jgt (se x maior
que y) ou jge (se x maior ou igual a y).
Expressões em formato intermediário usando a notação posfixa podem ser eficientemente
avaliadas em máquinas baseadas em pilhas, também conhecidas como máquinas de zero
endereços. Nesse tipo de máquinas, operandos são explicitamente introduzidos e retirados do
topo da pilha por instruções push e pop, respectivamente.
Além disso, a aplicação de um operador retira do topo da pilha seus operandos e retorna ao
topo da pilha o resultado de sua aplicação.
Por exemplo, a avaliação da expressão a*(b+c) em uma máquina baseada em pilha poderia
ser traduzida para o código
push a
push b
push c
add
mult

Transformação de código fonte em assembly

Para construirmos os programas em Assembly, devemos estruturar o fonte da seguinte forma


(usando TASM como montador).

.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

Como um exemplo asseguir temos um programa em assembly que muda o tamanho do


cursor.

.MODEL SMALL ;modelo de memória


.STACK ;espaço de memória para instruções do programa na pilha
.CODE ;as linhas seguintes são instruções do programa
mov ah,01h ;move o valor 01h para o registrador ah
mov cx,07h ;move o valor 07h para o registrador cx
int 10h ;interrupção 10h
mov ah,4ch ;move o valor 4ch para o registrador ah
int 21h ;interrupção 21h
.DATA
x db 1
END ;finaliza o código do programa

Tabela de equivalência de código assembly

Assembly ou linguagem de montagem é uma notação legível por humanos para o código de


máquina que uma arquitetura de computador específica usa, utilizada para programar códigos
entendidos por dispositivos computacionais, como microprocessadores e microcontroladores.
O código de máquina torna-se legível pela substituição dos valores em bruto por símbolos
chamados mnemónicos.

Por exemplo, enquanto um computador sabe o que a instrução-máquina IA-32 ( B0 61 ) faz,


para os programadores é mais fácil recordar a representação equivalente em

instruções mnemónicas  MOV AL, 61h . Tal instrução ordena que o valor hexadecimal 61


(97, em decimal) seja movido para o registrador 'AL'. Embora muitas pessoas pensem no
código de máquina como valores em binário, ele é normalmente representado por valores em
hexadecimal.

A tradução do código Assembly para o código de máquina é feita pelo montador


ou assembler. Ele converte os mnemónicos em seus respectivos opcodes, calcula os
endereços de referências de memória e faz algumas outras operações para gerar o código de
máquina que será executado pelo computador.

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

Um programa em código de máquina consiste de uma sequência de bytes que correspondem a


instruções a serem executadas pelo processador. As instruções do processador, chamadas
de opcodes, são representadas por valores em hexadecimal.

Código de máquina é um programa de computador escrito em linguagem de


máquina instruções que podem ser executados diretamente por um computador de unidade de
processamento central (CPU). Cada instrução faz com que a CPU para executar uma tarefa
muito específica, como uma carga, uma loja, um salto , ou um ALU operação em uma ou
mais unidades de dados em registros de CPU ou memória.
Código de máquina é uma linguagem estritamente numérica que se destina a correr tão rápido
quanto possível, e pode ser considerado como a representação de nível mais baixo de
um compilado ou montado programa de computador ou como um primitivo
e hardware dependente de linguagem de programação . Embora seja possível escrever
programas diretamente em código de máquina, é tedioso e propenso a erros para gerenciar
bits individuais e calcular endereços numéricos e constantes manualmente. Por esta razão, os
programas são muito raramente escritos diretamente em código de máquina em contextos
modernos, mas pode ser feito por baixo nível de depuração , o programa de patching ,
e linguagem de montagem desmontagem .

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

Cada processador ou processador família tem seu próprio conjunto de instruções . Instruções


são padrões de bits de que pelo projeto físico correspondem a diferentes comandos para a
máquina. Assim, o conjunto de instruções é específico para uma classe de processadores
usando (principalmente) a mesma arquitetura. Sucessores ou processador derivado projetos
geralmente incluem todas as instruções de um antecessor e pode adicionar instruções
adicionais. Ocasionalmente, um projeto sucessor irá descontinuar ou alterar o significado de
alguns códigos de instrução (normalmente porque ele é necessário para novos fins), afetando
compatibilidade de código até certo ponto; mesmo quase completamente processadores
compatíveis podem mostrar um comportamento ligeiramente diferente para algumas
instruções, mas isso raramente é um problema. Os sistemas podem também diferir em outros
detalhes, tais como o arranjo de memória, sistemas operativos, ou dispositivos periféricos .
Porque um programa baseia-se normalmente em tais factores, diferentes sistemas tipicamente
não irá executar o mesmo código de máquina, mesmo quando o mesmo tipo de processador é
usado.

Conjunto de instruções de um processador pode ter todas as instruções do mesmo


comprimento, ou pode ter instruções de comprimento variável. Como os padrões são
organizados varia fortemente com a arquitetura particular e muitas vezes também com o tipo
de instrução. A maioria das instruções têm um ou mais opcode campos que especifica o tipo
de instruções básicas (tais como aritmética, lógica, salto , etc.) e a operação real (como
adicionar ou comparar) e outros campos que podem dar o tipo do operando (s ), o modo de
endereçamento (s), o endereçamento de deslocamento (s) ou índice, ou o próprio valor real
(tais operandos constante contida numa instrução são chamados imediatos ).

Nem todas as máquinas ou instruções individuais têm operandos explícitas. Uma máquina


acumulador tem um operando esquerdo combinadas e resultar num acumulador implícito
para a maioria das operações aritméticas. Outras arquiteturas (como o 8086 eo 86-família)
têm versões acumulador de instruções comuns, com o acumulador considerado como um dos
registos gerais de instruções mais longas. Uma máquina de pilha tem a maioria ou todos os
seus operandos em uma pilha implícita. Instruções de uso especial também muitas vezes não
têm operandos explícitas (CPUID na arquitetura x86 escreve valores em quatro registradores
de destino implícitas, por exemplo). Esta distinção entre operandos explícitas e implícitas é
importante em geradores de código, especialmente na alocação de registradores e peças de
rastreamento alcance ao vivo. Um bom optimizador de código pode acompanhar operandos
implícitas, bem como explícitos que pode permitir que mais frequente constante de
propagação , de dobragem constante de registos (um registo atribuído o resultado de uma
expressão constante libertou-se, substituindo-o por isso constante) e outras melhorias de
código.

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.

Programação em código de maquina

Para se programar em código de máquina, deve-se obter os códigos de instruções do


processador utilizado contendo opcodes, operandos e formatos de cada instruçã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.

Essas informações escritas em Assembly seguindo a sintaxe da Intel, ficam no seguinte


formato:

mov dword [ebp + ebx*4], eax

Onde ebx é o registrador index, ebp é o registrador base e 4 a escala. Onde escala pode ser os
valores 1, 2, 4 ou 8.

 Algumas formas de endereçamento precisam de um deslocamento imediato que fica


logo após o byte ModR/M, ou o byte SIB se estiver presente. Caso um deslocamento
imediato seja necessário, este pode ter um tamanho de um, dois ou quatro bytes.
 Se uma instrução específica um operando imediato, o operando fica após todos os
bytes de deslocamento. E pode ter o tamanho de um, dois ou quatro bytes.

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.

Visualização de programas em linguagem de máquinas

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.

Exemplo de programa "Olá Mundo" para MS-DOS

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

A arquitetura MIPS fornece um exemplo específico para um código de máquina cujas


instruções são sempre 32 bits de comprimento. O tipo geral da instrução é dada
pelo op campo (operação), os 6 bits mais altos. J-tipo (salto) e I-tipo instruções (imediatas)
está completamente especificado por op . R-tipo (registo) instruções incluem um campo
adicional funct para determinar o funcionamento exacto. Os campos usados nestes tipos são:
6 5 5 5 5 6 bits
[ op | rs | rt | rd |shamt| funct] R-type
[ op | rs | rt | address/immediate] I-type
[ op | target address ] J-type

RS , RT , e Rd indicam registar operandos; shamt dá a quantidade de deslocamento;


eo endereço ou imediatos campos contêm um operando diretamente.

Por exemplo, somando os registos 1 e 2 e colocando o resultado em registo 6 é codificado:

[ 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

Saltar para o endereço de 1024:

[ op | target address ]
2 1024 decimal
000010 00000 00000 00000 10000 000000 binary

Relação com microcódigo

Em algumas arquiteturas de computadores , o código de máquina é implementado por uma


camada subjacente ainda mais fundamental chamada microcódigo , fornecendo uma interface
de linguagem de máquina comum através de uma linha ou a família de diferentes modelos de
computador com amplamente diferentes subjacentes fluxos de dados . Isto é feito para
facilitar a portabilidade de programas em linguagem de máquina entre os diferentes modelos.
Um exemplo deste uso é o IBM System / 360 família de computadores e seus sucessores.
Com larguras de caminho de fluxo de dados de 8 bits para 64 bits de e para além destes, que,
no entanto, apresentar uma arquitectura comum no nível da linguagem máquina ao longo de
toda a linha.

Usando microcódigo para implementar um emulador permite que o computador para


apresentar a arquitetura de um computador completamente diferente. A linha System / 360
usado isso para permitir a portabilidade programas de máquinas IBM anteriores para a nova
família de computadores, por exemplo, um IBM 1401/1440/1460 emulador do modelo IBM S
/ 360 40.

Relação com Bytecode

Código máquina é geralmente diferente do código de bytes (também conhecido como p-


código), que ou é executado por um interpretador ou o próprio compilado em código de
máquina para uma execução mais rápida (directa). Uma excepção é quando um processador é
concebido para usar um código de bytes determinado directamente como seu código de
máquina, como é o caso com os processadores Java .

Código de máquina e código de montagem às vezes são chamados nativo código quando se


refere a peças dependentes de plataforma de recursos de linguagem ou bibliotecas.

Armazenar na memória

A arquitetura Harvard é uma arquitetura de computador com caminhos separados fisicamente


de armazenamento e de sinal para o código (instruções) e dados . Hoje, a maioria dos
processadores implementar tais vias de sinalização separados por motivos de desempenho,
mas na verdade implementar uma arquitetura Harvard modificada , para que eles possam
suportar tarefas como carregar um executável do programa de armazenamento em
disco como dados e, em seguida, executá-lo. Arquitetura Harvard é contrastada com
a arquitetura de Von Neumann , onde os dados e código são armazenados na mesma memória
que é lido pelo processador permitindo que o computador para executar comandos.

Do ponto de vista de um processo , o espaço de código é parte de seu espaço de


endereços onde o código em execução é armazenado. Em multitarefa sistemas deste
compreende o programa do segmento de código e, geralmente, bibliotecas compartilhadas .
Em vários segmentos ambiente, diferentes fios de um processo de espaço de código partes,
juntamente com o espaço de dados, o que reduz a sobrecarga de comutação de
contexto consideravelmente em comparação com comutação processo.

Legibilidade por seres humanos


Pamela Samuelson escreveu que o código de máquina é tão ilegível que o Escritório de
Direitos Autorais dos Estados Unidos não pode identificar se um programa codificado em
particular é um trabalho original de autoria; no entanto, o US Copyright Office que permitem
o registro de direitos autorais de programas de computador e código de máquina de um
programa às vezes pode ser compilado de forma a tornar o seu funcionamento mais
facilmente compreensível para os seres humanos.

Professor de ciência cognitiva Douglas Hofstadter comparou o código de máquina de código


genético , dizendo que "Olhando para um programa escrito em linguagem de máquina é
vagamente comparável a olhar para um DNA átomo molécula por átomo".

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

Você também pode gostar