ATENÇÃO: O roteiro de aulas práticas está aqui.
referência: Aho & Ullman, 4.2.
add regdest, regop1, regop2
addi regdest, regop1, const
permite usar como um dos operandos (operando imediato uma constante de até 16 bits.
addi regdest, $0, valor
regdest
:
lw regdest, (rege)
onde rege
contém o endereço de memória de onde
deve ser copiada a palavra
regdest
:
sw regdest, (rege)
onde rege
contém o endereço de memória para onde
deve ser copiada a palavra
referência: Patterson&Hennessy, 4.3, 4.4
Suponha que as variáveis (int
)
a
, b
, c
, d
e e
estão alocadas respectivamente nos registradores
$s0
, $s1
, $s2
, $s3
e $s4
.
Como programar estruturas como:
if (a==b) c=d+e; d=a+c;ou
while (a<=b) { ... a++; } d=a+c;?
Instruções de desvio do assembler servem para "quebrar" a execução sequencial, fazendo com que, sob certas condições, deixe de valer a regra que diz que sempre a próxima instrução a ser executada é a próxima fisicamente na memória.
Existem instruções de desvio condicional e incondicional.
No MIPS os desvios condicionais são chamados branch
.
Um exemplode branch condicional é a instrução:
bne $s0, $s1, depois
Nesse caso, se o valor de $s0 não for igual ao valor de $s1,
o controle é desviado para o endereço indicado pelo label depois.
Para programar o if
acima, poderíamos então escrever:
bne $s0, $s1, depois add $s2, $s3, $s4 depois: add $s3, $s0, $s2Existem muitas outras instruções de desvio condicional (ver manual), como
beq
, beqz
, bge
, bgeu
, bgt
, etc.
Instruções de desvio incondicional são chamadas de jump
no MIPS.
Um exemplo de desvio incondicional é a instrução:
j depois
que faz com que o controle seja desviado para o endereço indicado pelo label depois, independentemente de qualquer condição.
Para implementar uma estrutura if ... else ...
é necessário utilizar um desvio condicional e um incondicional!
Tente fazê-lo!
Para programar o while
acima, poderíamos escrever:
loop: bgt $s0, $s1, depois # teste no inicio do loop; como fica o do..while? ... addi $s0, $s0, 1 j loop # desvia para teste depois: add $s0, $s1, $s2
A instrução de jump tem várias variantes.
Uma delas é a jr
, que desvia para um endereço
armazenado em um registrador.
Outra variante importantíssima
é a instrução jal
, que antes de desviar armazena
o endereço da próxima instrução (sequencial) no registrador
$ra
.
Essa instrução é usada para implementar chamadas
de funções.
Por convenção então, uma função é chamada por jal
.
Para retornar dela, pode-se usar
jr $ra
mas para isso é necessário garantir que a função não alterou
o conteúdo de $ra
(o que infelizmente vai ocorrer, por exemplo,
se ela chamar outra função), ou então "salvar" o conteúdo de $ra em
outro local e restaurá-lo antes do retorno:
f: ... # tarefas iniciais a serem discutidas depois move $s7, $ra ... # corpo da funcao move $ra, $s7 jr $ra
Outra convenção no retorno de funções: os resultados são colocados
em $vo
e $v1
referência: Patterson&Hennessy, 3.5
referência: Patterson&Hennessy, 4.2
Lembre-se da instrução jal
, usada para chamar
uma função. Essa instrução armazena o endereço de retorno
no registrador $ra
.
Assim, o retorno da função pode ser feito com
j $ra
No entanto, se a função por sua vez chama outra função, ie,
usa jal
, o conteúdo de $ra
não faz mais sentido na hora do retorno.
É necessário salvar o valor de retorno
em algum outro lugar.
Para isso (e muitas outras coisas) é usada a pilha de execução, ou pilha de ativação. A pilha é uma área de memória principal usada como pilha, com operações de push (empilha) e pop (desempilha).
Em geral, o hardware dá algum suporte à manutenção dessa
pilha.
No caso da máquina MIPS, um registrador específico,
$sp
, é dedicado ao enedereço do topo
da pilha.
Por motivos históricos, a pilha cresce em direção aos endereços
mais baixos de memória.
Ou seja, para alocar espaço para um endereço, devemos subtrair
4 de $sp
(lembre-se que um endereço ocupa 4 bytes!)
e para desalocar devemos somar 4 a $sp
.
Por exemplo:
----------------------------------------------- | | | | | | | | | | | | | | | ----------------------------------------------- ^ | $sp empilhar: sub $sp, $sp, 4 sw $ra, ($sp) # suponha que o valor de $ra e' a13f45c6 resultado: ----------------------------------------------- | | | | | | | | |a1|3f|45|c6| | | ----------------------------------------------- ^ | $sp desempilhar: lw $ra, ($sp) add $sp, $sp, 4 resultado: ----------------------------------------------- | | | | | | | | | | | | | | | ----------------------------------------------- ^ | $sp
Voltando ao exemplo da função, uma função como:
int boba1() { boba2(); return 1; }pode ser escrita em assembler como:
boba1: #salva valores sub $sp, $sp, 4 sw $ra, ($sp) jal boba2 addi $v0, $0, 1 #restaura valores lw $ra, ($sp) add $sp, $sp, 4 # retorna jr $ra
Como exemplo da necessidade de salvar mais valores além de $ra
,
imagine que a função boba1
precise utilizar um registrador
para alocar uma variável local:
int boba1() { int i; for (i=10;i>0;i--) boba2(); return 1; }
Uma possibilidade é usar um
Em vez de alocar a pilha posição por posição (subtrair 4 de cada vez),
é comum alocar de uma vez todo o espaço que a função vai precisar:
referência: Patterson&Hennessy, 3.6
Todas as CPUs oferecem algumas instruções que permitem manipular
bits individualmente dentro de palavras.
Tipicamente, temos instruções de shift e rotate,
que deslocam os bits para a direita ou esquerda dentro de
uma palavra, e instruções para operações lógicas (and, or, not, ...)
bit a bit.
As operações de shift left logical e shift right logical
permitem deslocar os bits de uma palavra n bits para a direita
ou esquerda, completando a palavra com zeros.
A operação shift right arithmetic desloca os bits
de uma palavra n bits para a direita replicando à esquerda
o valor anterior do bit 31.
Supondo que não haja problema de overflow, o shift de 1 bit para
a esquerda equivale à multiplicação de um número por 2 e o shift aritmético
de 1 bit para a direita equivale à divisão inteira de um número por 2.
Isso ocorre porque estamos sempre "subtraindo 1" antes de
fazer a divisão.
E por que o shift funciona para multiplicar e dividir?
Para números positivos, acontece exatamente a mesma coisa na base 10.
Para negativos é que não é óbvio...
Se x é negativo, rep(x)=2k+x:
2*rep(x) = 2*(2k+x) = 2k+1+ 2x
= 2k+2k+2x
sem overflow, 2x>2k-1, logo
2k+2k+2x > 2k
logo haverá um carry para fora, descartando 2k,
e ficaremos com
2k+2x = rep(2x)
rep(x) div 2 =
2k-1+x/2
mas quando fazemos o shift aritmético em um negativo,
colocamos 1 no bit mais significativo, o que equivale a
somar 2k-1, logo ficamos com
2k+x/2 = rep(x/2)
referência: Patterson&Hennessy, 4.4 referência: Patterson&Hennessy, 4.8
Vamos discutir como fica a declaração e utilização de vários
tipos de variáveis.
em assembler:
em assembler:
em assembler:
obs: O uso do nome
em assembler:
em assembler:
em assembler:
em assembler:
em assembler:
em assembler:
em assembler:
em assembler:
e se forem 2 arrays?
em assembler:
em assembler:
em assembler:
em assembler:
em assembler:
em assembler:
Estruturas introduzem o problema de alinhamento.
Cada campo da estrutura que corresponde a uma palavra deve
necessariamente começar em um endereço múltiplo de 4.
Assim, se tivermos uma declaração como:
onde um campo ocupando dois bytes precede um campo que corresponde
a uma palavra, o compilador terá que deixar dois bytes não utilizado
entre o array de caracteres e o inteiro:
Observe que de qualquer forma o início da estrutura tem que
ser alinhado, senão não saberemos quantos bytes devem
ser "pulados" para chegar a um endereço múltiplo de 4.
As variáveis declaradas localmente (dentro de funções)
são tipicamente alocadas na pilha de ativação.
Antes de dar exemplos de variáveis locais
alocadas na pilha, vamos rever o uso da pilha de ativação.
$t?
, mas a função boba2
poderia utilizar esse registrador e corromper o valor de i
(sujar o registrador).
Outra possibilidade é usar um $s?
, por exemplo, $s0
.
Mas a função que chamou boba1
pode estar usando esse
registrador.
Assim, é necessário salvar o valor de no início de
boba1
e restaurá-lo no final:
boba1:
#salva valores
sub $sp, $sp, 4
sw $ra, ($sp)
sub $sp, $sp, 4
sw $s0, ($sp)
#inicio do for
add $s0, $0, 10
rep:
blez $s0, depois
jal boba2
sub $s0, $s0, 1
j rep
depois:
addi $v0, $0, 1
#restaura valores
lw $s0, ($sp)
add $sp, $sp, 4
lw $ra, ($sp)
add $sp, $sp, 4
# retorna
jr $ra
boba1:
#salva valores
sub $sp, $sp, 8
sw $ra, 4($sp) # armazena em ($sp)+4
sw $s0, ($sp)
#inicio do for
add $s0, $0, 10
rep:
blez $s0, depois
jal boba2
sub $s0, $s0, 1
j rep
depois:
addi $v0, $0, 1
#restaura valores
lw $s0, ($sp)
lw $ra, 4($sp)
add $sp, $sp, 8
# retorna
jr $ra
5/10 e 7/10
Operadores Bit a Bit
Shift
li $s0, 22 # $s0 = 0000 ... 0000 0000 0000 0001 0110
sll $s1, $s0, 2 # $s1 = 0000 ... 0000 0000 0000 0101 1000
srl $s1, $s0, 2 # $s1 = 0000 ... 0000 0000 0000 0000 0101
li $s0, -6 # $s0 = 1111 ... 1111 1111 1111 1111 1010
sra $s1, $s0, 2 # $s1 = 1111 ... 1111 1111 1111 1111 1110
li $s0, 22 # $s0 = 0000 ... 0000 0000 0000 0001 0110
sra $s1, $s0, 2 # $s1 = 0000 ... 0000 0000 0000 0000 0101
li $s0, 22 # $s0 = 0000 ... 0000 0000 0000 0001 0110 22
sll $s1, $s0, 1 # $s1 = 0000 ... 0000 0000 0000 0010 1100 44
sra $s1, $s0, 1 # $s1 = 0000 ... 0000 0000 0000 0000 1011 11
li $s0, -6 # $s0 = 1111 ... 1111 1111 1111 1111 1010 -6
sll $s1, $s0, 1 # $s1 = 1111 ... 1111 1111 1111 1111 0100 12
sra $s1, $s0, 1 # $s1 = 1111 ... 1111 1111 1111 1111 1101 -3
o arredondamento da divisão inteira é sempre "para baixo",
ou seja, por exemplo, (-5 div 2) = -3:
li $s0, -6 # $s0 = 1111 ... 1111 1111 1111 1111 1011 -5
sra $s1, $s0, 1 # $s1 = 1111 ... 1111 1111 1111 1111 1101 -3
14/10 e 19/10
Representação Ponto Flutuante
21/10 e 27/10
Compilação de Mecanismos C
Variáveis Globais
int i; /* declaração sem inicialização */
.data
.align 2 # declara que deve ser colocado no próximo end. múltiplo de 4
i: .space 4 # ocupa 4 bytes
int i = 3; /* declaração sem inicialização */
.data
.align 2 # declara que deve ser colocado no próximo end. múltiplo de 4
i: .word 3
i += 1;
la $t0,i
lw $t1,($t0)
add $t1,1
sw $t1,($t0)
i
é uma facilidade do assembler: no
programa executável não existem nomes, apenas endereços!
float f; /* declaração sem inicialização */
# igual à variável inteira!
.data
.align 2 # declara que deve ser colocado no próximo end. múltiplo de 4
f: .space 4 # ocupa 4 bytes
float f = 3.0; /* declaração sem inicialização */
.data
.align 2 # declara que deve ser colocado no próximo end. múltiplo de 4
f: .float 3.0
char c; /* declaração sem inicialização */
.data
c: .space 1
char c = 'a'; /* declaração sem inicialização */
.data
c: .byte 97
c += 1;
la $t0,i
lb $t1,($t0)
add $t1,1
sb $t1,($t0)
int a[10];
.data
.align 2
a: .space 40 # cada inteiro ocupa 4 bytes!
int a[10] = {0,1,2,3,4,5,6,7,8,9}
.data
.align 2
a: .word 0,1,2,3,4,5,6,7,8,9
int a[10]; int i;
...
a[i]++;
# calcula o endereco
la $t0,a
la $t1,i
lw $t1,($t1)
sll $t1,$t1,2 # multiplica por 4
add $t0,$t0,$t1
# agora acessa a variavel
lw $t1,($t0)
add $t1,1
sw $t1,($t0)
int i, a[10], b[10];
...
a[i] = b[i];
# calcula os enderecos
la $t0,a
la $t1,b
la $t2,i
lw $t2,($t2)
sll $t2,$t2,2 # multiplica por 4
add $t0,$t0,$t2
add $t1,$t1,$t2
# agora acessa as variaveis
lw $t2,($t1)
add $t2,1
sw $t2,($t0)
char ac[10];
.data
ac: .space 10
int ac[10] = {'0','1','2','3','4','5','6','7','8','9'}
.data
ac: .byte 48,49,50,51,52,53,54,55,56,57
char ac[10]; int i;
...
a[i]++;
# calcula o endereco
la $t0,a
la $t1,i
lw $t1,($t1)
add $t0,$t0,$t1
# agora acessa a variavel
lb $t1,($t0)
add $t1,1
sb $t1,($t0)
struct {
int a;
float b;
char c[2];
} s;
.data
.align 2
s: .space 10 # reserva tamanho total
s.c[i]++; /* i foi declarada como inteira */
la $t0,i
lw $t1,($t0)
la $t1,s
add $t1,$t1,$t0
lw $t0,8($t1)
addi $t0,$t0,1
sw $t0,8($t1)
struct {
char c[2];
int a;
float b;
} s;
.data
.align 2
s: .space 12 # reserva tamanho total
26/10 e 4/11
Compilação de Mecanismos C
Variáveis Locais e Pilha
Procedimentos Recursivos
int fact (int n)
{
if (n < 1)
return 1;
else
return (n * fact (n - 1));
}
fact:
sub $sp,$sp,8 # aloca registro de ativação na pilha
sw $ra, 4($sp) # valor de $ra deve ser preservado
sw $a0, 0($sp) # o valor do argumento (n, em $a0) é usado pelo
# procedimento após o retorno da chamada recursiva
slt $t0,$a0,1
beq $t0,$zero,else
addi $v0,$zero,1 # se n < 1, inicia volta da recursão
j fim # com valor de retorno = 1 (em $v0)
else:
addi $a0,$a0,-1 # n >= 1 : chamada recursiva com argumento n-1
jal fact
lw $a0, 0($sp) # recupera valor original do argumento (n), salvo na pilha
mul $v0,$v0,$a0 # valor de retorno = n * fact (n-1)
fim:
lw $ra, 4($sp) # restaura o endereço de retorno em $ra
addu $sp,$sp,8 # elimina o registro de ativação
jr $ra
void f() {
double d[4];
int i;
int b[20];
...
}
O código gerado pelo compilador para esta função deve reservar espaço, na pilha de ativação, para essas variáveis.
f:
sub $sp,$sp,120 # 4 para $ra, 80 para b, 4 para i, 32 para d
# ($sp) aponta d, 32($sp) aponta i, 36($sp) aponta b
sw $ra,116($sp)
...
...
lw $ra,116($sp)
add $sp,$sp,120 # desaloca registro de ativação
O comando C
b[i]++
poderia ser traduzido por:
# cálculo do endereço de b[i]
lw $t0,32($sp)
sll $t0,$t0,2
move $t1,$sp
addi $t1,$t1,36 #inicio de b
add $t1,$t1,$t0 #endereco de b[i]
# atualizacao
lw $t0,($t1)
addi $t0,$t0,1
sw $t0,($t1)
A pilha também deve sempre ficar alinhada. Isso tem que ser levado em conta na hora de calcular o tamanho do registro de ativação. Declarações como:
void f() {
int i;
char a,b;
float f;
...
}
poderiam ser traduzidas para:
f:
sub $sp,$sp,16 # 4 para $ra, 4 para i, 1 para a, 1 para b, , 4 para f
# ($sp)->i, 4($sp)->a, 5($sp)->b, 8($sp)->f
sw $ra,12($sp)
...
...
lw $ra,12($sp)
add $sp,$sp,16 # desaloca registro de ativação
referência: Patterson&Hennessy, 3.6, A.6
referências:
referências:
referências:
open
: retorno de um descritor de arquivo
read
write
lseek
close
read
, write
, etc;
stdio
)
fopen
: retorno de um FILE*
fgetc
, fputc
fgets
, fputs
fread
, fwrite
fflush
fseek
fclose