Funções, convenção de chamada e stack frames

TL;DR


1. As duas instruções que definem chamadas

1
jalr x0, x1, 0   # salta para ra; descarta o link

Forma pseudo-instrução:

1
ret

2. Funções leaf vs non-leaf


3. Um stack frame canônico

Um prólogo/epílogo RV32 típico (não universal) é assim:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
addi sp, sp, -16
sw   ra, 12(sp)
sw   s0,  8(sp)
addi s0, sp, 16
...
# corpo da função
...
lw   ra, 12(sp)
lw   s0,  8(sp)
addi sp, sp, 16
ret

O que isso faz


4. Na prática: forçar um stack frame claro

Crie:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// src/stack_demo.c
#include "types.h"
#include "uart.h"

__attribute__((noinline))
u32 inner(u32 a, u32 b) {
  u32 x = a ^ 0xA5A5A5A5u;
  u32 y = b + 0x1234u;
  u32 z = x + y;
  return z;
}

__attribute__((noinline))
u32 outer(u32 v) {
  u32 local1 = v + 1u;
  u32 local2 = v + 2u;
  return inner(local1, local2);
}

int main(void) {
  uart_puts("outer=");
  uart_puthex32(outer(100u));
  uart_putc('\n');
  return 0;
}

Compile com flags que ajudam na leitura:

1
2
3
riscv64-unknown-elf-gcc -O0 -g -fno-omit-frame-pointer -ffreestanding -nostdlib \
  -march=rv32im -mabi=ilp32 -T src/link.ld \
  src/start.s src/uart.c src/stack_demo.c -o build/stack_demo.elf

Disassemble:

1
riscv64-unknown-elf-objdump -d -M numeric,no-aliases build/stack_demo.elf | less

O que localizar


5. Depure a pilha com GDB (comandos práticos)

Rode com o stub do GDB:

1
2
qemu-system-riscv32 -S -M virt -nographic -bios none \
  -kernel build/stack_demo.elf -gdb tcp::1234

Em outro terminal:

1
2
3
4
5
gdb-multiarch ./build/stack_demo.elf
(gdb) set architecture riscv:rv32
(gdb) target remote :1234
(gdb) b outer
(gdb) c

Agora inspecione:

(gdb) info registers sp ra s0 a0 a1
(gdb) bt
(gdb) info frame
(gdb) x/32wx $sp
(gdb) x/16i $pc

Interpretando x/32wx $sp

Você está fazendo dump de 32 words de memória a partir do stack pointer atual.


6. Corrupção de pilha: como acontece

Causas comuns:

Bug prático: overwrite off-by-one

Crie:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// src/overflow.c
#include "types.h"
#include "uart.h"

__attribute__((noinline))
u32 victim(u32 x) {
  volatile u32 canary = 0xCAFEBABEu;
  volatile u8 buf[16];

  // BUG: escreve 17 bytes em um buffer de 16 bytes
  for (int i = 0; i <= 16; i++) {
    buf[i] = (u8)i;
  }

  // retorno depende se o canary foi corrompido
  return (canary ^ x);
}

int main(void) {
  uart_puthex32(victim(0x12345678u));
  uart_putc('\n');
  return 0;
}

Compile e rode:

1
2
3
4
5
riscv64-unknown-elf-gcc -O0 -g -fno-omit-frame-pointer -ffreestanding -nostdlib \
  -march=rv32im -mabi=ilp32 -T src/link.ld \
  src/start.s src/uart.c src/overflow.c -o build/overflow.elf

qemu-system-riscv32 -M virt -nographic -bios none -kernel build/overflow.elf

Agora depure e observe o canary:

1
2
qemu-system-riscv32 -S -M virt -nographic -bios none \
  -kernel build/overflow.elf -gdb tcp::1234
(gdb) b victim
(gdb) c
(gdb) p/x canary
(gdb) next
(gdb) p/x canary

7. Argumentos além de a0..a7

O RISC-V usa a0..a7 para os primeiros 8 argumentos inteiros/ponteiros. Argumentos extras são passados na pilha.

Você pode demonstrar isso escrevendo uma função com 10 argumentos e inspecionando como o call site os prepara.


Exercícios

  1. Em stack_demo, identifique quais registradores carregam parâmetros para inner.
  2. Em stack_demo, determine o offset na pilha (a partir de sp ou s0) de local1 e local2, correlacionando disassembly com dumps de memória do GDB.
  3. Crie uma função com 9 parâmetros inteiros e encontre onde o parâmetro #9 vive.
  4. No exemplo de overflow, mude o loop para i < 16 e confirme que o canary permanece intacto.

Como testar suas respostas


Resumo

Você agora entende como chamadas de função no RV32 funcionam: ra, sp, prólogos/epílogos e estrutura de stack frame, e praticou depurar estado de pilha diretamente.

A seguir: linker scripts e mapas de memória, vamos controlar onde código/dados vivem, gerar arquivos .map e explicar por que imagens de firmware costumam começar em endereços como 0x80000000.