Detalhamento da Compilação de Procedimentos no MIPS

instrução MIPS LW e SW IF Simples no MIPS

Oi pessoal! No artigo anterior eu mostrei para vocês como funcionam os procedimentos aninhados recursivos no Assembly MIPS, e vamos aprofundar o assunto neste artigo.

 

 

Memória

 

Antes de continuarmos a falar sobre procedimentos, vamos entender um pouco o formato da memória para o MIPS. Ela é dividida em três partes:

 

Segmento de texto: é o segmento de memória que mantém as instruções do programa, começa no endereço 400000 hexa. O código em linguagem de máquina para rotinas no arquivo de origem é mantido aqui.

 

Segmento de dados: é dividido em dados dinâmicos e dados estáticos. Objetos cujo tamanho é conhecido pelo compilador e cujo tempo de vida é a execução inteira do programa estão contidos nos dados estáticos. Dados dinâmicos são alocados pelo programa enquanto ele é executado e, conforme é preciso alocar mais dados, o sistema operacional expande este segmento em direção ao segmento de pilha. A representação binária dos dados no arquivo de origem é mantida aqui.

 

Segmento de pilha: O tamanho máximo da pilha de um programa não é conhecido antecipadamente, assim, conforme a pilha vai crescendo, o sistema operacional vai expandindo o segmento de pilha em direção ao segmento de dados.

 

A Figura 1 ilustra esta divisão:

 

 

 

Convenção para Chamadas de Procedimentos

 

Uma convenção é um protocolo de software que controla o uso dos registradores por procedimentos. O MIPS utiliza as seguintes convenções para as chamadas de procedimentos:

 

Registradores 1 ($at), 26 ($k0) e 27 ($k1): são reservados para o montador e o sistema operacional.

 

Registradores de 4 a 7 ($a0 até $a3): são usados para passar os quatro primeiros argumentos para as rotinas sendo os argumentos restantes passados na pilha.

 

Registradores 2 e 3 ($v0 e $v1): são usados para retornar valores das funções

 

Registradores de 8 a 15, 24 e 25 ($t0 a $t9): são salvos pelo CALLER; mantém quantidades temporárias que não precisam ser preservadas entre as chamadas. Estes são registradores salvos pela rotina que faz uma chamada de procedimento.

 

Registradores de 16 a 23 ($s0 a $s7): são salvos pelo CALLEE; mantém os valores de longa duração os quais devem ser preservados entre as chamadas. Estes são registradores salvos pela rotina sendo chamada.

 

Registrador 28 ($gp): ponteiro global; aponta para o meio de um bloco de memória no segmento de dados estático.

 

Registrador 29 ($sp): stack pointer; aponta para o último local na pilha.

 

Registrador 30 ($fp): frame pointer.

 

Registrador 31 ($ra): endereço de retorno de uma chamada de procedimento.

 

 

Frame de Chamada de Procedimentos

 

Um frame de chamada de procedimento é um bloco de memória usado para manter valores passados a um procedimento como argumentos, a fim de salvar registradores que um procedimento pode modificar mas que o caller não deseja que sejam alterados, e fornecer espaço para variáveis locais a um procedimento. A Figura 2 ilustra o frame.

 

O frame pointer aponta para a primeira palavra do frame de pilha do procedimento em execução. Já o stack pointer aponta para a última palavra do frame, enquanto os quatro primeiros argumentos são passados em registradores e o quinto é o primeiro armazenado na pilha. O procedimento que está executando utiliza o frame pointer para acessar rapidamente os valores em seu frame de pilha. Na primeira situação, os seguintes passos são seguidos na chamada de procedimentos:

 

1. Passagem de argumentos: são armazenados em $a0 até $a3, o restante é armazenado na pilha aparecendo no início do frame de pilha.

 

2. Salvar registradores salvos pelo Caller: usa os registradores de $a0 a $a3 e de $t0 a $t9, sem salvar o seu valor. O valor deverá ser salvo antes da chamada se o Caller utilizar esses registradores após uma chamada.

 

3. Executar a instrução jal: desvia para a primeira instrução do Callee e salva o endereço de retorno em $ra

 

Importante ressaltar que o Caller e o Callee devem combinar a sequência dos passos, além disso esses passos podem começar sua execução a partir de situações diferentes: a primeira antes do caller invocar o calle, a segunda assim que o callee começa a executar, e a terceira antes do callee retornar ao caller. Na segunda situação, os seguintes passos são seguidos:

 

