Apostila
Apostila
Apostila
CENTRO DE TECNOLOGIA
DEPARTAMENTO DE ELETRÔNICA E COMPUTAÇÃO
CURSO DE ENGENHARIA ELÉTRICA
ORGANIZAÇÃO DE
COMPUTADORES
Organização
Prof. Leandro Michels
1
1 – Introdução aos Sistemas Computacionais
1.1. Processamento de dados
Um “dado” é qualquer característica de um objeto, ser ou sistema que possa ser registrado.
Por exemplo: uma sala de aula contém um determinado número de alunos. Esse número é um “dado” . O
número de lugares (carteiras) existentes na sala também é um “dado” . Assim como são “dados” a idade,
data de nascimento, nome, endereço e demais características de cada aluno. É claro que todos esses dados
podem ser coletados e armazenados com ou sem a ajuda de um computador. Por exemplo: podem ser
coletados através de observação direta e do preenchimento de questionários e podem ser armazenados
anotando os resultados em fichas que seriam arquivadas (armazenadas) em pastas. Esses são os
chamados “dados brutos”. Dados brutos, se manejados convenientemente e organizados segundo certas
diretrizes (ou seja, se submetidos a um “processo”), podem servir de base a tomadas de decisão.
Mas o que é um “processo”? Segundo o dicionário Houaiss, um dos significados do termo
(justamente aquele que nos interessa) é “modo de fazer alguma coisa, método, maneira, procedimento”.
Então, entende-se por “processo” um procedimento que consiste, por exemplo, em aplicar operações
aritméticas ou lógicas (como comparações) aos dados brutos coletados.
No contexto da sala de aula que estávamos discutindo, poderemos somar os dados referentes às
idades de cada aluno e dividir o total pelo número de alunos da sala (ou seja, efetuar um procedimento
que consiste em aplicar duas operações aritméticas aos dados brutos, portanto um “processo”) para obter
a idade média dos alunos daquela sala. Esta idade média, resultado deste “processo” aplicado aos dados,
é uma “informação”.
Portanto, “informação” é aquilo que se obtém ao processar dados, o resultado do processamento
de dados, um elemento que pode ser usado para tomar decisões. A idade média dos alunos, por exemplo,
pode ser usada para decidir o tamanho das carteiras e do restante do mobiliário das salas.
Outro exemplo de informação sendo usada em um processo decisório: se dividirmos o número de
alunos que ocuparão a sala pelo número de carteiras existentes na sala obteremos um novo número
fracionário (entre zero e um, presumindo-se que o número de carteiras seja igual ou maior que o de
alunos) que representa o coeficiente de ocupação da sala. Esse coeficiente (que é uma informação, já que
resultou de uma operação aplicada aos dados brutos, ou de um “processamento” de dados), pode orientar
os administradores da escola na distribuição de novos alunos pelas salas já ocupadas.
Então, existe uma diferença essencial entre um dado e uma informação. Um “dado” é uma
característica qualquer obtida diretamente de um objeto, um ser ou um sistema. Uma “informação” é a
conseqüência do processamento aplicado a esses dados, ou seja, é o resultado dos dados trabalhados e
organizados. E “processar dados” consiste em aplicar aos dados um conjunto de operações lógicas e
matemáticas que produzam uma informação que pode ser usada para tomar decisões.
Como você vê, o conceito de “processamento de dados” é muito simples. E é fácil concluir que
não é imprescindível o uso de máquinas para processar dados. Nos exemplos citados, qualquer pessoa
que saiba efetuar as operações aritméticas necessárias pode transformar dados em informações, portanto
“processá-los”. A diferença é que efetuar este processamento de dados usando um computador,
especialmente nos casos em que ele exige o encadeamento de operações complexas e utiliza grandes
quantidades de dados brutos, faz com que o resultado seja obtido mais rapidamente e menos passível de
erros.
Algo parecido acontece com muitos dos artefatos usados no mundo moderno. Por exemplo:
roupas podem ser feitas manualmente, com a ajuda apenas de tesoura, agulha, linha e alguma perícia da
parte de quem as faz. Uma boa costureira pode produzir diariamente um número limitado de camisas.
2
Mas a moderna indústria têxtil, com suas máquinas pesadas, pode produzir dezenas de milhares delas no
mesmo período.
A função de um computador é, então, processar dados. Para isso é necessário obter os dados
brutos no “mundo exterior”, ou seja, fora do computador, introduzi-los no computador, armazená-los
enquanto aguardam o processamento, efetuar as operações que consistem neste processamento e
armazenar ou encaminhar para o mundo exterior os resultados parciais e finais do processamento.
A computação pode ser definida como a busca de uma solução para um problema, a partir de
entradas (inputs), e através de um algoritmo. É com isto que lida a teoria da computação, subcampo da
ciência da computação e da matemática. Durante milhares de anos, a computação foi executada com
caneta e papel, ou com giz e ardósia, ou mentalmente, por vezes com o auxílio de tabelas.
3
Passo 2: Obtêm-se o valor de x;
Passo 3: Calcula-se: aux1=x*x;
Passo 4: Calcula-se: aux1=x*aux1;
Passo 5: Calcula-se: aux1=x*aux1;
Passo 6: Calcula-se: aux1=x*aux1;
Passo 7: Calcula-se: aux1=x*aux1;
Passo 8: Calcula-se: aux1=aux1*5;
Passo 9: Armazena-se o valor de aux1;
Passo 10: Calcula-se: aux2=3*x;
Passo 11: Calcula-se: y=aux1+aux2;
Passo 12: Calcula-se: y=y+1;
Programa 2: Implementação em calculadora científica
Instruções disponíveis: Além das anteriores, potenciação(^), logaritmo (log), logaritmo natural (ln),
exponenciação (exp)
Passo 1: Define-se variáveis auxiliares: aux1, aux2, aux3, aux4;
Passo 2: Obtêm-se o valor de x;
Passo 3: Calcula-se: aux1=x6;
Passo 4: Calcula-se: aux1=aux1*5;
Passo 5: Armazena-se o valor de aux1;
Passo 6: Calcula-se: aux2=3*x;
Passo 7: Calcula-se: y=aux1+aux2;
Passo 8: Calcula-se: y=y+1;
Observe que o algoritmo é baseado num conjunto de comandos lógicos e naturais aos seres
humanos. Contudo, os sistemas computacionais podem realizar um conjunto restrito de comandos (ou
instruções), que dependente do sistema físico disponível. Já o programa é uma seqüência lógica de
instruções que executa um determinado algortimo com um determinado conjunto de instruções. Observe
que embora os dois programas sejam distintos, ambos executam a mesma operação. Também pode ser
observar que o programa feito para a calculadora científica não funciona na calculadora convencional,
pois esta não dispõe das instruções de potenciação. Por outro lado, o programa da calculadora
convencional funciona também para a calculadora científica, ou seja, o programa daquela é compatível
com esta.
4
Contudo, as intruções utilizadas nos computadores são muito diferentes das instruções utilizadas nos
algoritmos. Por este motivo, desenvolveu-se as linguagens de programação, que são métodos
padronizados para expressar instruções para um computador. É um conjunto de regras sintáticas e
semânticas usadas para definir um programa de computador. Uma linguagem permite que um
programador especifique precisamente sobre quais dados um computador vai atuar, como estes dados
serão armazenados ou transmitidos e quais ações devem ser tomadas sob várias circunstâncias.
O conjunto de palavras (tokens), compostos de acordo com essas regras, constituem o código
fonte de um software. Esse código fonte é depois traduzido (através de um programa chamado
compilador) para código de máquina, que é executado pelo processador.
Existem, basicamente, dois tipos de linguagem de programação:
• Linguagem de baixo nível: são aquelas muito próximas das instruções da máquina. Conhecidas
como assembly (montagem) ou linguagem da máquina, exigem do programador o conhecimento
do hardware do sistema computacional;
• Linguagem de alto nível: São aquelas cujo nível de abstração é relativamente elevado, longe do
código de máquina e mais próximo à linguagem humana.
Exemplo: Algoritmo para executar a seguinte operação: y = (m1*m2) / 16
mov eax,m1
mov ebx,m2
imul ebx
shrd eax,edx,16
ret
ENDP
O uso de linguagem de alto nível permitem expressar suas intenções mais facilmente do que
quando comparado com a linguagem que um computador entende nativamente (código de máquina).
Assim, linguagens de programação são projetadas para adotar uma sintaxe de nível mais alto, que pode
ser mais facilmente entendida por programadores humanos. Linguagens de programação são ferramentas
importantes para que programadores e engenheiros de software possam escrever programas mais
organizados e com maior rapidez.
Um programa é feito usando linguagens de programação, ou através da manipulação direta das
instruções do processador. Qualquer computador moderno tem uma variedade de programas que fazem
diversas tarefas. Eles podem ser classificados em duas grandes categorias:
• Software de sistema: que incluiu o firmware (A BIOS dos computadores pessoais, por
exemplo), drivers de dispositivos, o sistema operacional e tipicamente uma interface gráfica que,
em conjunto, permitem ao usuário interagir com o computador e seus periféricos
5
• Software aplicativo: que permite ao usuário fazer uma ou mais tarefas específicas. Os softwares
aplicativos podem ter uma abrangência de uso de larga escala, muitas vezes em âmbito mundial;
nestes casos, os programas tendem a ser mais robustos e mais padronizados. Programas escritos
para um pequeno mercado têm um nível de padronização menor.
Normalmente, programas de computador são escritos em linguagens de programação e nunca em
linguagem de máquina. Os algoritmos e das linguagens de programação são objeto de estudo da ciência
da computação. Já a linguaguem de montagem e o funcionamento do hardware são objetos de estudo da
engenharia de computação, uma sub-área da engenharia elétrica.
6
show” e televisores. O conjunto de todos esses dispositivos forma uma unidade funcional conhecida
como “Dispositivos de Entrada e Saída”, E/S ou I/O (do inglês “Input/Output”).
Dados introduzidos no computador precisam de ser transportados ou transferidos entre seus
componentes internos. Esse transporte de dados é feito através de um ou mais dispositivo de
interconexão de dados denominado “barramento”. Como veremos adiante, dados serão codificados de tal
maneira que podem ser transportados sob a forma de pulsos elétricos (se isso lhe parece complicado,
espere um pouco que logo discutiremos a “digitalização de dados” e a tudo ficará mais claro). Então, para
transportar dados basta um conjunto de condutores elétricos que serão percorridos por tais pulsos e um
conjunto de circuitos eletrônicos que controlarão esse movimento de dados. “Barramento” é então o
“conjunto de condutores elétricos que transportam os dados entre os componentes internos de um
computador e seus circuitos de controle”.
Depois de introduzidos, os dados podem ser encaminhados diretamente à Unidade Central de
Processamento (UCP ou CPU, sigla da expressão em inglês “Central Processing Unit”) para serem
processados ou armazenados na Memória Principal para processamento posterior.
A “memória principal” é um dispositivo capaz de armazenar dados e restitui-los quando
necessário. Pode ser encarada como um conjunto (que pode ser muito grande) de “posições de memória”,
cada uma capaz de armazenar um número (com veremos adiante, todos os dados inseridos em um
computador foram antes convertidos em números através do procedimento denominado “digitalização de
dados”). Cada posição da memória principal é identificada por um “endereço” único (esse endereço
também é um número). Pode-se então recuperar qualquer dado armazenado na memória principal
conhecendo-se o endereço onde ele foi armazenado.
O componente restante, a Unidade Central de Processamento, é o mais importante de todo o
conjunto. Ele é um circuito integrado razoavelmente complexo, capaz de efetuar todas as operações
necessárias ao processamento de dados. Como seu nome indica, é ali que os dados serão processados.
Mais adiante discutiremos seus detalhes de funcionamento e veremos que embora o circuito integrado
seja efetivamente muito complexo, seu princípio de funcionamento é bastante simples (como tudo no
campo da informática, onde toda a complexidade é obtida combinando um imenso número de ações que,
tomadas individualmente, são de extrema simplicidade).
Um computador, então, é formado por três elementos principais, Dispositivos de Entrada e Saída,
Unidade Central de Processamento e Memória Principal, interligados e trocando dados entre si através de
um dispositivo de interconexão denominado Barramento.
Assim, os quatro elementos que constituem um computador são:
• Dispositivos de Entrada e Saída;
• Unidade Central de Processamento;
• Memória Principal e
• Barramento.
7
controle são igualmente executadas através de pulsos de corrente, ou “sinais”, propagados em condutores
elétricos (estes pulsos, ou sinais, são interpretados pelos componentes ativos, fazendo-os atuar ou não
dependendo da presença ou ausência dos sinais). Portanto estas duas funções, transporte e controle, não
exigem o concurso de componentes ativos para sua execução, basta a existência de condutores elétricos
(fios, cabos, filetes metálicos nas placas de circuito impresso, etc.).
Processar dados consiste basicamente em tomar decisões lógicas do tipo “faça isso em função
daquilo”. Por exemplo: “compare dois valores e tome um curso de ação se o primeiro for maior, um
curso diferente se ambos forem iguais ou ainda um terceiro curso se o primeiro for menor”. Todo e
qualquer processamento de dados, por mais complexo que seja, nada mais é que uma combinação de
ações elementares baseadas neste tipo de tomada de decisões simples. O circuito eletrônico elementar
capaz de tomar decisões é denominado “porta lógica” (logical gate).
Armazenar dados consiste em manter um dado em um certo local enquanto se precisar dele, de tal
forma que possa ser recuperado quando necessário. Dados podem ser armazenados de diferentes formas.
Nos computadores, como veremos em seguida, são armazenados sob a forma de uma combinação de
elementos que podem assumir apenas os valores numéricos “um” ou “zero”, ou os valores lógicos
equivalentes, “verdadeiro” ou “falso”. Para armazenar um desses elementos é necessário apenas um
dispositivo capaz de assumir um dentre dois estados possíveis (um deles representando o “um” ou
“verdadeiro”, o outro representando o “zero”, ou “falso”), manter-se nesse estado até que alguma ação
externa venha a alterá-lo (portanto, ser “bi-estável”), permitir que este estado seja alterado (pelo menos
uma vez) e permitir que esse estado seja “lido” ou “recuperado”. Um dispositivo que exiba estas
características denomina-se “célula de memória”. O circuito eletrônico elementar capaz de armazenar
dados é a célula de memória.
Tendo isto em vista, pode-se concluir que todo computador digital, por mais complexo que seja,
pode ser concebido como uma combinação de um número finito de apenas dois dispositivos básicos:
portas lógicas e células de memória, interligados por condutores elétricos.
Por enquanto, os sistemas dos computadores operam usando a lógica binária. O computador
representa valores usando dois níveis de voltagem, geralmente 0 Volts e +5 Volts. Com estes dois níveis
podemos representar dois valores diferentes, que poderiam ser quaisquer valores, mas convencionou-se
que representassem 0 (zero) e 1 (um). Estes dois valores, por coincidência, correspondem aos dois dígitos
usados pelo sistema de numeração binário. Unindo o útil ao agradável, basta transportar nossos
conhecimentos sobre o sistema decimal para o sistema binário.
As posições dos dígitos são numeradas da direita para a esquerda de 0 (zero) até a posição do
último dígito da esquerda e são a potência de 10 que multiplica o dígito. Se o valor possuir casas
decimais, como por exemplo 123.45, cada casa após a vírgula é numerada de -1 até a posição do último
dígito à direita e são a potência de 10 que multiplica o dígito. Portanto
8
(1 x 10^2) + (2 x 10^1) + (3 x 10^0) + (4 x 10^-1) + (5 x 10^-2)
ou seja, 100 + 20 + 3 + 0.4 + 0.05 = 123.45
Assim como nos números decimais, os zeros colocados à esquerda de dígitos binários não são
significantes. Podemos colocar infinitos zeros à esquerda de um número binário e, nem assim, seu valor
se modifica. Veja o número binário 101, que corresponde a 5 decimal:
Como ficou convencionado de que 1 byte possui oito bits, e um bit (derivado de binary digit)
representa um dígito binário, vamos adotar a convenção de grafá-los sempre em múltiplos de quatro
casas. Por exemplo, o decimal 5 poderá ser grafado como 0101, 00000101, 000000000101 ou mesmo
0000000000000101. No sistema decimal costumamos separar os dígitos em grupos de três: 1.748.345 é
mais legível que 1748345. No sistema binário agruparemos os dígitos em grupos de quatro, também para
melhorar a legibilidade. Por exemplo, 0000000000000101 será grafado 0000 0000 0000 0101.
Os dígitos dos números binários são numerados da direita para a esquerda iniciando-se com 0
(zero). Na verdade, esta numeração indica a potência de 2 do dígito em questão. Veja abaixo:
O bit na posição 0 (zero) é denominado de bit de ordem baixa (low order) ou menos
significativo. O bit na extremidade esquerda é denominado de bit de ordem alta (high order) ou mais
significativo.
Na matemática pura, qualquer valor pode ter um número infinito de dígitos (lembrando que os
zeros colocados à esquerda não alteram o valor do número). Com os computadores a coisa é um pouco
diferente, pois trabalham com um número específico de bits. Os grupos de dígitos binários mais
comumente utilizados pelos computadores são: bits únicos, grupos de 4 bits - chamados de nibble, grupos
de 8 - chamados de byte, grupos de 16 - chamados de word (word = palavra), etc. A razão da existência
destes grupos é funcional, característica dos chips 80x86.
O bit
A menor "unidade" de dados num computador binário é um bit. Como um único bit consegue
representar unicamente dois valores diferentes (tipicamente zero ou um), fica a impressão de que um bit
só consegue representar um número muito limitado de itens. Não é bem assim.
O que precisa ficar claro é a dualidade do bit. Esta dualidade pode se referir a itens de um mesmo
tipo ou a itens de natureza completamente diferente. Um bit pode representar 0 ou 1, verdadeiro ou falso,
ligado ou desligado, masculino ou feminino, certo ou errado. Um bit também pode representar quaisquer
dois valores (como 589 ou 1325) ou duas cores (como azul ou vermelho). Nada impede de que um bit
represente dois itens de natureza distinta, como 589 ou vermelho. Podemos representar qualquer par de
itens com um bit, mas apenas um único par de itens.
A coisa pode ficar ainda mais complexa quando bits diferentes representarem itens diferentes. Por
exemplo, um bit pode representar os valores 0 ou 1, enquanto outro bit adjacente pode representar os
valores falso ou verdadeiro. Olhando apenas para os bits, não é possível reconhecer a natureza do que
9
estejam representando. Isto mostra a idéia que está por trás das estruturas de dados num computador:
dados são o que você definiu. Se você usar um bit para representar um valor booleano (falso/verdadeiro),
então este bit, de acordo com a definição que você deu, representa falso ou verdadeiro. Para que o bit
tenha significado, é preciso manter a consistência: se você estiver usando um bit para representar falso ou
verdadeiro, este bit, em todos os pontos do seu programa, deve apenas conter a informação falso ou
verdadeiro; não pode ser usado para representar cores, valores, ou qualquer outro tipo de item.
Como a maioria dos itens que precisam ser representados possuem mais do que dois valores, bits
únicos não são o tipo de dado mais usado. Conjuntos de bits são os tipos mais utilizados em programas e
valem uma análise mais detalhada.
O nibble
Um nibble é um conjunto de quatro bits. Não seria um tipo de dado muito interessante não fosse a
existência de dois itens especiais: números BCD (binary coded decimal) e números hexadecimais. Um
dígito BCD ou um dígito hexadecimal precisa exatamente de quatro bits para ser representado. Com um
nibble podemos representar até 16 valores distintos. No caso dos números hexadecimais, cada um dos
valores 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, A, B, C, D, E e F é representado por quatro bits. Quaisquer 16 valores
distintos podem ser representados por um nibble, mas os mais importantes e conhecidos são os dígitos
BCD e hexadecimais.
O byte
Sem dúvida alguma, a estutura de dados mais importante usada pelo microprocessador 80x86 é o
byte. Um byte é um conjunto de oito bits, o menor item de dado endereçável no 80x86. Isto significa que
o menor item que pode ser acessado individualmente por um programa 80x86 é um valor de oito bits.
Para acessar qualquer coisa menor, é preciso ler o byte que contenha os dados e usar uma máscara para
filtrar os bits desejados. Os bits de um byte também são numerados da direita para a esquerda, de 0 a 7.
O bit 0 é o bit de ordem baixa (O.B.) ou menos significativo e o bit 7 é o bit de ordem alta (O.A.)
ou mais significativo. Os outros bits são referenciados pelos seus números. Observe que um byte possui
dois nibbles.
O nibble com os bits de 0 a 3 é o nibble de ordem baixa (O.B.) ou menos significativo e o nibble
com os bits de 4 a 7 é o nibble de ordem alta (O.A.) ou mais significativo. Como o byte possui dois
nibbles e cada nibble corresponde a um dígito hexadecimal, valores byte são expressos através de dois
dígitos hexadecimais.
Como um byte possui 8 bits, ele pode representar 2^8 = 256 valores diferentes. Geralmente um
byte é utilizado para representar valores numéricos positivos de 0 a 255, valores numéricos com sinal de
-128 a 127, os códigos dos caracteres ASCII e outros tipos especiais de dados não necessitem de mais do
que 256 valores diferentes. Muitos tipos de dados possuem menos do que 256 itens, de modo que oito
bits são suficientes para representá-los.
Uma vez que o 80x86 é uma máquina de bytes endereçáveis, é mais eficiente manipular um byte
completo do que um bit individual ou um nibble. Por esta razão, a maioria dos programadores usam o
byte completo para representar tipos de dados, mesmo quando possuam menos do que 256 itens. Por
exemplo, é comum representar os valores booleanos falso e verdadeiro com 0000 0000 e 0000 0001.
10
O uso mais importante do byte é, provavelmente, para representar um código de caracter. Todos
os caracteres digitados no teclado, mostrados na tela ou impressos numa impressora, possuem um valor
numérico. Para padronizar estes valores, criou-se o conjunto de caracteres ASCII. O conjunto ASCII
básico possui 128 códigos. Os 128 restantes são utilizados com valores para caracteres adicionais como
caracteres europeus, símbolos gráficos, letras gregas e símbolos matemáticos.
O word
O word (palavra) é um grupo de 16 bits, numerados da direita para a esquerda de 0 a 15.
É claro que um word também pode ser dividido em quatro nibbles. O nibble menos significativo
no word, de O.B., é o nibble 0 e o nibble mais significativo no word, de O.A., é o nibble 3.
Com 16 bits é possível obter 2^16 = 65.536 valores diferentes. Estes podem ser valores numéricos
positivos de 0 a 65.535, numéricos com sinal de -32.768 a 32.767 ou qualquer outro tipo de dado que
possua até 65.536 valores. Words são usados principalmente para três tipos de dados: valores inteiros,
deslocamentos (offsets) e valores de segmento.
Words podem representar valores inteiros de 0 a 65.535 ou de -32.768 a 32.767. Valores
numéricos sem sinal são representados pelo valor binário que corresponde aos bits no word. Valores
numéricos com sinal usam a forma de complemento de dois (adiante entraremos em detalhe). Valores de
segmento, que sempre têm comprimento de 16 bits, constituem o endereço de memória de parágrafos do
código, de dados, do segmento extra ou do segmento da pilha.
O double word
O double word (palavra dupla) é o que o nome indica: um par de words. Portanto, um double
word é um conjunto de 32 bits.
11
Double words podem representar todo tipo de coisa. Em primeiro lugar estão os endereços
segmentados. Outro item comumente representado por um double word são os valores inteiros de 32 bits,
que podem ir de 0 a 4.294.967.295, ou números com sinal, que podem ir de -2.147.483.648 a
2.147.483.647. Valores de ponto flutuante de 32 bits também cabem num double word. Na maioria das
vezes, os double words são usados para armazenarem endereços segmentados.
Finalmente, o quarto grupo de 32 códigos de caracteres ASCII são reservdos para os símbolos
alfabéticos minúsculos, cinco símbolos adicionais especiais e outro caracter de controle (delete). Observe
que os símbolos dos caracteres minúsculos usam os códigos ASCII de 61h a 7Ah. Se convertermos os
códigos dos caracteres maiúsculos e minúsculos para binário, é possível verificar que os símbolos
maiúsculos diferem dos seus correspondentes minúsculos em apenas um bit. Por exemplo, veja ao lado
os códigos para os caracteres "E" e "e".A única diferença entre estes dois códigos reside no bit 5.
Caracteres maiúsculos sempre possuem 0 no bit cinco, os minúsculos sempre possuem 1 no bit cinco.
Podemos usar esta característica para converter rapidamente maiúsculas em minúsculas e vice versa. Se o
caracter for maiúsculo, para forçá-lo para minúsculo basta setar o bit cinco para 1. Se o caracter for
minúsculo, para forçá-lo para maiúsculo basta setar o bit cinco para 0.
Na realidade, os bits cinco e seis determinam o grupo ao qual o caracter ASCII pertence:
Bit 6 Bit 5 Grupo
12
0 0 Caracteres de controle
0 1 Dígitos e caracteres de pontuação
1 0 Maiúsculos e especiais
1 1 Minúsculos e especiais
Podemos, por exemplo, transformar qualquer caracter maiúsculo ou minúsculo (ou especial) no
seu caracter de controle equivalente apenas zerando os bits cinco e seis.
Caract Decimal Hexa Agora observe ao lado os códigos ASCII para os caracteres dos
0 48 30h dígitos numéricos. A representação decimal destes códigos ASCII não
esclarece grande coisa mas, a representação hexadecimal revela algo muito
1 49 31h
importante - o nibble menos significativo do código ASCII é o equivalente
2 50 32h binário do número representado. Zerando o nibble mais significativo,
3 51 33h converte-se o código do caracter para a sua representação binária.
4 52 34h Inversamente, é possível converter um valor binário do intervalo de 0 a 9 para
5 53 35h a sua representação ASCII simplesmente setando o nibble mais significativo
6 54 36h em três. Note que é possível usar a operação lógica AND para forçar os bits
mais significativos para zero e usar a operação lógica OR para forçar os bits
7 55 37h
mais significativos para 0011 (três).
8 56 38h
9 57 39h Lembre-se de que não é possível transformar uma string de caracteres
numéricos na sua representação binária correspondente simplesmente zerando
o nibble mais significativo de cada dígito da string. Transformando 123 (31h
32h 33h) desta maneira resulta em três bytes: 010203h, e não no valor correto que é 7Bh. Transformar
uma string de dígitos requer um pouco mais de sofisticação. A transformação explicada acima só serve
para dígitos únicos.
O bit sete no ASCII padrão é sempre zero. Isto significa que o conjunto de caracteres ASCII
utiliza apenas a metade dos códigos possíveis num byte de oito bits. A IBM usa os 128 códigos restantes
para vários caracteres especiais (com acento, etc), símbolos matemáticos e caracteres de desenho de
linhas. Deve ficar claro que estes caracteres extras são uma extensão não padronizada do conjunto de
caracteres ASCII. É claro que o nome IBM tem peso e a maioria dos computadores baseados no 80x86 e
as impressoras acabaram incorporando os caracteres adicionais da IBM.
Apesar do fato de que é um padrão, codificar seus dados usando simplesmente os caracteres
padrão ASCII não garante a compatibilidade entre os sistemas se bem que, hoje em dia, dificilmente
encontraremos problemas. Como usaremos com frequência os caracteres ASCII em Assembly, seria
interessante guardar de cabeça alguns códigos ASCII importantes, como o do "A", do "a" e do "0".
13
2 – SUBSISTEMAS DE UM PC
2.1. Introdução
Recapitulando, tomaremos como base as máquinas VNA da família 80x86, que representa mais
de 85% dos computadores existentes. Nestes computadores, é na CPU onde todas as ações ocorrem.
Todas os cálculos acontecem dentro da CPU. Instruções da CPU e dados ficam na memória até que
sejam requeridas pela CPU. Para a CPU, muitos dispositivos de E/S se parecem com a memória porque a
CPU pode armazenar dados em dispositivos de saída ou ler dados de dispositivos de entrada. A maior
diferença entre memória e pontos de E/S é o fato de que pontos de E/S geralmente estão associados a
dispostivos externos.
2.2. CPU
Processador
Este é um dos componentes mais importantes de um computador. O processador é o responsável
por executar as instruções que formam os programas. Quanto mais rápido o processador executar essas
instruções, mais rápida será a execução dos programas
Placa-mãe (Motherboard)
14
• On-board:como o próprio nome diz, o componente on-board vem diretamente conectado aos
circuitos da placa mãe, funcionando em sincronia e usando capacidade do processador e memória
RAM quando se trata de vídeo, som, modem e rede. Tem como maior objetivo diminuir o preço
das placas ou componentes mas, em caso de defeito o dispositivo não será recuperável, no caso de
modem AMR, basta trocar a "placa" do modem AMR com defeito por outra funcionando, pois,
este é colocado em um slot AMR na placa-mãe. São exemplos de circuitos on-board: vídeo,
modem, som e rede.
Slots de expansão
Slot é um termo em inglês para designar ranhura, fenda, conector, encaixe ou espaço. Sua função
é ligar os perifericos ao barramento e suas velocidades são correspondentes as do seus respectivos
barramentos. Nas placas-mãe são encontrados vários slots para o encaixe de placas (vídeo, som, modem
e rede por exemplo).
Bom alguns exemplos de slots são os slots:
• ISA (Industry Standard Achitecture): Que é utilizado para conectar periféricos lentos, como a
placa de som e faz modem. (16 bits baixa velocidade)
• PCI: Utilizado por periféricos que demandem velocidade, como a placa de vídeo. (32 bits, alta
velocidade)
• AGP (Acceleratd Graphics Port): Utilizado esclusivamente por interface de vídeos 3D, é o tipo de
slot mais rápido do micro. (32 bits, alta velocidade)
Sistema de BIOS
BIOS, em computação, é a sigla para Basic Input/Output System (Sistema Básico de
Entrada/Saída) ou Basic Integrated Operating System (Sistema de Operação Básico Integrado). A BIOS é
o primeiro programa executado pelo computador ao ser inicializado. Sua função primária é preparar a
máquina para que outros programas, que podem estar armazenados em diversos tipos de dispositivos
(discos rígidos, disquetes, CDs, etc) possam ser executados. A BIOS é armazenada num chip ROM
(Read-Only Memory, Memória de Somente Leitura) localizado na placa-mãe, chamado ROM BIOS.
Funcionamento:
Quando o computador é inicializado, a BIOS opera na seguinte seqüência:
• Leitura da CMOS, onde as configurações personalizáveis estão armazenadas.
• POST (Power-On Self-Test ou Auto-Teste de Inicialização), que são os diagnósticos e testes
realizados nos componentes físicos (hd, processador, etc). Os problemas são comunicados ao
usuário por uma combinação de sons (Beeps) numa determinada sequência, ou exibidos na tela. O
manual do fabricante permite a identificação do problema descrevendo a mensagem que cada
sequência de sons representa.
• Ativação de outras BIOS possivelmente presentes em dispositivos instalados no computador (ex.
discos SCSI e placas de vídeo).
15
• Descompactação para a memória principal. Os dados, armazenados numa forma compactada, são
transferidos para a memória, e só aí descompactados. Isso é feito para evitar a perda de tempo na
tranferência dos dados.
• Leitura dos dispositivos de armazenamento, cujos detalhes e ordem de inicialização são
armazenados na CMOS. Se há um sistema operacional instalado no dispositivo, em seu primeiro
setor (o Master Boot Record) estão as informações necessárias para a BIOS encontrá-lo (este
setor não deve exceder 512 bytes).
Disco rígido
Assim como a memória RAM, o disco rígido armazena programas e dados, porém existem
algumas diferenças. O disco rígido tem uma capacidade milhares de vezes maior. Seus dados não são
perdidos quando o computador é desligado, coisa que acontece com a RAM. A memória RAM é muito
mais rápida, e é necessário que os programas e dados sejam copiados para ela para que o processador
possa acessá-los. Portanto o disco rígido armazena de forma permanente todos os programas e dados
existentes no computador. Os programas a serem executados e os dados a serem processados são
copiados para a memória RAM, e então o processador pode trabalhar com eles.
Placas de vídeo
É uma outra placa de circuito, também bastante importante. Ela é a responsável por gerar as
imagens que aparecem na tela do monitor. Quando é preciso gerar imagens com muitos detalhes, muito
16
sofisticadas e em alta velocidade, é também preciso ter uma placa de vídeo sofisticada. Hoje em dia
existem muitas placas de CPU que possuem embutidos os circuitos de vídeo (vídeo onboard). Esses PCs
portanto dispensam o uso de uma placa de vídeo. Ocorre que na maioria das casos, o vídeo onboard é de
desempenho modesto, inadequado para as aplicações que exigem imagens tridimensionais com alta
qualidade e alta velocidade.
Placa de som
É uma placa responsável por captar e gerar sons. Todos os computadores modernos utilizam sons,
portanto a placa de som é um dispositivo obrigatório. Existem muitas placas de CPU com “som
onboard”, que dispensam o uso de uma placa de som.
Placa de rede
É uma placa através da qual PCs próximos podem trocar dados entre si, através de um cabo
apropriado. Ao serem conectados desta forma, dizemos que os PCs formam uma “rede local” (LAN, ou
Local Area Network). Isto permite enviar mensagens entre os PCs, compartilhar dados e impressoras.
PCs utilizados em empresas estão normalmente ligados em rede.
Placa de modem
O modem é um dispositivo que permite que o computador transmita e receba dados para outros
computadores, através de uma linha telefônica. A principal aplicação dos modems é o acesso à Internet.
Quando ativamos uma conexão com a Internet, o modem “disca” para o provedor de acesso, que é a
empresa que faz a conexão entre o seu computador e a Internet. O tipo mais comum de modem é aquele
formado por uma placa de circuito. Existem outros tipos de modem. O “modem onboard” fica embutido
na placa de CPU, e o “modem externo” é um aparelho externo que faz o mesmo trabalho que um modem
interno (de placa).
Drive de disquetes
É uma unidade de armazenamento de dados que trabalha com disquetes comuns, cuja capacidade
é de 1.44 MB. São considerados obsoletos para os padrões atuais, devido à sua baixa capacidade de
armazenamento. A vantagem é que todos os PCs possuem drives de disquetes, portanto são uma boa
forma para transportar dados, desde que esses dados ocupem menos que 1.44 MB. Para transportar dados
em maiores quantidades, temos que usar um número maior de disquetes, ou então utilizar um meio de
armazenamento mais eficiente.
17
Drive de CD-ROM/DVD-ROM
Todos os PCs modernos possuem este tipo de drive. Ele permite usar discos CD-ROM, com
capacidade de 650 MB. Todos os programas modernos são vendidos na forma de CD-ROMs, portanto
sem este drive o usuário nem mesmo conseguirá instalar programas. O drive de CD-ROM é bastante
barato, mas não permite gravar dados. Existem entretanto modelos (chamados drives de CD-RW) que
permitem gravações, o que os torna um excelente meio para transporte e armazenamento de dados. Com
a queda acentuada dos preços desses drives, é possível que dentro de poucos anos, os drives de CD-RW
substituam os drives de CD-ROM.
Gravador de CDs
Trata-se de um drive de CD-ROM que permite fazer também gravações, utilizando CDs especiais,
chamados CD-R e CD-RW. Cada um deles armazena 650 MB, a mesma capacidade de um CD-ROM. A
diferença é que o CD-R pode ser gravado apenas uma vez. O CD-RW pode ser gravado e regravado mais
de 1000 vezes.
Monitor
É o dispositivo que contém a “tela” do computador. A maioria dos monitores utiliza a tecnologia
TRC (tubo de raios catódicos), a mesma usada nos televisores. Existem também os monitores de cristal
líquido (LCD) nos quais a tela se assemelha à de um computador portátil (notebook). Este tipo de
monitor ainda é muito caro, mas nos próximos anos tenderão a substituir os monitores convencionais.
Teclado
Certamente você não tem dúvidas sobre o que é um teclado de computador. Possuem pouco mais
de 100 teclas, entre letras, números, símbolos especiais e funções.
Mouse
Outro dispositivo bastante conhecido por todos aqueles que já tiveram um mínimo contato com
PCs. É usado para apontar e ativar comandos disponíveis na tela. A ativação é feita por pressionamento
de seus botões, o que chamamos de “clicar”.
Impressora
A impressora não faz parte do PC, ela é na verdade um segundo equipamento que se liga ao
computador, e serve para obter resultados impressos em papel, sejam eles textos, gráficos ou fotos.
Scanner
Este é um outro dispositivo opcional, que em alguns casos é vendido junto com o PC. Serve para
capturar figuras, textos e fotos. Uma fotografia em papel pode ser digitalizada, passando a poder ser
exibida na tela ou duplicada em uma impressora.
Interfaces
Interfaces são circuitos que permitem ligar dispositivos no computador. Muitas interfaces ficam
dentro do próprio computador e o usuário não as vê. São as interfaces internas, como a que controla o
disco rígido, a que controla o drive de disquetes, etc. Outras interfaces são usadas para a ligação de
18
dispositivos externos, e são acessíveis através de conectores localizados na parte traseira do computador.
É o caso da interface paralela, normalmente usada para a conexão da impressora, as interfaces seriais,
que servem para ligar o mouse e outros dispositivos, a interface de vídeo, que serve para ligar o monitor,
e assim por diante.
Gabinete
É a caixa externa do computador. No gabinete são montados todos os dispositivos internos, como
placa de CPU, placa de vídeo, placa de som, drive de disquetes, drive de CD-ROM, disco rígido, etc. Os
gabinetes possuem ainda no seu interior um outro dispositivo importante, a fonte de alimentação. Trata-
se de uma caixa metálica com circuitos eletrônicos cuja finalidade é receber a tensão da rede elétrica (110
ou 220 volts em corrente alternada) e gerar as tensões em corrente contínua necessárias ao
funcionamento das placas do computador. As fontes geram as tensões de +5 volts, +12 volts, +3,3 volts, -
5 volts e –12 volts.
Esses dispositivos também são opcionais, mas são muito importantes. Servem para melhorar a
qualidade da rede elétrica. O estabilizador serve para atenuar interferências, quedas de voltagem e outras
anomalias na rede elétrica. Melhor que o estabilizador, porém bem mais caro, é o no-break. Este aparelho
substitui o estabilizador, porém com uma grande vantagem: mantém o PC funcionando mesmo com
ausência de energia elétrica.
2.6 . Processadores
Os dois principais fabricantes de processadores são a Intel e a AMD. Cada um deles produz
modelos adequados a cada aplicação. Existem modelos básicos, para serem usados nos PCs mais simples
e baratos, modelos de médio e de alto desempenho:
19
Processadores Intel
Modelo Aplicação
Celeron Este é o processador mais simples fabricado recentemente pela Intel. Trata-se de uma
versão simplificada do Pentium III. A diferença principal é que possui apenas 128 kB
de cache L2, enquanto o Pentium III possui 256 kB.
Pentium III Este é o principal processador da Intel, usado nos PCs de médio e alto desempenho.
Pentium 4 Este é um novo processador recentemente lançado, que deverá futuramente substituir o
Pentium III.
Itanium Ainda vai demorar um pouco para os usuários de médio porte tenham acesso a este
processador. Ao ser lançado terá preços muito elevados, e será destinado a PCs super
avançados.
Processadores AMD
Modelo Aplicação
K6-2 Entre 1998 e 2000 este processador foi muito utilizado nos PCs de baixo custo.
Duron O AMD é o substituto do K6-2 para suprir o mercado de PCs simples. Podemos dizer
que assim como o Celeron é uma versão simplificada do Pentium III, o Duron é uma
versão simples do Athlon, o concorrente do Pentium III produzido pela AMD.
Velocidade do processador
20
GHz, e assim por diante. A partir de 1000 MHz passamos a usar a unidade GHz. Por exemplo, 1 GHz =
1000 MHz, 1.1 GHz = 1100 MHz, 1.13 GHz = 1130 MHz, etc.
Os fabricantes sempre produzem cada modelo de processador com vários clocks diferentes. O
Pentium III, por exemplo, era produzido (até o final do ano 2000) com os seguintes clocks: 500, 533,
550, 600, 650, 667, 700, 733, 750, 800, 850, 866, 900, 1000 MHz
Caches L1 e L2
Quase todos os processadores modernos possuem caches L1 e L2. O usuário não escolhe a
quantidade de cache que quer no seu computador. Ela é embutida no processador e não há como alterá-la.
A cache é uma pequena quantidade de memória super rápida e cara, que serve para acelerar o
desempenho da memória RAM (que por sua vez é maior, mais lenta e mais barata). Ela é necessária
porque as memórias comuns não são suficientemente rápidas para os processadores modernos. No início
do ano 2000, enquanto as memórias operavam com 100 ou 133 MHz, os processadores operavam com
400 MHz ou mais. No início de 2001, os processadores mais velozes operavam entre 1000 e 1500 MHz,
mas as memórias mais rápidas operavam entre 200 e 400 MHz. A cache serve para suprir esta
deficiência. Grandes lotes de dados são continuamente lidos da memória RAM e colocados na cache. O
processador encontrará então na cache, os dados a serem processados e instruções a serem executadas. Se
não existisse a cache o processador teria que trabalhar diretamente com a memória RAM, que é muito
lenta, o que prejudicaria bastante o seu desempenho.
A cache L2 acelera diretamente o desempenho da RAM. A cache L1, por sua vez, é ainda mais
rápida, e acelera o desempenho da cache L2. Este sistema torna o computador veloz, mesmo utilizando
memórias RAM lentas.
De um modo geral, uma quantidade maior de cache L1 e L2 resulta em maior desempenho, mas
este não é o único fator em jogo. Também entram em jogo a velocidade (clock) da cache e o seu número
de bits. A tabela que se segue mostra características das caches de alguns processadores.
Observe que a cache L1 de todos os processadores têm uma coisa em comum: sua velocidade é
indicada como FULL. Isto significa que a cache L1 sempre trabalha com o mesmo clock usado pelo
núcleo do processador. Por exemplo, se um processador opera com 800 MHz, a cache L1 opera com 800
MHz, e assim por diante. Vemos que existem diferenças nos tamanhos das caches L1 dos processadores
21
citados. Processadores com cache L1 maior tendem a levar vantagem sobre processadores com cache L1
menor.
O tamanho da cache L2 varia bastante de um modelo para outro. As primeiras versões do Pentium
III tinham cache L2 de 512 kB, mas operavam com a metade do clock do processador (FULL/2). Isto
significa que, por exemplo, em um Pentium III/500 antigo, a cache L2 operava com 250 MHz. As
versões novas do Pentium III têm cache L2 de apenas 256 kB, mas operando com a mesma freqüência do
processador. Situação semelhante ocorre com as versões novas e antigas do Athlon. O Celeron e o AMD
Duron também tem caches L2 operando com a mesma freqüência do núcleo. O processador mais fraco da
lista é o AMD K6-2. Este processador normalmente trabalha com uma cache L2 externa, instalada na
placa de CPU, com 512 kB ou 1 MB. Apesar do seu grande tamanho, esta cache L2 opera com clock de
apenas 100 MHz, daí o seu baixo desempenho.
Clock externo
Todos os processadores operam com dois clocks diferentes: clock interno e clock externo. O
clock interno está relacionado com o número de operações que o processador realiza por segundo. O
clock externo está relacionado com o número de acessos externos (principalmente à memória) realizados
por segundo. Um processador com clock externo de 100 MHz, por exemplo, é capaz de realizar, pelo
menos teoricamente, 100 milhões de acessos à memória por segundo. O clock externo é em geral bem
menor que o interno. O valor deste clock externo varia bastante de um processador para outro:
AMD Athlon 200 MHz, 266 MHz, 333 MHz, 400 MHz
É vantagem que o clock externo de um processador seja elevado. Processadores Celeron operam
com apenas 66 MHz externos, mas modelos mais recentes operam com 100 MHz. O Pentium III é
produzido em várias versões, sendo algumas de 100 e outras de 133 MHz. O K6-2 opera com 100 MHz
externos. Os processadores AMD Athlon e Duron operam com 200 e 266 MHz, e existe previsão de
lançamento de versões com até 400 MHz.
22
3 – SISTEMAS E CÓDIGOS COMPUTACIONAIS,
ARITMÉTICA BINÁRIA E OPERAÇÕES LÓGICAS
Problema do capítulo:
Estudando programação em C, você se depara com vários formatos possíveis para as variáveis
numéricas. Você observa que existem 2 formatos para variáveis de 32 bits: long e float. Porque o
formato long armazena valores de -2.147.483.648 a 2.147.483.647 enquanto o formato float armazena
variáveis de 1,17549435 .10-38 até 3,40282347 . 10+38?
3.1 – INTRODUÇÃO
No estudo da tecnologia digital, é de grande importância o conhecimento de alguns sistemas de
numeração e códigos numéricos. Entre os diversos sistemas de numeração e códigos numéricos
existentes, destacam-se:
• O SISTEMA DECIMAL;
• O SISTEMA BINÁRIO;
• O SISTEMA OCTAL;
• O SISTEMA HEXADECIMAL;
• O CÓDIGO BINÁRIO BCD ou 8421;
O sistema decimal é utilizado por nós no dia-a-dia e é, sem dúvida, o mais importante sistema
numérico. Trata-se de um sistema que possui dez algarismos, com os quais podemos formar qualquer
número através da lei geral de formação de números.
Os demais sistemas a serem estudados, também são importantes já que os sistemas digitais não
trabalham com números decimais, e sim com números binários. Isto porque os dispositivos eletrônicos
presentes nos circuitos digitais e nos computadores são projetados para operação em dois estados
(operação binária). No decorrer do estudo da eletrônica digital perceberemos a ligação existente entre os
circuitos lógicos digitais e o sistema binário, bem como a interligação entre os diversos sistemas de
numeração .
Os sistemas de numeração são definidos pela “base” que os mesmos utilizam, isto é, o número de
dígitos que o sistema utiliza.
Em qualquer um dos sistemas de numeração, um número é uma cadeia de dígitos, em que cada
posição tem um determinado peso dentro desta cadeia. O valor do número é o valor da soma dos
produtos dos dígitos pelo seu respectivo peso.
Para que tenhamos uma idéia do que significa um determinado número dentro do sistema de
numeração correspondente, é necessário que defina o valor da representação de um número. Isto é
mostrado a seguir :
N = ...+a3B3+a2B2+a1B1+a0B0+a-1B-1+a-2B-2+a-3B-3+...
onde:
23
A BASE de um sistema de numeração é igual ao número de dígitos que o sistema utiliza. O NOME
DO SISTEMA define o número de dígitos do sistema.
Portanto para o SISTEMA DECIMAL, como o próprio nome diz, utiliza “10 DÍGITOS” e possui
“BASE 10”.
= 64+16+8+4+0+1+0,5+0,25+0+0,0625 =(93,8125)10
24
3.3.2.1 – Números Inteiros
Para converter um número decimal inteiro em um número binário equivalente, deve-se dividir o
número decimal dado por “2” (base do novo sistema), anotando-se o resto e continuando o processo até
que o quociente seja igual a “0” (zero). A sequência de “0” e “1” constituídas pela sucessão dos restos
será o número no sistema binário. O sentido de leitura dos restos é do último obtido para o primeiro.
Ex :
a) (329)10 = (?)2 b) (78)10 = (?)2
329 /2
78 /2
“1” 164 /2
“0” 39 /2
“0” 82 /2
“1” 19 /2
“0” 41 /2
“1” 9 /2
“1” 20 /2
“1” 4 /2
“0” 10 /2
“0” 2 /2
“0” 5 /2
“0” 1 /2
“1” 2 /2
“1” 0
“0” 1 /2
“1” 0
(78)10 = (1001110)2
(329)10 = (101001001)2
0,6875 x 2 = 1,375
1 0,375 x 2 = 0,75
0 0,75 x 2 = 1,5
1 0,5 x 2 = 1,0
1 “0”
(0,6875)10 = (0,1011)2
25
b) (0,625)10 = (?)2
0,625 x 2 = 1,25
1 0,25 x 2 = 0, 5
0 0,5 x 2 = 1,0
1 “0”
(0,625)10 = (0,101)2
Observação:
Se tivermos um número decimal, como por exemplo (78,625)10 , deve-se converter separadamente
a parte inteira e a parte fracionária nos seus números binários equivalente e depois agrupa-se os números
obtidos.
Exemplo:
(78,625)10 = (?)2
(78)10 = (1001110)2
(0,625)10 = (0,101)2
(78,625)10 = (1001110,101)2
26
Exemplo:
1 0 -1 -2
(76,34)8 = (?)10
479 /8
“7” 59 /8
“3” 7 /8
“7” 0
(479)10 = (737)8
27
TABELA 1 - EQUIVALÊNCIAS
DECIMAL HEXADECIMAL BINÁRIO
0 0 0000
1 1 0001
2 2 0010
3 3 0011
4 4 0100
5 5 0101
6 6 0110
7 7 0111
8 8 1000
9 9 1001
10 A 1010
11 B 1011
12 C 1100
13 D 1101
14 E 1110
15 F 1111
8 C D 0 3
1000 1100 1101 0000 0011
(8CD03)16 = (10001100110100000011)2
(A35D)16 = (?)2
A 3 5 D
1010 0011 0101 1101
(A35D)16 = (1010001101011101)2
Exemplo:
a) (1110 1000 1101 0111)2 = (?)16
E 8 D 7
(1110100011010111)2 = (E8D7)16
28
b) (1 0101 0110 1011)2 = (156B)16
1 5 6 B
29
Tabela 2 – Equivalência DECIMAL - BCD
Exemplo:
(2945)10 = (?)BCD
2 9 4 5
0010 1001 0100 0101
(2945)10 = (0010100101000101)BCD
(1100110000110)BCD = (?)10
1 9 8 6
(1 1001 1000 0110)BCD = (1986)10
Observação:
Deve-se ter cuidado para não confundir um número “BCD” com um número binário. Abaixo são
mostradas as diferenças entre um 2número binário e um número BCD.
Exemplo:
Seja o seguinte número decimal: (115)10
a) (115)10 = (?)2
(115)10 = (111011)2
b) (115)10 = (?)BCD
(115)10 = (0001 0001 0101)BCD
30
Cada um dos x’s é “0” ou “1”. O código ASCII que representa a letra “A”, por exemplo, é o
seguinte: “1000001”.
A seguir na tabela 3 é mostrado o código ASCII e o seu equivalente alfanumérico.
31
Para números binários com mais de um dígito, aplica-se a adição coluna por coluna da mesma
forma que na adição de números decimais. Veja abaixo alguns exemplos.
Os exemplos mostrados acima, mostram a adição de dois números binários de 8 bits. Os circuitos
aritméticos que realizam operações aritméticas podem manipular simultaneamente somente dois números
binários. Quando mais de 2 números binários precisam ser adicionados, primeiramente adiciona-se 2
números e o resultado desta soma é adicionado ao terceiro número.
- -
Da mesma forma que a aritmética decimal, em algumas aplicações, todos os dados são ou positivos
ou negativos. Quando isto acontece, pode-se esquecer os sinais “+” e “-”e se concentrar somente na
magnitude dos números. Por exemplo, para 8 bits (8 dígitos) o menor número é 0000 0000 e o maior é
1111 1111.
Os números mostrados são chamados de números binários sem sinal algébrico, porque a totalidade
dos bits em um número binário é usada para representar a magnitude do número decimal correspondente.
A aritmética de números sem sinal, é bastante limitada, uma vez que para a subtração binária só
32
poderemos realizar esta operação desde que o minuendo seja maior do que o subtraendo e portanto a
diferença será positiva.
Um problema existente na adição de números binários é o estouro de capacidade, conhecido como
overflow. Por exemplo, na aritmética com números de 8 bits, a adição de dois números sem sinal
algébrico, cuja a soma é maior que 1111 1111 (255) provoca um estouro de capacidade, isto é haverá a
necessidade de mais dígitos para representar o número. Isto provoca a necessidade de circuitos lógicos
que indiquem que houve estouro na capacidade. Este circuito é chamado de sinalizador vai-um e detecta
um “vai-um” na nova coluna indicando que a resposta de 8 bits não é realidade (embora a resposta com 9
bits seja válida).
Exemplo:
COMPLEMENTO DE 2 = COMPLEMENTO DE 1 + 1
A' = A + 1
33
onde:
A’ é o completo de 2 de um número binário A e A é o complento de 1 do número binário A.
Inicialmente é necessário que se defina o que vem a ser o complemento de “1” de um número
binário. O complemento de “1” de um número, consiste em complementar todos os dígitos do número
binário, bit a bit.
Exemplo:
1) Seja o número binário: (A) = x 3x 2 x1x 0 =1000 o complemento de 1 deste número será:
( A) = x 3 x 2 x1x 0 = 0111 .
2) x 7 x 6 x 5 x 4 x 3 x 2 x1x 0 = 10101100
Já o complemento “2” (A’) é o número binário que resulta quando adiciona-se “1” ao complemento
de “1” ( A ) de um número binário qualquer (A). Veja os exemplos abaixo.
Exemplo:
1) Seja o seguinte número: A= 1011.
A = 0100
A' = A + 1⇒ A ⇒ 0100
+ 1
A' ⇒ 0101
2) A= 10101100
A = 01010011
A' → A ⇒ 01010011
+ 1
A' ⇒ 01010100
A utilização da representação de um número pelo seu complemento de 2, permite que se represente
números positivos e negativos, da seguinte forma: se o dígito mais significativo do número for “0”, o
número representado pelos demais dígitos é positivo; se o dígito mais significativo do número for “1”,
significa que o número é negativo e está representado pelo seu complemento 2. Abaixo é mostrado toda
a faixa possível de representação para números positivos e negativos com números binários de 4 dígitos.
Conforme verifica-se, com 4 dígitos pode-se representar desde -8 até +7 sendo esta a faixa possível de
representação. Na representação mostrada percebe-se que o complemento de 2 de um número positivo,
representa o seu negativo equivalente.
1000 (-8)
1001 (-7)
1010 (-6)
1011 (-5)
1100 (-4)
1101 (-3)
1110 (-2)
34
1111 (-1)
0000 (0)
0001 (+1)
0010 (+2)
0011 (+3)
0100 (+4)
0101 (+5)
0110 (+6)
0111 (+7)
Exemplo:
+3 → 0011 +7 → 0111
-3 → 1101 -7 → 1001
A seguir, são enfatizados alguns pontos que devem ser seguidos para a realização aritmética de
números binários com sinal através do método do complemento de 2.
• os números positivos sempre têm o bit mais significativo igual a “0”; o número negativo tem o
bit mais significativo igual a “1”;
• os números positivos são representados pela sua forma normal;
• os números negativos são representados pelo complemento de 2 do número;
35
Caso 02: Número positivo e número negativo menor.
Seja os seguintes números: (+125)10 : 0111 1101 e (-68)10 : 1011 1100.
Exemplo: (+125)10 + (-68)10
Com a aritmética de 8 bits, o último transporte (vai-um) na 9ª coluna é desprezado e a resposta são
os 8 bits restantes.
Conforme foi analisado, a adição binária através do complemento de 2 (para números negativos)
não mostra nenhuma dificuldade se a resposta está dentro da faixa de – 128 a +127 para números
de até 8 bits.
36
3.8.3.2 – Subtração Binária de Números com Sinal
No caso da subtração, os quatros casos possíveis de ocorrer são:
Através dos fundamentos da álgebra sabemos que somar um número negativo é o mesmo que
subtrair um número positivo. Seja o formato de uma subtração qualquer:
“+”
O último transporte deve ser ignorado e os 8 bits restantes são o próprio número binário:
1 0100 0011 ⇒ (+67)
Caso 02: Número positivo e número negativo menor.
Sejam os números: (+68)10 e (-27)10
Exemplo: (+68)10 - (-27)10
(+68) (+68) 0100 0100
- (-27) ⇔ + (+27) ⇔ 0001 1011
(+95) (+95) 0101 1111.
“+”
A resposta é diretamente obtida: 0101 1111 ⇒ (+95)
37
A resposta é diretamente obtida: (0111 1010) ⇒ (+122)
“+”
O último transporte deve ser ignorado e os 8 bits restantes são o próprio número binário:
1 0010 0011 ⇒ (+35)
3.8.3.3 – Estouro de Capacidade (Overflow)
Em cada uma das operações realizadas nos casos exemplos, os números adicionados consistiam
de 1 bit de sinal e 7 bits de magnitude. As respostas também são constituídas da mesma forma. Qualquer
transporte obtido após o 8º bit é desprezado. O estouro de capacidade real somente pode acontecer
quando a operação é feita entre 2 números positivos ou 2 números negativos. Nestes casos, quando
acontece o estouro de capacidade, o bit de sinal deverá apresentar sinal contrário ao dos números sob
operação. Isto pode ser facilmente detectado pela comparação entre os bits de sinal dos números e o bit
de sinal do resultado.
1010
x 0101
1010
0000
1010
0000
0110010 ⇒ (50)10
38
números BCD, podem acontecer 2 casos, que são: resultado da soma menor ou igual a 9 e resultado da
soma maior que 9.
Exemplos:
1)
6 0101
+4 0100
9 1001 ⇒ Número válido no código BCD
2)
45 0100 0101
+33 0011 0011
78 0111 1000 ⇒ Números válidos no código BCD
1)
6 0101
+7 0111
13 1101 ⇒ Número inválido no código BCD
+ 0110
0001 0011 ⇒ Resultado válido no código BCD
2)
47 0100 0111
+35 0011 0101
82 0111 1100 ⇒ Número inválido no código BCD
1 + 0110
1000 0010 ⇒ Resultado válido no código BCD
3)
1
59 0101 1001
+38 0011 1000
97 1001 10001 ⇒ Número inválido no código BCD
+ 0110
1001 0111 ⇒ Resultado válido no código BCD
39
3.9 – REPRESENTAÇÃO EM PONTO FLUTUANTE
Nos sistemas processados, o cálculo aritmético é uma das atividades mais freqüentes e com
importância fundamental para o desempenho de diversas aplicações. Com isso, o desempenho destes
sistemas, que é proporcional ao aumento da velocidade de processamento, nos leva ao compromisso de
acelerar as operações aritméticas.
Escolhida uma tecnologia de fabricação dos dispositivos, o ponto principal que determina tanto a
velocidade das operações aritméticas como a precisão em sua execução se refere a escolha do sistema de
representação numérica.
O sistema de representação numérica binária em ponto-fixo vem sendo utilizado com sucesso nas
mais variadas computações numéricas. Mas, devido à crescente evolução dos sistemas computacionais,
os limites impostos por esta representação começaram a aumentar. A necessidade cada vez maior de
representar uma informação numérica de valores máximos ou mínimos, evitando a freqüente ocorrência
de overflow, de underflow e de imprecisão nos cálculos aritméticos, trouxe a necessidade de uma nova
solução.
Na constante busca de uma representação numérica eficaz e que solucionasse a maioria dos
problemas computacionais que surgiram nos diversos campos científicos e comerciais entre outros, criou-
se uma que tem sido um grande alvo de estudos, devido às diversas vantagens que apresenta.
Este sistema, conhecido como "sistema de representação numérica binária em ponto flutuante", veio a
solucionar muitos dos problemas computacionais. Uma das vantagens deste sistema é a de permitir que
representemos um mesmo número binário fazendo uso de uma menor quantidade de bits do que os que
seriam necessários se representados em ponto-fixo. Como temos um aumento na escala de valores
representáveis, esse sistema proporciona também que manipulemos tais números com uma maior
precisão.
Entretanto, se por um lado esta representação nos oferece diversas vantagens em relação às outras
representações numéricas, em compensação, teremos uma maior complexidade no tratamento destes
números. Pois, no tratamento aritmético de números inteiros temos normalmente apenas uma ULA
(Unidade de Lógica e Aritmética) que se encarrega de todas as operações aritméticas. Já no caso de
números em ponto flutuante, devido as particularidades deste sistema, necessitaremos de dispositivos
próprios para cada operação aritmética, ou seja, existirão somadores/subtratores, multiplicadores,
divisores, extratores de raiz quadrada, etc.
Através de todo o nosso estudo envolvendo unidades de ponto flutuante, elaboramos um Manual
de Referência. Esta literatura servirá como orientação onde, a partir de cada necessidade, esta possa
encaminhar a pessoa dentre as várias soluções possíveis, indicando diversos métodos e técnicas para o
tratamento dos números em ponto flutuante.
N = (−1) S ⋅ M ⋅ B E
onde:
N é o número no formato exponencial,
S representa o sinal (S=0 positivo, S=1 negativo)
M representa a mantissa
B representa a base
E representa o expoente
40
3.9.1. Representação Normalizada
Quando utilizamos a notação científica, para um mesmo valor, poderemos ter um número infinito
de representações: m × Be = (m/B) × Be+1 = •••• = (B × m) × Be-1 .
Convém que se tenha uma única representação para cada valor. Adota-se, por isso, a chamada
forma normal. A condição para que uma notação científica ou exponencial esteja normalizada é dado pela
fórmula ao lado, que significa que a mantissa deverá ser maior ou igual a 1 e menor que 2:
1≤M<2
Exemplo: Representação normalizadas:
a) (1)10: N=(-1)0 . 1,0 . 20
b) (-5)10: N=(-1)-1 . 1,01 . 22
c) (-200)10: N=(-1)-1 . 1,1001 . 22
d) (0,0625)10: N=(-1)0 . 1,0 . 2-4
e) (0,1)10: N=(-1)0 . 1,10011001100110011001101 . 2-4
f) (1024)10: N=(-1)0 . 1,0 . 210
Segundo o padrão IEEE 754, um número binário representado em ponto flutuante é uma palavra
de N-bits binários que possui três campos:
b) Expoente: representado “em excesso” de 2(8-1)-1 =127 na precisão simples e 2(11-1)-1 = 1023 na
precisão dupla
c) Mantissa: como a representação é normalizada, sabe-se que o bit de maior ordem da mantissa deverá
ser “1”. Portanto, para ganhar-se mais uma posição, este bit não é explicitamente representado, sendo
assumido que seu valor = 1. A mantissa recebe o nome de “significando”.
41
d) Base implícita = 2 (não represntada).
Vale ressaltar que o bit oculto é subentendido e, portanto, não é representado. Contudo, deve-se
estar ciente da sua existência quando se for realizar conversões ou operações aritméticas. A figura abaixo
mostra como o sinal, o expoente e a mantissa são agrupados em um único número.
A norma IEEE 754 contempla ainda a representação de valores em fp que necessitem de maior
intervalo de representação e/ou melhor precisão, por várias maneiras. A mais adoptada pelos fabricantes
utiliza o dobro do n.º de bits, 64, pelo que é também conhecida pela representação em precisão dupla,
enquanto a representação por 32 bits se designa por precisão simples. Para precisão dupla, a norma
especifica, entre outros aspectos, que o expoente será representado por 11 bits e a parte fraccionária por
52 bits.
Formatos
Simples
Simples Duplo Duplo estendido
estendido
Largura do formato 32 ≥ 43 64 ≥ 79
Bits de precisão* p 24 ≥ 32 53 ≥ 64
Largura 8 ≥ 11 11 ≥ 15
*A linha "Bits de precisão" nos dá o número de bits na mantissa, incluindo o bit oculto
42
Single
0 1 8 9 31
Double
0 1 11 12 63
Sinal
0: + 11 bits para 52 bits para
1: - expoente fração
43
gama de valores decimais correspondentes ao intervalo de representação de n bits, de 0 a 2^(n-1),
de modo a que o 0 decimal passe a ser representado não por uma representação binária com tudo
a zero, mas por um valor no meio da tabela; usando 8 bits por exemplo, esta notação permitiria
representar o 0 pelo valor 127 ou 128; a norma IEEE adoptou o primeiro destes 2 valores, pelo
que a representação do expoente se faz por notação por excesso 127; o expoente varia assim entre
-127 e +128;
• valor decimal de um fp em binário (normalizado): V = (-1)^S * (1.M) * 2^E-127 , em que S, F
e E representam respectivamente os valores em binário dos campos no formato em fp;
• representação de valores desnormalizados: para poder contemplar este tipo de situação a
norma IEEE reserva o valor de E = 0000 0000b para representar valores desnormalizados, desde
que se verifique também que M diferente de 0; o valor decimal vem dado por V = (-1)^S *
(0.M) * 2^(-126)
• representação do zero: é o caso particular previsto em cima, onde E = 0 e F = 0 ;
• representação de (± infinito): a norma IEEE reserva a outra extremidade de representação do
expoente; quando E = 1111 1111b e M = 0 , são esses os "valores" que se pretendem representar;
• representação de n.º não real: quando o valor que se pretende representar não é um n.º real
(imaginário por exemplo), a norma prevê uma forma de o indicar para posterior tratamento por
rotinas de excepção; neste caso E = 1111 1111b e M diferente de 0 . Também conhecido por
NaN, que significa not a number. Estes são símbolos especiais que pode ser utilizados para
∞
inicializar variáveis e representar resultados indefinidos (ex.: − 2 , , + ∞ − ∞ , etc.). Há dois
∞
tipos de NaN: o quiet, para situações não criticas, e o signaling, para erros sérios
44
Infinito negativo 1 11...1 (255) 0 −∞
Quiet NaN 0 ou 1 11...1 (255) ≠0 NaN
Signaling NaN 0 ou 1 11…1 (255) ≠0 NaN
Positivo 0 0 < exp < 255 f 2exp−127 (1.f )
normalizado
Negativo 1 0 < exp < 255 f − 2exp−127 (1.f )
polarizado
Positivo 0 0 f ≠0 2−126 (0.f )
desnormalizado
Negativo 1 0 f ≠0 − 2−126 (0.f )
desnormalizado
Arredondamento:
45
Número inicial: -11,1100100 (este número está entre -11,1101 e -11,1100)
Tipo de arredondamento Valor final
round toward zero -11,1100
round toward + ∞ -11,1100
round toward − ∞ -11,1101
round to the nearest -11,1100
O round to the nearest é a forma que introduz, a princípio, menores erros pois ora os números são
aumentados ora diminuídos. Ela é forma default (aquela adotada se nenhuma outra for especificada).
Na round toward + ∞ os números são sempre aumentados enquanto na forma round toward
− ∞ os números são sempre diminuídos. Estas duas últimas formas podem ser utilizadas para avaliar se
os erros de arredondamento são ou não significativos: executam-se os cálculos desejados ora com um
tipo ora com outro tipo de arredondamento. Caso os resultados sejam iguais então os erros devido ao
arredondamento não são significativos.
Vários conversores para o formato IEEE-754 são disponibilizados na web, tais como:
http://babbage.cs.qc.edu/courses/cs341/IEEE-754.html
http://www.h-schmidt.net/FloatApplet/IEEE754.html
46
9) Se não, retorna ao passo três;
3.9.3.3. Multiplicação
1) Some os expoentes. Após, some o valor (-127) para retirar a polarização adicional inclusa;
2) Multiplique as mantissas;
3) Normalize o resultado (se necessário), deslocando à direita e incrementando o expoente;
4) Teste se overflow ou underflow;
5) Se sim, gera exceção;
6) Se não, arredonde a mantissa para o número de bits apropriado;
7) Testa se o resultado está normalizado;
8) Se não, retorna ao passo três;
9) Se sim, testa o sinal dos dois operandos. Se os sinais são os mesmos, o resultado é positivo. Caso
contrário, é negativo;
3.9.3.4. Divisão
1) Subtraia os expoentes. Após, some o valor (127) para adicionar a polarização necessária;
2) Divida as mantissas;
3) Normalize o resultado (se necessário), deslocando à esquerda e decrementando o expoente;
4) Teste se overflow ou underflow;
5) Se sim, gera exceção;
6) Se não, arredonde a mantissa para o número de bits apropriado;
7) Testa se o resultado está normalizado;
8) Se não, retorna ao passo três;
9) Se sim, testa o sinal dos dois operandos. Se os sinais são os mesmos, o resultado é positivo. Caso
contrário, é negativo;
47
Outra maneira de guardar a operação lógica AND é compará-la com a multiplicação - multiplique
os operandos que o resultado também é correto. Em palavras, "na operação lógica AND, somente se os
dois operandos forem 1 o resultado é 1; do contrário, o resultado é 0".
Um fato importante na operação lógica AND é que ela pode ser usada para forçar um resultado
zero. Se um dos operandos for 0, o resultado é sempre zero, não importando o valor do outro operando.
Na tabela acima, por exemplo, a linha que do operando 0, só possui 0s; e a coluna do operando 0,
também só possui 0s. Por outro lado, se um dos operandos for 1, o resultado é o outro operando.
Veremos mais a respeito logo adiante.
OR
A operação lógica OR (cuja tradução é OU) também é uma operação diádica. Meu mneumônico é
"se alguém me xingar OU se fizer uma rosquinha, então fico brava". Daí fica fácil fazer a tabela da
lógica:
OR 0 1
0 0 1
1 1 1
Em outras palavras, "se um dos operandos for verdadeiro, o resultado é verdadeiro; do contrário,
o resultado é falso". Se um dos operandos for 1, o resultado sempre será 1, não importando o valor do
outro operando. Por outro lado, se um dos operandos for 0, o resultado será igual ao outro operando.
Estes "efeitos colaterais" da operação lógica OR também são muito úteis e também serão melhor
analisados logo adiante.
XOR
A tradução de XOR (exclusive OR) é OU exclusivo (ou excludente). Esta operação lógica, como
as outras, também é diádica. A minha forma de lembrar é "ir ao supermercado XOR ir ao cinema, preciso
me decidir". Como não posso estar nos dois lugares ao mesmo tempo (um exclui o outro), então a tabela
da lógica XOR passa a ser a seguinte:
XOR 0 1
0 0 1
1 1 0
Se não for ao supermercado (0) e não for ao cinema (0), então não decidi o que fazer (0). Se for
ao supermercado (1) e não for ao cinema (0), então me decidi (1). Se não for ao supermercado (0) e for
ao cinema (1), então também me decidi (1). Se for ao supermercado (1) e for ao cinema (1), não decidi
nada (0) porque não posso ir aos dois lugares ao mesmo tempo. Em outras palavras, "se um dos
operandos for 1, então o resultado é 1; caso contrário, o resultado é 0".
Se os operandos forem iguais, o resultado é 1. Se os operando forem diferentes, o resultado é
zero. Esta característica permite inverter os valores numa sequência de bits e é uma mão na roda.
NOT
Esta é a operação lógica mais fácil, a da negação. NOT significa NÃO e, ao contrário das outras
operações, aceita apenas um operando (é monádica). Veja a tabela abaixo:
NOT 0 1
NOT 1 0
48
3.10.1. Operações lógicas com Números Binários e Strings de Bits
Como foi visto acima, as funções lógicas funcionam apenas com operandos de bit único. Uma vez
que o 80x86 usa grupos de oito, dezesseis ou trinta e dois bits, é preciso ampliar a definição destas
funções para poder lidar com mais de dois bits. As funções lógicas do 80x86 operam na base do bit a bit,
ou seja, tratam os bits da posição 0, depois os bits da posição 1 e assim sucessivamente. É como se fosse
uma cadeia de operações. Por exemplo, se quisermos realizar uma operação AND com os números
binários 1011 0101 e 1110 1110, faríamos a operação coluna a coluna:
1011 0101
AND 1110 1110
-----------
1010 0100
Como resultado desta operação, "ligamos" os bits onde ambos são 1. Os bits restantes foram
zerados. Se quisermos garantir que os bits de 4 a 7 do primeiro operando sejam zerados e que os bits 0 a
3 fiquem inalterados, basta fazer um AND com 0000 1111. Observe:
1011 0101
AND 0000 1111
-----------
0000 0101
Se quisermos inverter o quinto bit, basta fazer um XOR com 0010 0000. O bit (ou os bits) que
quisermos inverter, mandamos ligado. Os bits zerados não alteram os bits do primeiro operando. Assim,
se quisermos inverter os bits 0 a 3, basta fazer um XOR com 0000 1111.
1011 0101
XOR 0000 1111
-----------
1011 1010
E o que acontece quando usamos um OR com 0000 1111? Os bits 0 não alteram os bits do
primeiro operando e os bits 1 forçam os bits para 1. É um método excelente para ligar bits na (ou nas)
posições desejadas.
1011 0101
OR 0000 1111
-----------
1011 1111
Este método é conhecido como máscara. Através de uma máscara de AND é possível zerar bits.
Com uma máscara XOR é possível inverter bits e, através de uma máscara OR é possível ligar bits. Basta
conhecer as funções e saber lidar com os bits. Quando temos números hexadecimais, o melhor é
transformá-los em binário e depois aplicar as funções lógicas... é lógico ;))))
49
A operação de deslocamento para a esquerda move uma posição para a esquerda cada um dos bits
de uma string de bits (veja ao lado). O bit zero é deslocado para a posição 1, o da posição 1 para a
posição 2, etc. Surgem naturalmente duas perguntas: "O que vai para o bit zero?" e "Para onde vai o bit
7?" Bem, isto depende do contexto. Nós colocaremos um bit zero na posição zero e o bit sete "cai fora"
nesta operação.
Observe que, deslocar o valor para a esquerda é o mesmo que multiplicá-lo pela sua base (ou
radix). Por exemplo, deslocar um número decimal para a esquerda em uma posição (adicionando um zero
à direita do número) o multiplica por 10 (a sua base):
Como a base de um número binário é dois, o deslocamento em uma posição para a esquerda
multiplica-o por 2. Se deslocarmos um valor binário duas vezes para a esquerda, ele é multiplicado duas
vezes por 2, ou seja, é multiplicado por 4. Se o deslocarmos três vezes, será multiplicado por 8 (2*2*2).
Como regra, se deslocarmos um valor binário para a esquerda n vezes, isto o multiplicará por 2^n (2n ou
2 elevado a n).
Uma operação de shift para a direita funciona do mesmo modo que a anterior, exceto que os
dados são deslocados na direção oposta. O bit sete é movido para a posição seis, o bit seis para a cinco e
assim sucessivamente. Numa operação de deslocamento para a direita, introduzimos um zero no bit sete e
o bit zero será descartado.
Como o deslocamento para a esquerda equivale a uma multiplicação pela base, não é de se
admirar que um deslocamento para a direita equivale a uma divisão pela base. No sistema binário, se
fizermos n deslocamentos para a direita, o valor será dividido por 2n.
Existe um problema relacionado com a divisão efetuada por um shift para a direita: um shift para
a direita equivale a uma divisão de um número sem sinal por 2. Por exemplo, se deslocarmos uma
posição para a direita a representação sem sinal de 254 (0FEh), obtemos 127 (07Fh), exatamente o
esperado. Entretanto, se deslocarmos uma posição para a direita a representação binária de -2 (0FEh),
obtemos 127 (07Fh), o que não está correto. Este problema ocorre porque estamos introduzindo zero no
bit sete. Se o bit sete contém 1 antes do deslocamento, indicativo de número negativo nos inteiros com
sinal, e depois recebe zero, estamos alterando o sinal deste número (que passa de negativo para positivo).
Como este não é o propósito da divisão... dá erro.
Para usar um shift para a direita como um operador de divisão, antes é preciso definir uma
terceira operação de deslocamento: o deslocamento aritmético para a direita (arithmetic shift right). Um
shift aritmético para a direita funciona como o shift para a direita normal, com uma exceção: ao invés de
deslocar o bit sete para a posição seis, este bit é deixado intacto, ou seja, o bit sete não é zerado.
Isto geralmente produz o resultado esperado. Por exemplo, fazendo um shift aritmético para a
direita com -2 (0FEh), obtemos -1 (0FFh). Uma coisa, no entanto, não pode ser esquecida: esta operação
sempre arredonda os números para o inteiro que seja menor ou igual ao resultado, ou seja, arredonda para
baixo. Um shift artimético para a direita com 5 dá como resultado 2. Mas preste atenção. Um shift
aritmético para a direita com -1 dá como resultado -1, e não zero! O arredondamento se faz na direção do
50
menor valor e -1 é menor do que 0. Este não é um "bug" no shift aritmético para a direita, é apenas como
a divisão de inteiros foi definida.
Outro para muito útil é a rotação para a esquerda e para a direita. Estas operações se comportam
como os deslocamentos, com uma diferença importante: o bit que sai numa extremidade entra na
extremidade oposta.
51
EXERCÍCIOS: CAPÍTULO 3
1) Converta os números abaixo do sistema decimal para o sistema binário.
a) 13,45
b) 232,698
c) 98,075
5) Converta os números binários mostrados abaixo para a representação por complemento de “2”
a) 11011011
b) 01010110
c) 10011101
d) 01011011
e) 11101100
6) Seja os números decimais mostrados abaixo. Converta-os para números binários de 12 bits sem sinal
algébrico e efetue as operações de adição e subtração solicitadas em binário.
a) (123)10 + (498)10
52
b) (372)10 + (905)10
c) (253)10 - (105)10
d) (192)10 + (91)10
7) Seja os números decimais mostrados abaixo. Converta-os para números binários de 12 bits com sinal
algébrico representados pelo método do complemento de “2” e efetue as operações de adição e
subtração solicitadas em binário.
a) (+257)10 + (-178)10
b) (-372)10 + (-205)10
c) (+253)10 - (+105)10
d) (+192)10 + (+91)10
e) (+207)10 - (-78)10
f) (+536)10 - (+931)10
8) Seja os números decimais mostrados abaixo. Converta-os para números BCD e efetue as operações
de adição solicitadas em BCD.
g) (257)10 + (178)10
h) (372)10 + (205)10
i) (253)10 + (105)10
j) (192)10 + (91)10
9) Seja os números decimais mostrados abaixo. Converta-os para números binários e efetue as
operações de multiplicação solicitadas em Binário.
k) (27)10 x (18)10
l) (32)10 x (20)10
m) (53)10 x (10)10
n) (92)10 x (9)10
10) Conforme visto, os bits não tem nenhum significado inerente. Dada a seguinte seqüência de bits:
53
11) O número irracional π, como qualquer número irracional, não pode ser escrito com um número finito
de algarismos, seja qual base for utilizada. Consideremos aqui como o número π a seguinte aproximação:
π=3,14159265358979.
a) Converta este número para binário (ponha no formato 1,bbbb, com ao menos 25 bits após a virgula).
b) Arredonde o número pela forma round to the nearest e então escreva no IEEE 754 precisão simples.
c) Pegue o resultado encontrado anteriormente e reconverta para decimal. Qual a diferença deste número
para o valor de π considerado no item 4.1?
12) O número irracional π, como qualquer número irracional, não pode ser escrito com um número finito
de algarismos, seja qual base for utilizada. Consideremos aqui como o número π a seguinte aproximação:
π=3,14159265358979.
a) Converta este número para binário (ponha no formato 1,bbbb, com ao menos 25 bits após a virgula).
b) Arredonde o número pela forma round to the nearest e então escreva no IEEE 754 precisão simples.
c) Pegue o resultado encontrado anteriormente e reconverta para decimal. Qual a diferença deste número
para o valor de π considerado no item 4.1?
13) A propriedade associativa, para a soma, é válida para todos os números representados em ponto
flutuante? Se não, mostre um contra-exemplo.
X + (Y + Z) = (X + Y) + Z
54
4 – ARQUITETURA DOS COMPUTADORES
4.1.1. Barramentos
Um barramento é uma via de comunicação que liga dois ou mais dispositivos. Uma característica
chave do barramento é que é um meio de transmissão partilhado. Múltiplos dispositivos ligam ao
barramento e um sinal emitido por qualquer dispositivo fica disponível para ser recebido por todos os
outros dispositivos agarrados ao barramento. Se dois dispositivos emitem durante o mesmo período de
tempo, os seus sinais sobrepor-se-ão e tonar-se-ão adulterados. Por isso, apenas um dispositivo pode
emitir com sucesso em cada momento.
Em muitos casos, um barramento compreende múltiplos caminhos, ou linhas, de comunicação.
Cada linha é capaz de transmitir sinais representando os binários 1 e 0. Ao longo do tempo uma
sequência de dígitos binários pode ser transmitida através de um simples linha. Tomados em conjunto,
várias linhas de um barramento podem ser usadas para transmitir vários dígitos binários simultaneamente
(em paralelo). Por exemplo, uma unidade de 8-bits pode ser transmitida através de oito linhas de
barramento.
Os sistemas de computação contêm um certo número de diferentes barramentos que fornecem os
caminhos entre componentes nos vários níveis de hierarquia do sistema de computação. Um barramento
que liga os componentes principais (processador, memória e E/S) é chamado barramento de sistema. As
estrutura mais comuns de interligação num computador são baseadas no uso de um ou mais barramentos.
55
Sistema de conexão utilizando um barramento comum
Central
N N
A P P C
R/W R/W
R/W
N
P
R/W R/W
B P P D
N N
Dado: N bits
Endereçamento: P bits
Nº de endereços: 2p
Ex: P = 4 →24 =16 endereços distintos
Para comandar o processo de leitura/escrita, são utilizados fios independente dos fios de dados,
chamados de bits de endereçamento e bits de controle. Os bits de endereçamento indicam, através de um
código binário, com qual dispositivo a central precisa se comunicar. Além disto, são necessários bits de
controle, para indicar o tipo de operação a ser realizada (leitura ou escrita, entre outras). Os bits de
controle são também comandados pela central, que coordena toda a comunicação.
56
Interrupções
É o método usado pelos dispositivos para chamar a atenção da central que o dispositivo necessita
de “atenção” urgentemente. Por exemplo, caso o dispositivo D precisa comunicar algum evento a central,
ele envia uma informação através de bits de controle. A partir disto, o software da central irá processar
esta chamada e, se for de seu interesse, irá processar a informação. É assim que o
4.1.2. Registradores
É um dos principais blocos que formam os computadores. São unidades de armazenamento
temporário de dados com N bits. Possuem um conjunto de entradas e saídas de dados (D e F), cuja escrita
ou leitura é habilitada através dos bits de controle (na figura abaixo, R e W)
D
N
R CLK
E
W
N
F
onde: D representa a entrada, E o conteúdo armazenado e F a saída.
As ações realizadas por este registrador podem ser sumarizadas pela seguinte tabela, sendo todos
os comandos somente ativados na borda de subida do sinal de clock.
Ação Sinais de controle Resultado
Escrita (armazenamento) do valor da entrada W=1, R=0 E=D, F=0
Leitura (disponibilização) do valor armazenado W=0, R=1 E=E, F=E
Nenhuma ação W=0, R=0 E=E, F=0
57
D
R
W CLK
Z E
I
C
N
F
Ação Sinais de controle Resultado
Escrita (armazenamento) do valor da entrada W=1, R=0, Z=0, I=0, C=0 E=D, F=0
Leitura (disponibilização) do valor armazenado W=0, R=1, Z=0, I=0, C=0 E=E, F=E
Incremento do valor armazenado W=0, R=1, Z=0, I=1, C=0 E=E+1, F=0
Zera o valor armazenado W=0, R=1, Z=0, I=1, C=0 E=0, F=0
Complemento de 1 W=0, R=1, Z=0, I=0, C=1 E=/E +1
Nenhuma ação W=0, R=0, Z=0, I=0, C=0 E=E, F=0
W CLK
Z E
H
N N
F1 F2
Ação Sinais de controle Resultado
Escrita (armazenamento) do valor da entrada W=1, H=0, Z=0 E=D, F1=Z*, F2=E
Leitura (disponibilização) na saída W=0, H=1, Z=0 E=E, F1=E, F2=E
Zera o valor armazenado W=0, H=0, Z=1 E=0, F1=Z*, F2=0
Nenhuma ação W=0, H=0, Z=0 E=E, F1=Z*, F2=E
Z* alta impedância
58
D
W CLK
E
H
59
Somadores
São circuitos puramente computacionais que executam a soma de dois números sem sinal. Um bit
extra, de carry (CB), indica se ocorreu overflow. Observe que o circuito é assíncrono, ou seja, não
depende de sinal de sincronização (CLK)
A1 A2
N N
CB
+
N
Multiplicadores
São circuitos seqüenciais que realizam a multiplicação de dois números. O resultado apresenta o
dobro do número de bits dos sinais de entrada.
A1 A2
N N
CLK
x
2N
Comparadores
São circuitos combinacionais que realizam a comparação de dois números. O resultado apresenta
a comparação de dois números.
A1 A2
N N
> = <
> = <
60
Operadores lógicos
São circuitos combinacionais que realizam operações lógicas bit-a-bit entre duas palavras
binárias. Além da operação OR, estruturas similares são empregadas para efetuar as operações AND,
XOR e NOT.
A1 A2
N N
OR
N
IN
8
Rx
Wx
Zx CLK
GPR
Ix
Cx
8 8
+
8
Wa CLK
Za ACC
Ha
São necessários sucessivos ciclos de clock para realizar uma operação completa, através de uma
seqüência adequada de comandos. O controle destes comandos não faz parte da ULA, e sim, do
controlador do processador.
61
Rx
Wx
Zx
OP1 Ix
Control Cx
OP2
Wa
CLK Za
Ha
8 8 8
8
Rx
Rx Wx
Wx Zx CLK
GPR
Zx Ix
Ix Cx
Cx 8 8
OP1 Wacc
OP2 Zacc
Control
Hacc +
CLK Ha
Wa 8
Hb
Wb Wacc CLK
Hc Zacc ACC
Wc Hacc
8 8
62
1 2 3 4 5 6
CLK
Rx
Wx
Zx
Ix
Cx
Wacc
Zacc
Hacc
Ha
Wa
Hb
Wb
Hc
Wc
BUS A C ACC
Observe que as operações tem duração diferente. A subtração requer 2 ciclos a mais que a soma,
neste caso. Isto ocorre em praticamente todos os computadores, especialmente naqueles com tecnologia
CISC. Os computadores de tecnologia RISC, por outro lado, tem por objetivo fazer com que o tempo de
duração das instruções seja constante.
4.2. O Computador 1
A partir dos conceitos introduzidos acima, iremos expandir para criarmos um computador com
estrutura muito simples. Este computador apenas efetua 4 operações, e tem por objetivo introduzir o
modo como o computador interpreta e efetua operações.
63
Neste computador, ao contrário do anterior, as instruções estão armazenadas na memória, assim
como os dados do programa. Isto é feito através da divisão da palavra de dados, que neste caso é de 8
bits, em duas partes: instrução e dados. Os dois bits mais significativos representam a instrução a ser
efetuada e os seis demais o dado utilizado na instrução.
7 6 5 4 3 2 1 0
instrução argumento
opcode endereço ou dado
Logo, as instruções são codificadas, em binário, com dois bits. A codificação dos comandos é
mostrada na tabela a seguir, onde o Mnemônico é uma representação simplificada da instrução realizada
pelo processador.
Tabela de Operações
Opcode Operação Mnemônico
00 PARA STP
01 SOMA AO ACC ADD <m6>
10 SUBTRAI DO ACC SUB<m6>
11 SALVA ACC NA MEMÓRIA MOV<m6>
onde <m6> representa um endereço da memória.
Apesar de ter dois bits são utilizados para armazenar a instrução, o computador simples efetua
operações aritméticas em 8 bits. Contudo, os operandos podem ter somente 6 bits.
PC
Sinais de controle
Memória
Memória 6
RM 64x8
WM MAR A
MAR D
TPC 6
TIR 8
PC 8
IPC
IR
IR 8
WIR 2
GPR GPR
RX IR
WX 8
ZX 8
IX
2
+
CX 8
ACC
W Controlador ACC
Z
H
8
Sinais de
controle
64
IR: Intruction Register
Exemplo 1: Desenvolver um programa que possui 3 variáveis de entrada e saída e executa a operação
D=-A+B+C, onde onde A, B e C estão armazenados na memória. Mostrar como a memória fica
organizada se A=30, B=110 e C=20.
Vale lembrar que este processador inicia executando a instrução armazenada em <0>. Ele executa
uma instrução por vez. Após efetuar uma instrução, o processador executa a operação armazenada na
posição de memória subseqüente, até a operação de parada.
O programa é a parte desenvolvida pelo usuário, lógica, ou seja, é o software. Já as sub-etapas de
cada instrução, chamada de microinstruções, estão armazenadas em hardware. Contudo, elas também
poderiam ser feitas em software. Desta forma, se observa que não há uma distinção clara entre software e
hardware, pois é possível se executar as mesmas funções em software ou hardware, desde que a estrutura
seja adequada.
Programa (em Linguagem Assembly):
Mapa de Memória: As instruções encontram-se no início porque a primeira instrução a ser lida pelo
processador, quando inicializado, está localizado no endereço <00>.
1
2
3 RESERVADO
4 PARA
5 INSTRUCÕES
6
.
.
58
59 RESERVADO
60
61 PARA
62 DADOS
63
Transcrição do programa para a linguagem da máquina: Optou-se por utilizar-se os últimos quatro bytes
endereçáveis para o armazenamento das variáveis. Poderiam ser outras posições, mas optou-se por estas.
65
A=<60> →<111100> A=(+30) →(00011110)2
B=<61> →<111101> B=(110) →(01101110)2
C=<62> →<111110> C=(20) →(00010100)2
D=<63> →<111111>
0 10111100
1 01111101
2 01111110
3 11111111
4 00000000
.
.
60 00011110
61 01101110
62 00010100
63 00110010
Microinstruções: Agora será ampliada a análise, ilustrando as microinstruções (etapas) executadas por
cada instrução para efetuar a sua ação.
Instrução Micro-instruções
1. TB, RM MAR←PC
SUB <60> 1. TBC MAR←C
2. RM, TB
3. IPC PC←PC+1
4. TIR MAR←IR
5. RM, Wx GPR←Mem<IR>
6. Cx GPR← (GPR)barrado
7. Ix GPR← GPR+1
8. Rx + W ACC= ACC+ GPR
ADD <61> 1. TPC MAR←PC
2. RM, TB
3. IPC PC=PC+1
4. TIR
5. Wx, RM GPR←<61>
6. W, Rx ACC←ACC+GPR
ADD <62> 1. TPC MAR←PC
2. RM, TB
66
3. IPC PC=PC+1
4. RM, Wx GPR←<62>
5. Rx, W ACC←ACC+GPR
MOV<63> 1. TPC
2. RM, TB FETCH
3. IPC
4. TIR
5. H, Wm <63>←ACC
STP 1. TPC
2. RM, TB
3. IPC
Observações:
• Observando as micro-instruções das instruções realizadas por esse processador, observa-se a
existência de micro-instruções idênticas a todas as instruções. Essa seqüência tem por objetivo
buscar na memória a próxima instrução a ser realizada. Por esse motivo essa seqüência é
denominada de busca de instrução ou FETCH;
• Posição de dados e instrução alocados separadamente.
4.3. O Computador 2
O segundo computador é uma evolução do primeiro computador. Sua estrutura permite, além de
operações aritméticas, desvios no fluxo de execução de um programa, característica esta fundamental
para a execução dos algoritmos.
Este processador possui instruções de tamanho variável. Algumas instruções são de 1 byte e
outras de 2 bytes, sendo sempre o primeiro byte associado à ação e o segundo byte, quando houver,
associado ao argumento da instrução.
Este computador endereça 256 bytes de memória, cujo mapa de memória está mostrado na figura
abaixo. A parte inicial do mapa (00-CF) está reservada para o usuário (Programa e Dados). Outras três
partes da memória estão reservadas para rotinas de interrupção e para pilhas de dados, cujo propósito será
discutido na seqüência.
67
00
Programa
C8
Dados
D0
Int0
E0
Int1
F0
Pilha
Este novo processador possui a estrutura dada na próxima figura. Observe que o somador foi
substituído por uma ULA com várias operações aritméticas e lógicas, cujos detalhes internos não serão
mostrados.
Sinais de controle
Memória FLAGS 8 INT0
RM WFLAGS
11010000
WM RFLAGS
MAR SC 8 INT1
WMAR CC
11100000
PC SG
WPC CG Memória
RPC SE 8 8 8 SP
I/O 1111 256x8
DPC CE
IPC ALU 8 8
SP CNOT MAR A
ISP CAND
DSP COR 8
RSP CXOR PC D
GPR CADD
RX CCMP
8
WX LD
CX
IX 8 8
ZX
HXBUS
ACC IR GPR
HA
WA
ZA 8 8 8
WABUS
Int0 3
I/O
WIN Int1 Controlador ALU
= > C
WOUT 3 = > C
HIN 8
HOUT
INT0 Sinais de FLAGS ACC
WINT0 < = > C
controle
INT1
WINT1 8 8 8
IR
WIR
GPR: General Purpose Register
ALU: Arithmetic and Logic Unit
ACC: Accumulator
PC: Program Counter
MAR: Memory Address Register
68
IR: Intruction Register
FLAGS: State Register
SP: Stack Pointer Register
INT0: Interrupt 0 Pointer
INT1: Interrupt 1 Pointer
I/O: In/Out Register
69
CLE Limpa bit de igual
CLG Limpa bit de maior
STC Limpa bit de carry
STE Limpa bit de igual
STG Limpa bit de maior
Entrada e saída de dados (I/O)
IN Entrada de dados (armazena no ACC)
OUT Saída de dados (a partir do ACC)
Diversos
RET Retorno de interrupção
HLT Parada
I2 I1 I0 R1 R0 M2 M1 M0
I2 I1 I0 R0 Tipo de instrução
0 0 0 Especial
0 0 1 0 OR
0 0 1 1 XOR
0 1 0 AND
0 1 1 CMP
1 0 0 SUB
1 0 1 ADD
1 1 0 MOV registrador → memória
1 1 1 MOV memória → registrador
Se a instrução for OR, XOR, AND, SUB ou ADD, avalia-se M2, M1 e M0 para se saber se o
argumento é um endereço ou uma constante:
M2 M1 M0 Tipo de instrução
1 1 0 Endereço
1 1 1 Constante
I2 I1 I0 R1 R0 Tipo da instrução
0 0 0 0 0 Operação sem operando
0 0 0 0 1 Salto
0 0 0 1 0 Not
0 0 0 1 1 Ilegal (reservado)
70
Se (I1 I2 I3)=(0 0 0) e (R1 R2)=(00):
I2 I1 I0 R1 R0 M2 M1 M0 Tipo de instrução
0 0 0 0 0 0 0 0 Ilegal (não definida)
0 0 0 0 0 0 0 1 Ilegal (não definida)
0 0 0 0 0 0 1 0 Ilegal (não definida)
0 0 0 0 0 0 1 1 Ilegal (não definida)
0 0 0 0 0 1 0 0 RET - Retorno de interrupção
0 0 0 0 0 1 0 1 HLT - Parada
0 0 0 0 0 1 1 0 IN (I/O)
0 0 0 0 0 1 1 1 out (I/O)
Se (I1 I2 I3)=(0 0 0) e (R1 R2) = (0 1) define-se uma instrução de salto, que pode ser de vários tipos.
I2 I1 I0 R1 R0 M2 M1 M0 Tipo de instrução
0 0 0 0 1 0 0 0 JE - Salta se for igual
0 0 0 0 1 0 0 1 JNE - Salta se for diferente
0 0 0 0 1 0 1 0 JL - Salta se for menor(<)
0 0 0 0 1 0 1 1 JLE - Salta se for menor ou igual(=<)
0 0 0 0 1 1 0 0 JG - Salta se for maior(>0
0 0 0 0 1 1 0 1 JGE - Salta se for maior ou igual (>=)
0 0 0 0 1 1 1 0 JMP - Salto incondicional
0 0 0 0 1 1 1 1 Ilegal (não definida)
Outras instruções:
I2 I1 I0 R1 R0 M2 M1 M0 Tipo de instrução
1 1 1 1 0 1 1 0 NOT
1 1 1 1 1 1 1 1 PUSH
1 0 0 0 1 1 1 1 POP
1 1 1 1 1 0 0 0 CLC
1 1 1 1 1 1 0 0 CLE
1 1 1 1 1 0 1 0 CLG
1 1 1 1 1 0 0 1 STC
1 1 1 1 1 0 1 0 STE
1 1 1 1 1 0 1 1 STG
71
Exemplo: Implementar o seguinte programa com o computador apresentado:
1. A=10
2. B=0
3. Soma A+3
4. Repete enquanto A<20
a. B=B+1
b. A=A+3
c. Fim repete
5. C=A+B
Solução:
Observe que foram incluídos alguns marcadores para indicar os saltos. Esta é uma característica
da linguagem assembly, a utilização de uma estrutura de três colunas. A primeira, mais a esquerda,
corresponde ao label (etiqueta). Utilizada para efetuar os saltos. A segunda, chamada mnemônio , é uma
abreviação do código da função. E a última, o argumento(s), corresponde as constantes ou endereços
utilizados nas funções.
Esta linguagem não é obviamente a linguagem da máquina, embora esta muito próxima. É
necessário, ainda, se utilizar um compilador para traduzir para a linguagem da máquina. Este compilador
irá efetuar a tarefa de alocar as posições da memória e codificar as instruções para linguagem da
máquina.
O programa mostrado, para ser transcrito para a linguagem da máquina, deve haver uma
corresondência do comprimento das instruções e da posição das variáveis de memória utilizadas. Por
exemplo o programa acima, iniciando-se na posição <00h>, ocuparia as seguintes posições de memória:
72
Label Mnemônio Argumento Nº de posições de
Memória (em hex)
INICIO: AND 0 2 <00>
MOV B,Acc 2 <02>
MOV Acc,10 2 <04>
MOV A, Acc 2 <06>
ADD Acc,3 2 <08>
MOV A, Acc 2 <0A>
AUX1: CMP Acc, 20 2 <0C>
JGE AUX2 2 <0E>
MOV Acc, B 2 <10>
ADD Acc,1 2 <12>
MOV B, Acc 2 <14>
MOV Acc, A 2 <16>
ADD Acc,3 2 <18>
MOV A, Acc 2 <1A>
JMP AUX1 2 <1C>
AUX2: MOV Acc, A 2 <1E>
ADD Acc, B 2 <20>
MOV C, Acc 2 <22>
HLT 1 <24>
O compilador irá definir, também, o endereço do início (na memória) e a posição onde as
variáveis serão armazenadas.
Label Mnemônico Argumento
A EQU <11001001>
B EQU <11001001>
C EQU <11001010>
INICIO EQU <00000000>
73
0E 00001101 JGE
0F 00011110 <1E>
10 11100000 MOV mem→ reg
11 11001001 <B>
12 10100111 ADD
13 00000001 1
14 11000000 MOV reg→mem
15 11001001 <B>
16 11100000 MOV mem→ reg
17 11001000 <A>
18 10100111 ADD
19 00000011 3
1A 11000000 MOV mem→ reg
1B 11001000 <A>
1C 00001110 JMP
1D 01100000 <0C>
1E 11100000 MOV mem→ reg
1F 11001000 <A>
20 10100110 ADD
21 11001001 <B>
22 11000000 MOV reg→mem
23 11001010 <C>
24 00000101 HLT
C8
C9
CA
Execução do programa:
Instrução Micro-instrução
Inicialização 1. Zx,Z Zera Acc, Gpr
2. H,Wpc, Wip Zera Pc
And Constante 1. M = (100)2 Lê a posição de memória
Rm, Wip De Pc e transfere para Ip
2. Ipc Pc=Pc +1
3. Rm, Wx Gpr= 00000000(valor em<01>)
4. And, W Acc= Gpr E Acc
Ipc Pc= Pc+1
Mov mem, Reg 1. M = (100)2 Ip={Pc}
Rm, Wip
2. Ipc Pc=Pc+1
3. M=(100)2 Mar={03}
Rm, WMar
4. M=(011)2 <C9>= Acc
Wm, H
Ipc Pc= Pc+1
Mov Reg, mem 1. M = (100)2 Ip={Pc}
74
Rm, Wip
2. Ipc Pc=Pc+1
3. M=(100)2
Rm, Wx Gpr = {Pc}
4. Rx, W Acc= Gpr
Ipc
Salto incondicional
Jmp 1. M = (100)2 Ip={Pc}
Rm, Wip
2. Ipc Pc=Pc+1
3. Rpc, WMar Mar= Pc
4. M=(011)2 Pc=<Mar>
Rm, Wpc
CMP Acc e (vlr constante) 1. M = (100)2 Ip={Pc}
Rm, Wip
2. Ipc Pc=Pc+1
3. M=(100)2
Rm, Wx Gpr = {Pc}
4. Cx Gpr= (Gpr)barrado
5. Ix Gpr= Gpr+1
6. Rx, W Acc= Acc+Gpr
7. Af Flag ←valor indicando >,<,=
1. as de hardware
2. as de software, que são comandadas pelo usuário do computador.
Por exemplo, em um computador da família pc(x86) rodando o sistema operacional MS-DOS
existe uma família de interrupções relacionadas a este sistema. Cujo índice é 21 em hexa, (21H).
75
Durante a execução do mov, ocorreu mudança em Int0, Int=1( solicitação de interrupção).Nesse
instante, o processador irá deslocar a execução do programa para o endereço em Int0, executando então
as operações localizadas nessa nova posição. Após a execução da rotina de interrupção ele irá retornar
para o ponto onde o program foi interrompido para a execução da interrupção.
Contudo, para a execução do programa interrompido não seja corrompida, deve-se salvar os
conteúdos de Acc, Flag e a posição do programa (Pc) no instante em que a interrupção inicia, sendo esses
valores posteriormente recuperados, quando a rotina de interrupção se encerra. O local onde essas
informações são armazenadas é um espaço especial da memória denominado PILHA de dados. Os
processadores sempre reservam espaços em seus mapas de memória para esse tipo de armazenamento.
76
5 – ESTRUTURA DOS PROCESSADORES 80X86
5.1 . Barramentos
Os processadores 80x86 utilizam o barramento de dados para transportar dados entre os vários
componentes do computador. O tamanho deste barramento varia bastante na famíla 80x86. De certa
forma, este barramento define o "tamanho" do processador.
Nos sistemas 80x86 típicos, o barramento de dados contém 8, 16, 32 ou 64 linhas. Os
microprocessadores 8088 e 80188 tem um barramento de dados de oito bits (oito linhas de dados). Os
processadores 8086, 80186, 80286 e 80386SX tem um barramento de dados de 16 bits. O 80386DX,
80486, Pentium Overdrive têm um barramento de dados de 32 bits. Os processadores Pentium e Pentium
Pro têm um barramento de dados de 64 bits. Futuras versões do chip poderão ter barramentos maiores.
Ter um barramento de dados de oito bits não limita o processador a tipos de dados de oito bits.
Simplesmente significa que o processador pode acessar apenas um byte de dado por ciclo de memória
(veja abaixo "O subsistema da memória"). Isto significa que o barramento de oito bits em um 8088 pode
transmitir apenas a metade da informação por unidade de tempo (ciclo de memória) que um barramento
de 16 bits num 8086. Portanto, processadores com um barramento de 16 bits são naturalmente mais
rápidos do que processadores com um barramento de oito bits. O tamanho do barramento de dados afeta
a performance do sistema mais do que o tamanho de qualquer outro barramento.
Tamanho do Ouve-se falar com frequência em processadores de
Processador
Barramento de Dados oito, 16, 32 ou 64 bits. Como há uma pequena controvérsia
a respeito do tamanho de um processador, muitas pessoas
8088 8
aceitam que o número de linhas de dados no processador
80188 8 determina o seu tamanho. Uma vez que os barramentos da
família 80x86 são de oito, 16, 32 ou 64 bits, muitos acessos
8086 16 a dados são também de oito, 16, 32 ou 64 bits. Apesar de
ser possível processar 12 bits de dados com um 8088,
80186 16
muitos programadores processam 16 bits já que o
80286 16 processador buscará e manipulará 16 bits de qualquer
forma. Isto é porque o processador sempre busca oito bits.
80386SX 16
Buscar 12 bits requer duas operações de oito bits na
80386DX 32 memória. Já que o processador busca 16 bits e não 12, a
maioria dos programadores utilizam todos os 16 bits. Em
80486 32 geral, manipular dados que tenham oito, 16, 32 ou 64 bits é
mais eficiente.
80586 Pentium (Pro) 64
Embora os membros de 16, 32 e 64 bits da família
80x86 possam processar dados até a largura do barramento,
eles também podem acessar unidades menores de memória - de oito, 16 ou 32 bits. Então, qualquer coisa
que se possa fazer com um barramento de dados pequeno também é possível de ser feita com um
barramento de dados maior; o barramento de dados maior, contudo, pode acessar a memória mais
rapidamete e pode acessar fragmentos de dados maiores em apenas uma operação na memória. Você lerá
sobre a exata natureza desses acessos de memória mais adiante.
77
5.1.2. O barramento de endereços
O barramento de dados nos processadores do 80x86 transfere informação entre uma posição de
memória em particular ou entre dispositivo de E/S e a CPU. A única questão é, "qual é a posição de
memória ou o dispositivo de E/S?". O barramento de endereços responde esta questão. Para diferenciar
posições de memória de dispositivos de E/S, o projetista do sistema atruibui um único endereço de
memória para cada elemento de memória e dispositivo de E/S. Quando o software quiser acessar alguma
posição de memória ou um dispositivo de E/S em particular, ele coloca o endereço correspondente no
barramento de endereços. Circuitos associados à memória ou ao dispositivo de E/S reconhecem este
endereço e instruem a memória ou o dispositivo de E/S a ler o dado de ou para o barramento de dados.
Em ambos os casos, todas as outras posições de memória ignoram a requisição. Apenas o dispositivo
cujo indereço combina com o valor no barramento de endereço é que responde.
Com uma única linha de endereço, um processador poderia criar exatamente dois endereços
únicos: zero e um. Com n linhas, o processador pode fornecer 2^n endereços distintos (já que há 2^n
valores únicos em um número binário de n bits). Portanto, é o número de bits no barramento de
endereços que determina o número máximo de memória endereçável e de posições de E/S. O 8088 e o
8086, por exemplo, têm barramento de endereços de 20 bits. Portanto, eles podem acessar até 1.048.576
(ou 2^20) posições de memória. Barramentos de endereços maiores podem acessar mais memória. O
8088 e o 8086 por exemplo, sofrem de anemia do espaço de endereços - seus barramentos de endereço
são muito pequenos. Processadores mais recentes têm barramentos de endereços maiores:
Tamanho do Máximo de
Processador Unidade
Barramento de Dados Memória Endereçável
8088 20 1.048.576 1 Megabyte
8086 20 1.048.576 1 Megabyte
80188 20 1.048.576 1 Megabyte
80186 20 1.048.576 1 Megabyte
80286 24 16.777.246 16 Megabytes
80386SX 24 16.777.246 16 Megabytes
80386DX 32 4.294.976.296 4 Gigabytes
80486 32 4.294.976.296 4 Gigabytes
80586 Pentium Pro 32 4.294.976.296 4 Gigabytes
Futuros processadores 80x86 provavelmente suportarão barramentos de endereço de 48 bits. Hoje
em dia muitos programadores consideram quatro gigabytes de memória pouco (houve um tempo em que
um megabyte foi considerado muito mais do que qualquer um poderia precisar!). Felizmente, a
arquitetura do 80386, 80486 e chips mais novos permitem uma fácil expansão para barramentos de
endereço de 48 bits através de segmentação.
78
outra. Se a linha de leitura estiver baixa (zero lógico), a CPU está lendo dados da memória (isto é, o
sistema está transferindo dados da memória para a CPU). Se a linha de escrita está baixa, o sistema
transfere dados da CPU para a memória.
As linhas de controle de um byte são um outro conjunto importante de linhas de controle. Essas
linhas de controle permitem que processadores de 16, 32 e 64 bits trabalhem com fragmentos menores de
dados. Detalhes adicionais aparecerão na próxima seção.
A família 80x86, diferente de muitos outros processadores, fornece dois espaços de endereços
distintos: um para memória e um para E/S. Enquanto os barramentos de endereços de memória nos vários
processadores 80x86 variam de tamanho, o barramento de endereços de E/S é de 16 bits em todas as
CPUs do 80x86. Isto permite que o processador enderece até 65.536 posições diferentes de E/S.
Acontece que muitos dispositivos (como teclado, impressora, drives de disco, etc.) requerem mais do que
uma posição de E/S. Apesar disso, 65.536 posições de E/S são mais do que suficientes para a maioria das
aplicações. O projeto original do IBM PC permitia apenas o uso de apenas 1.024 posições.
Embora a família 80x86 suporte dois espaços de endereço, ela não tem dois barramentos de dados
(para E/S e memória). Em vez disso, o sistema compartilha o barramento de endereço para ambos os
endereços, de E/S e de memória. Linhas de controle adicionais decidem se o endereço é referente à
memória ou à E/S. Quando tais sinais estão ativos, os dispositivos de E/S utilizam o endereço nos 16 bits
menos significativos para o barramento de endereços. Quando inativos, os dispositivos de E/S ignoram
os sinais no barramento de endereços (o subsistema de memória assume a partir deste ponto).
Um processador 80x86 típico endereça no máximo 2^n posições diferentes de memória, onde n é
o número de bits no barramento de endereços. Como você já viu, os processadores 80x86 têm
barramentos de 20, 24 e 32 bits (com 48 bits a caminho).
É claro que a primeira pergunta que se poderia fazer é, "O que exatamente é uma posição de
memória?". O 80x86 permite memória endereçável por byte. Portanto, a unidade básica de memória é
um byte. Então, com 20, 24 e 32 linhas de endereço, os processadores 80x86 podem endereçar um
megabyte, 16 megabytes e quatro gigabytes de memória, respectivamente.
A discussão acima aplica-se apenas para o acesso de um único byte na memória. Então, o que
acontece quando o processador acessa uma word ou uma double word? Já que a memória consiste de um
array de bytes, como poderemos tratar valores maiores do que oito bits?
Diferentes sistemas têm diferentes soluções para este problema. A família 80x86 trata este
problema armazenando o byte menos significativo de uma word no endereço especificado e o byte mais
significativo na próxima posição. Então, uma word consome dois endereços de memória consecutivos (é
o que se poderia esperar, já que uma word consite de dois bytes). Da mesma forma, uma double word
consome quatro posições de memória consecutivas. O endereço para a double word é o endereço do seu
byte menos significativo. Os três bytes restantes seguem este byte menos significativo, com o byte mais
significativo aparecendo no endereço da double word mais três.
Bytes, words e double words podem começar em qualquer endereço válido da memória. Veremos
em breve, contudo, que iniciar grandes objetos em endereços arbitrários não e uma boa idéia.
Note que a probabilidade de que valores de um byte, de um word e de um double word se
sobreponham na memória é grande. Por exemplo, na Fig.3 poderíamos ter uma variável word
começando no endereço 193, um byte no endereço 194 e um valor double word começando no endereço
192. Todas essas variáveis estariam sobrepostas.
79
Os microprocessadores 8088 e 80188 têm um barramento de
dados de oito bits. Isto significa que a CPU pode transferir oito bits de
dados por vez. Já que cada endereço de memória corresponde a um byte
de oito bits, a melhor arquitetura (do ponto de vista do hardware), seria a
da Fig.4, onde os dados vêm da memória 8 bits por vez.
O termo "array de memória endereçável por byte" significa que a
CPU pode endereçar memória em fragmentos tão pequenos quanto um
único byte. Também significa que esta é a menor unidade de memória
Fig.4 - Dados saem da memória
que você pode acessar por vez com o processador. Isto é, se o
de 8 em 8 bits processador quiser acessar um valor de quatro bits, ele deve ler oito bits e
ignorar os quatro bits extras. Também significa que endereçabilidade por
byte não implica que a CPU possa acessar oito bits sem qualquer tipo de limite. Quando especificamos o
endereço 125 da memória, obtemos todos os oito bits daquele endereço, nada mais e nada menos.
Endereços são inteiros; não podemos, por exemplo, especificar o endereço 125,5 para trazer menos do
que oito bits.
O 8088 e o 80188 podem manipular valores de words e double words, mesmo com seus
barramentos de oito bits. Contudo, isto requer múltiplas operações de memória porque esses
processadores podem apenas mover oito bits de dados por vez. Carregar uma word requer duas operações
na memória; carregar uma double word requer quatro operações na memória.
Par Ímpar Os processadores 8086, 80186, 80286 e 80386SX têm um barramento de dados
de 16 bits. Isto permite que estes processadores acessem duas vezes mais memória na
6 7
mesma quantidade de tempo que seus irmãos de oito bits. Esses processadores
4 5 organizam a memória em dois bancos: um banco "par" e um banco "ímpar":
2 3 A Fig.5 ilustra a conexão à CPU (D0-D7 denota o byte menos significativo do
0 1 barramento de dados, D8-D15 denota o byte mais significativo do barramento de
dados).
80
Os membros de 16 bits da família 80x86 podem carregar
uma word de qualquer endereço arbitrário. Como mencionado
anteriormente, o processador busca o byte menos significativo do
valor no endereço especificado e o byte mais significativo no
endereço seguinte. Isto cria um pequeno problema se analisarmos
corretamente o diagrama da Fig.5. O que acontece quando
acessamos uma word num endereço ímpar? Suponha que você
queira ler uma word na posição 125. O byte menos significativo
vem da posição 125 e o byte mais significativo vem da posição
126. O que acontece? Acontece que há dois problemas neste caso.
Fig.5 - Dois bancos de memória
de 8 bits
As linhas 8 a 15 do barramento de dados (o byte mais
significativo) estão no banco ímpar, e as linhas 0 a 7 do barramento
de dados (o byte menos significativo) estão no banco par. Acessar a posição 125 da memória transferirá
dados para a CPU do byte mais significativo do barramento de dados; mas queremos este dado no byte
menos significativo! Felizmente, as CPUs do 80x86 reconhecem esta situação e automaticamente
transferem os dados em D8-D15 para o byte menos significativo.
O segundo problema é ainda mais obscuro. Quando acessamos words, na realidade estamos
acessando dois bytes separados, cada um dos quais tem seu próprio endereço de byte. Então surge a
questão, "Que endereço aparece no barramento de dados?" As CPUs de 16 bits do 80x86 sempre
colocam endereços pares no barramento de dados. Os bytes pares sempre aparecem nas linhas de dados
D0-D7 e os bytes ímpares sempre aparecem nas linhas de dados D8-D15. Se acessarmos uma word num
endereço par, a CPU pode trazer um fragmento inteiro de 16 bits em uma operação de memória. Da
mesma forma, se você acessar um único byte, a CPU ativa o banco apropriado (utilizando as linhas de
controle). Se o byte estiver em um endereço ímpar, a CPU automaticamente o moverá do byte mais
significativo no barramento para o byte menos significativo.
Então, o que acontece quando a CPU acessa uma word em um endereço ímpar, como no exemplo
dado anteriormente? Bem, a CPU não pode colocar o endereço 125 no barramento de endereços e ler os
16 bits da memória. Não há endereços ímpares resultantes de uma CPU de 16 bits do 80x86. Os
endereços são sempre pares. Portanto, se tentarmos colocar 125 no barramento de endereços, o que será
colocado será 124. Se lermos os 16 bits neste endereço, obteremos a word do endereço 124 (byte menos
significativo) e 125 (mais significativo) - e não o que esperávamos. Para acessar uma word em um
endereço ímpar é preciso ler o byte do endereço 126. Finalmente, deve-se trocar as posições desses bytes
internamente, uma vez que ambos entraram na CPU na metade errada do barramento de dados.
Felizmente, as CPUs de 16 bits do 80x86 ocultam esses detalhes. Seus programas podem acessar
words em qualquer endereço e a CPU acessará apropriadamente e trocará (se necessário) os dados na
memória. Contudo, acessar uma word em um endereço ímpar requer duas operações a mais na memória
(exatamente como o 8088/80188). Portanto, acessar words em endereços ímpares em um processador de
16 bits é mais lento do que acessar words em endereços pares. Organizando cuidadosamente como a
memória é utilizada, você pode dar mais velocidade ao seu programa.
Em processadores de 16 bits, acessar quantidades de 32 bits sempre precisam, no mínimo, de duas
operações de acesso à memória. Se você acessar 32 bits em endereços ímpares, o processador exigirá três
operações de memória para acessar o dado.
81
Os processadores de 32 bits do 80x86 (o
80386, 80486 e o Pentium Overdrive) utilizam
quatro bancos de memória conectados ao
barramento de dados de 32 bits.
O endereço colocado no barramento de
endereços é sempre algum múltiplo de quatro.
Utilizando várias linhas com capacidade de um byte,
a CPU pode selecionar quais dos quatro bytes
daquele endereço o software quer acessar. Assim
como nos processadores de 16 bits, a CPU
automaticamente rearranjará os bytes quando
necessário.
Fig.6 - Quatro bancos de memória
de 8 bits
Com a interface de memória de 32 bits, a
CPU do 80x86 pode acessar qualquer byte com uma
única operação de memória. Se não for igual a três (endereço MOD 4), então uma CPU de 32 bits pode
acessar uma word em qualquer endereço utilizando uma única operação na memória. Contudo, se o resto
for três, então ela gastará duas operações de memória para acessar a referida word.
Este é o mesmo problema encontrado com o processador de 16 bits, exceto por ele ocorrer com a
metade da frequência.
Uma CPU de 32 bits pode acessar uma double word com uma única operação de memória se o
endereço daquele valor for exatamente divisível por quatro. Se não, a CPU necessitará de duas operações
de memória.
Uma vez mais a CPU gerencia tudo isto automaticamente. Contudo, há o benefício da
performance em alinhar os dados adequadamente. Como uma regra geral deve-se sempre colocar valores
de words em endereços pares e valores de double words em endereços que são exatamente divisíveis por
quatro. Isto agilizará o programa.
Além das 20, 24 ou 32 linhas de endereço que acessam a memória, a família 80x86 fornece um
barramento de endereços de E/S de 16 bits. Isto dá às CPUs do 80x86 dois espaços de endereços
separados: um para memória e um para operações de E/S. As linhas do barramento de controle
diferenciam entre endereços de memória e E/S. Exceto por linhas de controle separadas e por um
barramento menor, acessos à E/S comportam-se exatamente como acessos à memória. Memória e
dispositivos de E/S compartilham o mesmo barramento de dados e as 16 linhas menos significativas do
barramento de endereços.
Há três limitações no subsistema de E/S do IBM PC; primeiro, as CPUs do 80x86 requerem
instruções especiais para acessar dispositivos de E/S; segundo, os projetistas do IBM PC usaram as
"melhores" posições de E/S para seus próprios propósitos, forçando os outros desenvolvedores a
utilizarem as posições de memória menos acessíveis; terceiro, os sistemas 80x86 não podem endereçar
mais do que 65.536 (2^16) endereços de E/S. Quando se considera que uma placa de vídeo VGA típica
requer mais de 128.000 posições diferentes, pode-se vislumbrar um problema como o tamanho do
barramento de E/S.
82
Felizmente desenvolvedores de hardware podem mapear seus dispositivos de E/S dentro para o
espaço de endereçamento da memória tão facilmente quanto para o espaço de endereçamento de E/S.
Então, utilizando o sistema de circuitos apropriados, podem fazer seus dispositivos de E/S se parecerem
exatamente com a memória. É como, por exemplo, os adaptadores de vídeo no IBM PC funcionam.
Acessar dispositivos de E/S é um assunto ao qual retornaremos mais adiante. Por enquanto
assumiremos que acessos à E/S e à memória funcionam da mesma forma.
Embora os computadores modernos sejam muito rápidos e fiquem mais rápidos a todo o
momento, eles ainda necessitam de uma quantidade finita de tempo para efetuar até as menores tarefas.
Nas máquinas de Von Neumann, como o 80x86, muitas operações são serializadas. Isto significa que o
computador executa comandos numa ordem prescrita. Não precisaria ser assim, por exemplo, para
executar a instrução I:=I*5+2; antes da instrução I:=J; na seguinte sequência:
I := J;
I := I * 5 + 2;
Fica claro que precisamos de alguma forma controlar qual instrução deve ser executada primeiro
e qual deve ser executada depois.
É óbvio que, em computadores reais, as operações não ocorrem instantaneamente. Mover uma
cópia de J para I leva um certo tempo. Da mesma forma, multiplicar I por cinco, depois adicionar dois e
armazenar o resultado em I também gasta tempo. Como seria de esperar, a segunda instrução em Pascal
acima leva um pouco mais de tempo para ser executada do que a primeira. Para aqueles interessados em
escrever software rápido, uma das primeiras perguntas seria - "Como fazer o processador executar
instruções e como medir o tempo gasto para serem executadas?"
A CPU é uma peça de circuito eletrônico muito complexa. Sem entrar em maiores detalhes,
vamos apenas dizer que operações dentro da CPU devem ser coordenadas com muito cuidado ou a CPU
produzirá resultados errados. Para garantir que todas as operações ocorrram no momento certo, as CPUs
do 80x86 utilizam um sinal alternado chamado clock do sistema.
Fig.8 - O Clock
As CPUs são um bom exemplo de um complexo sistema lógico sincronizado. O clock do sistema
coordena muitas das portas lógicas que compõem a CPU permitindo que elas operem em sincronia.
A frequência com que o clock alterna entre zero e um é a frequência do clock do sistema. O
tempo que ele leva para alternar de zero para um e voltar para zero é o período do clock. Um período
83
completo é também chamado de ciclo do clock. Em muitos computadores modernos o clock do sistema
alterna entre zero e um numa frequência que supera muitos milhões de vezes por segundo. Um chip
80486 comum cicla a 66 milhões de Hertz, ou seja, 66 MegaHertz (MHz). Frequências comuns para
modelos 80x86 variam de 5 MHz até 200 MHz ou mais. Note que um período do clock (a quantidade de
tempo para um ciclo completo) é o inverso da frequência do clock. Por exemplo, um clock de 1 MHz
teria um período de clock de um microsegundo (1/1.000.000 de um segundo). Da mesma forma, um
clock de 10 MHz teria um período de clock de 100 nanosegundos (100 bilhonésimos de um segundo).
Uma CPU rodando a 50 MHz teria um período de clock de 20 nanosegundos. Note que usualmente
expressamos períodos de clock em milhonésimos ou bilhonésimos de segundo.
Para garantir a sincronização, muitas CPUs iniciam uma operação em uma borda descendente
(quando o clock vai de um a zero) ou em uma borda ascendente (quando o clock vai de zero a um). O
clock do sistema gasta a maior parte do seu tempo no zero ou no um, e muito pouco tempo alternando
entre os dois. Portanto, o momento de alternância do clock é o ponto perfeito para uma sincronização.
Uma vez que todas as operações da CPU são sincronizadas pelo clock, a CPU não pode efetuar
nenhuma tarefa que seja mais rápida do que o clock. Contudo, o simples fato da CPU estar executando
em determinada frequência de clock não significa que esteja executando uma operação a cada ciclo.
Muitas operações precisam de vários ciclos de clock para serem completadas, o que significa que a CPU
geralmente efetua operações numa taxa significantemente mais baixa.
84
Fig.9 - Tempo de acesso à memória para leitura
A escrita de dados na memória é parecida (Fig.10). Note que a CPU não espera pela memória. O
tempo de acesso é especificado pela frequência do clock. Se o subsistema de memória não trabalha com a
rapidez suficiente, a CPU lerá uma "salada" de dados em uma operação de leitura e não armazenará
corretamente os dados numa operação de escrita. Isto certamente levará a uma falha no sistema.
Dispositivos de memória têm vários índices de avaliação, mas os dois principais são a capacidade
e a velocidade (tempo de acesso). Dispositivos RAM (random access memory) comuns têm capacidades
de quatro (ou mais) megabytes e velocidades de 50-100 ns. Pode-se comprar dispositivos maiores ou
mais rápidos, mas eles são muito mais caros. Um sistema comum 80486 de 33 MHz utiliza dispositivos
de memória de 70 ns.
Mas, espere aí! Em 33 MHz o período do clock é aproximadamente de 33 ns. Como pode um
projetista de sistema conseguir acompanhar o clock usando memórias de 70 ns? A resposta está nos
estados de espera (wait states).
85
5.4.3. Estados de espera
Um estado de espera não é nada mais do que um
ciclo de clock extra para dar a algum dispositivo o tempo
para completar uma operação. Por exemplo, um sistema
80486 de 50 MHz tem um período do clock de 20 ns. Isto
significa que necessitamos de uma memória de 20 ns. Na
realidade, a situação é pior do que isto. Em muitos
computadores há circuitos adicionais entre a CPU e a
memória: decodificadores e buffers lógicos. Estes circuitos
adicionais introduzem atrasos adicionais no sistema (veja a
Fig.11). Neste diagrama o sistema perde 10 ns para
bufferização e para a decodificação. Então, se a CPU exigir
Fig.11 - Retardos provocados pelo que os dados retornem em 20 ns, a memória deveria
decodificador e pelo buffer responder em menos do que 10 ns.
É claro que podemos comprar memórias de 10 ns. Contudo, são muito mais caras, volumosas,
consomem muito mais energia e geram mais calor. Estas são características ruins. Supercomputadores
utilizam este tipo de memória. Entretanto, supercomputadores também custam milhões de dólares,
ocupam salas inteiras, requerem refrigeração especial e têm fontes de eletricidade gigantescas. Não é
exatamente o tipo de equipamento que você quer sobre a sua mesa, ou é?
Se memórias mais baratas não funcionam com um processador rápido, como as empresas
conseguem vender PCs rápidos? Uma parte da resposta é o estado de espera. Por exemplo, se tivermos
um processador de 20 MHz com um tempo de ciclo de memória de 50 ns e perdermos 10 ns para a
bufferização e decodificação, vamos precisar de memórias de 40 ns. O que fazer se apenas pudermos
adquirir memórias de 80 ns para um sistema de 20 MHz? Adicionando um estado de espera para ampliar
o ciclo da memória para 100 ns (dois ciclos do clock) resolverá este problema. Subtraindo os 10 ns para a
decodificação e a bufferização ainda nos deixa com 90 ns. Portanto, uma memória de 80 ns, antes que a
CPU requisite os dados, responderá bem.
Quase todas as CPUs de propósito geral usadas atualmente fornecem um sinal no barramento de
controle que permite a inserção de estados de espera. Geralmente, se necessário, é o circuito de
decodificação que sinaliza para esta linha esperar um período do clock adicional. Isto dá à memória
tempo de acesso suficiente e o sistema funciona apropriadamente (veja na Fig.12).
Algumas vezes um único estado de espera não é suficiente. Considere o 80486 rodando a 50
MHz. O tempo normal do ciclo de memória é menos do que 20 ns. Então, menos do que 10 ns estão
disponíveis depois de subtrair o tempo de decodificação e bufferização. Se estivermos utilizando
memória de 60 ns no sistema, adicionar um único estado de espera não será o suficiente. Cada estado de
86
espera nos dá 20 ns, portanto, com um único estado de espera, precisaríamos de uma memória de 30 ns.
Para funcionar com a memória de 60 ns, precisaríamos adicionar três estados de espera (zero estados de
espera = 10 ns, um estado de espera = 30 ns, dois estados de espera = 50 ns e três estados de espera = 70
ns).
É desnecessário dizer que, do ponto de vista da performance do sistema, estados de espera não
são uma boa coisa. Enquanto a CPU está esperando por dados da memória, ela não pode operar naqueles
dados. Adicionando um único estado de espera ao ciclo da memória em uma CPU 80486 dobra a
quantidade de tempo necessária para acessar dados. Isto é a metade da velocidade de acesso à memória.
Executar com um estado de espera em todos os acessos à memória é quase como cortar a frequência do
clock do processador pela metade. Obtemos muito menos trabalho realizado na mesma quantidade de
tempo.
Você provavelmente já viu anúncios do tipo "80386DX, 33 MHz, RAM de 8 megabytes e 0
estados de espera... apenas $1.000!" Se você olhou bem nas especificações, notou que os fabricantes
estavam utilizando memórias de 80 ns. Como podiam construir sistemas que rodavam a 33 MHz e ter
zero estados de espera? Fácil. Eles estavam mentindo.
Não há como um 80386 rodar a 33 MHz, executando um programa arbitrário, sem nunca inserir
um estado de espera. É claramente impossível. Porém, é totalmente possível desenvolver um subsistema
de memória que, sob certas circustâncias especiais, consiga operar sem estados de espera em parte do
tempo.
Entretanto, não estamos fadados a execuções lentas devido à adição de estados de espera. Existem
muitos truques que os desenvolvedores de hardware podem usar para alçancar zero estados de espera na
maior parte do tempo. O mais comum deles é o uso de memória cache (pronuncia-se "cash").
87
Há um exemplo adicional de localidade temporal e espacial no exemplo acima, embora ele não
seja tão óbvio. Instruções de computador que indicam uma tarefa específica que o sistema deve realizar
também aparecem na memória. Essas instruções aparecem sequencialmente na memória - a localidade
espacial. O computador também executa essas instruções repetidamente, uma vez para cada iteração - a
localidade temporal.
Se olharmos para o perfil de execução de um programa comum, descobriremos que, em geral, o
programa executa menos da metade das instruções. Um programa comum, geralmente, utilizaria apenas
10 a 20% da memória destinada a ele. Num dado momento, um programa de um megabyte poderia talvez
acessar de quatro a oito kilobytes de dados e código. Então, se gastamos uma soma escandalosa de
dinheiro por uma RAM cara de zero estados de espera, não estaremos utilizando a maior parte dela em
qualquer dado momento! Não seria melhor se pudéssemos comprar uma pequena quantidade de RAMs
rápidas e redeterminar dinamicamente seus endereços à medida que o programa fosse executado?
É exatamente isto o que a memória cache faz. A memória cache reside entre a CPU e a memória
principal. É uma pequena porção de memória muito rápida (com zero estados de espera). Ao contrário da
memória convencional, os bytes que aparecem dentro de uma cache não têm endereços fixos. A memória
cache pode redeterminar o endereço de um dado. Isto permite que o sistema mantenha os valores
acessados recentemente na cache. Endereços que a CPU nunca acessou ou não acessou recentemente
ficam na memória principal (lenta). Já que a maior parte dos acessos à memória correspondem a
variáveis acessadas recentemente (ou em posições próximas de posições acessadas recentemente), o dado
geralmente aparece na memória cache.
A memória cache não é perfeita. Embora um programa possa gastar um tempo considerável
executando código num local, ele eventualmente chamará um procedimento ou desviará para alguma
seção distante de código, fora da memória cache. Nestes casos, a CPU precisa acessar a memória
principal para buscar os dados. Como a memória principal é lenta, haverá a necessidade de inserir
estados de espera.
Um acerto de cache ("cache hit") ocorre sempre que a CPU acessar a memória e encontrar o dado
na cache. Neste caso, a CPU pode realmente acessar o dado com zero estados de espera. Uma falha de
cache ("cache miss") ocorre se a CPU acessar a memória e o dado não estiver presente na cache. Então a
CPU precisa ler o dado da memória principal, causando uma perda de performance. Para tirar vantagem
da localidade de referência, a CPU copia dados para dentro da cache sempre que ela acessar um endereço
ausente na cache. Como é provável que o sistema acesse aquela mesma posição pouco tempo depois,
tendo aquele dado na cache, o sistema economizará estados de espera.
Como descrito acima, a memória cache trata dos aspectos temporais de acesso à memória, mas
não dos aspectos espaciais. Armazenar posições da memória quando são acessadas não agilizará o
programa se acessarmos constantemente posições consecutivas (localidade espacial). Para resolver este
problema, muitos sistemas de cache lêem muitos bytes consecutivos da memória quando ocorre uma
falha de cache. O 80486, por exemplo, lê 16 bytes de uma vez quando a cache falha. Se lermos 16 bytes,
porque lê-los em blocos ao invés de quando precisarmos deles? Muitos chips de memória disponíveis
atualmente têm modos especiais que permitem o acesso rápido de várias posições de memória
consecutivas no chip. A cache tira proveito desta capacidade para reduzir o número médio de estados de
espera necessários para acessar a memória.
Se escrevermos um programa que acessa a memória randomicamente, utilizar a memória cache,
na verdade, pode torná-lo mais lento. Ler 16 bytes a cada falha de cache é caro se apenas acessarmos uns
poucos bytes na linha de cache correspondente. Apesar de tudo, os sistemas de memória cache
funcionam muito bem.
Não deve ser surpresa que a proporção entre acertos e falhas de cache aumenta com o tamanho
(em bytes) do subsistema de memória cache. O chip 80486, por exemplo, tem 8.192 bytes de cache por
88
chip. A Intel declara obter uma taxa de acerto de 80 a 95% com esta cache (significa que em 80 a 95%
das vezes a CPU encontra o dado na cache). Isto parece muito impressionante. Contudo, se brincarmos
um pouco com os números, veremos que isto não é tão impressionante assim. Suponha que consideremos
os 80% do número. Então, na média, um em cada cinco acessos à memória não estará na cache. Se
tivermos um processador de 50 MHz e um tempo de acesso à memória de 90 ns, quatro dos cinco acessos
precisam de apenas um ciclo de clock (já que eles estão na cache) e o quinto necessitará de
aproximadamente 10 estados de espera. No total, o sistema necessitará de 15 ciclos de clock para acessar
cinco posições de memória ou, em média, três ciclos de clock por acesso. Isto é o equivalente a dois
estados de espera adicionados a cada acesso à memória. Cocê acredita agora que sua máquina roda com
zero estados de espera?
Há duas formas de melhorar a situação. Primeiro, podemos adicionar mais memória cache. Isto
melhora a taxa de acerto de cache, reduzindo o número de estados de espera. Por exemplo, aumentando a
taxa de acerto de 80% para 90% permite que 10 posições de memória sejam acessadas em 20 ciclos. Isto
reduz o número médio de estados de espera por acessos à memória para um estado de espera - uma
melhora substancial. Só que não podemos desmontar um chip 80486 e soldar mais memória cache no
chip. Contudo, a CPU do 80586/Pentium tem uma cache significantemente maior do que a do 80486 e
opera com muito menos estados de espera.
Uma outra forma de melhorar a
performance é construir um sistema de cache de
dois níveis. Muitos sistemas 80486 funcionam
desta forma. O primeiro nível é a chache de 8.192
bytes no chip. O nível seguinte, entre a cache no
chip e a memória principal, é uma cache
secundária construída no sistema de circuitos da
placa do computador.
Uma cache secundária comum possui algo
Fig.13 - Cache de dois níveis
em torno de 32.768 bytes a um megabyte de
memória. Tamanhos comuns em subsistemas de PC são 65.536 e 262.144 bytes de cache.
Você poderia perguntar "Por que se incomodar com uma cache de dois níveis? Por que não
utilizar uma cache que tenha 262.144 bytes?" Bem, a cache secundária geralmente não opera com zero
estados de espera. Circuitos que oferecem 262.144 bytes de memória de 10 ns (com tempo total de
acesso de 20 ns) seriam muito caros. Por isso a maioria dos projetistas de sistemas utilizam memórias
mais lentas que requerem um ou dois estados de espera. Isto ainda é muito mais rápido do que a memória
principal. Combinada com a cache no chip da CPU, obtém-se uma melhor performance do sistema.
Considere o exemplo anterior, com uma taxa de acerto de 80%. Se a cache secundária precisar de
dois ciclos para cada acesso à memória e de três ciclos para o primeiro acesso, então uma falha de cache
na cache do chip precisará de seis ciclos de clock. Tudo indica que a média da performance do sistema
será de dois clocks por acesso à memória, o que é um pouco mais rápido do que os três necessários pelo
sistema sem a cache secundária. Além do mais, a cache secundária pode atualizar seus valores em
paralelo com a CPU. Deste modo, o número de falhas de cache (o que afeta a performance da CPU)
diminui.
Você provavelmente está pensando "Até agora tudo isto parece interessante, mas o que tem a ver
com programação?" Na verdade, pouco. Escrevendo seus programas cuidadosamente para tirar vantagem
da forma como o sistema de memória cache funciona, você pode melhorar a performance dos mesmos.
Colocando as variáveis usadas com mais frequência na mesma linha de cache, você pode forçar o sistema
de cache a carregar essas variáveis como um grupo, economizando estados de espera extras em cada
acesso.
89
Se você organizar seu programa do forma que ele tenha a tendência de executar a mesma
sequência de instruções repetidamente, ele terá um alto grau de localidade temporal de referência e, dessa
forma, será executado mais rapidamente.
Os registradores da CPU
Os registradores da CPU são posições de memória muito especiais construídas com flip-flops. Os
registradores não fazem parte da memória principal - a CPU os implementa em chips. Vários membros
da família 80x86 têm registradores de tamanhos diferentes. As CPUs do 886, 8286, 8486 e 8686 (x86 de
agora em diante) têm exatamente quatro registradores, todos com capacidade de 16 bits. Toda aritmética
e todas as operações de localização ocorrem nos registradores da CPU.
AX Acumulador Como o processador x86 tem poucos registradores, vamos
chamaremos cada registrador pelo seu próprio nome ao invés de
BX Registrador do endereço base
nos referirmos a eles pelo seu endereço. Os nomes para os
CX Contador registradores do x86 podem ser vistos na tabela ao lado.
DX Registrador de Dados Além dos registradores citados, que são visíveis ao
programador, os processadores x86 também têm um registrador
apontador de instruções que contém o endereço da próxima
instrução a ser executada. Há também um registrador flag que guarda o resultado de uma comparação. O
registrador flag lembra se um valor era menor do que, igual a, ou maior do que outro valor.
Como registradores são chips e são manipulados especialmente pela CPU, eles são muito mais
rápidos do que a memória. Acessar uma posição de memória requer um ou mais ciclos de clock, acessar
dados em um registrador geralmente toma zero ciclos de clock. Portanto, devemos tentar manter
variáveis em registradores. Conjuntos de registradores são muito pequenos e a maioria dos registradores
tem propósitos especiais que limitam seu uso como variáveis, mas eles são ainda um excelente local para
armazenar dados temporários.
A unidade aritmética e lógica (ALU - Arithmetic and Logical Unit) é onde a maioria das ações
ocorrem dentro da CPU. Por exemplo, se quisermos adicionar o valor cinco ao registrador AX, a CPU
1. Copia o valor de AX para dentro da ALU
2. Envia o valor cinco para a ALU
3. Instrui a ALU para somar esses dois valores
4. Move o resultado de volta para o registrador AX
90
A unidade de interface do barramento
A Unidade de Interface do Barramento (BIU - Bus Interface Unit) é responsável por controlar os
barramentos de endereço e de dados quando estes acessarem a memória principal. Se uma cache estiver
presente no chip da CPU, então a BIU também é responsável por acessar os dados na cache.
Uma pergunta que se impõe neste ponto é "Como exatamente a CPU efetua a atribuição de
tarefas?" Isto é efetuado fornecendo à CPU um conjunto fixo de comandos, ou instruções, para trabalhar.
Tenha em mente que os desenvolvedores da CPU constroem esses processadores utilizando portas
lógicas para executar essas instruções. Para manter o número de portas lógicas como um conjunto
razoavelmente pequeno (dezenas ou centenas de milhares), os projetistas da CPU devem restringir
obrigatoriamente o número e a complexidade dos comandos que a CPU reconhece. Este pequeno
conjunto de comandos é o conjunto de instruções da CPU.
Os programas nos primeiros sistemas de
computadores (pré-Von Neumann) eram frequentemente
"montados" em forma de circuitos. Isto é, as conexões do
computador determinavam que tipo de problema o
computador resolveria. Para alterar o programa, alguém tinha
que reconectar o sistema de circuitos - uma tarefa muito
difícil. O avanço seguinte no projeto de computadores foi o
sistema de computador programável, um que permitia ao
Fig.14 - Sockets de instruções
programador reconectar com facilidade o sistema utilizando
uma sequência de sockets e conectores. Um programa de
computador consistia de um conjunto de linhas de buracos (sockets), cada linha representando uma
operação durante a execução do programa. O programador podia selecionar uma das várias instruções
plugando um conector em um socket em particular para a instrução desejada (Fig.14).
É claro que a maior dificuldade deste esquema era
que o número de instruções possíveis era severamente
limitado pelo número de sockets que alguém poderia
fisicamente colocar em cada linha. Contudo, os
desenvolvedores da CPU rapidamente descobriram que,
com uma pequena quantidade de circuitos lógicos
adicionais, eles poderiam reduzir o número de sockets
Fig.15 - Codificação de instruções necessários de n buracos para n instruções para lg(n) [log
na base 2] buracos para n instruções. Eles fizeram isto
atribuindo um código numérico para cada instrução e então codificavam aquela instrução como um
número binário utilizando lg(n) buracos (Fig.15).
Esta adição requer oito funções lógicas para decodificar os bits A, B e C do painel de sianis, mas
os circuitos extras compensavam porque reduziam o número de sockets que deveriam ser repetidos para
cada instrução.
91
É claro que muitas instruções da CPU não são
auto-suficientes. Por exemplo, a instrução move é um
comando que move dados de uma posição para outra
(por exemplo, de um registrador para outro). Portanto, a
instrução move requer dois operandos: um operando
origem e um operando destino. Os projetistas da CPU
geralmente codificavam esses operandos origem e
destino como parte da instrução da máquina, certos
sockets correspondendo ao operando origem e certos
sockets correspondendo ao operando destino. A Fig.16
mostra uma possível combinação de sockets para
manipular os operandos. A instrução move moverá
Fig.16 - Operandos de instruções
dados do registrador origem para o registrador destino, a
instrução add somará o valor do registrador origem com
o valor do registrador destino, etc.
Um dos primeiros avanços no projeto de computadores que a VNA propiciou foi o conceito de
um programa armazenado. Um dos grandes problemas do método de programação com o painel de sinais
era que o número de passos do programa (instruções de máquina) ficava limitado pelo número de linhas
dos sockets disponíveis na máquina. John Von Neumann e outros identificaram uma relação entre os
sockets do painel de sinais e os os bits na memória. Perceberam que poderiam armazenar os equivalentes
binários de um programa de máquina na memória principal, buscar cada programa na memória e carregá-
lo num registrador de decodificação especial que se conectava diretamente ao circuito de decodificação
de instruções da CPU.
O truque, é claro, foi adicionar ainda mais circuitos à CPU. Este circuito, a unidade de controle
(UC), busca os códigos das instruções (também conhecidos como códigos de operação ou opcodes) na
memória e os move para o registrador de decodificação de instruções. A unidade de controle contém um
registrador especial, o apontador de instrução, o qual contém o endereço de uma instrução executável. A
unidade de controle busca este código de instrução na memória e o coloca no registrador de
decodificação para a execução. Depois de executar a instrução, a unidade de controle incrementa o
apontador de instrução e busca a próxima instrução na memória para que seja executada, repetindo isto
sucessivamente.
Quando desenvolvem um conjunto de instruções, os projetistas de CPUs geralmente escolhem
opcodes que são um múltiplo de oito bits para que a CPU possa buscar instruções completas na memória
com facilidade. O objetivo do projetista de CPUs é atribuir um número apropriado de bits ao campo da
classe da instrução (move, add, subtract, etc.) e aos campos de operandos. Escolher mais bits para
campos de instrução permite que se tenha mais instruções e escolher bits adicionais para os campos de
operandos permite que se selecione um número maior de operandos (por exemplo, posições de memória
ou registradores). Essas são complicações adicionais. Algumas instruções têm apenas um operando ou,
talvez, não tenham nenhum. Ao invés de gastar os bits associados a esses campos, os projetistas de CPUs
geralmente reutilizam esses campos para codificar opcodes adicionais, mais uma vez com o zuxílio de
alguns circuitos adicionais. A família de CPUs 80x86 da Intel leva isto ao extremo, com instruções
variando de um a aproximadamente dez bytes de tamanho. Uma vez que isto é um pouco mais difícil de
ser tratado neste estágio inicial, as CPUs do x86 utilizarão um esquema de codificação diferente e muito
mais simples.
92
formas), add, sub, cmp, and, or, not, je, jne, jb, jbe, ja, jae, jmp, brk, iret, halt, get e put. Os parágrafos
seguintes descrevem como cada uma delas funciona.
A instrução mov, na verdade, são duas classes de instruções fundidas na mesma instrução. As
duas formas da instrução mov são as seguintes:
mov reg, reg/mem/const
mov mem, reg
onde reg é um dos registradores ax, bx, cx ou dx, const é uma constante numérica (utilizando notação
hexadecimal) e mem é um operando especificando uma posição de memória. A próxima seção descreve
as possíveis formas que o operando mem pode assumir. O operando "reg/mem/const" indica que este
operando em particular pode ser um registrador, uma posição de memória ou uma constante.
As instruções aritméticas e lógicas têm as seguintes formas:
add reg, reg/mem/const
sub reg, reg/mem/const
cmp reg, reg/mem/const
and reg, reg/mem/const
or reg, reg/mem/const
not reg/mem
A instrução add adiciona o valor do segundo operando ao primeiro (registrador) operando,
deixando a soma no primeiro operando. A instrução sub subtrai o valor do segundo operando do
primeiro, deixando a diferença no primeiro operando. A instrução cmp compara o primeiro operando
com o segundo e salva o resultado desta comparação para que possa ser utilizada com uma das instrução
de desvio condicional (descrita a seguir). As instruções and e or calculam as operações lógicas
correspondentes em nível de bits sobre os dois operandos e armazenam o resultado dentro do primeiro
operando. A instrução not inverte os bits em um único operando de memória ou registrador.
As instruções de transferência de controle interrompem a execução sequencial das instruções na
memória e transferem incondicionalmente, ou depois de testar o resultado de instruções cmp anteriores, o
controle para algum outro ponto na memória. Essas instruções incluem as seguintes:
ja dest -- Desvia se maior
jae dest -- Desvia se maior ou igual
jb dest -- Desvia se menor
jbe dest -- Desvia se menor ou igual
je dest -- Desvia se igual
jne dest -- Desvia se não igual
jmp dest -- Desvio incondicional
iret -- Retorna de uma interrupção
As seis primeiras instruções desta classe permitem que se verifique o resultado de instruções cmp
anteriores com valores maiores, maiores ou iguais, menores, menores ou iguais, iguais ou diferentes. Por
exemplo, se compararmos os registradores ax e bx com a instrução cmp e executarmos a instrução ja, a
CPU do x86 desviará para a posição de destino especificada se ax for maior do que bx. Se ax não for
maior do que bx, o controle continuará com a próxima instrução do programa. A instrução jmp transfere
incondicionalmente o controle para a instrução no endereço destino. A instrução iret retorna o controle
de uma rotina de serviço de interrupção, a qual será discutida adiante.
As instruções get e put permitem a leitura e a escrita de valores inteiros. get pára e espera que o
usuário entre com um valor hexadecimal e então armazena o valor dentro do registrador ax. put exibe
(em hexadecimal) o valor do registrador ax.
As instruções restantes, halt e brk, não requerem quaisquer operandos. halt encerra a execução do
programa e brk pára o programa deixando-o num estado em que ele pode ser reiniciado.
93
Os processadores x86 requerem um único opcode para cada uma das instruções, não apenas para
as classes de instruções. Embora "mov ax, bx" e "mov ax, cx" sejam da mesma classe, elas devem ter
opcodes diferentes para que a CPU possa diferenciá-las. Contudo, antes de olhar todos os opcodes, talvez
fosse uma boa idéia aprender algo sobre todos os operandos possíveis que essas instruções possam
precisar.
5.5.2. Modos de endereçamento no x86
As instruções no x86 utilizam cinco tipos diferentes de operandos: registradores, constantes e três
esquemas de endereçamento de memória. Estas formas são chamadas de modo de endereçamento. Os
processadores x86 permitem o modo de endereçamento por registrador, o modo de endereçamento
imediato, o modo de endereçamento indireto, o modo de endereçamento indexado e o modo de
endereçamento direto. Os parágrafos a seguir explicam cada um desses modos.
Os operandos registradores são os mais fáceis de entender. Considere as formas a seguir da
instrução mov:
mov ax, ax
mov ax, bx
mov ax, cx
mov ax, dx
A primeira instrução não executa absolutamente nada. Ela copia o valor do registrador ax para o
próprio registrador ax. As três instruções restantes copiam o valor de bx, cx e dx para dentro do
registrador ax. Note que os valores originais de bx, cx e dx permanecem os mesmos. O primeiro
operando (o destino) não está limitado a ax; você pode mover valores para quaisquer dos registradores
citados.
Constantes também são fáceis de serem tratadas. Considere as seguintes instruções:
mov ax, 25
mov bx, 195
mov cx, 2056
mov dx, 1000
Essas instruções são todas diretas. Carregam seus respectivos registradores com a constante
hexadecimal especificada.
Há três modos de endereçamento que tratam do acesso a dados na memória. Esses modos de
endereçamento têm as seguintes formas:
mov ax, [1000]
mov ax, [bx]
mov ax, [1000+bx]
A primeira instrução utiliza o modo de endereçamento direto para carregar ax com o valor de 16
bits armazenado na memória começando na posição hexadecimal 1000. A instrução mov ax, [bx] carrega
ax com a posição de memória especificada pelo conteúdo do registrador bx. Este é um modo de
endereçamento indireto. Ao invés de utilizar o valor de bx, esta instrução acessa a posição de memória
cujo endereço aparece em bx. Note estas duas instruções a seguir:
mov bx, 1000
mov ax, [bx]
são equivalentes à instrução
mov ax,[1000]
É claro que a última é preferível. Contudo, há muitos casos onde o uso de endereçamento indireto
é mais rápido, mais curto e melhor. Nós veremos alguns exemplos quando analisarmos individualmente
os processadores da família x86 logo adiante.
94
O último modo de endereçamento é o modo de endereçamento indexado. Um exemplo deste
modo de endereçamento de memória é
mov ax, [1000+bx]
Esta instrução soma os conteúdos de bx com 1000 para produzir o valor do endereço de memória
a procurar. Esta instrução é útil para acessar elementos de arrays, registros e outras estruturas de dados.
A instrução básica tem o tamanho de um ou de três bytes. O opcode da instrução é um único byte
que contém três campos. No primeiro campo, os três bits mais significativos definem a classe da
instrução. Eles fornecem oito combinações. Como foi dito acima, há 20 classes de instruções. Como não
podemos codificar 20 classes de instruções com três bits, teremos que usar alguns truques para tratar das
outras classes. Como mostrado na Fig.17, o opcode básico codifica as instruções mov (duas classes, uma
onde o campo rr especifica o destino, e outra onde o campo mmm especifica o destino), as instruções
add, sub, cmp, and e or. Há uma classe adicional: special. A classe de instrução special fornece um
mecanismo que nos permite expandir o número de classes de instruções disponíveis. Voltamos já a este
assunto.
Para determinar um opcode de instrução em particular, precisamos apenas selecionar os bits
apropriados para os campos iii, rr e mmm. Por exemplo, para codificar a instrução mov ax, bx,
selecionamos iii=110 (mov reg, reg), rr=00 (ax) e mmm=001 (bx). Isto produz a instrução de um byte
11000001 ou 0C0h.
Algumas instruções do x86 requerem mais do que um byte. Por exemplo, a instrução mov ax,
[1000] que carrega o registrador ax com a posição de memória 1000. A codificação para este opcode é
95
11000110 ou 0C6h. Contudo, a codificação do opcode de mov ax,[2000] também é 0C6h. Estas duas
instruções fazem coisas diferentes, uma carrega o registrador ax com a posição de memória 1000h
enquanto a outra carrega o registrador ax com a posição de memória 2000h. Para codificar um endereço
nos modos de endereçamento [xxxx] ou [xxxx+bx], ou para codificar a constante no modo de
endereçamento imediato, é preciso que o opcode seja seguido pelo endereço de 16 bits ou pela constate,
na sequência byte menos significativo e byte mais significativo. Assim, os três bytes codificados para
mov ax, [1000] seriam 0C6h, 00h, 10h e os três bytes codificados para mov ax, [2000] seriam 0C6h, 00h,
20h.
O opcode special permite que a CPU do x86 expanda o conjunto de instruções disponíveis. Este
opcode trata muitas instruções com zero e um operando, como mostrado nas duas figuras a seguir:
Há quatro classes de instruções com apenas um operando. A primeira codificação (00), mostrada
na Fig.18b, expande o conjunto de instruções para um conjunto de instruções de zero operandos. O
segundo opcode é também um opcode de expansão que fornece todas as instruções de desvio do x86:
O terceiro opcode é para a instrução not. Esta é a operação de not lógico em nível de bits, que
inverte todos os bits no operando registrador destino ou na memória. O quarto opcode de um único
operando não está determinado. Qualquer tentativa de executar este opcode parará o processador com um
erro de instrução ilegal. Desenvolvedores de CPUs frequentemente reservam opcodes não determinados
como este para futuramente poder ampliar o conjunto de instruções (como a Intel fez quando passou do
processador 80286 para o 80386).
Há sete instruções de desvio ou salto no conjunto de instruções do x86. Todas têm a seguinte
forma:
jxx endereço
96
A instrução jmp copia o valor imediato de 16 bits (endereço) que segue o opcode para o
registrador IP. Portanto, a CPU extrairá a próxima instrução deste endereço alvo. Na realidade, o
programa "pula" do ponto da instrução jmp para a instrução no endereço indicado.
A instrução jmp é um exemplo de instrução de desvio incondicional. Ela sempre transfere o
controle para um endereço especificado. As seis instruções restantes são instruções de desvio
condicional. Elas testam alguma condição e o desvio só acontece se a condição for verdadeira. Se a
condição for falsa, simplesmente a próxima instrução é executada. Estas seis instruções, ja, jae, jb, jbe, je
e jne permitem que se teste maior que, maior ou igual a, menor que, menor ou igual a, igual e diferente.
Estas instruções são normalmente executadas imediatamente após uma instrução cmp, pois cmp seta as
flags correspondentes a menor que e igual que as instruções de desvio condicional testam. Note que há
oito opcodes possíveis de desvio, mas o x86 utiliza apenas sete deles. O oitavo opcode é um outro
opcode ilegal.
O último grupo de instruções, as instruções sem operandos, aparecem na Fig.18c. Três dessas
instruções são opcodes de instrução ilegais. A instrução brk suspende a atividade da CPU até que o
usuário a reinicie manualmente. Isto é útil para interromper um programa durante a execução para
observar resultados. A instrução iret (interrupt return) retorna o controle de uma rotina de serviço de
interrupção. Discutiremos rotinas de serviço de interrupção mais adiante. A instrução halt encerra a
execução do programa. A instrução get lê um valor hexadecimal fornecido pelo usuário e coloca este
valor no registrador ax. A instrução put devolve o valor do registrador ax.
97
Uma descrição de cada um dos passos pode ajudar
a esclarecer o que a CPU faz. No primeiro passo, a CPU
busca o byte da instrução na memória. Para isto, ela põe o
valor do registrador IP no barramento de endereços e lê o
byte naquele endereço. Isto levará um ciclo do clock.
Fig.18d - Registrador de 16 bits e
posições de memória pares e ímpares Depois de buscar o byte da instrução, a CPU
atualiza o IP de forma que ele aponte para o próximo byte
do fluxo de instruções. Se a instrução corrente for uma instrução multibyte, o IP apontará para o
operando da instrução. Se a instrução corrente for uma instrução de um único byte, o IP apontará para a
próxima instrução. Isto toma um ciclo de clock.
O próximo passo é decodificar a instrução para ver o que ela faz. Entre outras coisas, isto indicará
se a CPU precisa buscar bytes de operandos adicionais na memória. Isto levará um ciclo de clock.
Durante a decodificação, a CPU determina os tipos
de operandos que a instrução requer. Se a instrução
precisar de um operando constante de 16 bits (isto é, se o
campo mmm é 101, 110 ou 111), então a CPU busca esta
constante na memória. Este passo pode precisar de zero,
Fig.18e - Word alinhado = 1 ciclo de clock um ou dois ciclos do clock. Ele requer zero ciclos se não
houver operando de 16 bits, requer um ciclo se o operando
de 16 bits for um word alinhado (isto é, começa num
endereço par - veja a Fig.18e) e requer dois ciclos de clock
se o operando for um word não alinhado (isto é, não
começa num endereço par - veja a Fig.18f).
Se a CPU buscar um operando de 16 bits na
memória, ela precisa incrementar o IP em dois para que
Fig.18f - Word não alinhado = 2 ciclos de clock ele aponte o byte seguinte ao operando. Esta operação
toma zero ou um ciclo de clock. Nenhum ciclo se não
houver operando e um se houver um operando.
A seguir, a CPU calcula o endereço do operando na memória. Este passo é necessário apenas
quando o campo mmm do byte de instrução for 101 ou 100. Se o campo mmm contiver 101, então a CPU
calcula a soma do registrador bx com a constante de 16 bits. Isto requer dois ciclos, um para buscar o
valor de bx e outro para calcular a soma de bx com xxxx. Se o campo mmm contiver 100, então a CPU
usa o valor de bx como endereço de memória - isto requer um ciclo. Se o campo mmm não contiver 100
ou 101, então este passo não ocupa ciclos.
Buscar um operando toma zero, um, dois ou três ciclos, dependendo do operando em questão. Se
o operando for uma constante (mmm=111), então este passo não requer ciclos porque esta constante na
memória já foi extraída no passo anterior. Se o operando for um registrador (mmm = 000, 001, 010 ou
011) então este passo ocupa um ciclo de clock. Se este operando for word alinhado na memória
(mmm=100, 101 ou 110), então este passo toma dois ciclos do clock. Se ele não for alinhado na
memória, são necessários três ciclos de clock para obter seu valor.
O último passo da instrução mov é armazenar o valor na posição destino. Uma vez que o destino
da instrução load sempre é um registrador, esta operanção gasta um único ciclo.
A instrução mov, no total, consome entre cinco e onze ciclos, dependendo dos seus operandos e
de seus alinhamentos (endereço inicial) na memória.
A CPU faz o seguinte com uma instrução mov mem, reg:
• Busca o byte de instrução na memória (um ciclo do clock).
98
• Atualiza o IP para que aponte para o próximo byte (um ciclo do clock).
• Decodifica a instrução para ver o que fazer (um ciclo do clock).
• Se necessário, busca um operando da memória (zero ciclos se no modo de endereçamento [bx],
um ciclo se no modo de endereçamento [xxxx], [xxxx+bx] ou xxxx e o valor xxxx imediatamente
seguinte ao opcode inicia em um endereço par, ou dois ciclos do clock se o valor xxxx inicia em
um endereço ímpar).
• Se necessário, atualiza o IP para apontar para o operando (zero ciclos se não existir tal operando e
um ciclo se o operando estiver presente).
• Calcula o endereço do operando (zero ciclos se o modo de endereçamento não for [bx] ou
[xxxx+bx], um ciclo se o modo de endereçamento for [bx] ou dois ciclos se o modo de
endereçamento for [xxxx+bx]).
• Obtém o valor do registrador para armazenar (um ciclo do clock).
• Armazena o valor buscado na posição destino (um ciclo se for um registrador, dois ciclos se o
operando for word alinhado na memória ou três ciclos se o operando for uma posição alinhada na
memória em um endereço ímpar).
O tempo para os dois últimos ítens é diferente do outro mov porque a primeira instrução pode ler
dados da memória; esta versão da instrução mov carrega seus dados de um registrador. Esta instrução
toma cinco a onze ciclos para ser executada.
As instruções add, sub, cmp e or fazem o seguinte:
• Busca o byte de instrução na memória (um ciclo do clock).
• Atualiza o IP para apontar para o próximo byte (um ciclo do clock).
• Decodifica a instrução (um ciclo do clock).
• Se necessário, busca um operando constante da memória (zero ciclos se for o modo de
endereçamento [bx], um ciclo se for o modo de endereçamento [xxxx], [xxxx+bx] ou xxxx e o
valor xxxx imediatamente seguinte ao opcode iniciar em um endereço par ou dois ciclos se o
valor xxxx iniciar em um endereço ímpar).
• Se necessário, atualiza o IP para apontar para o operando constante (zero ou um ciclo do clock).
• Calcula o endereço do operando (zero ciclos se o modo de endereçamento não for [bx] ou
[xxxx+bx], um ciclo se o modo de endereçamento for [bx] ou dois ciclos se o modo de
endereçamento for [xxxx+bx]).
• Obtém o valor do operando e o envia para a ALU (zero ciclos se for uma constante, um ciclo se
for um registrador, dois ciclos se for um operando word alinhado na memória ou três ciclos for
um operando alinhado na memória em um endereço ímpar).
• Busca o valor do primeiro operando (um registrador) e o envia para a ALU (um ciclo do clock).
• Instrui a ALU para somar, subtrair, comparar ou realizar and ou or lógico dos valores (um ciclo
do clock).
• Armazena o resultado no primeiro operando registrador (um ciclo do clock).
Estas instruções requerem entre oito a dezessete ciclos do clock para serem executadas.
A instrução not é similar à descrita acima, apesar de poder ser um pouco mais rápida porque
possui apenas um operando:
• Busca o byte de instrução na memória (um ciclo do clock).
• Atualiza o IP para apontar para o próximo byte (um ciclo do clock).
• Decodifica a instrução (um ciclo do clock).
• Se necessário, busca um operando constante da memória (zero ciclos se o modo de
endereçamento for [bx], um ciclo se o modo de endereçamento for [xxxx], [xxxx+bx] ou xxxx e o
99
valor xxxx imediatamente seguinte ao opcode iniciar em um endereço par ou dois ciclos do clock
se o valor xxxx iniciar em um endereço ímpar).
• Se necessário, atualiza o IP para apontar para o operando constante (zero ou um ciclo do clock).
• Calcula o endereço do operando (zero ciclos se o modo de endereçamento não for [bx] ou
[xxxx+bx], um ciclo se o modo de endereçamento for [bx] ou dois ciclos se o modo de
endereçamento for [xxxx+bx]).
• Obtém o valor do operando e o envia para a ALU (zero ciclos se for uma constante, um ciclo se
for um registrador, dois ciclos se for um operando word alinhado na memória ou três ciclos for
um operando alinhado na memória em um endereço ímpar).
• Instrui a ALU para realizar o not lógico dos valores (um ciclo do clock).
• Armazena o resultado de volta no operando (um ciclo do clock se for um registrador, dois ciclos
se for uma posição word alinhada na memória ou três ciclos de clock se for uma posição de
memória em um endereço ímpar).
O modo de operação da instrução de desvio incondicional é idêntica à instrução mov reg, xxxx
exceto que o registrador destino é o registrador IP do x86 e não ax, bx, cx ou dx.
As instruções brk, iret, halt, put e get não são de interesse no momento. Elas aparecem no
conjunto de instruções principalmente para programas e experiências. Não podemos simplesmente dar a
elas contagens de ciclos já que elas podem levar uma quantidade indefinida de tempo para completar
suas tarefas.
100
O processador 886
O processador 886 é o mais lento da família x86. Os tempos para cada uma das instruções já
foram discutidos na seção anterior. A isntrução mov, por exemplo, precisa de cinco a doze ciclos de
clock para ser executada, dependendo dos operandos. A tabela seguinte mostra os tempos para as várias
formas de instrução nos processadores 886.
Modo de endereçamento da instrução mov (ambas as formas) add, sub, cmp, and, or not jmp jxx
reg, reg 5 7
reg 6
[bx] 9-11
[xxxx] 10-13
[xxxx+bx] 12-15
Existem três coisas importantes que precisam ser ressaltadas. Primeiro, instruções mais longas
precisam de mais tempo para serem executadas. Segundo, instruções que não fazem referência à
memória geralmente são executadas mais rápido. Isto é ainda mais relevante se existirem estados de
espera associados ao acesso à memória (a tabela acima pressupõe estado de espera zero). Finalmente,
instruções que usam modos de endereçamento complexos rodam mais devagar. Instruções que utilizam
operandos registradores são mais curtas, não acessam a memória e não usam modos de endereçamento
complexos. Este é o motivo pelo qual devemos tentar manter as variáveis em registradores.
O processador 8286
O segredo para aumentar a velocidade de um processador é realizar operações em paralelo. Se, nos
tempos dados para o 886, pudéssemos realizar duas operações a cada ciclo de clock, a CPU executaria
instruções com o dobro da velocidade na mesma velocidade de clock. Entretanto, não é tão simples assim
decidir executar duas operações por ciclo de clock. Muitos dos passos da execução de uma instrução
compartilham unidades funcionais da CPU (unidades funcionais são grupos de lógica que realizam uma
operação em comum, por exemplo, a ALU e a unidade de controle). Uma unidade funcional só comporta
uma operação por vez. Portanto, não é possível realizar, simultaneamente, duas operações que usem a
mesma unidade funcional (por exemplo, incrementar o registrador IP e adicionar dois valores). Outra
dificuldade que pode ocorrer com certas operações concorrentes é quando uma das operações depende do
resultado da outra. Por exemplo, os dois últimos passos da instrução add envolvem a soma de valores e o
armazenamento do resultado. Não podemos armazenar a soma num registrador antes de ter calculado a
soma. Existem também outros recursos que a CPU não pode compartilhar entre as etapas de uma
instrução. Por exemplo, existe apenas um barramento de dados. A CPU não pode buscar um opcode de
instrução no momento em que estiver tentando armazenar dados na memória. O truque no projeto de uma
101
CPU que execute diversos passos em paralelo é organizar estes passos para reduzir conflitos ou então
adicionar lógica de modo que as duas (ou mais) operações possam ocorrer simultaneamente, executando-
as em unidades funcionais diferentes. Considere novamente as etapas que a instrução mov reg,
mem/reg/const necessita:
• Buscar o byte da instrução na memória.
• Atualizar o registrador IP para que aponte para o próximo byte.
• Decodificar a instrução para ver o que ela faz.
• Se necessário, buscar um operando de 16 bits da instrução na memória.
• Se necessário, atualizar o IP para que aponte após o operando.
• Calcular o endereço do operando, se necessário (isto é, bx+xxxx).
• Buscar o operando.
• Armazenar o valor obtido no resgistrador de destino.
A primeira etapa usa o valor do registrador IP (de modo que não podemos sobrepor esta etapa
com a incrementação do IP) e usa o barramento para buscar o opcode da instrução na memória. Cada
passo que se segue depende do opcode, tornando pouco provável a sobreposição desta etapa com
qualquer outra. O segundo e o terceiro passos não compartilham nenhuma unidade funcional, nem a
decodificação de um opcode depende do valor no registrador IP. Portanto, podemos modificar com
facilidade a unidade de controle de modo que ela incremente o registrador IP ao mesmo tempo em que
decodifica a instrução. Isto corta um ciclo na execução da instrução mov. A terceira e a quarta etapa
(decodificar e opcionalmente buscar o operando de 16 bits) não parecem ser apropriadas para serem
executadas em paralelo porque a instrução precisa ser decodificada para se determinar se a CPU precisa
ou não buscar um operando de 16 bits na memória. Entretanto, poderíamos projetar a CPU para se
adiantar e sempre buscar o operando, de modo que estivesse disponível se por acaso for requisitado.
Apesar disso, esta idéia apresenta um problema: precisamos do endereço do operando (o valor no
registrador IP) e precisamos esperar até que o IP esteja atualizado antes de buscarmos este operando. Se
estivermos incrementando o IP no mesmo momento em que estivermos decodificando a instrução,
teremos que aguardar o próximo ciclo para buscar o operando. Uma vez que os próximos três passos são
opcionais, existem várias sequências de instruções possíveis neste ponto: 1. (passos 4, 5, 6 e 7) por
exemplo, mov ax, [1000+bx]; 2. (passos 4, 5 e 7) por exemplo, mov ax, [1000]; 3. (passos 6 e 7) por
exemplo mov ax, [bx] e 4. (passo 7) por exemplo mov ax, bx. Nos passos indicados, a etapa 7 depende
sempre do conjunto das anteriores. Portanto, a etapa 7 não pode ser executada em paralelo com qualquer
uma das outras. A etapa 6 também depende da etapa 4. O passo 5 não pode ser executado em paralelo
com o passo 4 porque este usa o valor do registrador IP, mas pode ser executado em paralelo com
qualquer outro passo. Portanto, podemos cortar um ciclo das primeiras duas sequências citadas, ou seja:
1. (passos 4, 5/6 e 7); 2. (passos 4 e 5/7); 3. passos 6 e 7) e 4. (passo 7). É óbvio que não existe maneira
de sobrepor a execução dos passos 7 e 8 na instrução mov porque ela precisa buscar o valor antes de
armazená-lo. Combinando estes passos obtemos os seguintes para a isntrução mov:
• Buscar o byte da instrução na memória.
• Decodificar a instrução e atualizar IP.
• Se necessário, buscar um operando de 16 bits da instrução na memória.
• Calcular o endereço do operando, se necessário (isto é, bx+xxxx).
• Buscar o operando, se necessário atualizar IP para apontar após xxxx.
• Armazenar o valor obtido no resgistrador de destino.
Adicionando uma pequena quantidade de lógica à CPU, cortamos um ou dois ciclos na execução
da instrução mov. Esta otimização simples funciona também para a maioria das outras instruções. Um
outro problema com a execução da instrução mov refere-se ao alinhamento do opcode. Considere a
instrução mov ax, [1000] que aparece na localização 100 na memória. A CPU gasta um ciclo buscando o
102
opcode e, depois de decodificar a instrução e determinando que ela possui um operando de 16 bits, gasta
dois ciclos adicionais para buscar este operando na memória (porque este operando aparece no endereço
ímpar 101). O que é engraçado é que este ciclo de clock extra, para obter estes dois bytes, é
desnecessário. Afinal de contas, a CPU obteve o byte menos significativo do operando quando foi buscar
o opcode (lembre-se, as CPUs x86 são processadores de 16 bits e sempre pegam 16 bits da memória),
então porque não salvar este byte e usar apenas um ciclo de clock adicional para obter o byte mais
significativo? Isto economizaria um ciclo no tempo de execução se a instrução começar num endereço
par (de modo que o operando caia no endereço ímpar). Haveria a necessidade de adicionar apenas um
registrador de um byte e de uma pequena quantidade adicional de lógica para atingir este objetivo, um
esforço que valeria a pena. Enquanto adicionamos um registrador para funcionar como buffer de bytes de
operandos, vamos considerar algumas otimizações adicionais que usariam a mesma lógica. Por exemplo,
verifique o que acontece quando a instrução mov acima citada é executada. Se buscarmos o opcode e o
byte menos significativo do operando no primeiro ciclo e o byte mais significativo do operando no
segundo ciclo, na verdade lemos quatro bytes, e não três. Este quarto byte é o opcode da próxima
instrução. Se pudermos salvar este opcode até a execução da próxima instrução, poderíamos cortar um
ciclo do seu tempo de execução porque não precisaria buscar o byte do opcode. Mais do que isto, como a
decodificador da instrução está ocioso enquanto a CPU estiver executando a instrução mov, podemos
decodificar a próxima instrução enquanto a instrução atual estiver sendo executada e cortamos mais um
ciclo da execução da próxima instrução. Em média, buscaremos este byte extra em cada uma das
instruções seguintes. Portanto, implementando este esquema simples, conseguiremos cortar dois ciclos de
cerca de 50% das instruções que executarmos. Podemos fazer mais alguma coisa com os outros 50% das
instruções? A resposta é sim.
Note que a execução de uma instrução mov não acessa a memória a cada ciclo de clock. Por
exemplo, enquanto armazenamos dados no registrador de destino, o barramento está ocioso. Nestes
intervalos de tempo, quando o barramento está ocioso, podemos antecipar a busca (pre-fetch) de opcodes
de instruções e de operandos, salvando estes valores para a execução da próxima instrução. O maior
aperfeiçoamento do processador 8286 em relação ao 886 é a fila de busca antecipada (pre-fetch queue).
Sempre que a CPU não estiver usando a Unidade de Interface do Barramento (Bus Interface Unit - BIU),
a BIU pode buscar bytes adicionais do fluxo de instruções. Sempre que a CPU precisar de uma instrução
ou de um byte de operando, ela pega o próximo byte disponível na fila de busca antecipada. Como a BIU
pega dois bytes por vez da memória e a CPU geralmente consome menos do que dois bytes por ciclo de
clock, qualquer byte que a CPU, normalmente, fosse buscar do fluxo de instruções já estará na fila de
pré-busca. Note, entretanto, que não temos garantia nenhuma que todas as instruções e operandos estejam
na fila quando precisarmos deles. Por exemplo, a instrução jmp 1000 tornará o conteúdo da fila inválido.
Se esta instrução aparecer nas localizações 400, 401 e 402 da memória, a fila de busca antecipada conterá
os bytes dos endereços 403, 404, 405, 406, 407, etc. Após carregar o IP com 1000, os bytes dos
endereços 403, etc não nos servem mais. Neste caso o sistema precisa fazer uma pausa para buscar o
double word no endereço 1000 antes de poder continuar. Outro aperfeiçoamento que podemos fazer é
sobrepor a decodificação de instruções ao último passo da instrução anterior. Depois que a CPU
processar o operando, o próximo byte disponível na fila de pré-busca é um opcode e a CPU pode
decodificá-lo antes da sua execução. É claro que, se a instrução atual modificar o registrador IP, qualquer
tempo gasto decodificando a próxima instrução será perdido mas, como isto ocorre em paralelo com
outras operações, não torna o sistema mais lento. A sequência de otimizações do sistema requer algumas
modificações de hardware. Um diagrama do sistema pode ser visto na Fig.19.
103
Fig.19 - A fila de busca antecipada ou prefetch queue
A sequência de execução de instruções agora assume a ocorrência dos seguintes eventos no fundo
- Eventos de pré-busca da CPU:
• Se a fila de busca antecipada não estiver cheia (geralmente ela poder conter entre oito e trinta e
dois bytes, dependendo do processador) e a BIU estiver ociosa no ciclo de clock atual, buscar o
próximo word na memória no endereço contido no IP no início do ciclo de clock.
• Se o decodificador de instruções estiver ocioso e a instrução atual não necessitar de um operando,
começar a decodificar o opcode na frente da fila de pré-busca (se existir), caso contrário começar
a decodificar o terceiro byte da fila de pré-busca (se existir). Se o byte desejado não estiver na
fila, não executar este evento. Este processo de execução de instruções parte de algumas hipóteses
otimistas: que todo opcode e operando necessário esteja presente na fila de busca antecipada e
que o opcode da instrução atual esteja decodificado.Caso uma das hipóteses não seja verdadeira, a
execução de uma instrução 8286 será retardada enquanto o sistema busca dados da memória ou
decodifica a instrução. São os seguintes os passos para cada uma das instruções do 8286:
• mov reg, mem/reg/const
o Se necessário, calcular a soma de [xxxx+bx] (1 ciclo, se necessário).
o Buscar o operando fonte. Zero ciclos se for constante (assumindo que já esteja na fila de
pré-busca), um ciclo se for um registrador, dois ciclos se for um valor alinhado na
memória par, três ciclos se for um valor alinhado na memória ímpar.
o Armazenar o resultado no registrador destino, um ciclo.
• mov mem, reg
o Se necessário, calcular a soma de [xxxx+bx] (1 ciclo, se necessário).
o Buscar o operando fonte (um registrador), um ciclo.
o Armazenar no operando destino. Dois ciclos se for um valor alinhado na memória par e
três se estiver alinhado na memória ímpar.
• instr reg, mem/reg/const (instr = add, sub, cmp, and, or)
o Se necessário, calcular a soma de [xxxx+bx] (1 ciclo, se necessário).
o Buscar o operando fonte. Zero ciclos se for uma constante (que esteja na fila de pré-
busca), um ciclo se for um registrador, dois ciclos se for um valor alinhado na memória
par e três se estiver alinhado na memória ímpar.
o Buscar o valor do primeiro operando (um registrador), um ciclo.
o Calcular a soma, a diferença, etc., como indicado, um ciclo.
o Armazenar o resultado no registrador destino, um ciclo.
• not mem, reg
o Se necessário, calcular a soma de [xxxx+bx] (1 ciclo, se necessário).
o Buscar o operando fonte. Um ciclo se for um registrador, dois ciclos se for um valor
alinhado na memória par e três se estiver alinhado na memória ímpar.
104
o Fazer o not lógico do valor, um ciclo.
o Armazenar o resultado, um ciclo se for um registrador, dois ciclos se for um valor
alinhado na memória par e três se estiver alinhado na memória ímpar.
• jcc xxxx (salto condicional cc=a, ae, b, be, e, ne)
o Testar as flags da condição atual (menor que e igual), um ciclo.
o Se os valores da flag forem apropriados para o desvio condicional em questão, a CPU
copia o operando de 16 bits da instrução para o registrador IP, um ciclo.
• jmp xxxx
o A CPU copia o operando de 16 bits da instrução para o registrador IP, um ciclo.
Assim como para o 886, não vamos considerar os tempos de execução das outras instruções x86
porque a maioria deles são indeterminados. As instruções de salto parecem ser executadas com grande
rapidez no 8286. Na realidade, podem ser muito lentas. Não se esqueça de que o salto de uma localidade
para outra invalida o conteúdo da fila de pré-busca. Assim, apesar de parecer que a instrução jmp seja
executada em um ciclo de clock, ela força a CPU a descartar a fila de busca antecipada e, portanto, gastar
vários ciclos para buscar a próxima instrução, operandos adicionais e decodificando a instruçao. Na
realidade, apenas após duas ou três instruções após a instrução jmp é que a CPU volta ao ponto em que a
fila de pré-busca esteja funcionando adequadamente e a CPU esteja decodificando opcodes em paralelo
com a execução da instrução anterior. Isto revela um aspecto muito importante: se quisermos escrever
programas rápidos, é melhor evitar saltos de uma lado para outro. Note que as instruções de saltos
condicionais apenas invalidam a fila de pré-busca caso sejam realizados. Se a condição for falsa, a
execução continua com a próxima instrução e os valores da fila de pré-busca e os opcodes pré-
decodificados são usados normalmente. Portanto se, enquanto estivermos escrevendo um programa, for
possível determinar qual das condições é a mais provável (por exemplo, menor que versus não menor
que), deveríamos optar pela condição menos provável para o desvio para que a mais provável continue na
linha de execução.
O tamanho das instruções (em bytes) também pode afetar a performance da fila de busca
antecipada. Buscar um único byte de instrução nunca gasta mais do que um ciclo de clock, mas buscar
uma instrução de três bytes sempre gasta dois ciclos. Portanto, se o objetivo de uma instrução de desvio
forem duas instruções de um byte, a BIU poderá buscar as duas instruções num único ciclo de clock e
começar a decodificar a segunda enquanto a primeira estiver sendo executada. Se estas instruções forem
de três bytes, a CPU pode não ter tido tempo suficiente para buscar e decodificar a segunda ou a terceira
instrução no momento em que tiver terminado a primeira. Portanto, sempre que possível, devemos usar
instruções curtas para melhorar a performance da fila de busca antecipada. A tabela seguinte mostra os
tempos de execução (otimistas) das instruções do 8286:
Modo de endereçamento da instrução mov (ambas as formas) add, sub, cmp, and, or not jmp jxx
reg, reg 2 4
reg, xxxx 1 3
reg 3
105
[bx] 5-7
[xxxx] 5-7
[xxxx+bx] 6-8
Observe como a instrução mov é muito mais rápida no 8286 que no 886. Isto se deve à fila de pré-
busca, a qual permite que o processador sobreponha a execução de instruções adjacentes. Entretanto, esta
tabela mostra um quadro exageradamente otimista. Não se esqueça da premissa "assumindo que o opcode
esteja presenta na fila de busca antecipada e que tenha sido decodificado". Imagine a seguinte sequência
de três instruções:
????: jmp 10001000
: jmp 20002000
: mov cx, 3000
A segunda e a terceira instruções não serão executadas tão rapidamente quanto sugerem os
tempos da tabela acima. Sempre que o valor do registrador IP for modificado, a CPU descarrega a fila de
pré-busca. Neste caso, a CPU não pode buscar e decodificar a próxima instrução. Pelo contrário, ela
precisa buscar o opcode, decodificá-lo, etc, aumentando o tempo de execução destas instruções. Neste
ponto, o único progresso que fizemos foi executar a operação de "atualizar o IP" em paralelo com alguma
outra etapa. Habitualmente, a inclusão da fila de pré-busca melhora a performance. Este é o motivo pelo
qual a Intel fornece uma fila de pré-busca em todos os modelos 80x86, a partir do 8088. Nestes
processadores, a BIU constantemente busca dados para a fila de pré-busca sempre que o programa não
estiver lendo ou escrevendo dados. As filas de busca antecipada funcionam melhor quando existir um
barramento de dados largo. O processador 8286 roda muito mais rápido do que o 886 porque é capaz de
manter a fila de pré-busca cheia. Entretanto, observe as seguintes instruções:
100: mov ax, [1000]
105: mov bx, [2000]
10A: mov cx, [3000]
Como os registradores ax, bx, cx e dx são de 16 bits, aqui está o que acontece (assumindo que a
primeira instrução esteja na fila de pré-busca e decodificada):
• Buscar o byte do opcode na fila de pré-busca (zero ciclos).
• Decodificar a instrução (zero ciclos).
• Existe um operando para esta instrução, portanto, pegá-lo na fila de pré-busca (zero ciclos).
• Pegar o valor do segundo operando (um ciclo). Atualizar IP.
• Armazenar o valor pego no registrador destino (um ciclo). Buscar dois bytes do fluxo de código.
Decodificar a próxima instrução.
106
• Buscar o byte do opcode na fila de pré-busca (zero ciclos).
• Decodificar a instrução (zero ciclos).
• Existe um operando para esta instrução. Pegá-lo na fila de pré-busca (zero ciclos).
• Pegar o valor do segundo operando (um ciclo). Atualizar IP.
• Armazenar o valor pego no registrador destino (um ciclo). Buscar dois bytes do fluxo de código.
Decodificar a próxima instrução.
Como podemos ver, a segunda instrução precisa de um ciclo de clock a mais do que as outras
duas. Isto ocorre porque a BIU não pode preencher a fila de pré-busca na mesma velocidade com que a
CPU executa as instruções. O problema fica ainda mais acentuado quando o tamanho da fila de busca
antecipada estiver limitado a um certo número de bytes. Este problema não ocorre no processador 8286
mas, quase que com certeza, ocorre nos processadores 80x86. Logo veremos que os processadores 80x86
tendem a esvaziar a fila de pré-busca com certa facilidade. É claro que, quando a fila de pré-busca está
vazia, a CPU precisa esperar que a BIU busque novos opcodes na memória e o programa fica mais lento.
Executar instruções mais curtas ajuda a manter a fila de pré-busca preenchida. Por exemplo, o 8286 é
capaz de carregar instruções de dois bytes com um único ciclo de memória, mas leva 1.5 ciclo de clock
para buscar uma única instrução de três bytes. A execução de quatro instruções de um byte normalmente
é mais demorada do que a execução de uma instrução de três bytes, mas isto dá tempo para que a fila de
pré-busca seja preenchida e que novas instruções sejam decodificadas. Em sistemas que possuam uma
fila de busca antecipada é possível encontrar oito instruções de dois bytes que operam mais rápido do que
um conjunto equivalente de quatro instruções de quatro bytes. O motivo é que a fila de pré-busca tem
tempo de ser preenchida quando as instruções são mais curtas. Moral da história: quando se programa
para um processador com uma fila de busca antecipada, usar sempre as instruções mais curtas possíveis
para realizar as tarefas necessárias.
O processador 8486
Executar instruções em paralelo usando uma unidade de interface de barramento e uma unidade
de execução é um caso especial de pipelining. A tradução de pipe line é oleoduto e, no sentido figurado,
é fonte de informações. Obviamente, na informática, o termo é usado com o sentido de fonte de
informações. Neste texto vou manter "pipeline" para designar fonte de informações e "pipelining" para
designar a capacidade de obter informações. Bem, voltando à vaca fria, o 8486 incorpora pipelining para
melhorar sua performance. Com apenas algumas poucas exceções, veremos que a pipelining permite
executar uma instrução por ciclo de clock.
A vantagem trazida pela fila de pré-busca é que ela deixa a CPU sobrepor buscas e decodificações
de instruções à execução destas instruções. Isto é, enquanto uma instrução está sendo executada, a BIU
está buscando e decodificando a próxima instrução. Se partirmos do princípio de que podemos adicionar
ainda mais um pouco de hardware, então poderemos executar praticamente todas as operações em
paralelo. Esta é a idéia da pipelining.
O pipeline do 8486
Considere os passos necessários para realizar uma operação genérica:
• Buscar um opcode.
• Decodificar o opcode e (em paralelo) pré-buscar um possível operando de 16 bits.
• Calcular o modo de endereçamento complexo (por exemplo, [xxxx+bx]), se aplicável.
• Buscar o valor origem na memória (se for um operando na memória) e o valor do registrador
destino (se aplicável).
• Calcular o resultado.
107
• Armazenar o resultado no registrador destino.
Assumindo que estejamos dispostos a gastar um grana extra com silício, podemos construir um
"mini-processador" que gerenciará cada uma das etapas acima citadas. A organização seria algo parecido
com o mostrado na Fig.20.
Se projetarmos uma peça de hardware para cada estágio da pipeline, praticamente todos as etapas
poderão ocorrer em paralelo. É claro que não poderemos buscar e decodificar o opcode de uma dada
instrução simultaneamente, mas poderemos buscar um opcode enquanto a instrução anterior estiver
sendo decodificada. Se tivermos uma pipeline de n-estágios, então poderemos ter n instruções sendo
executadas simultaneamente. O 8486 é um processador que possui uma pipeline de seis estágios ou seja,
é capaz de sobrepor a execução de seis instruções diferentes.
A Fig.21, Execução de Instruções numa Pipeline, mostra uma pipelining. T1, T2, T3, etc,
representam "tics" consecutivos do clock do sistema. Quando T=T1, a CPU busca o byte do opcode da
primeira instrução.
Em T=T2, a CPU começa a decodificar o opcode da primeira instrução. Em paralelo, a pipeline
busca os 16 bits da fila de busca antecipada caso a instrução tenha um operando. Como a primeira
instrução não precisa mais dos circuitos de busca de opcodes, a CPU instrui a pipeline para buscar o
opcode da segunda instrução, o que ocorre em paralelo com a decodificação da primeira. Observe que,
neste ponto, há um pequeno conflito. A CPU está tentando buscar o próximo byte da fila de pré-busca
para usá-lo como operando ao mesmo tempo em que a pipeline está buscando 16 bits da fila de pré-busca
para usá-los como operando. As duas coisas podem ser feitas simultaneamente? Veremos a solução logo
a seguir.
Em T=T3, a CPU calcula o endereço do operando da primeira instrução, se houver. A CPU nada
faz na primeira instrução se esta não usar o modo de endereçamento [xxxx+bx]. Durante T3, a CPU
também decodifica o opcode da segunda instrução e busca os operandos necessários. Finalmente, a CPU
também busca o opcode da terceira instrução. A cada tic do clock, mais um passo da execução de cada
instrução na pipeline é completado e a CPU busca outra instrução na memória.
Em T=T6, a CPU completa a execução da primeira instrução, calcula o resultado da segunda, etc,
e, finalmente, busca o opcode da sexta instrução na pipeline. O importante é notar que, após T=T5, a
CPU completa uma instrução a cada ciclo do clock. No momento em que a CPU preencher a pipeline, ela
completa uma instrução em cada ciclo do clock. Observe que isto é verdadeiro mesmo se houver modos
de endereçamento complexos que precisam ser calculados, operandos de memória que precisam ser
buscados ou outras operações que usem ciclos num processador sem pipeline. Tudo que precisamos fazer
é adicionar mais estágios à pipeline e continuar a processar cada instrução num único ciclo de clock.
108
Baias numa pipeline
Infelizmente o cenário apresentado no tópico anterior é simplista demais. Existem dois obstáculos
nesta pipeline simples: contenção do barramento entre instruções e uma execução de programa não
sequencial. Os dois problemas podem aumentar o tempo médio de execução das instruções na pipeline.
A conteção de barramento ocorre sempre que uma instrução precisar acessar algum item na
memória. Por exemplo, se uma instrução mov mem, reg precisar armazenar dados na memória e uma
instrução mov reg, mem estiver lendo dados da memória, deve ocorrer uma contenção nos barramentos
de dados e de endereços porque a CPU tentará buscar e escrever dados na memória simultaneamente.
Uma maneira simplista de lidar com a conteção de barramento é através de uma baia de pipeline.
A CPU, quando confrontada com uma contenção, dá prioridade para a instrução que estiver mais
adiantada na pipeline. A CPU suspende a busca de opcodes até que a instrução atual busque (ou
armazene) seu operando. Isto faz com que a nova instrução na pipeline ocupe dois ciclos ao invés de um
(veja a Fig.22).
Este exemplo é apenas um dos casos de conteção de barramento. Existem inúmeros outros. Por
exemplo, como foi dito anteriormente, a busca de operandos de instrução necessita do acesso à fila de
pré-busca no mesmo momento em que a CPU precisa buscar um opcode. Além disso, em processadores
um pouco mais avançados que o 8486 (por exemplo, o 80486) existem outras fontes de contenção de
barramento. De acordo com esquema simples da Fig.22, é pouco provável que a maioria das instruções
sejam executadas a um clock por instrução (CPI = Clock Per Instruction).
Felizmente, o uso inteligente de um sistema cache pode eliminar muitas das baias de pipeline
como as analisadas acima. O próximo tópico, sobre cache, descreverá como isto é feito. Entretanto, nem
mesmo com um cache é possível prevenir baias na pipeline. Aquilo que não é possível consertar com
hardware pode ser arrumado com software. Se evitarmos usar a memória, podemos reduzir a conteção de
barramento e os programas serão mais rápidos. Da mesma forma, usando instruções mais curtas também
reduz a contenção de barramento e a possibilidade do aparecimento de baias na pipeline.
O que acontece quando uma instrução modifica o registrador IP? No momento em que a instrução
jmp 1000 for completada, já teremos começado outras cinco instruções e estamos a apenas um ciclo de
clock distante do término da primeira delas. É óbvio que a CPU não precisa executar estas instruções,
pois geraria resultados impróprios.
A única solução razoável é descarregar toda a pipeline e começar a buscar opcodes novamente.
Entretanto, este procedimento causa uma severa perda de tempo de execução. Tomará seis ciclos de
clock (o comprimento da pipeline do 8486) antes que a próxima instrução seja completada. Fica bem
claro que precisamos evitar o uso de instruções que interrompem a execução sequencial de um programa.
Isto também mostra um outro problema - o comprimento da pipeline. Quanto maior for a pipeline, mais
pode ser realizado por ciclo no sistema. Entretanto, aumentando a pipeline pode tornar um programa
109
lento se houver uma porção de desvios. Infelizmente não é possível controlar o número de estágios de
uma pipeline. Pode-se, no entanto, controlar o número de instruções de transferência que aparecem num
programa. Obviamente, num sistema com pipeline, seu uso deve ser mantido ao mínimo necessário.
110
mov cx, ax
add ax, ax
Este código possui apenas cinco bytes ao invés dos 12 bytes do exemplo anterior. O código
anterior precisará no mínimo de cinco ciclos de clock para ser executado e, se surgirem problemas de
contenção de barramento, ainda mais. O último exemplo usa apenas quatro. Além do mais, o segundo
exemplo libera o barramento em três dos quatro períodos de clock de modo que a BIU pode carregar
opcodes adicionais. Lembre-se, mais curto com frequência significa mais rápido.
Imagine, por um momento, que a CPU tenha dois espaços de memória separados, um para
instruções e outro para dados, cada qual com seu próprio barramento. Esta é a chamada Arquitetura
Harvard porque a primeira máquina deste tipo foi construída em Harvard. Numa máquina Harvard não
haveria contenção de barramento e a BIU poderia continuar a busca de opcodes através do barramento de
instruções enquanto acessasse a memória através do barramento de dados/memória.
No mundo real praticamente não existem máquinas Harvard verdadeiras. Os pinos extras que o
processador precisa para oferecer dois barramentos fisicamente separados aumenta o custo do
processador e introduz muitos outros problemas de engenharia. Entretanto, projetistas de
microprocessadores descobriram que podem obter muitos dos benefícios da arquitetura Harvard com
poucas das desvantagens usando caches on-chip separados para dados e instruções. CPUs avançadas
usam uma arquitetura interna Harvard e uma arquitetura externa Von Neumann. A figura abaixo mostra a
estrutura do 8486 com caches separados de dados e instruções.
‘Cada caminho dentro da CPU representa um barramento independente. Os dados podem fluir em
todos os barramentos concorrentemente. Isto significa que a fila de busca antecipada pode estar puxando
opcodes de instrução da cache de instruções enquanto a unidade de execução estiver escrevendo dados na
cache de dados. Agora a BIU busca opcodes na memória apenas quando não puder localizá-los na cache
de instruções. De modo parecido, a cache de dados funciona como buffer de memória. A CPU usa o
barramento dados/endereços apenas quando estiver lendo um dado que não encontrou na cache de dados
ou quando estiver enviando dados de volta para a memória principal.
111
Aliás, o 8486 manipula o problema da contenção na
busca de operandos de instrução/opcode de um jeito malandro.
Ao adicionar um cirtuito decodificador extra, ele decodifica em
paralelo a instrução no início da fila de pré-busca e mais três
bytes da fila. Então, se a instrução anterior não exigiu um
operando de 16 bits, a CPU usa o resultado do primeiro
decodificador; se a instrução anterior usou um operando, a CPU
usa o resultado do segundo decodificador.
Apesar de não podermos controlar a presença, tamanho ou tipo
da cache numa CPU, como programadores de Assembly
precisamos estar cientes de como a cache opera para podermos
escrever os melhores programas. Caches de instrução on-chip
geralmente são pequenas (8.192 bytes no 80486, por exemplo).
Fig.24 - Caches separadas no 8486
Portanto, quanto mais curtas forem as instruções, mais delas
cabem na cache (está se cansando das "instruções mais curtas"?).
Quanto mais isntruções tivermos na cache, menos contenções de barramento irão ocorrer. Do mesmo
modo, usando registradores para guardar resultados temporários faz com que menos grupos estejam na
cache, evitando que ela tenha que enviar ou trazer dados da memória com muita frequência. Use os
registradores sempre que possível!
Os perigos no 4846
Há um outro problema com o uso de uma pipeline: o perigo dos dados. Vamos dar uma olhada no
perfil de execução da seguinte sequência de instruções:
mov bx, [1000]
mov ax, [bx]
Quando estas duas instruções são executadas, a pipeline terá um aspecto parecido com o mostrado
na Fig.25. Note um problema importante. Estas duas instruções buscam o valor de 16 bits cujo endereço
aparece na localização 1000 da memória. Mas esta sequência de instruções não funciona direito!
Infelizmente a segunda instrução já usou o valor de bx antes que a primeira carregasse o conteúdo da
memória na localização 1000 (T4 e T6 na Fig.25).
112
Processadores CISC, como os 80x86, manipulam estes perigos automaticamente criando baias na
pipeline para sincronizar as duas instruções. A execução do 8486 acaba sendo algo como mostrado na
Fig.25a.
Atrasando a segunda instrução dois ciclos de clock, o 8486 garante que a instrução carregue ax do
endereço apropriado. Infelizmente, agora a segunda instrução é executada em três ciclos de clock ao
invés de um. No entanto, requerer dois ciclos de clock extras é melhor do que produzir resultados
incorretos. Mas é possível reduzir o impacto destes perigos na velocidade de execução através de
software.
Observe que o perigo dos dados ocorre quando o operando fonte de uma instrução era o operando
destino da instrução anterior. Não há nada de errado carregar bx de [1000] e depois carregar ax de [bx], a
não ser que uma instrução ocorra logo depois da outra. Imagine que a sequência tivesse sido:
mov cx, 2000
mov bx, [1000]
mov ax, [bx]
Podemos reduzir o perigo que existe nesta sequência de código simplesmente rearranjando as
instruções. Observe abaixo:
mov bx, [1000]
mov cx, 2000
mov ax, [bx]
Neste caso a instrução mov ax precisa apenas de um ciclo de clock adicional ao invés de dois.
Inserindo duas instruções entre as instruções mov bx e mov ax pode-se eliminar completamente os
efeitos do perigo.
Num processador com pipeline, a ordem das instruções num programa pode afetar
dramaticamente a performance deste programa. Verifique sempre se há possíveis perigos na sequência de
instruções e elimine-os sempre que possível rearranjando as instruções.
O processador 8686
Com a arquitetura de pipeline do 8486 obtivemos, na melhor das hipóteses, tempos de execução
de um CPI (clock por instrução). Seria possível executar instruções mais rápido do que isto? À primeira
vista parece não haver a mínima possibilidade de executar mais de uma instrução por ciclo de clock.
Entretanto, lembre-se de que uma instrução única não é uma operação única. Nos exemplos mostrados
anteriormente, cada instrução precisava de seis a oito operações para ser completada. Adicionando sete
ou oito unidades à CPU poderíamos executar estas oito operações em um ciclo de clock e efetivamente
obter um CPI. Se adicionarmos mais hardware e executarmos, digamos, 16 operações simultâneas,
poderemos obter 0.5 CPI? A resposta é um sonoro "sim". Uma CPU que inclui este hardware adicional é
uma CPU superescalar e pode executar mais de uma instrução durante um único ciclo de clock. Esta é a
capacidade que o processador 8686 apresenta.
113
U ma CPU superescalar possui, essencialmente,
várias unidades de execução. Se encontrar duas ou mais
instruções no fluxo de instruções (isto é, na fila de
busca antecipada) que possam ser executadas
independentemente, ela o fará.
Existem uma porção de vantagens na arquitetura
superescalar. Imagine as seguintes instruções no fluxo
de instruções:
mov ax, 1000
mov bx, 2000
Se não houver outros problemas ou perigos no
código circundante e se todos os seis bytes destas duas
Fig.26 - A CPU superescalar 8686 instruções já estiverem na fila de pré-busca, não existe
motivo que impeça a CPU de buscar e executar as duas
instruções em paralelo. Só é preciso um pouco de silício extra no chip da CPU para implementar duas
unidades de execução.
Além de aumentar a velocidade de instruções independentes, uma CPU superescalar também
aumenta a velocidade de sequências de programa que possuam perigos. Uma das limitações da CPU
8486 é que, quando ocorre uma situação de perigo, a instrução faltosa vai desarranjar completamente a
pipeline com baias. Toda instrução seguinte também terá que esperar que a CPU sincronize a execução
das instruções. Com uma CPU superescalar, no entanto, depois de uma situação de perigo, a execução
das instruções seguintes pode continuar através da pipeline, contanto que elas mesmas não possuam
perigos. Isto alivia (apesar de não eliminar) o cuidado que se precisa ter com a sequência das instruções.
Como programadores da linguagem Assembly, o modo como escrevemos o software para uma
CPU superescalar pode afetar dramaticamente a sua performance. A primeira regra, e a mais importante,
você já está careca de saber: usar instruções curtas. Quanto mais curtas forem as instruções, mais
instruções a CPU pode buscar numa única operação. Portanto, a probabilidade da CPU executá-las em
menos de um CPI aumenta. A maioria das CPUs superescalares não duplicam completamente a unidade
de execução. Pode haver múltiplas ALUs, unidades de ponto flutuante, etc. Isto significa que certas
sequências de instruções podem ser executadas com muita rapidez e outras não. É preciso estudar a
composição exata de cada CPU para decidir quais são as sequências de instruções que resultam na
melhor performance.
114
dados para um dispositivo de saída, a CPU simplesmente move estes dados para um local especial na
memória (no espaço de endereços de E/S se for E/S mapeada ou para um endereço no espaço de
endereços de memória se estiver usando E/S mapeada na memória). Para ler dados de um dispositivo de
entrada, a CPU simplesmente move os dados do endereço (E/S ou memória) deste dispositivo para a
CPU. Apesar de geralmente existirem mais estados de espera associados a dispositivos periféricos típicos
do que à memória, as operações de entrada ou saída são muito semelhantes às operações de leitura ou
escrita na memória.
Para o computador, uma porta E/S é um
dispositivo que se parece com uma célula de memória
que tenha conexões com o mundo exterior. Uma porta de
E/S usa tipicamente um latch, ao invés de um flip-flop,
para implementar a célula de memória. Quando a CPU
escreve num endereço associado ao latch, o dispositivo
Fig.27 - Porta de E/S
latch captura os dados e os disponibiliza num conjunto de
fios externos, independentes da CPU e do sistema de
memória (Fig.27).
Observe que as portas de E/S podem ser apenas para leitura, apenas para escrita ou para leitura e
escrita. A porta da Fig.27, por exemplo, é apenas para escrita. Como as saídas do latch não voltam para o
barramento de dados da CPU, a CPU não é capaz de ler os dados que o latch contém. As linhas de
decodificação de endereços e de controle de escrita precisam estar ativas para que o latch funcione - a
linha de decodificação de endereços está ativa quando estiver sendo feita a leitura do endereço do latch,
mas a linha de controle de escrita não.
A Fig.28 mostra como criar uma porta para
leitura/escrita. Os dados escritos na porta de saída
retornam para um latch transparente. Sempre que a
CPU lê o endereço decodificado, as linhas de leitura
e de decodificação estão ativas e isto ativa o latch
inferior. Os dados escritos previamente na porta de
saída são colocados no barramento de dados da
CPU, permitindo que a CPU leia estes dados. Uma
porta apenas para leitura (entrada) é simplesmente a
metade inferior desta figura - o sistema ignora
qualquer dado escrito na porta de entrada.
115
que acesse a memória também pode acessar a porta de E/S mapeada para a memória. No x86, as
instruções mov, add, sub, cmp, and, or e not podem ler a memória; as instruções mov e not podem
escrever dados na memória. A E/S mapeada usa instruções especiais para acessar as portas E/S. Por
exemplo, as CPUs x86 usam as instruções get e put, a família 80x86 da Intel usa as instruções in e out.
As instruções in e out do 80x86 funcionam como a instrução mov, com a diferença de que colocam seus
endereços no barramento de endereços E/S ao invés de colocá-los no barramento de endereços de
memória.
Os subsistemas E/S mapeada para a memória e E/S mapeada necessitam da CPU para mover
dados entre o dispositivo periférico e a memória principal. Por exemplo, para passar uma sequência de
dez bytes de uma porta de entrada para a memória, a CPU precisa ler os valores um a um e armazená-los
na memória. Para dispositivos de E/S de velocidade muito alta, a CPU pode ser muito lenta processando
os dados um byte por vez. Tais dispositivos geralmente possuem uma interface para o barramento da
CPU para que possam ler e escrever diretamente na memória. Este acesso é conhecido como acesso
direto à memória porque o dispositivo periférico acessa a memória diretamente sem usar a CPU como
intermediário. Isto frequentemente permite que a operação de E/S ocorra em paralelo com outras
operações da CPU, aumentando a velocidade geral do sistema. Note, entretanto, que a CPU e o
dispositivo DMA não podem usar simultaneamente os barramentos de endereços e de dados. Portanto, o
processamento concorrente apenas ocorre quando a CPU possuir uma cache e estiver executando código
e acessando dados encontrados na cache (que é quando o barramento está liberado). Apesar disso, mesmo
se a CPU precisar parar e esperar que a operação DMA seja completada, ainda assim a E/S é muito mais
rápida porque muitas das operações no barramento durante a E/S ou a entrada/saída mapeada para a
memória são instruções de busca ou de acesso à porta de E/S, as quais não estão presentes durante
operações DMA.
116
A primeira instrução busca o dado da porta de entrada de status. A segunda instrução faz um and
lógico deste valor com 1 para clarear os bits de número um até quinze e liga o bit zero do status atual da
porta da impressora. Observe que isto produz o valor zero em bx se a impressora estiver ocupada e
produz o valor um em bx se a impressora estiver pronta para aceitar dados adicionais. A terceira
instrução checa bx para verificar se ele contém zero (isto é, a impressora está ocupada). Se a impressora
estiver ocupada, este programa salta de volta para o endereço zero e repete o processo tantas vezes
quantas forem necessárias até que o bit de status da impressora seja 1.
O código seguinte fornece um exemplo de leitura de teclado. Ele considera que o bit de status do
teclado seja o bit zero do endereço 0FFE6h (valor zero significa que nenhuma tecla foi digitada) e que o
código ASCII da tecla apareça no endereço 0FFE4h quando o bit zero localizado em 0FFE6h contenha o
valor 1:
0000: mov bx, [FFE6]
0003: and bx, 1
0006: cmp bx, 0
0009: je 0000
000C: mov [FFE4], ax
Este tipo de operação de E/S, onde a CPU fica testando constantemente uma porta para verificar
se há dados disponíveis é a apuração (polling), isto é, a CPU faz a apuração (faz a contagem de votos) da
porta para saber se há dados disponíveis ou se a porta é capaz de aceitar dados. A E/S apurada é
inerentemente ineficiente. Imagine o que acontece se o usuário demorar 10 segundos para apertar uma
tecla - a CPU fica presa no loop sem fazer nada (além de testar a porta de status do teclado) durante estes
10 segundos.
Em sistemas de computadores pessoais antigos (por exemplo, o Apple II), era exatamente desta
forma que o programa lia dados do teclado - se quisesse ler uma tecla do teclado, iria fazer a apuração da
porta de status do teclado até que uma tecla estivesse disponível. Estes computadores não podiam realizar
outras operações enquanto estivessem esperando que uma tecla fosse digitada. Mais importante ainda é
que, se passasse muito tempo entre a checagem da porta de status do teclado, o usuário poderia digitar
uma segunda tecla e a primeira seria perdida.
A solução para este problema é fornecer um mecanismo de interrupção. Uma interrupção é um
evento de harware externo (como apertar uma tecla) que faz com que a CPU interrompa a sequência de
instruções atual e chame uma rotina de serviço de interrupção especial (ISR - Interrupt Service Routine).
Uma rotina de serviço de interrupção tipicamente salva todos os registradores e flags (para não causar
distúrbio na computação que está sendo interrompida), realiza as operações que sejam necessárias para
manipular a fonte da interrupção, restaura os registradores e flags e depois retoma a execução do código
que foi interrompida. Em muitos sistema de computador (por exemplo o PC), muitos dispositivos de E/S
geram uma interrupção sempre que tiverem dados disponíveis ou estiverem prontos para aceitar dados da
CPU. A ISR processa rapidamente a requisição num segundo plano, permitindo que alguma outra
computação prossiga no primeiro plano.
CPUs que permitem interrupções precisam fornecer algum mecanismo para que o programador
possa especificar o endereço da ISR que deve ser executada quando ocorrer uma interrupção. Um vetor
de interrupção é, tipicamente, uma localização especial de memória que contém o endereço da ISR que
corresponde à interrupção. As CPUs x86, por exemplo, possuem dois vetores de interrupção: um para
uma interrupção de propósito geral e um para uma interrupção de reset (a interrupção de reset
corresponde a pressionar o botão de reset da maioria dos PCs). A família 80x86 da Intel possui até 256
vetores de interrupção diferentes.
Após uma ISR completar sua operação, ela devolve o controle para a tarefa do primeiro plano
com uma instrução especiald de "retorno de interrupção". No x86 a instrução iret (interrupt return) realiza
117
esta tarefa. Uma ISR deveria sempre terminar com esta instrução para que a ISR possa devolver o
controle ao programa que ela interrompeu.
Um sistema de entrada baseado em interrupções típico usa a ISR para ler dados de uma porta de
entrada e os coloca num buffer sempre que os dados estiverem disponíveis. O programa no primeiro
plano pode ler estes dados do buffer à vontade, sem que qualquer dado da porta seja perdido. Da mesma
forma, um sistema de saída baseado em interrupções típico (que recebe uma interrupção assim que um
dispositivo de saída esteja pronto para aceitar mais dados) pode remover dados do buffer sempre que o
dispositivo periférico estiver pronto para receber novos dados.
118
6 – LINGUAGEM ASSEMBLY
Este capítulo é um elo importante entre a Organização de Sistemas e a Linguagem Assembly
Básica, assuntos dos capítulos anteriores. Do ponto de vista da organização de sistemas, este capítulo
cobre o endereçamento e a organização da memória, os modos de endereçamento da CPU e a
representação de dados na memória. Do ponto de vista da programação em linguagem Assembly, este
capítulo apresenta os conjuntos de registradores do 80x86, os modos de endereçamento do 80x86 e os
tipos de dados compostos. Este é um capítulo vital. Se você não conseguir entender o material que ele
contém, com certeza terá dificuldade em entender os capítulos seguintes. Portanto, nem é preciso dizer...
119
O registrador bp (Base Pointer = Ponteiro da Base) é semelhante ao registrador bx. Geralmente é
usado para acessar parâmetros e variáveis locais num procedimento.
O registrador sp (Stack Pointer = Ponteiro da Pilha) tem um propósito muito especial - manter a
pilha do programa. Normalmente este registrador seria usado para cálculos aritméticos. A operação
adequada da maioria dos programas depende de uso criterioso deste registrador.
Além dos oito registradores de 16 bits, as
CPUs do 8086 também possuem oito
registradores de 8 bits. A Intel chamou estes
registradores de al, ah, bl, bh, cl, ch, dl e dh. Você
deve ter notado a semelhança entre estes nomes e
os dos registradores de 16 bits (para ser exato, ax,
bx, cx e dx). Os registradores de 8 bits não são
independentes. al representa "byte menos
significante (low order) de ax". ah representa
"byte mais significante (high order) de ax". Os
Fig.1 - Registradores de uso geral do 8086 nomes dos outros sete registradores de 8 bits
significam a mesma coisa em relação a bx, cx e
dx. A Fig.1 mostra o conjunto de registradores de propósito geral.
Note que os registradores de 8 bits não formam um conjunto independente de registradores -
alterar al mudará o valor de ax, o mesmo acontecendo se alterarmos ah. O valor de al corresponde
exatamente aos bits 0 a 7 de ax. O valor de ah corresponde aos bits 8 a 15 de ax. Portanto, qualquer
modificação em al ou ah modificará o valor de ax. Do mesmo modo, modificando o valor de ax
modificará tanto al quanto ah. Observe, no entanto, que modificar al não afeta o valor de ah e vice versa.
Estas afirmações também são válidas para bx/bl/bh, cx/cl/ch e dx/dl/dh.
Os registradores si, di, bp e sp são de 16 bits totais. Não há como acessar bytes individuais nestes
registradores como nos bytes menos e mais significativos de ax, bx, cx e dx.
120
O registrador de segmento extra, es, é apenas o que o nome indica. Programas para o 8086 usam
com frequência este registrador para ganhar acesso a segmentos quando for difícil ou impossível
modificar outros registradores de segmento.
O registrador ss aponta para o segmento que contém a pilha (stack) do 8086. A pilha é onde o
8086 armazena informações importantes sobre o estado da máquina, endereços de retorno de subrotinas,
parâmetros de procedimentos e variáveis locais. Em geral, não se modifica o registrador de segmento de
pilha porque muitas coisas do sistema dependem dele.
Apesar de, teoricamente, ser possível armazenar dados nos registradores de segmento, a idéia não
é das melhores. Estes registradores têm uma função muito especial - apontar para blocos de memória que
podem ser acessados. Qualquer tentativa de usar estes registradores para outros propósitos pode resultar
numa complicação considerável, principalmente se pretendermos trabalhar com CPUs melhores, como a
80386.
121
modo real (o modo de emulação do 8086). A flag NT (nested task - tarefa aninhada) controla a operação
de uma instrução de retorno de interrupção (IRET). A NT normalmente é zero para programas em modo
real.
Além dos bits extras no registrador de flags, o 80286 tem também cinco registradores adicionais
usados pelo sistema operacional para apoiar a administração da memória e processos múltiplos: o
machine status word (msw - word de status da máquina), o global descriptor table register (gdtr -
registrador da tabela de descritores globais), o local descriptor table register (ldtr - registrador de tabelas
de descritores locais), o interrupt descriptor table register (idtr - registrador da tabela de descritores de
interrupção) e o task register (tr - registrador de tarefas).
O único uso do modo protegido do 80286 por um programa é o acesso a mais do que 1 megabyte
de RAM. Entretanto, como o 80286 está absolutamente obsoleto e como existem meios melhores de se
acessar mais memória, não tem porque prolongar esta descrição.
122
Os 80386/486 também adicionam oito
registradores de debug. Programas como o
Codeview ou o Turbo Debugger podem usar estes
registradores para colocar pontos de parada
(breakpoints) quando se está tentando localizar
erros num programa. Apesar de não usarmos estes
registradores em programas aplicativos, o uso de
debuggers reduz o tempo que se gasta eliminando
erros dos nossos programas. É claro que programas
deste tipo funcionam só a partir do 80386.
Finalmente, os processadores 80386/486
adicionaram um conjunto de registradores de teste
que testam se o processador está operando
adequadamente quando o sistema é ligado. A Intel
Fig.2 - Modelo de programação para o 80386/486/Pentium
colocou estes resgistradores no chip para permitir o
teste logo após a manufatura dos mesmos, mas é
claro que os projetistas de sistemas podem tirar vantagem destes registradores para fazer testes de power-
on.
Na maioria das vezes, programadores de Assembly não precisam se preocupar com os
registradores extras adicionados nos processadores 80386/486/Pentium. Entretanto, a ampliação para 32
bits e os registros de segmento extras são bastante úteis. Para o programador de aplicações, o modelo de
programação para o 80386/486/Pentium tem o aspecto mostrado na Fig.2.
O valor no barramento de endereços que corresponde ao índice fornecido a este array. Por
exemplo, escrever dados na memória é equivalente a:
Memoria [endereco] := Valor_para_Escrever;
CPUs 80x86 diferentes possuem barramentos de endereços diferentes que controlam o número
máximo de elementos no array Memoria. Entretanto, independentemente do número de linhas de
endereço do barramento, a maioria dos sistemas de computador não possuem um byte de memória para
cada localização endereçável. Por exemplo, os processadores 80386 possuem 32 linhas de endereço que
permitem até quatro gigabytes de memória. São raros os sistemas 80386 que possuem quatro gigabytes
de memória. Geralmente, os sistemas baseados no 80x86 possuem 256 megabytes.
O primeiro megabyte de memória, do endereço zero até 0FFFFFh, é especial no 80x86. Isto
corresponde ao espaço de endereços total dos microprocessadores 8088, 8086, 80186, 80188. A maioria
dos programas DOS limitam seus endereços de programa e dados para localizações neste intervalo.
Endereços limitados a esta faixa são chamados de endereços reais, devido ao modo real do 80x86.
123
6.2.1. Segmentos no 80x86
Não é possível discutir o endereçamento de memória da família de processadores 80x86 sem
antes analisar a segmentação. Entre outras coisas, a segmentação proporciona um mecanismo de
gerenciamento de memória muito poderoso. Ela permite que programadores dividam seus programas em
módulos que operem de forma independente. Segmentos proporcionam um modo de implementar com
facilidade programas orientados a objeto. Segmentos permitem que dois processos compartilhem dados
com facilidade. No final das contas, a segmentação é realmente uma capacidade interessante. Por outro
lado, se perguntarmos a dez programadores o que eles pensam da segmentação, pelo menos nove vão
reclamar, dizendo que é horrorosa. Porque este tipo de opinião?
Bem, acontece que a segmentação também tem uma característica elegante: ela permite ampliar o
endereçamento do processador. No caso do 8086, a segmentação permitiu que os projetistas da Intel
ampliassem a memória endereçável máxima de 64 K para 1 Mega. Uau, isto soa bem. Então, porque é
que todo mundo reclama? Bem, uma pequena aula de história será necessária para entender o que é que
deu errado.
Em 1976, quando a Intel começou a projetar o processador 8086, a memória era muito cara.
Computadores pessoais, como eram na época, tinham tipicamente 4000 bytes de memória. Quando a
IBM lançou o PC, cinco anos mais tarde, 64K era uma memória considerável, um megabyte era uma
quantidade astronômica. Os projetistas da Intel perceberam que 64K de memória continuaria sendo uma
quantidade grande durante a vida do 8086. O único erro que fizeram foi subestimar a sobrevida do 8086.
Imaginaram que duraria cerca de cinco anos, como o processador anterior, o 8080. Na época, eles tinham
planos para uma porção de outros processadores, e "86" não era o sufixo dos nomes deles. A Intel
imaginou que estava sossegada, segura de que um megabyte seria mais do que suficiente até que
lançassem algo melhor.
Infelizmente a Intel não contava com o sucesso do IBM PC e da enorme quantidade de software
que apareceria para ele. Ao redor de 1983 já estava muito claro que a Intel não poderia abandonar a
arquitetura 80x86. Estava preso a ele, mas aí o pessoal começou a esbarrar no limite de 1 megabyte do
8086. Então a Intel nos deu o 80286. Este processador podia endereçar até 16 megabytes de memória.
Com certeza, mais do que suficiente. O único problema era que todo o maravilhoso software escrito para
o IBM PC tinha sido escrito de uma forma que não poderia aproveitar qualquer memória acima de um
megabyte.
Acabou ficando claro que o máximo de memória endereçável não era a queixa principal de todos.
O problema real era que o 8086 era um processador de 16 bits, com registradores de 16 bits e endereços
de 16 bits. Isto limitava o processador a endereçar pedaços de memória de 64K. O uso esperto da
segmentação pela Intel ampliava isto para um megabyte mas, endereçar mais do que 64K por vez exigia
algum esforço. Endereçar mais dos que 256K de uma vez exigia muito mais esforço.
Apesar do que você possa ter ouvido, a segmentação não é ruim. De fato, ela é um esquema de
gerenciamento de memória realmente bom. O que é ruim é a implementação de segmentação feita pela
Intel em 1976 que é usada até hoje. Não podemos culpar a Intel por isto - ela resolveu o problema nos
anos 80 com o lançamento do 80386. O verdadeiro culpado é o MS-DOS que força os programadores a
continuar usando o estilo de segmentação de 1976. Felizmente os sistemas operacionais mais novos,
como Linux, UNIX, Windows 9x, Windows NT e OS/2 não sofrem dos mesmos males que o MS-DOS.
Além disso, os usuários finais parecem mais propensos a mudar para estes sistemas operacionais mais
novos de modo que os programadores podem aproveitar as vantagens das novas características da família
80x86.
124
Terminada a aula de história, provavelmente seja uma
boa idéia explicar o que vem a ser a segmentação. Considere a
concepção atual de memória: ela se parece com um array linear
de bytes. Um único índice (endereço) seleciona algum byte em
particular deste array. Chamemos este tipo de endereçamento de
endereçamento linear ou plano. O endereçamento segmentado
usa dois componentes para especificar uma localização na
memória: um valor de segmento e um deslocamento dentro
deste segmento. Na forma ideal, os valores do segmento e do
Fig.3 - Endereçamento por valores deslocamento são independentes. O melhor jeito de descrever o
de segmento e de deslocamento
endereçamento segmentado é com um array bidimensional. O
segmento fornece um dos índices e o deslocamento o outro (veja na Fig.3).
Agora você deve estar se perguntado porque tornar este processo mais complexo. Endereços
lineares parecem funcionar bem, para que se incomodar com este esquema de endereçamento de duas
dimensões? Bem, vamos analisar o modo como costumamos escrever um programa. Se tivéssemos que
escrever, digamos, uma rotina para calcular o seno de x e precisássemos de algumas variáveis
temporárias, provavelmente não usaríamos variáveis globais. Pelo contrário, provavelmente usaríamos
variáveis locais dentro da função seno. Num sentido amplo, esta é uma das características da
segmentação - a possibilidade de ligar blocos de variáveis (um segmento) a um determinado trecho de
código. Poderíamos, por exemplo, ter um segmento contendo as variáveis locais para seno, um segmento
para raiz quadrada, um segmento para DRAWWindow, etc. Uma vez que as variáveis para a rotina do
seno aparecem no segmento do seno, é pouco provável que elas afetem as variáveis que pertencem à
rotina de raiz quadrada. De fato, no 80286 e posteriores, operando em modo protegido, a CPU pode
prevenir uma rotina de modificar acidentalmente as variáveis de um outro segmento.
Um endereço segmentado completo contém um componente segmento e um componente
deslocamento. Neste texto escreveremos endereços segmentados como segmento:deslocamento. Do 8086
até o 80286, estes dois valores são constantes de 16 bits. No 80386 e posteriores, o deslocamento pode
ser uma constante de 16 ou de 32 bits.
O tamanho do deslocamento limita o tamanho máximo do segmento. No 8086 com
deslocamentos de 16 bits, um segmento não pode ser maior do que 64K; pode ser menor (e a maioria dos
segmentos são), mas nunca maior. Os processadores 80386 e posteriores permitem deslocamentos de 32
bits com segmentos de até 4 giagbytes.
A porção do segmento é de 16 bits em todos os processadores 80x86. Isto permite que um
programa possa ter até 65.536 segmentos diferentes. A maioria dos programas possui menos do que 16
segmentos (mais ou menos) de modo que isto não é uma limitação prática.
É claro que, apesar do fato de que a família 80x86 usar
endereçamento segmentado, a memória (física) conectada à CPU
continua sendo um array linear de bytes. Existe uma função que
transforma o valor do segmento num endereço da memória física. O
processador, então, soma o deslocamento a este endereço físico para
obter o endereço real de dados na memória. Neste texto vamos nos
referir aos endereços de programas como endereços segmentados ou
Fig.4 - Endereço físico (linear)
endereços lógicos. O endereço linear que aparece no barramento de
endereços é o endereço físico (veja a Fig.4).
No 8086, 8088, 80186 e 80188 ( e outros processadores operando em modo real), a função que
mapeia um segmento para um endereço físico é muito simples. A CPU multiplica o valor do segmento
por 16 (10h) e soma a porção do deslocamento. Por exemplo, considere o seguinte endereço segmentado
1000:1F00. Para transformá-lo num endereço físico basta multiplicar o valor do segmento (1000h) por 16
125
(ou 10h). Esta multiplicação é muito simples, basta adicionar um zero ao número: 1000h x 10h = 10000h.
Adicionando o deslocamento obtemos 10000h + 1F00h = 11F00h. Portanto, 11F00h é o endereço físico
que corresponde ao endereço segmentado 1000:1F00.
Alerta: um erro muito comum quando se efetua este cálculo é esquecer que se está trabalhando
em hexadecimal, e não no sistema decimal. É impressionante o número de pessoas que somam 9 + 1 e
obtém 10h ao invés de 0Ah, que é o correto.
A Intel, quando projetou os processadores 80286 e posteriores, não ampliou o endereçamento
adicionando novos bits nos registradores de segmento. Ao invés disso, mudou a função que a CPU usa
para transformar um endereço lógico num endereço físico. Se escrevermos um código que dependa da
função "multiplique por 16 e some o deslocamento", o programa rodará apenas em processadores 80x86
operando no modo real e estaremos limitados a um megabyte de memória.
Nos processadores 80286 e posteriores, a Intel introduziu segmentos em modo protegido. Entre as
mudanças efetuadas, a Intel reformou completamente o algoritmo para mapear segmentos para o espaço
de endereços lineares. Ao invés de usar uma função (como a que multiplica o valor do segmento por
10h), os processadores em modo protegido consultam uma tabela para calcular o endereço físico. Eles
usam o valor do segmento como um índice de array. O conteúdo do elemento do array selecionado
fornece (entre outras coisas) o endereço inicial do segmento. A CPU soma este valor com o
deslocamento para obter o endereço físico (veja a Fig.5).
126
normalizados assumem uma forma especial que os tornam únicos, isto é, a não ser que dois valores
segmentados normalizados sejam exatamente iguais, eles não apontam para o mesmo objeto na memória.
Existem várias maneiras diferentes (na verdade, 16) de criar endereços normalizados. Por
convenção, a maioria dos programadores (e das linguagens de alto nível) definem um endereço
normalizado da seguinte forma:
• A porção do segmento no endereço pode ser qualquer valor de 16 bits.
• A porção do deslocamento precisa ser um valor entre 0 e 0Fh.
Ponteiros normalizados que estejam nesta forma são muito fáceis de serem convertidos para
endereços físicos. Tudo que precisamos fazer é anexar o dígito hexadecimal referente ao deslocamento
ao valor do segmento. A forma normalizada de 1000:1F00 é 11F0:0. Podemos obter o endereço físico
anexando o deslocamento (zero) ao fim de 11F0 para obter 11F00.
É muito fácil transformar um valor segmentado arbitrário num endereço normalizado. Primeiro
transformamos o endereço segmentado num endereço físico usando a função "multiplica por 16 e soma o
deslocamento". Depois inserimos os dois pontos entre os últimos dois dígitos do resultado de cinco
dígitos:
1000:1F00 => 11F00 => 11F0:0
Note que esta discussão se refere somente aos processadores 80x86 operando em modo real. No
modo protegido não existe uma correspondência direta entre endereços segmentados e endereços físicos,
de modo que esta técnica não funciona. Entretanto, este texto trata principalmente de programas que
rodam em modo real e, por isso, ponteiros normalizados são citados com frequência.
127
economizar dois bytes por acesso à memória não valem a pena se acessarmos dados o tempo todo em
segmentos diferentes. Felizmente, a maioria de acessos consecutivos à memória ocorrem dentro do
mesmo segmento, ou seja, carregar registradores de segmento não é algo feito com frequência.
128
6.3.2. Modos de endereçamento de memória no 8086
O 8086 fornece 17 modos diferentes de acesso à memória. Pode parecer um exagero, mas
felizmente a maioria dos modos de endereçamento são apenas variações de um outro. São fáceis de
aprender e é imperioso que se aprenda! A chave para uma boa programação Assembly é o uso apropriado
dos modos de endereçamento.
Os modos de endereçamento do 8086 incluem apenas por deslocamento, base, deslocamento mais
base, base mais índice e deslocamento mais base mais índice. As variações destas cinco formas é que
resultam nos 17 modos diferentes de endereçamento.
129
Os modos de endereçamento indireto com registradores
As CPUs 80x86 permitem acessar a memória indiretamente através de um registrador usando os
modos de endereçamento indireto por registradores. Existem quatro formas neste modo de
endereçamento, melhor demonstradas nas seguintes instruções:
mov al, [bx]
mov al, [bp]
mov al, [si]
mov al, [di]
Estas quatro formas de endereçamento referenciam
o byte no deslocamento indicado pelos registradores bx,
bp, si ou di. Como padrão, o modo de endereçamento [bp]
usa o segmento da pilha (ss).
Podemos usar os símbolos dos prefixos de cancelamento
de segmento se quisermos acessar dados em segmentos
Fig.8a - Modo indireto com registrador BX diferentes. As instruções seguintes demonstram o uso
destes cancelamentos:
mov al, cs:[bx]
mov al, ds:[bp]
mov al, ss:[si]
mov al, es:[di]
130
mov al, ss:disp[bx]
mov al, es:disp[bp]
mov al, cs:disp[si]
mov al, ss:disp[di]
Podemos substituir si ou di para obter os modos de
endereçamento [si+desloc] e [di+desloc].
Saiba que a Intel ainda se refere a estes modos de
endereçamento como endereçamento de base e
endereçamento indexado. A literatura da Intel não faz
diferença entre estes dois modos, com ou sem a constante.
Fig.9b - Modo indexado com BP e pilha Se verificarmos como o hardware trabalha, esta até que é
uma definição razoável. Do ponto de vista do
programador, entretanto, estes modos de endereçamento são úteis para coisas totalmente diversas. Este é
o motivo pelo qual este texto usa termos diferentes para nominá-los. Infelizmente existe pouco consenso
no uso dos termos no mundo 80x86.
Fig.10a - Modo de base indexada com BX Fig.10b - Modo de base indexada com BP e pilha
Imagine que bx contenha 1000h e que si contenha 880h. Neste caso, a instrução mov al,[bx][si]
carrega al com o valor da posição DS:1880h. Da mesma forma, se bp contiver 1598h e di contiver 1004,
mov ax,[bp+di] carrega os 16 bits da posição SS:259C e SS:259D em ax.
Como padrão, os modos de endereçamento que não envolvem bp usam o segmento de dados.
Também como padrão, os que tiverem bp como operando usam o segmento da pilha.
Podemos substituir si da Fig.10a por di para produzir o modo de endereçamento [bx+di] e
substituir si da Fig.10b por di para produzir o modo de endereçamento [bp+di].
131
mov al, [bp][di][desloc]
Fig.11a - Modo de base indexada mais deslocamento Fig.11b - Modo de base indexada mais deslocamento
com BX com BP e pilha
132
• Escolhendo desloc da coluna um, nada da coluna dois e [di] da coluna três obtém-se o
endereçamento desloc[di].
• Escolhendo desloc, [bx] e [di], obtém-se desloc[bx][di].
• Pulando as colunas um e dois e escolhendo [si] obtemos [si].
• Pulando a coluna um e escolhendo [bx] e depois [di] obtemos [bx][di].
Do mesmo modo, se tivermos um modo de endereçamento que não pode ser construído a partir deste
diagrama, então ele não é válido. Por exemplo, desloc[dx][si] é ilegal porque [dx] não consta em
nenhuma das colunas do diagrama.
133
7 – SISTEMA OPERACIONAL
INTERFACE HARDWARE/SOFTWARE:
Podemos enxergar diversas camadas entre o usuário e o computador, conforme mostra a figura a
seguir. O usuário interage com aplicativos desenvolvidos por programadores que fazem uso de utilitários
tais como editores, compiladores, interpretadores, montadores etc… Por baixo destes aplicativos e
utilitáriuos pode haver uma interface amigável. Esta interface atuará sobre o sistema operacional (shell +
kernel) que por sua vez utilizará recursos da BIOS. Todas estas camadas por sua vez serão de fato
executadas pela unidade de controle (microprograma). Por último, estão os dispositivos físicos de entrada
e saída, registradores e memória.
Usuário
Aplicativos
Utilitários
Interface
Shell
S.O.
kernel
BIOS
Unidade de Controle
Mem, Regs e
Portas de E/S
Primeiramente, temos que observar que o hardware (unidade de controle) executa instruções em
linguagem de máquina, tais como aquelas executadas pelo computador Ramsés. Estas instruções devem
estar na memória e o processador trata simplesmente de executar o seguinte algoritmo:
Loop
RI := MEM (PC) ; /* lê da memória a instrução apontada por PC e a coloca no reg. de
instrução RI */
Decodifica (RI); /* decodifica o significado da instrução */
Executa ; /* executa ainstrução, ou seja, dispara sinais de controle na
arquitetura, ativando registradores, ULAs, canais de dados etc… a
fim de executar a instrução */
Incrementa (PC) ; /* faz o ponteiro de instrução apontar para a próxima a ser
executada */
Forever ;
Uma vez que o computador é ligado ele fica realizando este ciclo ininterruptamente etá que o
computador seja desligado.
134
Mais então, o hardware não distingue se a instrução é do sistema operacional ou se é de uma
aplicação qualquer?
Sim, é isto mesmo. O hardware executa as instruções sem se importar à quem elas pertencem.
Como as instruções de um programa são colocadas na memória, para serem executadas pelo
hardware?
Bem, primeiramente você deve escrever um programa fonte utilizando para isto um editor de
textos/caracteres. Este programa fonte deve ser escrito em uma linguagem de programação para o qual
existe um compilador associado. Este programa fonte nada mais é do que um "monte" de caracteres que
para o hardware não tem significado algum. Então, voce deve transformar este programa fonte em um
programa executável, utilizando para isto um compilador adequado. Se voce escreve um programa em
linguagem Pascal, voce deve utilizar um compilador Pascal. Se voce escreve um compilador em
linguagem C, então você deve utilizar um compilador C.
O compilador gera então um programa executável, ou seja, uma sequencia de instruções em
linguagem de máquina que pode então ser executada pelo hardware.
Mas então, um programa compilado para um tipo de arquitetura pode ser executado em outro tipo
de arquitetura?
Não, pois cada tipo de arquitetura possui um conjunto específico de instruções que ela pode
decodificar e executar. Obviamente, o compilador sabe disto e gera o código dependente da arquitetura.
Embora você possa encontrar um mesmo compilador que roda em vários tipos de arquiteturas, cada
versão deste compilador foi modificada para permitir gerar o código corretamente para determinado tipo
de arquitetura. Por exemplo, voce pode encontrar compiladores C para computadores Pentium e Sparc,
mas cada um foi implementado de forma a gerar código diferente sob cada plataforma.
Mas, depois que um programa fonte foi compilado e está pronto para ser executado, quem o coloca
na memória?
O sistema operacional (S.O.), depois que usuário o solicita através da linha de comando, por
exemplo. O loader (carregador) é um software componente do sistema operacional que se encarrega de
ler o programa executável do disco e o colocar na memória, fazendo os reajustes finais nos endereços
caso seja necessário.
135
Mas onde estão a BIOS e o Start Up?
São programas residentes, que vem em memória ROM juntamente com o computador. O
processador quando é ligado passa a executar automaticamente esta memória, sem saber quem está ali.
Isto é feito porque o contador de programa recebe automaticamente o endereço inicial do Start Up.
Então, o Start Up faz uma checagem do equipamento e posteriormente faz a busca e carga do setor pré-
definido onde deve estar o Boot. Após ter carregado o Boot em uma posição pré-definida, faz a
transferencia do contador de programa (PC := Posição_Inicial_do_Boot).
Resumindo: O hardware executa o Startup, que já está em posição pré-definida da memória ROM. O
Startup busca o Boot que deve estar em uma posição pré-definida do disco e transfere o controle para ele.
O Boot busca e transfere o controle para o SO. O SO busca e transfere o controle para a Aplicação,
conforme mostra a figura abaixo.
Memória
Aplicação
S.O.
Boot
BIOS / Start Up
Unidade de Controle
Mas então, como o SO consegue controlar a execução de outras aplicações após ele ter passado o
controle para uma delas?
A questão é que mesmo após o SO ter passado o controle para uma aplicação, após um tempo não
muito longo o controle volta novamente para o SO, que terá então a chance de escalonar uma outra
aplicação para a qual transferirá o controle. Esta garantia de retorno do controle do hardware mesmo após
o SO ter entregado o controle para uma aplicação é feita pelo mecanismo de interrupção, que pode ser
feito via hardware quanto via software.
Por exemplo, a cada fração de segundo o relógio do hardware interrompe fisicamente qualquer
computação e transfere o controle para uma rotina do SO que trata do relógio. Esta rotina na verdade é o
proprio escalonador do SO. Sempre o escalonador do SO tem a sua execução ativada automaticamente
pelo hardware o SO pode então escalonar outra aplicação a ser executada. Uma outra forma de retornar o
controle para o SO é através das rotinas de E/S. Neste caso, sempre que uma aplicação solicita uma
136
operação de entrada e saída ocorre uma interrupção de software e uma rotina do SO que trata daquela
operação é chamada para execução. Assim, novamente o SO tem o controle da situação.
Bem, não podemos esquecer que a cada troca de aplicação que está sendo executada, o contexto
(conteúdo de registradores, ponteiros, tabelas etc…) desta aplicação deve ser salvo na memória para ser
posteriormente descarregado na máquina.
137