Procedimentos e pilha

Chamadas leaf e non-leaf, registos preservados, frames e passagem de argumentos.

TLDR

Perguntar sobre esta página

ChatGPTClaudePerplexityGrokGeminiCopiar e abrirDeepSeekCopiar e abrir

Chamar uma rotina exige guardar um endereço de retorno e acordar onde seguem os argumentos e o resultado. A convenção de chamadas também distingue os registos que a rotina pode alterar dos que tem de repor, para que o chamador possa continuar a execução sem conhecer todas as instruções da rotina chamada.

Chamar e voltar

jal ra,rotina coloca o endereço da instrução seguinte em ra e muda o PC. ret equivale a jalr zero,0(ra), por isso volta sem criar um novo endereço de retorno.

Se a chamada estiver em 0x00400020, ra recebe 0x00400024. Uma rotina leaf não chama outras rotinas e pode usar esse ra diretamente para voltar, enquanto uma non-leaf faz outra chamada que sobrescreve ra e tem de preservar o retorno para o seu próprio chamador antes dessa chamada. Isto não contradiz ra ser caller-saved: é precisamente porque a chamada interior pode alterá-lo que a non-leaf guarda o valor de que ainda precisa.

Quem conserva o quê

RegistosRegra
a0 a a7, t0 a t6, raCaller-saved: podem mudar numa chamada
s0 a s11, spCallee-saved: o chamado repõe o valor anterior
gp, tpReservados pela convenção, não usar como temporários

Os primeiros oito argumentos inteiros seguem em a0 a a7; resultados inteiros usuais em a0 e a1. Mais argumentos podem seguir na pilha, segundo o contrato de chamada.

Se o chamador precisa de t0 depois da chamada, tem três opções: guardá-lo na sua própria pilha, recalculá-lo depois ou movê-lo para um registo s. A terceira só vale se esse s já estiver preservado no seu próprio frame. A obrigação mantém-se: esse s tem de voltar intacto ao seu próprio chamador se a rotina respeita a ABI. Se o chamado usa s0, guarda o s0 que recebeu e restaura-o antes de voltar. Um registo ser callee-saved não significa que já contenha um valor útil para a rotina: significa que o chamador o vai reencontrar intacto.

Frames e alinhamento

A pilha cresce para endereços menores. Reservar 16 bytes faz addi sp,sp,-16; libertar faz addi sp,sp,16. A ABI normal mantém sp alinhado a 16 bytes. Um frame pode reservar mais espaço do que os valores guardados para cumprir esse alinhamento.

Não escrevas abaixo de sp sem reservar o espaço. Guarda e recupera os valores nos mesmos offsets. Todos os caminhos de retorno têm de libertar o frame e restaurar o que foi preservado.

Exemplo non-leaf

Queremos f(x)=2(x+1)f(x)=2(x+1). A rotina dobro é leaf e devolve 2x2x. A rotina mais_um_dobro calcula x+1, chama dobro e devolve o resultado. Guarda ra porque a chamada interior o altera; guarda s0 porque o usa.

Chamada non-leaf com preservação
.text
.globl main
main:
    andi sp, sp, -16
    li s0, 99
    li a0, 7
    jal ra, mais_um_dobro
    li a7, 1
    ecall
    li a0, 10
    li a7, 11
    ecall
    mv a0, s0
    li a7, 1
    ecall
    li a7, 10
    ecall
mais_um_dobro:
    addi sp, sp, -16
    sw ra, 12(sp)
    sw s0, 8(sp)
    addi s0, a0, 1
    mv a0, s0
    jal ra, dobro
    lw s0, 8(sp)
    lw ra, 12(sp)
    addi sp, sp, 16
    ret
dobro:
    slli a0, a0, 1
    ret
Resultado

Executa o código para ver a saída e os gráficos.

Se SS é o sp recebido, reservar 16 bytes põe sp em S−16S-16. Os offsets são relativos ao novo sp:

EndereçoOffset no frameConteúdo
SSFora do frameLimite superior; sp recebido
S−4S-412(sp)Word com ra recebido
S−8S-88(sp)Word com s0 recebido, 99
S−16S-16 a S−9S-90 a 7Espaço não usado

O frame ocupa os 16 bytes de S−16S-16 a S−1S-1. Descer sp reserva endereços; não guarda automaticamente os registos.

A rotina coloca 8 em s0 e passa esse valor em a0. A chamada a dobro modifica ra e produz o resultado em a0. O epílogo restaura s0 e ra sem alterar a0, liberta os 16 bytes e volta. Guardar ra depois do jal preservaria o retorno interior, não o retorno correto.