* Aloca memória para o frame subtraindo o tamanho do frame do stack pointer.

 

* Salva os registradores salvos pelo Callee no frame. O caller espera encontrar os registradores inalterados após a chamada, por isso é necessário salvar. Frame pointer é salvo para cada um dos procedimentos que aloca um novo frame de pilha. Se o Calle fizer uma chamada, então $ra precisa ser salvo, caso contrário não. Quaisquer outros registradores usados pelo Calle devem ser salvos.

 

* Estabelece o $fp somando o tamanho do frame de pilha menos 4 a $sp e armazenando a soma no $fp

 

* Na terceira situação, os passos são os seguintes:

 

* O valor de retorno da função deve ser colocado no registrador $v0, se houver

 

* Os registradores salvos devem ser restaurados

 

* O frame de pilha deve ser removido somando o tamanho do frame a $sp

 

* Deve retornar para o endereço em $ra

 

 

Um último detalhe interessante é que, linguagens de programação que não tem a funcionalidade de recursão, não precisam alocar frames em uma pilha.

 

 

Diretivas do montador

 

A diretiva .text define um bloco de instruções, a diretiva .data define um bloco de dados, a diretiva .align n define que os itens nas linhas seguintes devem ser alinhados em um limite de 2n bytes, por exemplo, .align 2 indica que o próximo item deverá estar em um limite da palavra. A diretiva .globl “nome” indica que nome é um símbolo global e deve ser visível ao código armazenado em outros arquivos, sendo nome o nome que você define, e a diretiva .asciiz armazena uma string terminada em nulo na memória. Nós já usamos em nossos exemplos a diretiva .text, no próximo exemplo vamos aprender a usar outras.

 

 

Chamadas ao Sistema

 

Em linguagens de programação de médio e alto nível, normalmente nós dizemos onde os programas começam e terminam usando algum símbolo como as Chaves { }, ou uma indentação específica, etc. Além disso, as linguagens também permitem forçar o término do programa com break ou um system 0, por exemplo, enfim, há formas de sairmos do programa. Até agora todos os programinhas que fizemos não precisamos especificar um fim, mas quando fizermos a compilação de um programa principal será necessário por um fim na execução do programa. Lembrem-se, o programa principal chama outras funções, cria a pilha, ou frame, de chamadas e retorno de funções, então precisamos dar fim na execução, caso contrário o programa pode entrar em loop infinito. É nossa responsabilidade, como programadores, dizer para onde as instruções estão indo e quando elas terminam. A Tabela 1 mostra algumas das chamadas ao sistema que podemos utilizar no MIPS.

 

Serviço

Código

Argumentos

Resultado

Observações

print_int

1

$a0 = integer

 

Recebe um inteiro e o imprime no console

print_float

2

$f12 = float

 

Imprime um único número de ponto flutuante

print_double

3

$f12 = double

 

Imprime um número de precisão dupla

print_string

4

$a0 = string

 

Recebe um ponteiro para uma string terminada em nulo e a escreve no console

read_int

5

 

Integer em $v0

Leem uma linha inteira da entrada incluindo o caractere de newline. Caracteres após o número são ignoradoso

read_float

6

 

Float em $f0

read_double

7

 

Double em $f0

read_string

8

$a0 = buffer

$a1 = tamanho

 

Igual a fgets do UNIX

sbrk

9

$a0 = valor

Endereço em $v0

Retorna um ponteiro para um bloco de memória contendo n bytes adicionais

exit

10

  

Interrompe o programa que está sendo executado

print_char

11

$a0 = char

 

Escreve um único caractere

read_char

12

 

Char em $v0

Le um único caractere

open

13

$a0 = nome de arquivo (string)

$a1 = flags

$a2 = modo

Descritor de arquivo em $a0

Chamadas da biblioteca padrão do UNIX

read

14

$a0 = descritor de arquivo

$a1 = buffer

$a2 = tamanho

Número de caracteres lidos em $a0

write

15

$a0 = descritor de arquivo

$a1 = buffer

$a2 = tamanho

Número de caracteres escritos em $a0

close

16

$a0 = descritor de arquivo

 

 

 

Compilação de um programa com Main

 

Vamos compilar agora um código em C que tem um programa principal usando o Frame de Pilha:

 

 

