O que existe dentro de um programa pausado? Registradores, memória e stack traces
O Vigia já congelou o processo. Agora precisamos responder: onde ele está, quais valores carrega e quem chamou a função atual?
42 soma += item;
43● mostre(soma);
44 retorne soma;RIP 0x4011a7
RSP 0x7fff…
CC → instrução originalVOCABULÁRIO VISUAL
As peças que tornam a pausa observável.
O kernel guarda registradores da thread, memória do processo e motivo da parada. Memória é compartilhada entre threads; registradores e stack corrente pertencem a cada thread. O Vigia enumera /proc/PID/task e permite thread N, embora os fixtures usem uma thread.
RIP → próxima instrução RSP → topo atual da stack RBP → base do frame, quando preservado RAX → retorno inteiro comum RDI, RSI, RDX, RCX, R8, R9 → primeiros argumentos inteiros
(vigia) registers
rip 0x0000000000401172 <nivel_tres+18>
rsp 0x00007fffffffe210
rbp 0x00007fffffffe240
rdi 0x0000000000000007Isso retoma registradores e chamadas dos artigos de CPU e funções, agora observados num processo real.
Antes de ler, o Vigia consulta mapas do processo, valida overflow de endereço + tamanho e recusa regiões ilegíveis. Leituras usam palavras e remontam bytes. No x86_64, o byte menos significativo vem primeiro na memória.
endereço 00 01 02 03 04 05 06 07 bytes 78 56 34 12 00 00 00 00 valor 0x0000000012345678 ← little-endian
(vigia) examine 0x7fffffffe210 32
7fffffffe210: 40 e2 ff ff ff 7f 00 00 84 11 40 00 00 00 00 00Com frame pointers, cada frame começa com o RBP anterior e, logo acima, o endereço de retorno. Seguimos essa cadeia apenas enquanto os endereços são alinhados, crescentes dentro da stack e os retornos caem em regiões executáveis.
endereços maiores
[retorno para main] frame nivel_um
[RBP anterior] ◀ RBP
[retorno para nivel_um] frame nivel_dois
[RBP anterior]
[retorno para nivel_dois] frame nivel_tres
◀ RSP
endereços menoresfn unwind_frame(pid: Pid, rbp: u64) -> Result<Frame> {
let previous_rbp = read_u64(pid, rbp)?;
let return_addr = read_u64(pid, rbp + 8)?;
validate_frame(previous_rbp, return_addr)?;
Ok(Frame { previous_rbp, return_addr })
}Ordenamos símbolos de função por endereço. Para cada retorno, encontramos o maior símbolo que não ultrapassa o endereço e mostramos o deslocamento.
(vigia) backtrace
#0 0x401172 nivel_tres+18
#1 0x4011a4 nivel_dois+31
#2 0x4011ce nivel_um+24
#3 0x40120a main+42Parâmetros ainda vivos podem estar nos registradores da ABI. Locais do fixture ficam em offsets conhecidos de RBP, mas o Vigia não generaliza: otimização pode movê-los, eliminá-los ou mantê-los apenas em registradores.
-fomit-frame-pointer, tail calls e inlining quebram a cadeia simples. Executáveis podem trazer regras de unwind em .eh_frame. O Vigia 0.2 reconhece o limite e usa regras simples da seção; não tenta implementar todas as expressões.
| Build | Resultado |
|---|---|
-O0 -fno-omit-frame-pointer | cadeia RBP completa |
-O2 | frames podem sumir ou fundir |
| sem símbolos | endereços sem nomes |
Agora lemos registradores, memória, instruções e frames e produzimos stack trace simbólico. Porém 0x401172 não contém “fixture.c:17”. Essa ponte depende de informações deixadas pelo compilador: DWARF.
Fontes: System V AMD64 ABI e proc_pid_maps(5).
PROJETO COMPLETO · ESTADO EXATO DESTE CAPÍTULO
Baixe o Vigia 0.2.
Pacote exclusivo com código Rust, fixture em C, opções de compilação, testes e limites documentados para Linux x86_64. O Vigia controla apenas filhos criados por ele.