Detalhamento da Compilação de Procedimentos no MIPS - Embarcados
No artigo anterior mostrei como funcionam os procedimentos aninhados recursivos no Assembly MIPS. Veja agora a compilação de Procedimentos nesse core.
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.
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.
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.
$a0 = integer
Recebe um inteiro e o imprime no console
$f12 = float
Imprime um único número de ponto flutuante
$f12 = double
Imprime um número de precisão dupla
$a0 = string
Recebe um ponteiro para uma string terminada em nulo e a escreve no console
Integer em $v0
Leem uma linha inteira da entrada incluindo o caractere de newline. Caracteres após o número são ignoradoso
Float em $f0
Double em $f0
$a0 = buffer
$a1 = tamanho
Igual a fgets do UNIX
$a0 = valor
Endereço em $v0
Retorna um ponteiro para um bloco de memória contendo n bytes adicionais
Interrompe o programa que está sendo executado
$a0 = char
Escreve um único caractere
Char em $v0
Le um único caractere
$a0 = nome de arquivo (string)
$a1 = flags
$a2 = modo
Descritor de arquivo em $a0
Chamadas da biblioteca padrão do UNIX
$a0 = descritor de arquivo
$a1 = buffer
$a2 = tamanho
Número de caracteres lidos em $a0
Número de caracteres escritos em $a0
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:
main(){ n1 = 10; n2 = 15; printf(soma(n1,n2)); } int soma ( int n1, int n2) { return ( n1 + n2 ); }
n2 = 15;
printf(soma(n1,n2));
int soma ( int n1, int n2) {
return ( n1 + n2 );
Vou passar o código inteiro primeiro e depois vou explicando linha por linha ok, então, olhem atentamente:
.text .globl main main: subu $sp, $sp, 32 sw $ra, 20($sp) sw $fp, 16($sp) addiu $sp, $sp, 28 li $a0, 10 li $a1, 15 jal soma move $a1, $v0 .data $LC: .asciiz " a soma é: " .text li $v0, 4 la $a0, $LC syscall li $v0, 1 move $a0, $a1 syscall lw $ra, 20($sp) lw $fp, 16($sp) addiu $sp, $sp, 32 j fim # encerra o programa .text soma: subu $sp, $sp, 32 sw $ra, 20($sp) sw $fp, 16($sp) addiu $fp, $sp, 28 sw $a0, 0($fp) add $s0, $a0, $a1 add $v0, $s0, $zero jr $ra fim: li $v0, 10 syscall
subu $sp, $sp, 32
sw $ra, 20($sp)
sw $fp, 16($sp)
addiu $sp, $sp, 28
li $a1, 15
jal soma
move $a1, $v0
$LC:
.asciiz " a soma é: "
la $a0, $LC
move $a0, $a1
lw $ra, 20($sp)
lw $fp, 16($sp)
addiu $sp, $sp, 32
j fim # encerra o programa
addiu $fp, $sp, 28
sw $a0, 0($fp)
add $s0, $a0, $a1
add $v0, $s0, $zero
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.
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!
Funções e Procedimentos – Parte 1
Técnicas de Mapeamento de Memória em Linguagem C
<< Compilando Procedimentos Recursivos e Aninhados no MIPSMIPS: Instruções de Multiplicação >>
AnteriorAnteriorWebinar Gravado: Soluções de Teste para Tecnologias Emergentes 5G-NR e IoT-LPWAN
PróximoPrograma de Parcerias em IoT 2018 – Selecionados para fase de desenvolvimento de projetosPróximo