Vou passar o código inteiro primeiro e depois vou explicando linha por linha ok, então, olhem atentamente:

 

 

Vídeo da execução do código:

 

 

Começamos a compilação do código C para Assembly MIPS com a diretiva .text para indicar que ali é um segmento de texto seguido por um bloco global indicado pela diretiva .globl main, que é o programa MAIN. Dentro do rótulo main usamos o frame de pilha ao invés da pilha simples, assim as linhas de 5 a 8 são referentes à configuração do frame de pilha do programa principal: cria um frame de pilha com 32 bytes (o tamanho mínimo é de 24 bytes) e salva os registradores ($fp e $ra) salvos pelo Callee que serão modificados. As linhas de 10 e 11 atribuem valores aos parâmetros da função e a linha 12 chama o procedimento soma com a instrução jal. A linha 14 move o conteúdo do registrador de retorno ($v0) para outro registrador ($a1), para que seja liberado e usado novamente. Na linha 16 começamos um segmento de dados pois vamos imprimir no console a frase "A soma é : " e o resultado da soma efetuada pelo procedimento soma. Definimos um rótulo LC que contém a nossa frase, que nada mais é que uma string sendo necessário usar a diretiva .asciiz e em seguida voltamos ao segmento de texto (linha 20).

 

Da linha 21 a 27 imprimimos no console, a linha 21 envia um comando de impressão de string para o sistema o qual o código é 4. A linha 22 pega a string que será impressa, por meio da chamada ao rótulo LC o qual tem seu conteúdo armazenado no registrador $a0. Na linha 23 usamos syscall para efetuar a chamada ao sistema de fato, que imprimirá na tela a string "a soma é: ". Feito isto, agora imprimimos o número inteiro (resultado da soma), usando o código 1 na linha 25 e, na linha 26 movemos o resultado do procedimento soma que está em $a1 para o registrador $a0, imprimindo de fato na linha 27 com o comando syscall. As próximas três linhas fazem o restauro dos valores do endereço de retorno (linha 29), do frame pointer (linha 30) e remove o frame de pilha (linha 31). A linha 33 finaliza o programa com um jump para o procedimento fim, que encerra a execução deste código assembly.

 

Das linhas 35 a 44 temos o código assembly referente ao procedimento soma, que já vimos anteriormente, mas agora usamos o frame de pilha, assim, a linha 37 reserva o espaço do frame, a linha 38 salva o endereço de retorno, a linha 39 salva o frame pointer, a linha 40 prepara o frame pointer, a linha 41 salva o argumento, a linha 42 realiza a soma propriamente dita, a linha 43 retorna o valor da soma e a linha 44 retorna para quem chamou. Das linhas 46 a 48 temos o código correspondente ao fim da execução, a linha 47 configura o código 10, avisando o sistema operacional que este é o fim da execução deste código e, a linha 48 com o comando syscall efetua de fato a chamada para o sistema finalizar a execução.

 

 

Conclusão

 

Não conseguiu entender alguma coisa? Deixe sua dúvida aqui nos comentários e que tentarei responder o mais rápido possível. Espero que tenham curtido aprender um pouco mais sobre procedimentos no MIPS. Aguardo vocês nos próximos artigos!

 

 

Saiba mais

 

Funções e Procedimentos - Parte 1

Compilando Switch/Case no MIPS

Técnicas de Mapeamento de Memória em Linguagem C

Outros artigos da série

<< Compilando Procedimentos Recursivos e Aninhados no MIPS

NEWSLETTER

Receba os melhores conteúdos sobre sistemas eletrônicos embarcados, dicas, tutoriais e promoções.

Obrigado! Sua inscrição foi um sucesso.

Ops, algo deu errado. Por favor tente novamente.

Elaine Cecília Gatto
Bacharel em Engenharia de Computação. Mestre em Ciência da Computação. Co-fundarora e Líder das #GarotasCPBr. Pesquisadora Convidada no Grupo de Pesquisa de "Artes em Tecnologias Emergentes" do Programa de Pós Graduação em Design na UNESP Campus Bauru. Cantora, Docente no Magistério Superior, Geek, Nerd, Otaku e Gamer. Apaixonada por Michael Jackson, Macross, Séries e Filmes de Super Heróis, Cervejas e Vinhos. Mais informações sobre mim você encontra em: http://lattes.cnpq.br/8559022477811603.

Deixe um comentário

avatar
 
  Notificações  
Notificar