No exemplo executável, main alinha o sp inicial do simulador com andi sp,sp,-16. Isso é preparação do programa principal, não uma operação para uma rotina fazer ao sp do chamador. Uma rotina comum assume o alinhamento acordado e deve devolver exatamente o sp recebido.

Chamada auxiliar dentro de um ciclo

Para contar carateres alfanuméricos, a rotina percorre a string com o apontador em s0 e o contador em s1, ambos preservados no seu frame. Em cada iteração copia o byte para a0 com lbu, chama is_alnum e só depois avança s0 e soma o resultado de a0 a s1. Guardar o apontador em t0 falharia porque a chamada pode alterar t0 (caller-saved).

Contar alfanuméricos com auxiliar em ciclo
.data
texto: .asciz "A7"
.text
.globl main
main:
  andi sp, sp, -16
  la a0, texto
  jal ra, conta_alnum
  li a7, 1
  ecall
  li a7, 10
  ecall
conta_alnum:
  # a0: base da string asciz; devolve a contagem em a0
  # s0: apontador, s1: contador; esta rotina preserva ambos para o chamador
  addi sp, sp, -16
  sw ra, 12(sp)
  sw s0, 8(sp)
  sw s1, 4(sp)
  mv s0, a0
  li s1, 0
ciclo:
  lbu t0, 0(s0)
  beq t0, zero, fim
  mv a0, t0
  jal ra, is_alnum
  add s1, s1, a0
  addi s0, s0, 1
  j ciclo
fim:
  mv a0, s1
  lw s1, 4(sp)
  lw s0, 8(sp)
  lw ra, 12(sp)
  addi sp, sp, 16
  ret
is_alnum:
  # a0: byte; devolve 1 se letra ou digito ASCII, 0 caso contrario
  # so usa a0 e t1 (caller-saved); nao toca em s0/s1
  li t1, 48
  bltu a0, t1, nao
  li t1, 58
  bltu a0, t1, sim
  li t1, 65
  bltu a0, t1, nao
  li t1, 91
  bltu a0, t1, sim
  li t1, 97
  bltu a0, t1, nao
  li t1, 123
  bltu a0, t1, sim
nao:
  li a0, 0
  ret
sim:
  li a0, 1
  ret
Resultado

Executa o código para ver a saída e os gráficos.

O programa completo testa "A7" e imprime 2:

Byte em s0a0 antes da auxiliara0 depoiss1 depoisPasso seguinte
A6511Avançar s0 um byte
75512Avançar s0 um byte
00Não há chamada2, resultado da contagem2Restaurar o frame e voltar

s0 e s1 sobrevivem a cada jal porque is_alnum respeita os registos callee-saved; esta implementação usa apenas a0 e t1. Por sua vez, conta_alnum guardou no seu frame os valores de s0 e s1 recebidos de main e repõe-nos no fim. Preservar o estado do ciclo e preservar o estado do chamador são obrigações em níveis diferentes.

Validar com saída antecipada

Contar alfanuméricos percorre todo o texto. Para saber se todos os bytes são alfanuméricos, basta encontrar um que não seja: nesse caso devolvemos 0 imediatamente. Só devolvemos 1 quando chegamos ao terminador sem encontrar um erro.

Adotamos um contrato explícito: a0 recebe o endereço de uma string ASCII terminada em zero, com memória válida até esse terminador. A auxiliar is_alnum recebe um byte em a0 e devolve exatamente 0 ou 1, respeitando a ABI. A string vazia devolve 1, pois não contém nenhum byte inválido. Se a aplicação exigir pelo menos um carácter, deve rejeitar o terminador inicial num teste separado.

Antes de cada leitura, s0 aponta para o próximo byte e todos os anteriores já passaram na auxiliar. Não precisamos de um contador. Precisamos de conservar s0 entre chamadas, preservar o s0 recebido e guardar o retorno exterior em ra.

# a0: string ASCII terminada em zero; resultado 0 ou 1 em a0
# is_alnum: byte em a0; devolve 0 ou 1; respeita a ABI
# String vazia e valida segundo o contrato desta rotina.
valida_alnum:
    addi sp, sp, -16
    sw ra, 12(sp)
    sw s0, 8(sp)
    mv s0, a0
valida_ciclo:
    lbu a0, 0(s0)
    beq a0, zero, valida_sim
    jal ra, is_alnum
    beq a0, zero, valida_fim
    addi s0, s0, 1
    j valida_ciclo
valida_sim:
    li a0, 1
valida_fim:
    lw s0, 8(sp)
    lw ra, 12(sp)
    addi sp, sp, 16
    ret

