Seminário final INE 5309 - Assembly Language February 26, 2007 Luiz Henrique Bincoletto Tomazella 0513238-0 Marcelo de Aguiar Patricio 0513231-2 1 1 Introdução Sparc (Scalable Processo Architecture) é um arquiterura RISC desenvolvida e licenciada por SPARC International Inc. um consórcio de construtores de computadores que controlam o design. O Escalable do nome significa que o design permite a compatibilidade com programas futuros. Ele foi desenvolvido ao mesmo tempo que a arquitetura MIPS. mas em Berkeley em vez de Stanford. Como MIPS, a arquitetura é aberta (não proprietária) e o nome SPARC é lincenciado por SPARC International para os fabricantes que quiserem implementar a arquitetura. Alguns dos fabricantes SPARC incluem Sun Microsystem, Texas Instruments, Toshiba, Fujitsy, Cypress, e Tatung, entre outros. Essas companias usam o chip para uma faixa de aplicação muito grande, desde laptops até super computadores. SPARC teve sua primeira implementação pela SUN em 1987, usando a versão 7 da arquitetura SPARC, desde então existiram 2 mudanças principais: SPARC v8 em 1990, e a ultima impletamentação SPARC v9 em 1994, que introduziu suporte a endereçamento de 64-bits, e é compativel com aplicações de 32-bits. figura 1: Diagrama de Blocos 2 2 Instruction Set As instruções da linguagem assembly do SPARC podem ser dos seguintes tipos: • Aritimética: Adição ou subtração, com instruções adicionais para cada conjunto de códigos e instruções especiais para rotinas de multiplicação e para ler e escrever do registrador Y (um registrador especial usado na multiplicação e divisão) • Lógico: ands, ors, nors e xors, com instruçãoes adicionais para cara cojunto de códigos. • Shift: logical left, logical right e aritimético right. • Load: loads para bytes com e sem sinal, halfwords com e sem sinal, words e doublewords. • Store: Store para low-order byte ou low-order half do registrador, registrador (word), 2 registradores (doubleword) ou swap (load e store ao mesmo tempo). • Integer branch: SPARC tem um um registrador de condição que armazena o resultados de comparções e outras operações (hi, lo, less than, etc). Instruções de desvio leêm o código de condição respectivo de cada comando. Os branchs equal, not equal, less than, less or equal, greater than or equal, greater than e são aplicados sobre unsigned integer. Também há branchs para overflow de signed integer. • Control: Esses são comandos que chamam sub-rotinas ou ajudam na chamada. Call usa um jump incondicional para qualquer endereço na memória. Jumps faz a mesma coisa mas com registrador e cálculo de deslocamento para determinar o endereço alvo. Uma instrução de retorno retorna uma routina trap e restaura o estado do usuário antes da chamda do trap. Uma instrução “sethi” é incluida para criar uma constante imediata para outras instruções. Comandos Save e Restore salvam e restauram a janela de registradores sobre chamadas de funções. • Floating-Point: SPARC inclui separados conjuntos de comandos sobre ponto flutuante para load, store, adds, multiplications, divisions, compares e branches. • Pseudo-Instruction: SPARC inclui também pseudos instruções para melhorar a legibilidade onde a operação que está sendo executada não pôde parecerem óbvias. 3 3 Instruction types Como muitas máquinas RISC, SPARC e MIPS tem muitas instruções dividas em poucos tipos de instruções. Tendo poucos tipos de instruções, a arquitetura das máquinas é mais fácil de planejar e otimizar. Ambos SPARC e MIPS usam instruções de 32 bits e tem 3 tipos de instruções. No MIPS são chamadas de R-format, I-format e J-format e no SPARC são chamadas de tipo 1, tipo 2 e tipo 3. As duas máquinas tem tipos para instruções aritiméticas (R-format e tipo 3), para instruções branchs (I-format e tipo 2) e para instruções de jump (Jformat e tipo 1). Diferentemente do MIPS , o SPARC as instruções de load e store são do tipo 3 e também adiciona ao tipo 2 instrução chamada sethi que é silimiar a instrução lui do MIPS. 3.1 Tipo 1 - Instruções de jump . +————————————————————-+ | 01 | 30 bit | +————————————————————-+ Como a instrução Jump do MIPS (J-format), há somente uma única instrução no SPARC que é do tipo 1 chamda de CALL. Quando essa instrução é encontrada, o controle é transferido imediatamente para a nova localização dada pelos 30 bits da constante da instrução. Para determinar a localização a constante é deslocada 2 bits a esquerda para gerar um enderço de 32-bit e o PC é setado com esse valor mais o PC corrente. A mudança de endereço é relativa ao PC para permitir o programa ser movido na memória sem aftor os endereços especificados pelas instruções de chamada. 3.2 3.2.1 Tipo 2 - Branch e Sethi instructions Primeiro formato: . +—————————————————————–+ | 00 |a(1)| cond(4) | op2(3)| 22 bit constant | +—————————————————————–+ Diferentemente do MIPS, o SPARC fornece muitos tipos de instruções de branch. O tipo do branch é determinado pelos bits cond. Note que é permitidos desvios até 221 , para desvios maiores um tipo especial de é necessário. 3.2.2 Segundo formato: . +————————————————————-+ | 00 |rd (5) |100| 22 bit constant | 4 +————————————————————-+ Esse formato é utilizado para instrução sethi. Essa instrução é usada para carregar (load) uma instrução de 32-bit em um registrador. Para fazer o load, essa instrução carregara os 22 bits mais significativos e então uma instrução “or” é chamada para carregar os outros 10 bits da palavra. 3.3 Tipo 3 - Algebric Instructions Essas são as instruções mais comuns. São elas instruções algébricas, load/store (exeto sethi). Elas tem uma registrador de destino chamado rd, um especificador de instrução chamado op3 e registrador fonte, chamado rs1, o outro registrador fonte é chamado de rs2, ou pode ser um imediato de 13 bits. Note que como esse formato inclui load/store, Há um problema potencial pois o endereçamento de memória ficará limitado a um imediato de 13 bits ou 213 . Para esses casos, o endereço de instruções é relativo ao frame pointer. Comparado ao MIPS, carregar da memória será mais lento, uma vez que no MIPS instruções do I-format podem naturalmente se dirigir a uma faixa maior da memória e consequentemente não necessitará de uma instrução adicional para carregar um endereço no registrador antes da instrução load (como o SPARC necessita fazer). Two Source Register Instruction: +———————————————————————+ | 10 | rd (5) | op3 (6) | rs1 (5) |0| unused (8) | rs2 (5) | +———————————————————————+ One Source Register and Constant Instruction: +———————————————————————+ | 11 | rd (5) | op3 (6) | rs1 (5) |1| signed 13 bit const | +———————————————————————+ Como podemos ver , o opcode 10 é relacionado a instruções algébricas com 2 operandos e um registrador de destino. O opcod 11 é relacionado a operações com imediatos e load/store. 5 4 Register layout Tipicamente microprocessadores RISC são projetados para ter um pequeno número de instruções que pode ser decodificados e executado rapidamente. Para fazer as instruções executarem rapidamente, o número de acessos a memória tem que ser o menor possível. Para ajudar a limitar o número de acesso a memória, máquinas RISC tem um grande número de registradores para o programador/compilador usar. No MIPS, há 32 registradores que o prorama pode usar também no SPARC há 32 registradores que o programa pode usar. Desde a otimização do SPARC para chamada de sub-rotinas, o número atual de registradores no microprocessador é muito maior que 32 (frequentemente existem 124 registradores), mas apenas 32 registradores são visíveis para o programa em um dado momento (mais detalhes em 5. Subroutine Process). Registradores podem ser organizados pela sua função, pois eles são criados com algum propósito, se um registrador não é utilizado para uma finalidade que ele é projetado, ele está disponível para ser usado por qualquer instrução. Os registradores mais gerais são os temporários. Esses registradores existem para armazenar valores carregados da memória ou calculados pela ALU antes de ser salvos na memória. Eles não são preservados entre chamadas de sub-rotinas. O MIPS tem 10 registradores temporários que são $t0-$t9. O SPARC possui somente 8 registradores temporários que são %l0 - %l7, mas caso seja preciso outros registradores podem ser usados como temporários. Para acesso a memória e para chamadas de sub-rotinas, tanto o MIPS quanto o SPARC fornecem alguns poucos registradores especializados. Tanto o MIPS quanto o SPARC tem um stack point $sp e %sp, frame pointer $fp e %fp. MIPS reserva o registrador $at para o compilador, os registradores $k0 e $k1 para o sistema operacional e um registrador $gp para o global memory pointer. O MIPS tem 4 registradores reservados que o programa não pode contar para uso geral e o SPARC não tem essa limitação. Embora o SPARC tenha mais instruções de branch que o MIPS, ainda assim muitas comparações precisam ser feitas com a constante zero. Consquentemente, ambos reservam um registrador para essa constante. Esse registrador é $zero no MIPS e %g0 no SPARC. Para fornecer chamadas de sub-rotinas alguns registradores precisam ser designados para a passagem de parâmetros e outros para o retorno das funções. O MIPS tem 4 registradores com a função de serem parâmetros de funções ($a0, $a3) e 2 registradores para valores de retorno das funções ($v0 - $v1) e um registrador para armazenar o endereço de memória que a função deve retornar o PC quando a sub-rotina acabar. O SPARC tem 6 registradores para parâmetros de funções (%i0 - %i5), e 6 para valores de retorno das funções (%o0 - %o5) e para o endereço de retorno a função o registrador %i7. O SPARC tem 7 registradores que o MIPS não possui chamados de %g1 %g7. Esses registradores são “globais” e seus valores não são alterados entre chamadas de sub-rotinas. Atualmente, todas sub-rotinas usam os mesmos registradores globais, consequentemente o SPARC não possui o registrador $gp, que o MIPS possiu. Caso seja necessário mais registrados globais a memória é 6 usada. Nas duas arquiteturas caso seja necessário mais argumentos do que os registradores disponíveis a pilha é usada. O SPARC tem mais registradores disponíveis para o programa em um certo momento e tem mais otimizações para chamadas de sub-rotinas. SPARC tem uma clara vantagem sobre MIPS no projeto dos registradores. figura 2: Layout dos registradores Comparação entre os registradores do MIPS e SPARC: 7 Number 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 MIPS $zero $at $v0 $v1 $a0 $a1 $a2 $a3 $t0 $t1 $t2 $t3 $t4 $t5 $t6 $t7 $s0 $s1 $s2 $s3 $s4 $s5 $s6 $s7 $t8 $t9 $k0 $k1 $gp $sp $fp $ra Always returns zero Reserved for Assembler Func Return 1 Func Return 2 Func Arg 1 Func Arg 2 Func Arg 3 Func Arg 4 Temp 1 Temp 2 Temp 3 Temp 4 Temp 5 Temp 6 Temp 7 Temp 8 Saved 1 Saved 2 Saved 3 Saved 4 Saved 5 Saved 6 Saved 7 Saved 8 Temp 9 Temp 10 OS Kernel 1 OS Kernel 2 Ptr To Global Mem Stack Pointer Frame Pointer Func Return Address 8 SPARC %g0 %g1 %g2 %g3 %g4 %g5 %g6 %g7 %o0 %o1 %o2 %o3 %o4 %o5 %sp %o7 %l0 %l1 %l2 %l3 %l4 %l5 %l6 %l7 %i0 %i1 %i2 %i3 %i4 %i5 %fp %i7 Always returns zero Global 1 Global 2 Global 3 Global 4 Global 5 Global 6 Global 7 For Sub Arg 1 For Sub Arg 2 For Sub Arg 3 For Sub Arg 4 For Sub Arg 5 For Sub Arg 6 Stack Pointer Subroutine Return Address Local 1 Local 2 Local 3 Local 4 Local 5 Local 6 Local 7 Local 8 Func Arg 1 Func Arg 2 Func Arg 3 Func Arg 4 Func Arg 5 Local 6 Frame Pointer Func Return Address 5 Subroutine Register Allocation Se uma sub-rotina é chamada o programa precisa salvar o estado da máquina naquele momento. Em particular, um certo número de registradores precisam ser salvos. Isso inclui stack pointer, frame pointer, program counter e um número de registradores de uso geral. No MIPS, o processador depende exclusivamente do programa para salvar os registrados que serão usados pela sub-rotina. Consequentemente, quando uma sub-rotina é chamada, o programa tem a obrigação de ajustar o stack pointer para criar espaço para os registradores que precisam ser salvos e então colocar os registradores na pilha. Um programa em MIPS irá colocar o $gp, o $ra, e os registrados $s0 $s7 na pilha quando entrar em uma sub-rotina. Então quando a sub-rotina é completada, os registradores que foram salvos devem ser restaurados e a subrotina deve voltar para o endereço de retorno, $ra. Para garantir que os registradores salvos serão restaurados, a máquina MIPS gasta alguns ciclos de clock para salvar e depois alguns ciclos de clock para restaurar os registrados, isso sempre que uma sub-rotina é chamada, consequentemente gastando uma quantidade significante de ciclos de clock. A SUN, criou um número grande de registradores para o seu processador, mas uma sub-rotina em particular só pode ver 32 registradores. Esses registradores consistem em in (%i0 - %i7), out (%o0 - %07), local (%l0 - %l7) e global (%g0 - %g7). Para criar o “register file”, o SPARC pode salvar o estado da máquina sem pedir ao programa para salvar o estado de seus registradores em pilha. Para fazer isso é criado uma janela sobre os registradores. Quando uma sub-rotina é chamada, o processador renomeia os registrados out (%o0 - %07), antes que a sub-rotina processe os registradores in (%i0 - %i7), e cria novos registradores locais (%l0 - %l7) e out (%o0 - %07) para a sub-rotina. Esses novos registradores out pode se transformar em registradores in para uma sub-rotina chamada dentro dessa sub-rotina. 9 figura 3 Desse modo, o SPARC garante uma rápida chamada a uma sub-rotina se a sub-rotina tiver menos que 8 argumentos e o “register file” não estiver cheio. Se a sub-rotina tem muitos argumentos, o processador deve confiar ao programa para salvar o estado da máquina na pilha como no MIPS. Se o processador não consegue mais alocar registradores no “register file”, então a sub-rotina chamada a mais tempo é salva na pilha para liberar mais 32 registradores para a nova sub-rotina. 10 figura 4 11 6 Pipelining Arquitetura SPARC suporta pipelining. Isso evita hazards de 2 maneiras: load delay slots e branch delay slots. Isso faz com que não exista forward de cálculo de endereços e nem de valores aritiméticos como no MIPS, aparentemente tendo um ciclo a menos no pipeline. Uma CPU SPARC usa 4 passos por instrução, os passos são Fetch, Execute, Memory e Write Back. Durante o Execute parte da instrução é executada, a seguinte instrução será o Fecth, e quando a segunda instrução precisar do cálculo da primeira, o calculo é efetuado. 6.1 Load delay slots Sabesse que uma instrução load fornece dados para uma instrução posterior. Supondo que há uma instrução load precedendo uma instrução de adição e que o valor carregado será usado na soma. O valor necessário para soma não foi carregado quando a adição for utilizá-lo. O load estará acessando a memória quando a soma precisar do valor. Para resolver esse problema, o SPARC adiciona um “load delay slot” para atrasar a execução da instrução add. CPU cycle: F E M W ld %r10+offset,%r11 [load to r11] F E M W add %r11,%r12,%r13 [add r11+r12] 0 1 2 3 4 tabela 1 - Time in machine cycles CPU cycle: F E M W D D D F E 0 1 2 3 tabela 2 - Time in ld %r10+offset,%r11 [load to r11] D D [Delay] M W add %r11,%r12,%r13 [add r11+r12] 4 machine cycles Agora o valores necessários para fazer a soma estaram disponíveis no tempo correto, não ocorrendo mais problemas. O MIPS faz algo parecido, porém ele possui forwarding para resolver esse problema. 6.2 Branch delay slots Instruções branch chamam um sub-programa ou outro programa. No SPARC, branches leêm um código de condição setado anteriormente por uma comparação e então o branch é executado, no MIPS a comparação é feita no branch e então calculado o endereço do desvio. CPU cycle: 12 F E M W [branch] F E M W [próxima instrução] 0 1 2 3 4 tabela 3 - Time in machine cycles O endereço que a instrução branch está sendo executada, a máquina carregará a instrução seguinte a primeira instrução, como esse endereço ainda não está disponível quando a segunda instrução está sendo carregada isso por causar problemas. A arquitetura de SPARC pede que o programador compartilhe de alguma da responsabilidade para este problema. O programador pode inserir uma instrução que será executada depois do branch ou inserir um nop (no operation), para efetivamente atrasar a chamada da sub-rotina ou programa enquanto o branch calcula o endereço. Essa é uma responsabilidade incoveniente para o programador ou compilador, pois tem que decidir entre uma instrução ou um nop. Isso é diferente no MIPS, que insere delays para o programador. O que a máquina pode fazer pelo programador é manter dois contadores para o programa, o contador adicional é chamado de %npc ou “next program counter”. A instrução começa a ser executada, F-E-M-W em um ciclo que é apontado por %pc, e a outra instrução que está sendo buscada (F) é apontada por %npc. O %pc é sempre carregado com o endereço do %npc. Assim a instrução que vem depois do branch será sempre executada. Mas caso o branch seja executado, o %npc é atualizado com o endereço do desvio. 13 7 Exercícios Como foi pedido, foi feita a reimplementação dos dois exercícios. O multiplicador e o salvamento de contexto. Porem os dois exercícios foram feitos no mesmo programa, que é listado abaixo:. 7.1 Código .file "mult.c" .section ".rodata" .align 8 .LLC0: .asciz "****************\noverflow\n****************\n\n" .section ".text" .align 4 .global multiplicacao .type multiplicacao, #function .proc 04 multiplicacao: !#PROLOGUE# 0 save %sp, -112, %sp !#PROLOGUE# 1 smul %i0, %i1, %i0 mov %i0, %l1 sra %l1, 31, %l1 rd %y, %l0 cmp %l0, %l1 be .LL2 sethi %hi(.LLC0), %o0 call printf, 0 or %o0, %lo(.LLC0), %o0 .LL2: ret restore .size multiplicacao, .-multiplicacao .section ".rodata" .align 8 .LLC1: .asciz "Insira um numero: " .align 8 .LLC2: .asciz "%d" .align 8 14 .LLC3: .asciz "Insira outro numero: " .align 8 .LLC4: .asciz "\n = Resultado: %d\n" .section ".text" .align 4 .global main .type main, #function .proc 04 main: !#PROLOGUE# 0 save %sp, -120, %sp !#PROLOGUE# 1 st %g0, [%fp-20] st %g0, [%fp-24] sethi %hi(.LLC1), %o0 call printf, 0 or %o0, %lo(.LLC1), %o0 sethi %hi(.LLC2), %l0 or %l0, %lo(.LLC2), %o0 call scanf, 0 add %fp, -20, %o1 sethi %hi(.LLC3), %o0 call printf, 0 or %o0, %lo(.LLC3), %o0 or %l0, %lo(.LLC2), %o0 call scanf, 0 add %fp, -24, %o1 ld [%fp-20], %o0 call multiplicacao, 0 ld [%fp-24], %o1 mov %o0, %o1 sethi %hi(.LLC4), %o0 call printf, 0 or %o0, %lo(.LLC4), %o0 ret restore %g0, 0, %o0 .size main, .-main .ident "GCC: (GNU) 3.4.2" 7.2 Considerações sobre o desenvolvimento do exercício: Para auxiliar na criação da estrutura do programa foi criado um programa em c e gerado um arquivo em assembly com o seguinte comando: gcc -S -O mult.c. 15 Uma vez criado o arquivo em assembly as alterações foram feitas nesse arquivo, chamado mult.s. Uma vez o arquivo em assembly finalizado ele foi compilado com o comando: gcc mult.s -o mult, assim gerando um arquivo binário para ser executado. O if (z>0) ..... foi feito simplesmente para auxiliar na implementação em assembly. Código do programa em c: int multiplicacao (int a, int b){ int z; z=a*b; if (z>0) {printf("overflow");} return(z); } int main(){ int x = 0; int y = 0; printf("Insira um numero: "); scanf("%d",&x); printf("Insira outro numero: "); scanf("%d",&y); printf("Resultado: %d\n",multiplicacao(x,y)); return(0); } Foi utilizada a máquina VENUS da rede da inf, utilizando processador Sparcr Ultra I V9 (informações fornecidas). Para fazer a multiplicação fui utilizado a instrução smul, que faz a multiplicação de dois números de 32 bits. Existe uma instrução chamada mulx, que faz a multiplicação entre 64 bits, mais essa instrução não foi aceita pelo processador o que nos faz concluir que essa instrução só está disponível em versões mais novas do processador. O SPARC possui um código de condição que indica quando houve overflow (V) e existe uma instrução bvs (branch on overflow set) que faz o desvio se o bit de condição (V) estiver setado. Isso facilitaria muito a implementação do exercício mais infelizmente ele também não pode ser implementado, não por um problema de não reconhecimento da instrução como no caso anterior, mas na falta de uma instrução que setasse o bit de condição. Analisando o manual encontramos as seguintes definições para as instruções de multiplicação de 32 bits. 16 Opcode op3 Operation UMUL 00 1010 Unsigned Integer Multiply SMUL 00 1011 Signed Integer Multiply UMULcc 01 1010 Unsigned Integer Multiply and modify cc’s SMULcc 01 1011 Signed Integer Multiply and modify cc’s tabela 4 - Instruções para multiplicação 32 bits. Bit icc.n icc.z icc.v icc.c xcc.n xcc.z xcc.v xcc.c tabela 5 UMULcc / SMULcc Set to 1 if product{31} = 1; otherwise, set to 0 Set to 1 if product{31:0}= 0; otherwise, set to 0 Set to 0 Set to 0 Set to 1 if product{63} = 1; otherwise, set to 0 Set to 1 if product{63:0} = 0; otherwise, set to 0 Set to 0 Set to 0 - Alteração nos bits de condição Como se pode ver na tabela 5, nenhuma das instruções seta o bit de condição (V) assim optamos por utilizar a instrução smul, que trabalha com número com sinal mais não seta nenhum bit de condição. Assim tivemos que pesquisar outra maneira de detectar overflow na multiplicação. Consultando o manual encontramos a seguinte definição. • 32-bit overflow after UMUL/UMULcc is indicated by Y <> 0. • 32-bit overflow after SMUL/SMULcc is indicated by Y <> (R[rd] > > 31), where > > indicates 32-bit arithmetic rightshift. 17 Figura 5: Esquema da multiplicação Então implementamos a segunda opção que diz respeito a instrução escolhida. Para implementar o exercício de salvamento de contexto foi mais simples, uma vez que noo arquivo mult.s gerado pelo gcc essa tarefa já estava feita, pois o SPARC facilita essa implementação. A única coisa que precisa ser feita é chamar a instrução save %sp, imediato, %sp no início da função e restore no fim. A instrução save tem um imediato que é o numero de bytes que serão salvos, ou melhor o número de registrados a serem movidos no “register file”. Como o arquivo foi complidado com a opção de otimização então o compilador otimizou o salvamento de contexto, porém foram feitos testes e foi verificado que se não for complidado com otimização todo ínicio de sub-rotina tem uma instrução save %sp, -120, %sp, aonde 120 corresponde a 30 registradores 120 / 4 = 30. Assim tem uma diferença entre os 32 registradores disponíveis e os 30 movidos, mas não conseguimos chegar a uma conclusão sobre essa diferença. 18