Roteiro de Aulas Teóricas- Software Básico

ATENÇÃO: O roteiro de aulas práticas está aqui.

17/08 e 19/08

Apresentação do Curso

referência: Aho & Ullman, 4.2.

Representação de Dados

  1. representação de inteiros não negativos: uso da base 2
  2. representação de caracteres

24/08 e 26/08

Arquitetura Típica de uma Máquina

Elementos Básicos

CPU

Registradores no MIPS

Memória Principal

Instruções do MIPS

Instruções Aritméticas

Instruções de Transferência

referência: Patterson&Hennessy, 4.3, 4.4

31/08 e 2/09

Instruções de tomada de decisão

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, $s2
Existem 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

14/09 e 16/09

Representação de Números Inteiros com Sinal

Representação em Sinal e Magnitude

Representação por Complemento a 1

Representação por Complemento a 2

referência: Patterson&Hennessy, 4.2

21/09 e 23/09

Pilha de Execução

Necessidade de "espaço" para salvar valores:

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.

Como funciona a pilha:

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 $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

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:

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

referência: Patterson&Hennessy, 3.6

5/10 e 7/10

Operadores Bit a Bit

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.

Shift

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.

	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

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.

	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

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.

	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  

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:

  1. multiplicação:

    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)

  2. divisão:

    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

14/10 e 19/10

Representação Ponto Flutuante

referência: Patterson&Hennessy, 4.8

21/10 e 27/10

Compilação de Mecanismos C

Variáveis Globais

Vamos discutir como fica a declaração e utilização de vários tipos de variáveis.

  1. inteiros
  2. ponto flutuante
  3. caracteres
  4. arrays de inteiros
  5. arrays de caracteres
  6. structs

26/10 e 4/11

Compilação de Mecanismos C

Variáveis Locais e Pilha

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.

Procedimentos Recursivos

Variáveis Locais

Considere a declaração:

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

9/11 e 11/11

Compilação, Montagem, Ligação e Carga de Programas

Alocação de memória para um programa :

referências:

  • Patterson & Hennessy, A.5

    Hierarquia de tradução de um programa

    Referências Externas

    Relocação

    Montagem

    Estrutura de um Módulo Objeto (Genérica)

    referências:

    Compilação, Montagem, Ligação e Carga de Programas (cont)

    Ligação (Link-edição)

    16/11 e 18/11

    Carga

    Ligação Dinâmica

    referências:

    Chamadas ao Sistema Operacional

    Sistema Operacional

    Chamadas ao Sistema

    Simulação de system calls no SPIM

    Bibliotecas de Chamadas

    O padrão POSIX

    Manipulação de Arquivos no POSIX

    Biblioteca ANSI C de Entrada/Saída Bufferizada