O trecho define apenas a rotina; a auxiliar tem o contrato indicado acima. Em A7, chama-a com 65 e 55, depois encontra zero e devolve 1. Em A-7, chama-a com 65 e 45; a segunda chamada devolve 0 e o programa sai sem ler o 7. A string vazia termina sem chamadas à auxiliar.

Os ramos de sucesso e de erro passam pelo mesmo epílogo. Assim ambos restauram s0, ra e sp, sem substituir o resultado em a0. Guardar o apontador apenas em a0 perderia esse endereço na primeira chamada, porque a0 passa a conter o resultado da auxiliar.

Chamadas dentro de ciclos e recursão

Cada chamada recursiva necessita do seu próprio frame para os argumentos ou resultados parciais que ainda vai usar. Não guardes o estado de todas as chamadas numa única variável global. Define primeiro o caso base para que a recursão termine; depois verifica que cada caminho tem um epílogo correto.

Os ecall de impressão e saída pertencem ao ambiente RARS: a7=1 imprime o inteiro de a0, a7=11 imprime um carácter e a7=10 termina. Não substituem a convenção de chamadas entre rotinas e não são serviços portáveis para qualquer sistema operativo.

Para retomar a relação entre a pilha e sp, podes consultar Rodrigo Dias:

Abrir no YouTube

Exercícios

Por que a rotina não volta?

Uma rotina executa jal ra,auxiliar e depois ret, sem guardar o ra recebido. Explica para onde o ret salta e mostra como corrigir.

Pista

O jal interior substitui o retorno exterior.

Mais uma pista

Guarda ra antes do jal e restaura antes do ret.

Solução

O jal coloca em ra o endereço da instrução seguinte dentro da própria rotina. Após a auxiliar voltar, o ret usa esse mesmo endereço interior e não regressa ao chamador exterior. Reserva um frame alinhado, guarda ra, chama, restaura ra e liberta o frame antes de ret. Uma leaf que não chama outras rotinas não precisa desta preservação só por ser chamada.

Confere a tua resposta:

  • A chamada interior sobrescreve ra.
  • Guarda o retorno exterior antes do jal e restaura-o antes do ret.

Erros frequentes

Não guardes ra depois da chamada, porque já perdeu o valor exterior.

Voltar à explicação
Valores vivos numa chamada

Uma rotina guarda um apontador em t0 e um contador em t1, chama uma auxiliar e continua a usá-los. A auxiliar cumpre a ABI mas modifica t0 e t1. Quem tem de corrigir o programa?

Pista

Os registos t são caller-saved, por isso a auxiliar pode alterá-los sem violar a ABI.

Solução

O chamador tem de guardar t0 e t1, ou colocar os valores em s0 e s1, preservando os antigos s0,s1 no seu próprio frame. A auxiliar pode modificar t0,t1 sem violar a ABI. A convenção não garante preservação só porque o nome sugere uma variável importante.

Confere a tua resposta:

  • A correção cabe ao chamador.
  • Preserva valores vivos ou usa registos s com preservação correta.

Erros frequentes

Não imponhas uma restrição extra à auxiliar sem a declarar no contrato.

Voltar à explicação
Espaço e alinhamento

Uma non-leaf precisa de guardar ra,s0,s1, três words. O sp recebido está alinhado a 16. Propõe offsets e o menor tamanho do frame e mostra a libertação.

Pista

12 bytes de dados não obriga a frame de 12 bytes.

Mais uma pista

Arredonda a reserva para manter alinhamento de 16.

Solução

Reserva 16 com addi sp,sp,-16. Pode guardar ra em 12(sp), s0 em 8(sp), s1 em 4(sp). Restaura nesses mesmos offsets, faz addi sp,sp,16 e ret. O espaço em 0(sp) pode ficar livre. Com frame de 12, sp ficaria desalinhado na chamada interior.

Confere a tua resposta:

  • Reserva mínima 16 bytes para manter alinhamento.
  • Offsets distintos dentro do frame, restaurados antes de libertar exatamente 16 bytes.

Erros frequentes

Não escrevas os três valores antes de reservar o frame nem libertes um tamanho diferente.

Voltar à explicação
Resultados parciais independentes

Uma rotina recursiva precisa do argumento original depois da chamada interior. Pode conservá-lo só em a0? Explica que estado pertence a cada frame.

Pista

a0 passa argumentos e recebe resultados.

Mais uma pista

A chamada interior pode alterar a0 e ra.

Solução

Não pode contar com a0 preservado. Cada frame guarda o argumento original ou um resultado parcial necessário, além de ra e de registos s usados. A chamada interior tem o seu próprio frame. Ao voltar, a rotina recupera os seus dados e combina-os com o resultado devolvido. O caso base deve evitar nova chamada e todos os caminhos repõem sp.

