Baixe o app para aproveitar ainda mais
Prévia do material em texto
PDF gerado usando o pacote de ferramentas em código aberto mwlib. Veja http://code.pediapress.com/ para mais informações. PDF generated at: Fri, 06 Dec 2013 10:33:27 UTC Introdução à Arquitetura de Computadores Conteúdo Páginas Capa 1 Introdução 2 O que é o MIPS? 4 Instruções do MIPS 5 Representação das Instruções 9 As Pseudo-Instruções 10 Suporte à Funções 11 Representação Numérica 14 As Operações da Unidade Lógica Aritmética 17 Referências Fontes e Editores da Página 20 Fontes, Licenças e Editores da Imagem 21 Licenças das páginas Licença 22 Capa 1 Capa Introdução à Arquitetura de Computadores Clique aqui para ir ao índice Ir para o índice A imagem de capa deste wikilivro é de uma réplica do Cray-1 - o primeiro supercomputador da história. A réplica está em um museu alemão. O Cray-1 foi construído em 1971 e era mais veloz qque qualquer outro computador da época. De fato, demorou décadas para que surgissem computadores capazes de superar o Cray-1 em velocidade. Ele era capaz de efetuar 160 milhões operações aritméticas por segundo e custava U$700.000,00. O desenvolvimento de uma máquina tão rápida só foi possível graças à pesquisas em Arquitetura de Computadores. A Arquitetura de Computadores é ciência de selecionar e interconectar componentes de hardware para produzir computadores que atendam a requisitos funcionais, de desempenho e custo. Ela também pesquisa formas de tornar os computadores máquinas mais eficientes, baratas e/ou rápidas. Clique aqui para ir ao índice Ir para o índice Introdução 2 Introdução O Que é Arquitetura de Computadores A Arquitetura de Computadores é o projeto conceitual e fundamental da estrutura operacional de um sistema computacional. Ela é o estudo dos requisitos necessários para que um computador funcione e de como organizar os diversos componentes para obter melhores desempenhos. Como computador entendemos qualquer tipo de dispositivo capaz de receber uma entrada e que retorna uma saída após realizar uma série de operações com base nos valores recebidos e armazenados. Existem vários tipos de computadores. Uma das formas de classificá-los é por meio das seguintes categorias: • Desktop: Computadores de baixo-custo e com desempenho razoável para um usuário "comum". • Servidor: Máquinas projetadas para ter um desempenho considerado bom para uma aplicação muito grande e complexa ou então para um número muito grande de operações mais simples. Alguns servidores são simples computadores de Desktop melhorados. Entretanto, existem também aqueles que possuem arquiteturas muito mais sofisticadas que contam com dezenas ou até mesmo centenas de processadores. • Sistemas Embarcados: Possuem um uso dedicado à uma única tarefa e normalmente vem embutidos em outros aparelhos como celulares, microondas, elevadores ou veículos. Possuem uma Entrada/Saída muito simples. Os princípios estudados em Arquitetura de Computadores são fundamentais para se projetar máquinas realmente eficientes. Computadores e as Várias Camadas de Abstração Computadores são aparelhos extremamente complexos. Para compreender o seu funcionamento, precisamos entender várias camadas de abstração diferente. A camada mais baixa de todas é aquela formada por transistores, tensão e corrente elétrica. Quem costuma lidar com esta camada são físicos e engenheiros elétricos. Nesta camada estuda-se o funcionamento de transistores e circuitos sempre levando em conta as propriedades físicas da corrente elétrica. Abaixo vemos um desenho representando um transistor. Uma camada acima, estão as portas lógicas - todas elas compostas por transistores. Neste nível estuda-se como criar estruturas mais complexas combinando-se as diversas portas como AND, OR e NOT para criar estruturas como multiplexadores, flip-flops e somadores. Neste estágio poode-se usar lingüagens como o Verilog ou VHDL para programar circuitos. Abaixo vemos desenhos que representam várias portas lógicas: Introdução 3 Subindo mais um nível de abstração, começamos a lidar com estruturas mais complexas como registradores e unidades lógicas aritméticas - todas compostas por muitos flip-flops, somadores e multiplexadores. Vemos como todas essas estruturas realmente geram as instruções de cada máquina e como cada instrução funciona. É neste nível que costuma trabalhar um Arquiteto. Este será o nível que será abordado ao longo deste Wiki-livro. Abaixo mostramos a imagem de uma Unidade Lógica Aritmética - estrutura usada por computadores para realizar cálculos: Um nível além, estuda-se como combinar as instruções da camada anterior para realizar comandos mais sofisticados como as operações da lingüagem C e como coordenar o funcionamento de um sistema operacional por meio de interrupções e outros recursos. A imagem abaixo é um diagrama que representa o Kernel de um Sistema Operacional sendo usado como um meio de comunicação entre o Software e o Hardware: Introdução 4 Acima desta camada, está o estudo do funcionamento de funções de bibliotecas, APIs e a programação de aplicativos e programas de computador simples.E finalmente, na camada de abstração mais superior está o funcionamento de um programa de computador do ponto de vista do usuário. Como utilizar um aplicativo já criado. O que é o MIPS? O Processador MIPS O MIPS é o nome de uma arquitetura de processadores baseados no uso de registradores. As suas instruções tem à disposição um conjunto de 32 registradores para realizar as operações. Entretanto, alguns destes registradores não podem ser usados por programadores, pois são usados pela própria máquina para armazenar informações úteis. O que é o MIPS? 5 Processadores MIPS são do tipo RISC (Reduced Instruction Set Computer - ou seja, Computadores com Conjunto de Instruções Reduzidas). Isso significa que existe um conjunto bastante pequeno de instruções que o processador sabe fazer. Combinando este pequeno número, podemos criar todas as demais operações. Pela sua elegância e simplicidade, processadores MIPS são bastante usados em cursos de arquiteturas de muitas universidades. Ele é considerado um processador bastante didático. Projetos MIPS são atualmente bastante usados em muitos sistemas embarcados como dispositivos Windows CE, roteadores Cisco e video-games como o Nintendo 64, Playstation, Playstation 2 e Playstation Portable. A Filosofia do Projeto do MIPS Quando o MIPS estava sendo desenvolvido, quatro regras foram definidas para guiar o projeto. Elas são a filosofia do MIPS: •• A simplicidade favorece a regularidade. • O menor é (quase sempre) mais rápido. Levando em conta que uma corrente elétrica se propaga cerca de um palmo por nanosegundo, circuitos simples são menores e portanto, mais rápidos. •• Um bom projeto demanda compromissos. • O caso comum DEVE ser mais rápido. É muito melhor tornar uma instrução que é usada 90% do tempo 10% mais rápida do que fazer com que uma instrução usada em 10% das vezes torne-se 90% mais rápida. Esta regra é baseada na "Lei de Amdahl". Foram estas as regras usadas para decidir vários aspectos do funcionamento do MIPS - incluindo quais seriam suas instruções e como elas funcionariam. Instruções do MIPS Vamos começar agora a estudar as instruções que existem em um computador MIPS. Para praticar e testar as instruções caso você não tenha uma máquina MIPS é usar um simulador como o SPIM. O download do simulador SPIM para Linux, Mac OS, Unix e Windows pode ser feito no link: [1]. O objetivo deste capítulo não é fornecer um guia de referência exaustivo sobre todas as instruções dos MIPS. Iremos apenas apresentar um sub-conjunto mínimo de todas as instruções do MIPS que são o mínimo necessário para termos um computador funcionando. Instruções Aritméticas Simples add $r1, $r2, $r3 # Esta instrução soma o conteúdo dos registradores # $r2 e $r3 colocando o conteúdo no registrador $r1 addi$r4, $r1, 9 # Agora estamos somando o conteúdo do registrador # $r1 com o valor imediato 9 e armazenando o # resultado em $r4. O número imediato deve ter # 16 bits. addu $r5, $r6, $r4 # Quase igual ao add. Mas agora assumimos que # todos os valores são não-negativos. addiu $r7, $r8, 10 # Somamos o conteúdo de $r8 com o valor imediato # 10 e armazenamos o resultado em $r7. Assume-se Instruções do MIPS 6 # que todos os valores são não-negativos. sub $r1, $r2, $r3 # Subtrai-se o conteúdo de $r3 do conteúdo de $r2 # e coloca-se em $r1. Também existe subi, subu e # subiu que tem comportamento semelhante a addi, # addu e addiu, mas para a subtração. Instruções de Operadores Lógicos and $r1, $r2, $r3 # Realiza uma operação AND bit-a-bit entre $r3 e $r2. # O resultado é armazenado em $r1. andi $r1, $r2, 42 # Realiza uma operação AND bit-a-bit entre $r2 e o valor # imediato 42. O resultado é armazenado em $r1. O número # imediato deve caber em 16 bits. or $r1, $r2, $r3 # Realiza uma operação OR bit-a-bit entre $r3 e $r2. # O resultado é armazenado em $r1. ori $r1, $r2, 42 # Realiza uma operação OR bit-a-bit entre $r2 e o valor # imediato 42. O resultado é armazenado em $r1. O número # imediato deve caber em 16 bits. Podemos notar que as instruções seguem uma lógica clara. Até agora todas seguem uma mesma lógica. Além destas, não existem instruções mais complexas como potências ou raízes quadradas.Somente as operações matemáticas mais simples são representadas. Podemos ver aquelas 4 regras que formam a filosofia do projeto do MIPS em ação. Instruções de Uso de memória As instruções do uso da memória seguem uma lógica diferente das instruções de operações aritméticas. Pra começar, existem três tipos de instruções capazes de copiar dados da memória para os registradores. lw $r1, 4($r2) # Load Word: Esta instrução carrega uma palavra (estrutura de 4 bytes) # localizada no endereço representado pela soma do valor # armazenado no registrador $r2 mais 4. O resultado é armazenado em $r1. lh $r1, 6($r3) # Load Half: Esta instrução carrega uma estrutura de 2 bits localizada # no endereço representado pela soma do valor armazeado no # registrador $r3 mais o número 6. O resultado é armazenado em $r1. lb $r1, 16($r2)# Load Byte: Esta instrução carrega um byte (8 bits) localizado no # endereço representado pela soma do valor armazenado em $r2 mais o # número 16. O resultado é armazenado em $r1. Perceba que desta forma é rápido de carregar o conteúdo de um valor em um vetor. Basta saber o endereço do valor inicial do vetor e o índice de sua posição. Por exemplo, suponha que eu possua um vetor de caracteres char vetor[5]. Supondo que o registrador $r1 contenha o endereço de vetor[0], para armazenar o valor de vetor[3] no registrador $r2 e assumindo que cada caractere possui um byte, basta usar a seguinte instrução: lb $r2 24($r1) Instruções do MIPS 7 Colocamos o 24 lá porque cada caractere ocupa 8 bits e queremos pegar o quarto caractere da seqüência. Logo, precisamos nos deslocar 24 bits para acharmos o caractere certo. Afinal, a distância entre o primeiro elemento do vetor armazenado em $r1 e o quarto item do vetor é de 24: [0][ ][ ][ ][ ][ ][ ][ ].[1][ ][ ][ ][ ][ ][ ][ ].[2][ ][ ][ ][ ][ ][ ][ ].[3][ ][ ][ ][ ][ ][ ][ ] Para armazenarmos conteúdo de um registrador na memória também existem 3 comandos: sw $r1, 4($r2) # Store Word: Esta instrução carrega uma palavra (estrutura de 4 bytes) # localizada no registrador $r1 e armazena no endereço representado # pela soma do valor armazenado no registrador $r2 mais 4. sh $r1, 4($r2) # Store Half: Esta instrução carrega uma estrutura de 2 bits # localizada no registrador $r1 e armazena no endereço representado # pela soma do valor armazenado no registrador $r2 mais 4. sb $r1, 4($r2) # Store Byte: Esta instrução carrega um byte (8 bits) # localizado no registrador $r1 e armazena no endereço representado # pela soma do valor armazenado no registrador $r2 mais 4. Vamos ver um exemplo de conversão de um código escrito em C para a lingüagem Assembly do MIPS assumindo que um int da lingüagem C tenha 4 bits: /* CÓDIGO C */ typedef struct A{ int x; int y; int z; int w; }aType; aType V[16]; (...) aPtr = &(V[3]); m = aPtr -> y; n = aPtr -> w; aPtr -> x = m + n; Vamos assumir que o vetor V esteja no endereço 0x0800.0000. A estrutura aType é formada por 4 valores numéricos consecutivos, cada um com 4 bits. Logo, cada aType ocupa 16 bits (4*4). Ou, em hexadecimal, 0x010 bits. Veja uma representação de um aType como é armazenado na memória: [x][x][x][x][y][y][y][y][z][z][z][z][w][w][w][w] Temos um vetor com 16 estruturas como esta chamado V. Logo, o vetor ocupa um espaço de 256 bits. Com estas informações, concluímos que o código em Assembly necessário para fazer a operação mostrada no código em C mostrado acima é: Instruções do MIPS 8 la $r1, 0x0800.0030 # A instrução Load Address carrega em $r1 o endereço de V[3]. Falaremos sobre o la depois. lw $r2, 4($r1) # Carregamos para $r2 o quarto bit à partir de V[3]. Ou seja, o valor de y. lw $r3, 12($r1) # Agora carregamos para $r3 o valor do inteiro w. add $r4, $r2, $r3 # Somamos os valores de y e w colocando o valor em $r4. sw $r4, 0($r1) # Colocamos o valor da soma no lugar do x. Instruções de Controle de Fluxo Agora veremos instruções capazes de controlar o fluxo de execução de instruções de um computador. A primeira delas é a instrução beq, ou Branch if Equal: beq $r1, $r2, DESTINO O que esta instrução faz é verificar se o valor de $r1 é igual à $r2. Caso isso seja verdadeiro, ela muda o valor do registrador PC (Program Counter) que guarda o endereço da próxima instrução a ser executada. Se o valor passado como destino for 0, nada acontece. Nenhuma instrução é pulada, o programa executa a próxima instrução normalmente, independente de $r1 == $r2 ser verdadeiro ou falso. Se o valor passado for "1", pula-se uma instrução se $r1 for igual a $r2. Se o valor passado for "2", pulam-se duas instruções e assim por diante. Também pode-se passar valores negativos. Um valor de "-1" faz com que o programa entre em um loop infinito, nunca mais passando para a próxima instrução. Uma "-2" faz com que voltemos uma instrução anterior, e assim por diante. Entretanto, o valor de DESTINO só pode ser no máximo um valor com 16 bits. Com um valor destes, só podemos pular no máximo cerca de 32 000 instruções para frente ou para trás. Isso não é um problema na maioria das vezes é muito raro existir um programa que exija que você pule uma quantidade tão grande de instruções. Assumindo que 4 linhas de código e Assembly correspondem a 1 linha em código C, para que pulemos uma quantidade tão grande de instruções, teríamos que fazer um if com um bloco com mais de 8.000 linhas de código. Isso é algo extremamente difícil de ocorrer em um programa real. Mas caso você realmente precise pular mais de 32 000 instruções, isso é perfeitamente possível, desde que você altere antes o valor do registrador PC. Com isso, perde-se um pouco de desempenho, mas torna desvios de código extremamente grandes possível. E o importante é que o caso comum (programas em C com blocos menores de 8.000 linhas) roda mais rápido (regra 4 da Filosofia de Projeto do MIPS). Além do beq, temos também o bne, ou Branchif Not Equal: bne $r1, $r2, DESTINO Ele funciona da mesma forma que o beq. A diferença é que ele pula um determinado número de instruções somente se o valor dos dois registradores for diferente. E finalmente, temos a instrução j, ou Jump: j ENDEREÇO Ele faz com que o programa passe a executar a instrução que é encontrada no endereço dado. O endereço passado para a instrução j é sempre um número de 26 bits. Entretanto, os endereços da máquina sempre são números de 32 bits. Então como será possível saber qual o endereço em que está a instrução desejada? É muito simples. Instruções sempre estarão em endereços múltiplos de 4. Então sabemos que os dois últimos bits do endereço são sempre 0. Não precisamos armazená-los. Já os dois primeiros bits, retiramos do PC, pois é sempre muito mais provável que nós usemos o controle de fluxo para saltar para uma posição não muito distante. Caso precisemos saltar para um posição muito distante, basta alterarmos o valor do PC primeiro. Da mesma forma que foi visto no beq, o caso comum executa mais rápido desta forma. Isso é muito bom, mesmo que tenhamos mais trabalho e perda de desempenho nas pouquíssimas vezes nas quais precisamos dar saltos muito longos. Instruções do MIPS 9 Instruções de Comparações Por fim, precisamos ver ainda instruções de comparação. Um exemplo é o slt, ou Set Less Than: slt $r1, $r2, $r3 Ela armazena 1 em $r1 se $r2 < $r3 e 0 caso contrário. Resumo dos Modos de Endereçamento Nas instruções do MIPS podemos representar os endereços de dados das seguintes formas: • A Registrador: Representamos o dado passando o nome do registrador no qual ele está contido. Ex: add $r1, $r2, $r2. • Base-Deslocamento: Representamos o dado passando o endereço de um vetor no qual ele está e a quantidade de bits a serem deslocados. Ex: lw $r5, 4($r65). • Imediato: Passamos o dado escrevendo o seu valor imediato. Ex: addi $r1, $r2, 456. • Relativo ao PC: Passamos o dado descrevendo o seu valor relativo ao endereço da instrução atual. Ex: beq $r1, $r2, DESTINO. • Absoluto: passamos o valor informando o seu endereço (pseudo-)absoluto. Ex: j DESTINO. Existem apenas estas 5 formas de endereçamento no MIPS. Através delas, efetuamos todas as operações necessárias. Referências [1] http:/ / pages. cs. wisc. edu/ ~larus/ spim. html Representação das Instruções Sabemos que tudo dentro do computador é representado por meio de bits - que podem ter o valor de 0s e 1s. Até mesmo as instruções de um processador MIPS. Vamos estudar agora como uma instrução é representada a nível de bits. O MIPS reconhece 3 famílias diferentes de instruções. São elas: As Instruções Tipo R Como exemplo de instruções do tipo R vistas, temos: add, addu, sub, subu, or e and. Elas são codificadas da seguinte forma: [o][o][o][o][o][o] - [u][u][u][u][u] - [t][t][t][t][t] - [d][d][d][d][d] - [s][s][s][s][s] - [f][f][f][f][f][f] Onde a letra "o", representa o opcode, ou Código de Operação. Ele avisa mais-ou-menos o que a instrução vai fazer. Por exemplo, uma instrução de soma add é muito semelhante à instrução xor que faz um XOR lógico bit-a-bit. A única diferença é que na soma, devemos armazenar um bit a mais para representar o "vai um" quando somamos 1+1. Logo, as duas instruções tem o mesmo opcode. As letras "u" e "t" representam os operandos das instruções. Ou melhor, os registradores nos quais estão os operandos. A letra "d" é o registrador de destino onde deve ser armazenado o resultado. A letra "s" ou Shampt representa o deslocamento de bits, ou seja um shift que pode se usado após a operação. Finalmente, a letra "f" é o código de função. É por meio dela que diferenciamos instruções semelhantes que tem o mesmo opcode, como add e or. Representação das Instruções 10 As Instruções Tipo I Como exemplo deste tipo de instrução, podemos citar todas aquelas que contam com um valor imediato, como addi, subi, ori, beq e bnq. Eles são codificados da seguinte forma: [o][o][o][o][o][o] - [u][u][u][u][u] - [t][t][t][t][t] - [i][i][i][i][i][i][i][i][i][i][i][i][i][i][i][i] A letra "o" representa o código da instrução. A letra "u" represeenta o número do registrador onde a o resultado da operação é colocado. Já a letra "t" representa o número do registrador em que está um dos operandos. Já o "i" representa o número imediato. Agora vemos o porquê do valor passado como imediato nunca poder exceder os 16 bits. As Instruções Tipo J A única instrução vista no capítulo anterior do tipo J é a j. Ela é codificada da seguinte forma: [o][o][o][o][o][o] - [d][d][d][d][d][d][d][d][d][d][d][d][d][d][d][d][d][d][d][d][d][d][d][d][d][d] O "o" representa o código da operação j e d representa o destino. As Pseudo-Instruções A linguagem Assembly de uma máquina costuma ser um reflexo direto de como são implementadas as instruções de um determinado processador. Entretanto, nem todas as instruções que temos à disposição quando programamos em Assembly são instruções verdadeiras para o processador. Algumas delas são na verdade pseudo-instruções. Pseudo-instruções costumam ser substituídas pelo montador ao gerar instruções para o computador na forma de Lingüagem de Máquina. Pseudo-Instruções são na verdade combinações de mais de uma instrução. Vejamos agora alguns exemplos: A Pseudo-Instrução move move $r1, $r2 # Copia o conteúdo do registrador $r2 para $r1 Ela é na verdade implementada da seguinte forma: addu $r1, $r0, $r2 # Soma $r2 com zero e coloca o resultado em $r1 O registrador $r0 usado acima não é um registrador comum. Ele sempre possui o valor "0" e é um dos poucos registradores cujo valor nunca pode ser alterado pelo programador. As Pseudo-Instruções 11 A Pseudo-Instrução Load Address la $r1, ENDEREÇO # Coloca o valor numérico de 32 bits "ENDEREÇO" em $r1 Esta instrução é muito útil para fazeer registradores receberem o valor de ponteiros para outros locais de memória. De fato, usamos esta pseudo-instrução no Capítulo anterior quando convertemos um código em C para o Assembly do MIPS. Ela é na verdade implementada desta forma: lui $r1, constHI # Carrega-se os 16 bits mais significativos em $r1 ori $r1, $r0, constLO # Executa-se um OR bit-a-bit entre o registrador # com os 16 bits mais significativos e os 16 bits # menos significativos O que a instrução lui, ou Load Upper Immediate faz é uma operação shift de 16 bits para a esquerda e coloca no registrador indicado. Este passo é necessário porque valores imediatos passados para instruções só podem ter 16 bits por causa da limitação de espaço das instruções do Tipo I. Também existe a instrução li, ou Load Immediate que faz exatamente a mesma coisa que o la. Note que o montador que remove as pseudo-instruções e as substitui por instruções que realmente existem no hardware sempre podem verificar se o valor que queremos carregar no registrador cabe em 16 bits. Neste caso, a instrução gerada fica bem mais simples e rápida: addu $r1, $r0, $r2 # Soma $r2 com 0 e coloca em $r1 Suporte à Funções Funções são ferramentas muito importantes, pois tornam código escrito muito mais usável e torna a tarefa de programar mais produtiva. Elas permitem que um programador se concentre em apeenas uma tarefa de cada vez. Portanto, é essencial que um processador possua suporte á elas. Veremos como isso ocorre no MIPS. As Etapas Necessárias para Invocar Funções Quando queremos chamar uma função, as seguintes etapas devem ser cumpridas: •• O programa principal deve colocar os parâmetros da função em um local que ela possa acessar. •• O programa principal deve ceeder o controle para a função. •• A função deve coletar todos oos parâmetros deixados pelo programa principal. •• A função deve executar a tarefa desejada. •• A função deve aramazenar seus resultados em um lugar em que o programa principal possa acessar. •• A função deve retornar o fluxo do código parao ponto imediatamente após ser chamada. Para que estes requisitos sejam cumpridos, as seguintes convenções fora criadas: • Os registradores $r4, $r5, $r6 e $r7 seriam usados para armazenar parâmetros de funções. Por causa desta funcionalidade, tais registradores podem ser chamados pelos seus "apelidos": $a0, $a1, $a2 e $a3. • Os registradores $r2 e $r3 seriam usados para as funções armazenarem seus valores de retorno para o programa principal. Por isso, eles costumam ser chamados de $v0 e $v1. Tais regras são apenas convenções. Nada impede que um programador as desrespeite e use outros registradores para estabelecer comunicação entre funções e programas principais. Entretanto, para que um programa funcione bem em conjunto com todos os demais, é importante quee tais convenções sejam seguidas. Suporte à Funções 12 Além dos registradores convencionados acima, o registrador $r31 também tem um papel importante. Ele sempre armazena o endereço de retorno para o qual a última função chamada deve retornar. Por ter uma função tão importante, este registrador é mais conhecido pelo apelido $ra. Instruções de Uso de Funções: jal e jr A instrução que usamos para invocar uma função chama-se jal, ou Jump and Link. Ela é usada da seguinte forma: jal ENDEREÇO_DA_FUNÇÃO Entretanto, só devemos chamar esta função depois de já termos salvo os argumentos nos registradores apropriados. Depois da função executar todas as operações e salvar os resultados apropriados em $v0 e $v1, podemos usar a instrução jr, ou Jump Register. Normalmente usamos a instrução passando para ela o valor do registrador $ra, que contém o endereço certo para voltarmos: jr $ra Como exemplo de como isso pode ser feito, vamos converter para Assembly o seguinte código em C: int example(int a, int b, int c, int d){ int f; f = (a + b) - (c + d) return f; } O código no Assembly do MIPS ficaria assim: example: # Label add $t0, $a0, $a1 # Soma a + b add $t1, $a2, $a3 # Soma c + d sub $v0, $t0, $t1 # Subtrai os dois valores e coloca o resultado no registrador de retorno jr $ra # Retorna o controle para a função principal Entretanto, observe que para realizarmos os cálculos necessários, precisamos usar registradores. Ao fazer isso, existe o grande risco de apagarmos dados importantes do programa principal. Para evitar isso, existe uma convenção que diz que os registradores $r8 até $r15, $r24 e $r25 são registradores temporários. Por isso, eles costumam ser chamados de $t0 até $t9. Após chamar uma função, o programa principal não deve esperar que os valores destes registradores sejam os mesmos. Somente dados que estejam nos registradores $r16 até $r23 são sempre preservados entre funções. Por esta razão, tais registradores permanentes (salvos) são chamados de $s0 até $s7. Funções com Muitas Variáveis Existem funções que podem precisar receber mais do que 4 parâmetros. Entretanto, só temos registradores suficientes para armazenar 4 parâmetros. Nós também podemos querer retornar mais do que os 2 valores permitidos pelos registradores. Ou então, nossa função pode precisar armazenar mais do que 10 valores temporários. O que fazer para resolver isso? Como o espaço dentro de registradores é bastante limitado, só nos resta apelar para a pilha da memória. Por convenção, o registrador $r29, ou $sp (Stack Pointer) armazena o endereço na pilha de onde novos valores podem ser colocados. Acessando seu endereço e alterando-o podemos guardar valores na memória. O acesso à pilha da memória é muito mais lento que o acesso à registradores, mas este é o único modo de podermos armazenar uma quantidade muito maior de informação. Suporte à Funções 13 Por motivos históricos, a pilha "cresce" sempre dos valores de endereços altos para valores menores. Ou seja, para alocar espaço para mais valores, precisamos sempre decrementar o seu valor. Incrementando-o, estamos na verdade removendo os últimos dados da pilha. Vamos reescrever agora o código Assembly visto acima, desta vez preeservando os valores dos registradores $t0 e $t1 para vermos como ficaria o resultado: example: # Label addi $sp, $sp, -8 # Alocamos espaço para dois valores de 4 bits. sw $t1, 4($sp) # Preservamos o valor de $t1 à partir do 4o bit após o Stack Pointer sw $t0, 0($sp) # Preservamos o valor de $t0 sobre o Stack Pointer add $t0, $a0, $a1 # Soma a + b add $t1, $a2, $a3 # Soma c + d sub $v0, $t0, $t1 # Subtrai os dois valores e coloca o resultado no registrador de retorno lw $t0, 0($sp) # Retiramos o valor antigo salvo de $t0 lw $t1, 4($sp) # Retiramos da pilha o valor antigo de $t1 addi $sp, $sp, 8 # Voltamos o Stack Pointer para a posição original apagando de vez as variáveis locais jr $ra # Retorna o controle para a função principal Funções Recursivas e Sub-Funções Da mesma forma que o programa principal invocou uma função, uma função também pode invocar outras funções. Se la for recursiva, ela pode até mesmo invocar clones de si mesma para realizar uma tarefa. Como implementar isso sendo que temos apenas um registrador $ra? Se chamarmos outra função, o $ra original é sobrescrito e podemos perder a capacidade de voltar ao programa principal. Além disso, valores temporários que representam variáveis locais podem ser perdidos. A única forma de evitar isso é enviar para a pilha tudo aquilo que precisa ser salvo - da mesma forma que fizemos com alguns valores no exemplo acima. Para mostrar isso na prática vamos implementar em Assembly a seguinte função em C: /* Calcula Fatorial */ int fat(int n){ if(n < 1) return 1; else return (n * fat(n - 1)); } O resultado final é algo como: fat: # Início da função addi $sp, $sp, -8 # Aloca espaço na pilha para 2 valores sw $ra, 4($sp) # Guarda valor de retorno na pilha sw $a0, 0($sp) # Guarda primeiro argumento na pilha slti $t0, $a0, 1 # $t0 = ( $a0 < 1) beq $t0, $zero, L1 # Se $a0 >= 1, vá para L1 addi $v0, $zero, 1 # Senão, coloque 1 como valor de retorno addi $sp, $sp, 8 # Apague as duas variáveis locais salvas na pilha jr $ra # Encerra a função L1: addi $a0, $a0, -1 # Se $a >= 1, decremente $a0 Suporte à Funções 14 jal fat # Chame um clone recursivo de fat que retorna fat($a0-1) lw $a0, 0($sp) # Recupere os valores iniciais do parâmetro da função lw $ra, 4($sp) # Recupere o endereço de retorno antigo addi $sp, $sp, 8 # Apague as duas variáveis locais salvas na pilha mul $v0, $a0, $v0 # Coloque $a0 * fat($a0 - 1) como valor de retorno jr $ra # Encerra a função Por fim, uma última coisa útil que é interessante comentar é que o Stack Pointer ($sp) pode ser alterado várias vezes ao longo de uma função. Para manter memorizado o valor do endereço da pilha no início da função, costuma-se usar o registrador $r30 como um Frame Pointer ($fp). Nos exemplos acima, isso não foi preciso, pois só mudamos o valor do Stack Pointer no começo e fim de cada função. Mas em alguns programas utilizar este registrador pode ser útil. Representação Numérica Números podem ser representados em qualquer tipo de base. Humanos costumam representar números na base decimal. Esta possui este nome por possuir 10 dígitos diferentes: 0, 1, 2, 3, 4, 5, 6, 7, 8 e 9. Como computadores só são capazes de reconhecer dois tipos diferentes de estados, é natural para eles representar números na base binária. Ou seja, eles só compreendem os valores "0" e "1". Vamos ver agora coo podemos representar diversos tipos de números usando apenas dois tipos diferentes de sinais.Números Naturais Representar Números Naturais é simples bastante simples - mesmo quando só podemos usar dois tipos diferentes de sinais. O 0 decimal é representado como 0. O 1 decimal também é representado como 1. Já o 2 decimal é representado com "10". Enfim, quando contamos os números naturais na base binária, fazemos da seguinte forma: BASE | BINÁRIA | 0 1 10 11 100 101 110 111 1000 1001 1010 | BASE | DECIMAL | 0 1 2 3 4 5 6 7 8 9 10 Quando representamos números desta forma, não temos números negativos. Logo, nenhum número possui sinal algum. Por isso, chamamos tais números de unsigned (sem sinal). Atualmente, a maioria das máquinas possui representação numérica 32 bits. Isso significa que seus números são compostos por 32 dígitos. Com uma representação destas, o menor número binário sem sinal que podemos representar é 00000000000000000000000000000000 e o maior é 1111111111111111111111111111111. Convertemos tais valores para decimal, chegamos à conclusão que a maioria dos computadores só lida com números sem sinal cujo valor esteja entre 0 e 4.294.967.295 - que são os equivalentes decimais destes números. Para descobrir qual é o equivalente decimal de um número binário de 32 bits, usa-se a fórmula abaixo: onde é o dígito mais significativo e é o bit menos significativo. Representação Numérica 15 Números Inteiros Nem sempre os números com os quais queremos lidar são naturais. Pode haver necessidade de realizarmos operações com números negativos. Para tal, devemos encontrar uma forma de representarmos números negativos. Uma das primeiras formas tentadas de fazer isso foi reservando o primeiro bit de um número para representar o sinal. Assim, um 0000000 (...) 001 representa +1 e um 10000 (...) 001 representaria um -1. Entretanto, tal forma de representação já foi abandonada há muito tempo. Um dos principai motivos para o abandono desta representação está no fato de sempre termos que verificar o primeiro bit para descobrir como efetuar uma soma ou subtração entre dois números. Além disso, tal representação tornaria possível um "+0" e um "-0". A forma de se representar números inteiros em computadores modernos é chamada de Representação em Complemento de 2. Ela é feita da seguinte forma: Os 31 bits mais à esquerda representam sempre números positivos. Calculamos o que eles representam com a mesma fórmula vista acima. Entretanto, o primeiro bit representa sempre "0" se o seu valor for "0" ou " se o seu valor for "1". Assim, a fórmula para se calcular o que representa um número binário inteiro em decimal é: onde é o dígito mais significativo e é o bit menos significativo. A representação em complemento de 2 é boa pelos seguintes motivos: •• Para somar dois números inteiros, usa-se o mesmo método. Não precisamos verificar o sinal deles. Com isso, podemos criar um circuito mais rápido. •• Descobrir o inverso de um número também é simples. Basta invertermos todos os bits e, em seguida, somarmos 1. Sempre funciona. •• O "0" é sempre "0". Não existe uma versão negativa ou positiva deste número. Por meio desta notação, números tão pequenos como -4.294.967.296 e tão grandes como 4.294.967.295 podem ser representados. Números Reais Por fim, também é muito útil que computadores possam representar números reais. Vamos estudar agora como eles costumam ser representados em computadores. A forma mais lógica e versátil de representarmos um número real é por meio da notação científica. Um número decimal em notação científica sempre toma a seguinte forma: onde N é sempre um número real maior ou igual à 1 e menor que 10 que pode ser positivo ou negativo enquanto M é um número inteiro qualquer (que pode ser positivo ou negativo). Por exemplo: representa aproximadamente quantos segundos existe em um século enquanto representa quantos segundos existem em um nanossegundo. Alternativamente, podemos representar também números em notação científica utilizando uma base binária: . Lembre-se que aquele "10" significa na verdadee "2" em representação decimal. Ou seja, o número representado é na verdade 0,5 e não 0,1. Enfim, quando temos um número em notação científica binária, ele sempre tem a seguinte forma: , onde "S" representa o sinal (que pode ser positivo ou negativo), "XXXXX" são a parte fracionária do número e "YYYYYY" representa o expoente. Então, para representarmos números reais, precisamos representar no espaço de 32 bits os valores do sinal, fração e expoente. No MIPS, isso é feito da seguinte forma: Representação Numérica 16 S -- E E E E E E E E -- F F F F F F F F F F F F F F F F F F F F F F F O Sinal é representado em um único bit (1 para negativo e 0 para positivo), o expoente é representado por 8 bits e a fração é representada por 23 bits. Assim, o exemplo (ou 1/2 em decimal) é representado: 0 -- 1 1 1 1 1 1 1 1 -- 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 Com esta representação, números quase tão pequenos como ou quase tão grandes como podem ser armazenados. Entretanto, note que a nossa precisão não é perfeita. Existem muitos números que não podem ser representados desta forma. Por isso, sempre é preciso realizar arredondamentos. E quanto mais operações realizamos, mais erros acabam sendo acumulados. Por essa razão, a maioria dos computadores possui também uma forma mais precisa de representação de números reais. Essa forma normalmente ocupa o dobro do espaço do que costuma ocupar. No MIPS uma representação desti tipo ocupa 64 bits. Ela funciona da seguinte forma: S -- E E E E E E E E E E E -- F F F F F F F F F F F F F F F F F F F F F F F F F F F F F F F F F F F F F F F F F F F F F F F F F F F F Apesar de gastarmos o dobro do espaço de armazenamento desta forma, aumentamos consideravelmente a faixa de valores representados e a precisão com a qual podemos representá-los. Overflow e Underflow Overflow é o nome do resultado incorreto ao qual chegamos quando tentamos somar números grandes demais e o resultado não pode ser armazenado e nem representado em 32 bits. Ele pode ocorrer quando efetuamos as seguintes operações: •• Somamos dois números positivos •• Somamos dois números negativos •• Subtraímos um número negativo de um positivo. É de responsabilidade do hardware sempre verificar se ocorreu um overflow nestes casos. Normalmente, para fazer a verificação basta conferir os sinais dos números. Em caso afirmativo, ele deve ativar um sinal de exceção. O que é feito caso uma excessão seja detectada depende de cada programa. O MIPS, por exemplo, sempre armazena o endeereço da última instrução que gerou excessão em um registrador especial chamado EPC. A instrução mfc0 (Move from System Control) é usada para copiar este endereço para um registrador de propósito geral que pode ser conferido por algum programa. Cabe ao programador deste programa decidir o que fazer. Em lingüagens de alto nível, o comportamento depende da lingüagem usada. C, por exemplo, ignora todos os Overflows. Ao contrário de Fortran ou Ada que requerem que o programa sempre seja avisado. Underflow é o que acontecee quando lidamos com um número real tão próximo de 0 que o valor do expoente não pode ser corretamente representado. Quando um programa requer uma precisão tão grande assim, recomenda-se o uso de pontos flutuantes de dupla precisão (double) para evitar este inconveniente. As Operações da Unidade Lógica Aritmética 17 As Operações da Unidade Lógica Aritmética Computadores são máquinas de calcular. Logo, é fundamental que eles sejam capazes de realizar operações básicas como adição, subtração, divisão e multiplicação. Vamos ver agora como tais operações são implementadas dentro da Unidade Lógica Aritmética de um Processador (ULA). A Adição e Subtração A Adição é uma das operações mais simples. Um computador realiza ela de uma maneira semelhante à usada por nós humanos. Ele começa somando os bits menos significativos. Caso tenha que somar "1"e "1", o resultado fica sendo "0" e passamos um "vai-um" para o bit à esquerda. Veja abaixo um desenho de um circuito capaz de somar dois números de 8 bits: Embora a imagem mostre apenas um circuito que soma dois números de 8 bits, não é muito difícil perceber que a lógica para fazer um somador é a mesma, tanto para números com 8 como para 32 bits. Basta adicionar mais somadores. Perceba que o circuito também é capaz de detectar a presença de Overflow no caso de operações com números sem sinal. Ele pode ser usado tanto para somar números com ou sem sinal. Graças à representação de números inteiros por complemento de dois, não é difícil conseguir isso. O mesmo circuito acima também pode ser reaproveitado para realizar subtrações. Basta inverter antes todos os bits do segundo operando e somar 1 à ele. Com isso, estamos na verdade somando o primeiro operando com o negativo do segundo operando. As Operações da Unidade Lógica Aritmética 18 A Multiplicação Vamos agora estudar como construir um circuito capaz de realizar multiplicações. Primeiro vamos tentar realizar uma multiplicação pelo método convencional que usamos através de papel e caneta. Isso pode nos dar uma pista de como fazer uma operação de multiplicação. Vamos usar a base binária: 1000 x1001 ---- 1000 00000 000000 1000000 ------- 1001000 Perceba que a multiplicação é um conjunto de somas. Sempre estamos somando o valor 0 ou o valor do multiplicador após este passar por uma operação de SHIFT para a esquerda. O que determina se o que iremos somar é um 0 ou o valor do multiplicador após um SHIFT são os bits do multiplicando. Se o bit da posição n for um 0, somamos 0 e se for 1, somamos com o valor do multiplicando "shiftado" n vezes. Um exemplo de um circuito seqüencial capaz de somar dois números de 4 bits é mostrado na imagem abaixo: Note que multiplicando números de 4 bits, nós precisamos armazenar os 8 bits possíveis para o resultado. Afinal, o resultado de uma multiplicação é capaz de ter o número de bits igual à soma dos multiplicandos. Máquinas MIPS lidam com o problema dividindo o resultado pela metade. os bits mais significativos terminam em um registrador chamado Hi e os menos significativos acabam em um registrador chamado Lo. Entretanto, o circuito conforme mostrado acima tem muitos problemas. O principal é que ele é muito complexo. Perceba que o circuito para multiplicar número de 4 bits é pelo menos 3 vezes mais complexo e lento que o usado para realizar a adição de 8 bits (afinal, o circuito de multiplicação precisa realizar a adição de 3 pares de valores de 8 bits). Um circuito destes para calcular a multiplicação de números com um valor ainda maior de bits seria ainda mais complexo que isso. Com isso, chegamos à conclusão que a multiplicação é uma operação complexa demais para ser efetuada por meio de uma simples lógica combinacional de portas lógicas. Um circuito de multiplicação combinacional seria muito caro e esquentaria demais. Por isso, sua velocidade teria que ser sacrificada aumentando a distância dos transistores. Por isso, utilizam-se circuitos seqüenciais para realizar esta operação. As Operações da Unidade Lógica Aritmética 19 O diagrama abaixo mostra descreve o funcionamento de um circuito seqüencial capaz de realizar a multiplicação: O que acontece no diagrama acima no caso de multiplicarmos dois números de 32 bits é o seguinte: • 1- O "Produto" é um registrador inicializado em 0. • 2- O produto e o multiplicando são passados para a ULA que soma os dois. • 3- O resultado (o valor do próprio multiplicando) pode ir para o registrador "Produto" ou não. Quem toma a decisão é o "Controle". • 4- Se o último bit do Multiplicador for "1", o Controle permite a escrita do registrador "Produto". Se for "0", ele impede. • 5- O Multiplicando sofre um shift para a esquerda e o Multiplicador um Shift para a direita. • 6- Se é a 32a vez que você chega à esta instrução, encerre. A multiplicação terminou. Caso contrário, volte ao passo 2. O diagrama acima já é uma forma bem melhor de multiplicarmos. Mas ainda existem otimizaçãoes que podem ser feitas para acelerarmos ainda mias a multiplicação: •• Podemos utilizar um único registrador de 64 bits para armazenar tanto o multiplicador como o produto. O multiplicador vem antes e o produto logo depois. Assim, cada vez que fazemos um shift para a direita no multiplicador, aproveitamos o espaço liberado mais à esquerda para colocar um novo bit do produto. Desta forma, não é necessário somar números de 32 bits, apenas somar o suficiente para descobrir qual o próximo bit do produto a ser colocado. •• Compiladores substituem multiplicação por potências de 2 por operações de shift que são mais rápidas. Para multiplicações envolvendo números negativos, basta invetermos os números transformando-os em positivos. Em seeguida, fazemos a multiplicação normalmente e observamos os sinais do multiplicador e multiplicando. Se forem iguais, masntemos os números como positivos. Caso contrário, convertemos o produto para negativo. Fontes e Editores da Página 20 Fontes e Editores da Página Capa Fonte: http://pt.wikibooks.org/w/index.php?oldid=204857 Contribuidores: Master, Raylton P. Sousa, 1 edições anónimas Introdução Fonte: http://pt.wikibooks.org/w/index.php?oldid=239231 Contribuidores: Master, Raylton P. Sousa, Thiagoharry, 5 edições anónimas O que é o MIPS? Fonte: http://pt.wikibooks.org/w/index.php?oldid=199789 Contribuidores: GrooveDog, Master, Raylton P. Sousa, Thiagoharry, 9 edições anónimas Instruções do MIPS Fonte: http://pt.wikibooks.org/w/index.php?oldid=250818 Contribuidores: Kernelsafe, Master, Raylton P. Sousa, Thiagoharry, 5 edições anónimas Representação das Instruções Fonte: http://pt.wikibooks.org/w/index.php?oldid=199791 Contribuidores: Master, Raylton P. Sousa, Thiagoharry As Pseudo-Instruções Fonte: http://pt.wikibooks.org/w/index.php?oldid=199786 Contribuidores: Master, Mike.lifeguard, Raylton P. Sousa, Thiagoharry, 1 edições anónimas Suporte à Funções Fonte: http://pt.wikibooks.org/w/index.php?oldid=239421 Contribuidores: Master, Raylton P. Sousa, Thiagoharry, 3 edições anónimas Representação Numérica Fonte: http://pt.wikibooks.org/w/index.php?oldid=199790 Contribuidores: Master, Raylton P. Sousa, Thiagoharry, 3 edições anónimas As Operações da Unidade Lógica Aritmética Fonte: http://pt.wikibooks.org/w/index.php?oldid=199785 Contribuidores: Master, Raylton P. Sousa, Thiagoharry Fontes, Licenças e Editores da Imagem 21 Fontes, Licenças e Editores da Imagem Imagem:Cray-1-deutsches-museum.jpg Fonte: http://pt.wikibooks.org/w/index.php?title=Ficheiro:Cray-1-deutsches-museum.jpg Licença: Creative Commons Attribution 2.5 Contribuidores: Clemens PFEIFFER Imagem:Transistor_npn.png Fonte: http://pt.wikibooks.org/w/index.php?title=Ficheiro:Transistor_npn.png Licença: GNU Free Documentation License Contribuidores: Original uploader was Pulsar at fr.wikipedia Imagem:Logic-gate-index.png Fonte: http://pt.wikibooks.org/w/index.php?title=Ficheiro:Logic-gate-index.png Licença: GNU Free Documentation License Contribuidores: EugeneZelenko, Ilmari Karonen, Jjbeard, Qvarie, Stefan506, The wub, Tothwolf, WikipediaMaster, 2 edições anónimas Imagem:ALU_Block_Diagram.png Fonte: http://pt.wikibooks.org/w/index.php?title=Ficheiro:ALU_Block_Diagram.png Licença: Creative Commons Attribution-Share Alike Contribuidores: Tapani Taivalantti Imagem: Kernel_Layout.svg Fonte: http://pt.wikibooks.org/w/index.php?title=Ficheiro:Kernel_Layout.svg Licença: Creative Commons Attribution-Sharealike 3.0 Contribuidores: Bobbo Imagem:Toshiba_TC86R4400MC-200_9636YJA_top.jpg Fonte: http://pt.wikibooks.org/w/index.php?title=Ficheiro:Toshiba_TC86R4400MC-200_9636YJA_top.jpg Licença: GNU Free Documentation License Contribuidores: EugeneZelenko, Morkork, Qurren, Sdrtirs, 3 edições anónimas Imagem:Circuito_soma.pngFonte: http://pt.wikibooks.org/w/index.php?title=Ficheiro:Circuito_soma.png Licença: GNU Free Documentation License Contribuidores: Thiagoharry Imagem:Circuito_multiplicacao.png Fonte: http://pt.wikibooks.org/w/index.php?title=Ficheiro:Circuito_multiplicacao.png Licença: GNU Free Documentation License Contribuidores: Thiagoharry Imagem:Diagrama_circuito_multiplicacao.png Fonte: http://pt.wikibooks.org/w/index.php?title=Ficheiro:Diagrama_circuito_multiplicacao.png Licença: Creative Commons Attribution-Sharealike 3.0 Contribuidores: Thiagoharry Licença 22 Licença Creative Commons Attribution-Share Alike 3.0 //creativecommons.org/licenses/by-sa/3.0/
Compartilhar