ADRIANOLAUREANO← Artigos
SÉRIE VIGIA · 01/03PTRACERUST

Como um debugger interrompe um programa? Construindo breakpoints do zero em Rust

Um ponto vermelho aparece ao lado da linha. O programa começa, corre por milhões de instruções e congela exatamente ali. Quem mandou a CPU parar?

ARTIGO 27 · VIGIA 0.1 · PROCESSO E BREAKPOINTS

1processo controlado

1byte alterado

5passos por breakpoint

VIGIA · CONGELANDO UM INSTANTE DA EXECUÇÃO
CÓDIGO-FONTE42 soma += item;
43● mostre(soma);
44 retorne soma;
VIGIA 0.1controle · inspeção · significado
CPU PAUSADARIP 0x4011a7
RSP 0x7fff…
CC → instrução original

VOCABULÁRIO VISUAL

As peças que tornam a pausa observável.

01Debuggeeprocesso observado pelo debugger
02ptraceinterface Linux de controle de processo
03SIGTRAPsinal de interrupção de depuração
04RIPendereço da próxima instrução
05INT 3instrução x86 de breakpoint
06ASLRaleatorização dos endereços de carga
A CPU só conhece bytes e endereços; o Vigia reconstrói o significado por camadas.
01
O debugger não engole o programa.
Dois processos e o kernel entre eles

O executável é carregado pelo sistema como vimos em Como um computador executa um programa por dentro. O Vigia é um processo; o programa observado, ou debuggee, é outro. O kernel autoriza o primeiro a observar eventos e alterar o estado pausado do segundo.

VIGIA ── pedidos ptrace ──▶ KERNEL ── controla ──▶ DEBUGGEE
  │                           │                       │
CLI e breakpoints        permissões e sinais      executa na CPU
O debugger não substitui o processador nem executa o programa dentro de si.
02
Controle antes da primeira instrução do programa.
fork, TRACEME, exec e wait

O Vigia cria um filho com fork. No filho, PTRACE_TRACEME pede rastreamento e execve troca o código pelo executável-alvo. O kernel o pausa e notifica o pai. Só então instalamos breakpoints.

match unsafe { fork()? } {
    ForkResult::Child => {
        ptrace::traceme()?;
        execv(&program, &argv)?;
    }
    ForkResult::Parent { child } => session.wait_initial_stop(child)?,
}

O loop classifica Stopped, PtraceEvent, Exited e Signaled. Um sinal inesperado não é engolido: o usuário decide se deve entregá-lo ao processo.

03
Uma função possui um endereço — mas ele pode se mover.
Símbolos ELF, PIE e ASLR

A tabela de símbolos ELF relaciona calcular a um valor. Em executável não-PIE didático, esse valor já é virtual. Em PIE, ele é relativo à base escolhida pelo loader. O Vigia lê /proc/PID/maps, identifica o mapeamento do executável e calcula endereço_runtime = base + valor_relativo.

Estratégia didática

Os fixtures do primeiro artigo são compilados com símbolos, sem PIE e sem otimização. O Vigia também detecta PIE e mostra o cálculo, sem recomendar desativar ASLR globalmente.

04
O ponto vermelho é um byte CC.
Instalando INT 3 sem destruir os vizinhos

No x86_64, INT 3 foi projetada para depuração e ocupa um byte, 0xCC. ptrace lê e escreve uma palavra de máquina. Portanto preservamos os outros sete bytes.

let word = ptrace::read(pid, address as AddressType)? as u64;
let original = (word & 0xff) as u8;
let patched = (word & !0xff) | 0xcc;
ptrace::write(pid, address as AddressType, patched as i64)?;
palavra original:  55 48 89 E5 48 83 EC 10
alterar byte 0:    CC 48 89 E5 48 83 EC 10
                   ▲ os outros bytes permanecem
O executável no disco não muda; apenas a memória privada do processo controlado.
05
Parar é só o meio da dança.
SIGTRAP, RIP, single-step e reinstalação

Ao executar INT 3, a CPU avança o RIP e gera trap. O endereço real do breakpoint é RIP - 1. Se apenas continuarmos, pularemos a instrução original.

1 instalar CC
2 CPU executa CC → SIGTRAP → RIP já aponta depois
3 restaurar byte original e recuar RIP em 1
4 single-step executa somente a instrução restaurada
5 reinstalar CC e continuar
Essa sequência permite atingir o mesmo breakpoint novamente, inclusive dentro de loops.

O gerenciador guarda endereço, byte original, estado e contador de acertos. Breakpoints múltiplos são independentes; inserir duas vezes no mesmo endereço incrementa referência, não perde o byte original.

06
Uma sessão observável.
run, break, continue, delete e quit
$ vigia ./fixtures/chamadas
(vigia) break calcular
breakpoint 1 em 0x401156 <calcular>
(vigia) run
parado: breakpoint 1 · RIP=0x401156
(vigia) continue
processo terminou com código 0

quit remove breakpoints, envia SIGTERM ao filho, espera um prazo curto e usa SIGKILL somente se necessário. O Vigia não anexa automaticamente a processos externos e não contorna Yama ou permissões.

07
Vigia 0.1: o mecanismo completo, sem mágica.
Testes e limites

Fixtures conhecidos comprovam breakpoint por endereço e símbolo, dois breakpoints simultâneos, repetição num loop, entrega de sinal e limpeza ao sair. Linux x86_64 é exigido. GDB e LLDB tratam threads, arquiteturas, formatos, remotos e muitos eventos que não cabem aqui.

Fontes: ptrace(2), signal(7) e ELF specification.

PROJETO COMPLETO · ESTADO EXATO DESTE CAPÍTULO

Baixe o Vigia 0.1.

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.

Baixar Vigia 0.1 (.zip)Linux x86_64 · Rust · ELF · processos-fixture

O que este artigo fez você pensar?

Dúvidas, experiências e contrapontos ajudam a próxima pessoa a enxergar o assunto por outro ângulo.

Todos passam por moderação. Ao enviar, você concorda com a política de privacidade.

Receba os próximos artigos.

Uma mensagem quando uma nova investigação estiver pronta. Só isso.