Confere a tua resposta:

  • Cada frame guarda o estado necessário da sua chamada.
  • A0 e ra podem mudar na chamada interior; inclui caso base e restauração.

Erros frequentes

Uma variável global única não conserva estados distintos de chamadas simultaneamente ativas.

Voltar à explicação
Rejeitar sem perder o retorno

Escreve uma rotina RV32 que recebe em a0 o endereço de uma string ASCII terminada em zero, válida em memória até esse terminador. Deve devolver 1 se todos os bytes antes de zero forem alfanuméricos e 0 caso contrário. A string vazia devolve 1. Usa a auxiliar is_alnum, que recebe um byte em a0, devolve exatamente 0 ou 1 e pode alterar qualquer registo caller-saved, cumprindo a ABI. Não implementes a auxiliar.

Faz a rotina sair ao encontrar o primeiro byte inválido. Segue A7, A-7 e a string vazia, indicando quantas vezes chama a auxiliar e como repõe o estado do chamador.

Pista

O apontador tem de sobreviver à chamada, mas a0 muda de endereço para byte e depois para resultado. Decide onde guardar cada valor vivo.

Solução

Antes de cada leitura, todos os bytes anteriores passaram na auxiliar. Conservamos o apontador em s0 e guardamos o s0 recebido e ra num frame de 16 bytes. Uma solução é:

# a0: string ASCII terminada em zero; resultado 0 ou 1 em a0
# is_alnum: byte em a0; devolve 0 ou 1; respeita a ABI
# String vazia e valida segundo o contrato desta rotina.
valida_alnum:
    addi sp, sp, -16
    sw ra, 12(sp)
    sw s0, 8(sp)
    mv s0, a0
valida_ciclo:
    lbu a0, 0(s0)
    beq a0, zero, valida_sim
    jal ra, is_alnum
    beq a0, zero, valida_fim
    addi s0, s0, 1
    j valida_ciclo
valida_sim:
    li a0, 1
valida_fim:
    lw s0, 8(sp)
    lw ra, 12(sp)
    addi sp, sp, 16
    ret

A7 faz duas chamadas, com 65 e 55, e devolve 1 ao encontrar o zero. A-7 faz duas chamadas, com 65 e 45; a segunda devolve 0 e a rotina volta sem ler o terceiro byte. Vazio não faz chamadas e devolve 1. Todos os caminhos passam por valida_fim, que repõe s0, ra e sp sem alterar o resultado em a0. Não precisamos de preservar o byte depois da chamada: só interessa o resultado devolvido.

Confere a tua resposta:

  • Testei o zero antes da auxiliar e aceitei vazio segundo o contrato.
  • Rejeitei no primeiro byte inválido, sem ler os restantes.
  • Preservei apontador, ra e s0 recebido, e restaurei sp nos dois resultados.

Erros frequentes

Testar a0 depois de jal testa o resultado da auxiliar, não o byte original. Guardar o apontador num t sem o preservar permite que a auxiliar o destrua.

Voltar à explicação

À tua maneira

Aparência

Modo de cor
Tema
Cor de destaque

Leitura

Fonte de leitura

Álgebra, lógica e uma ideia de cada vez.

Código

Fonte do código

Navegação

Cursor de leitura, sem edição. h, j, k, l movem; / pesquisa nesta página. v seleciona caracteres; V seleciona linhas; y copia. Tab permite destacar ou comentar. Espaço ou Enter abre blocos recolhíveis. Nos cartões, h, j, k, l mudam o foco.

CSS personalizado

Ativa uma sugestão ou guarda o teu CSS. Só muda este navegador.

Usa --page, --text e --accent para acompanhar o tema.

Seletores e recuperação

.site-header, .prose, .course-card, [data-reading-history], [data-playground] e .page-actions.

Abre a aparência com a tecla vírgula ou no rodapé. Se o CSS esconder os controlos, abre esta página sem CSS personalizado.

Pesquisar

Escreve para pesquisar em todo o site.

para escolher · Enter para abrir · Esc para fechar

Copiar fórmula

A cópia automática não está disponível. Copia o LaTeX abaixo.

Pesquisar nesta página

Enter procura · n / N repetem · Esc cancela. Ctrl / Cmd K pesquisa todo o site.

Atalhos de teclado

Clica numa tecla para a mudar. Esc cancela. Backspace desativa.

PesquisarCtrl / Cmd K

Os atalhos não interferem enquanto escreves. Tab e Enter funcionam sempre.