Funções e a pilha de memória

Funções e a pilha de memória

Uma função é um bloco nomeado de instruções que você pode usar mais de uma vez. Você salta para ela, ela executa sua tarefa e devolve o controle ao local de onde você a chamou. Isso permite escrever um trecho de lógica uma vez e usá-lo de vários lugares, em vez de repetir as mesmas linhas por todo o programa.

Use para chamar uma função: ele salta para o rótulo e armazena o local de retorno em ra. A função termina com , que salta de volta para o endereço em ra. e são pseudo-instruções: nomes convenientes que o montador expande nas instruções reais, e .

Os argumentos são colocados em a0, a1 e nos registradores seguintes. O valor de retorno é colocado em a0. Esse arranjo é a convenção de chamada, o acordo compartilhado que permite que partes separadas de código funcionem juntas.

Uma função square() para calcular o quadrado
li a0, 6 # argumento
call square # chama, e ra lembra onde voltar
li a7, 1 # square deixa o resultado em a0
ecall
li a7, 10
ecall
square:
mul a0, a0, a0 # a0 = a0 * a0
ret # volta para ra

Siga a chamada. coloca o argumento 6 em a0. salta para o rótulo square e armazena o endereço de retorno em ra. Dentro da função, calcula 6 * 6 = 36 e deixa o resultado em a0, o registrador usado para valores de retorno. salta de volta para a instrução logo após a chamada, onde e imprimem 36, e então o programa termina.

Chamada de função e retorno
caller
call A
...
A
addi sp, sp, -4
sw ra, 0(sp)
call B
lw ra, 0(sp)
addi sp, sp, 4
ret
B
mul a0, a0, a0
ret
STACK
sp →
0x7ffffff0
0x7fffffec

O chamador está prestes a chamar A.

Observe a linha destacada fazer a viagem completa de ida e volta. caller executa , que salva automaticamente o ponto de retorno para caller no registrador ra. Antes que A possa executar , ele encontra um problema: a instrução está prestes a sobrescrever ra com um novo endereço de retorno, destruindo aquele que A precisa para voltar para casa. Para corrigir isso, A abre espaço na pilha () e salva o ra original ali (). Agora é seguro executar . B executa e retorna com um comum, aterrissando de volta dentro de A. Por fim, A restaura o ra original da pilha (), limpa o ponteiro da pilha (), e seu próprio usa esse valor restaurado para saltar com sucesso de volta para caller. Se A tivesse pulado essa etapa, a instrução teria sobrescrito permanentemente ra, e o final de A saltaria para o lugar errado em vez de voltar para casa.

A pilha

Existem apenas alguns registros, portanto, funções que chamam outras funções precisam de espaço temporário. A pilha é a memória que você utiliza por meio do ponteiro de pilha sp: subtraia para liberar espaço, armazene o que for necessário manter e restaure antes de .

A pilha é uma estrutura Last-In, First-Out (LIFO): o valor armazenado mais recentemente é o primeiro que você recupera. No RISC-V, a pilha cresce para baixo na memória. Subtrair de sp reserva um novo espaço que é seguro usar. Quando você recupera os valores, deve fazê-lo na ordem inversa à do armazenamento, ou o fluxo de execução do programa será corrompido.

Salvar e restaurar ra entre chamadas
addi sp, sp, -4 # abre espaço para uma palavra
sw ra, 0(sp) # salva o endereço de retorno
# ... chame outras funções aqui ...
lw ra, 0(sp) # restaura
addi sp, sp, 4 # libera o espaço
ret

Todo / sobrescreve ra, então, se sua função chama outra função, a chamada interna destrói seu endereço de retorno e seu próprio salta para o lugar errado. Para evitar isso, reserva 4 bytes na pilha, e salva ra ali. Depois que as chamadas internas terminam, restaura o endereço de retorno, e devolve o espaço antes de .

⚠
Se você chamou qualquer coisa, restaure ra antes de , e sempre retorne sp ao valor que ele tinha no início. Uma pilha que não corresponde ao seu estado inicial é a causa usual de travamento em programas em assembly.