rr para C/C++ – depurador de gravação e reprodução
(rr-project.org)- rr é uma ferramenta que, no Linux, grava uma vez uma execução que falhou e depois permite reproduzir repetidamente essa mesma execução com o gdb, facilitando rastrear bugs de C/C++ difíceis de reproduzir
- Durante a reprodução, o espaço de endereçamento, os registradores, os dados de chamadas de sistema e o layout de memória permanecem os mesmos em todas as vezes, para que pistas de depuração como endereços de objetos ou a ordem dos eventos não se percam
- Suporta comandos normais do gdb, scripting e integração com IDEs, além de combinar watchpoints de dados de hardware com execução reversa para voltar ao ponto em que um valor foi alterado
- Lida com Firefox, Chrome, QEMU, LibreOffice, programas em Go, cargas de trabalho multiprocessos e até contêineres, mas há limitações no suporte a emulação de núcleo único, memória compartilhada e cobertura de CPU e chamadas de sistema
- É útil para salvar falhas intermitentes ou falhas geradas por fuzzer e injeção aleatória de falhas para análise repetida, reduzindo o custo de correção de bugs e melhorando a qualidade do software
Modelo de depuração do rr
- rr é uma ferramenta de depuração para C/C++ no Linux que, em vez de substituir o gdb, reforça o fluxo de trabalho com gdb com recursos de gravação e reprodução
- Depois de salvar uma execução em que a falha ocorreu, você pode depurar essa mesma execução quantas vezes quiser
- A reprodução não varia de forma não determinística como uma execução ao vivo; ela sempre prossegue com o mesmo estado
- Fornece execução reversa eficiente sob o gdb
- É possível definir breakpoints e data watchpoints
- Dá para usar reverse-execute rapidamente até um ponto de interesse
- É usado em aplicações reais, e vários desenvolvedores o utilizam para corrigir bugs de verdade
Recursos suportados e escopo de uso
- Os recursos do rr são voltados para aplicações reais e fluxos de trabalho baseados em gdb
- Overhead menor do que ferramentas similares, especialmente em cargas de trabalho majoritariamente single-thread
- Suporte a gravação e reprodução de várias aplicações, como Firefox, Chrome, QEMU, LibreOffice e programas em Go
- Suporte a gravação, reprodução e depuração de cargas de trabalho multiprocessos, incluindo contêineres inteiros
- Suporte a scripting do gdb e integração com IDE
- Suporte a traces duráveis, compactados e portáveis entre máquinas
- Oferece Chaos mode, que expõe melhor bugs intermitentes
Fluxo de gravação e reprodução
- A aplicação é gravada com
rr record /your/application --args - Toda a execução, incluindo a falha, é salva em disco e depois pode ser depurada com
rr replay - Durante a reprodução, o que se depura não é a execução ao vivo, mas o trace gravado
- O espaço de endereçamento, o conteúdo dos registradores e os dados de chamadas de sistema permanecem idênticos em toda reprodução
- A maioria dos comandos comuns do gdb pode ser usada normalmente
- Quando for preciso reiniciar a sessão de depuração, use o comando
rundo gdb para reproduzir a gravação desde o início- Mesmo após reiniciar, a mesma execução será reproduzida novamente
- O estado de depuração é preservado entre reinícios
- Até o ponteiro
thisde objetos alocados dinamicamente permanece igual entre sessões de reprodução- Como a alocação de memória é idêntica toda vez, é possível fixar endereços monitorados de forma explícita
Execução reversa e watchpoint
- O rr permite um estilo de depuração que volta do resultado para a causa ao investigar por que um valor foi definido incorretamente
- No exemplo de depuração de layout do Firefox, depois de confirmar que o valor de
mRect.widthestava errado, usa-sewatch -l mRect.widthereverse-cont - Combinando watchpoint de hardware e execução reversa, encontra-se o ponto em que o valor mudou de
11220para12000 - Essa abordagem reduz o trabalho de voltar do ponto em que o problema aparece até o ponto real da alteração
Instalação e início
- Para compilar a partir do código-fonte, basta seguir as instruções de Building And Installing
- Quando os pacotes não servirem, recomenda-se compilar a partir do código-fonte
- Às vezes, mudanças no kernel ou atualizações do sistema operacional exigem mudanças no rr
- O exemplo de instalação no Fedora baixa
rr-5.9.0-Linux-$(uname -m).rpme instala comsudo dnf install - O exemplo de instalação no Ubuntu baixa
rr-5.9.0-Linux-$(uname -m).debe instala comsudo dpkg -i
Como lida com falhas intermitentes
- A motivação original do rr era facilitar a depuração de falhas intermitentes
- Falhas intermitentes são difíceis de depurar porque podem não aparecer em certas execuções
- É possível gravar execuções de teste com baixo overhead e, quando a falha ocorrer, reproduzir repetidamente essa execução no depurador
- A reprodução determinística também permite acumular pistas continuamente na depuração de bugs comuns
- Em depuradores tradicionais, ao executar de novo, o endereço do objeto de interesse ou a ordem de eventos importantes pode mudar, tornando inútil a informação anterior
- No rr, o conhecimento sobre a execução com falha permanece válido mesmo após reproduções repetidas
- Como a depuração normalmente é um processo de voltar do resultado para a causa, poder executar para trás no tempo facilita encontrar a origem
- O rr fornece um sistema prático de gravação e reprodução com baixo overhead e um backend prático para os comandos de execução reversa do gdb
- É uma ferramenta funcional usada regularmente por desenvolvedores em muitos projetos grandes e pequenos
Estado garantido pela reprodução determinística
- O rr grava grupos de processos em espaço de usuário no Linux
- Captura todas as entradas que os processos gravados recebem do kernel e todos os efeitos não determinísticos da CPU
- A reprodução garante a preservação do fluxo de controle no nível de instrução, da memória e do conteúdo dos registradores
- O layout de memória é sempre o mesmo, os endereços dos objetos não mudam, os valores de registradores são idênticos e as chamadas de sistema retornam os mesmos dados
- Fuzzers e ferramentas de injeção aleatória de falhas ficam ainda mais poderosos quando usados com rr
- Essas ferramentas são boas em provocar falhas intermitentes, mas pode ser difícil reproduzir a mesma falha depois
- O rr grava a execução aleatória e, em caso de falha, permite depurar o problema de forma determinística com o registro salvo
Diferenças em relação a ferramentas tradicionais de gravação e reprodução
- O rr implementa a antiga ideia de depuração por record-and-replay com metas de projeto específicas
- O foco inicial foi o Firefox
- Muitas técnicas de gravação e reprodução exigem uma linguagem específica ou não escalam bem, o que dificulta lidar com o Firefox
- Como o Firefox é uma aplicação complexa, se algo for útil para depurá-lo, provavelmente também será útil de forma geral
- A possibilidade de adoção ampla é uma prioridade
- O rr funciona em kernels Linux comuns e hardware de uso geral
- Não exige mudanças na configuração do sistema
- Muitas outras técnicas exigem mudanças no kernel ou a execução do sistema operacional em máquina virtual
- O objetivo é ter baixo overhead em tempo de execução
- Para substituir o fluxo de trabalho com gdb, é preciso obter resultados em velocidade semelhante à de usar o próprio gdb
- Baixo overhead também reduz a interferência nos testes
- A simplicidade do projeto é valorizada
- Evita abordagens que dependem de técnicas complexas, como instrumentação binária dinâmica
- A simplicidade também contribui para a robustez e o baixo overhead do rr
Desempenho e limitações
- O overhead do rr varia conforme a carga de trabalho da aplicação
- Na suíte de testes do Firefox, o desempenho de gravação é prático
- Em alguns casos, o slowdown cai para ≤ 1.2x
- Uma suíte de testes que leva 10 minutos passa a levar cerca de 12 minutos ao ser gravada com rr
- O overhead pode variar bastante dependendo da carga de trabalho
- Em programas majoritariamente single-thread, ele tem overhead muito menor que sistemas concorrentes de gravação e reprodução já conhecidos
- As principais limitações são as seguintes
- Como emula uma máquina de núcleo único, programas paralelos sofrem slowdown por rodarem como se estivessem em um único núcleo
- Não é possível gravar processos que compartilham memória com processos fora da árvore de gravação
- Recursos como X shared memory são desativados automaticamente no processo gravado
- Exige CPU x86 relativamente recente ou certos CPUs ARM, como Apple M1+
- É necessário conhecer todas as chamadas de sistema executadas pelo processo gravado
- Há suporte para uma ampla faixa de chamadas de sistema necessárias para o Firefox e várias aplicações, mas ele não é completo
- Chamadas de sistema não suportadas podem ser reportadas em GitHub issue
- Pode ser necessário atualizar para acompanhar mudanças no kernel, atualizações de bibliotecas do sistema e novas famílias de CPU
Materiais de referência e comunidade
- O Extended Technical Report é um material de visão geral que cobre o funcionamento e o desempenho do rr
- A rr wiki aborda tópicos técnicos relacionados ao rr
- Perguntas podem ser feitas na mailing list ou em #rr on chat.mozilla.org
- Vídeos de demonstração também são oferecidos
1 comentários
Comentários do Hacker News
Já usei o rr com muito sucesso para fazer engenharia reversa em uma base de código grande, combinando watchpoints de mudança de variável com reverse-continue
Levou um tempo para extrair a lógica central mais profunda, mas foi útil
Também vale mencionar que alguém tentou portar o rr para Rust, escreveu cerca de 60 mil linhas e depois arquivou o projeto
Parece um estudo de caso interessante para comparar impactos, vantagens, limitações e dificuldades de reescrever algo de C++ para Rust
https://github.com/sidkshatriya/rd/
Mas um grande motivo para ter sido difícil levar isso para a versão oficial foi que 1) o rr tem muitos ajustes complexos acumulados ao longo de 10 anos para lidar com comportamentos estranhos de kernel/processo, e havia receio de perder isso na portabilidade, e 2) também seria preciso portar o projeto de código-fonte fechado remix[0], construído sobre o rr
[0] https://robert.ocallahan.org/2020/12/rr-remix-efficient-repl...
Não conheço bem Rust, mas imagino que a interoperabilidade com C seja boa. Se o software já funciona bem, qual seria a vantagem de uma reescrita em Rust?
Olhando por alto, o rr 1.0 levou 3 anos, teve grandes contribuições de umas 3 ou 4 pessoas, e parece ter tido pelo menos 5 contribuidores. O rr de hoje é o resultado disso somado a 10 anos de trabalho adicional
Fico me perguntando se ele é mesmo só para C/C++
Pelo meu entendimento limitado, um depurador precisa de uma lista de símbolos, arquivos .pdb do Windows e o equivalente no Linux, entender chamadas de sistema e outras coisas parecidas. Eu achava que ele não se importava muito com o que gerou o binário a ser depurado. Claro, assumindo código nativo
Queria saber se o rr também não funciona com linguagens como Rust, Zig, Odin e Nim. Naturalmente não esperaria isso para linguagens com memória gerenciada como Python, JS e C#
Fica bem prático em conjunto com o alocador do Zig. Na liberação ele escreve bytes 0xaa e não reutiliza o endereço, então fica mais provável haver um crash, e depois dá para colocar um watchpoint naquela memória e voltar até o momento em que foi liberada
https://github.com/python/devguide/issues/1283
https://morepypy.blogspot.com/2016/07/reverse-debugging-for-...
https://github.com/mesalock-linux/mesapy/blob/mesapy2.7/READ...
Ele mostra o sistema apenas pela perspectiva do GDB, mas pode funcionar tanto com linguagens interpretadas quanto compiladas
O que não funciona é quando o driver atualiza diretamente endereços mapeados. CUDA é um exemplo: para reproduzir isso, seria preciso modelar a interação com o driver, e isso já é verdade mesmo antes de chegar ao UVM
Outra coisa boa é que o RR grava a árvore de processos, então fica fácil inspecionar outros processos iniciados pelo executável
Mas o rr precisa encapsular todas as chamadas de sistema, então ele é bem específico de Linux e não roda no Windows. O formato de informação de símbolos no Linux é DWARF
O rr é realmente muito legal, mas quase toda vez que eu decidia tirá-lo do arsenal pesado era por causa de bugs de concorrência, e por isso muitas vezes o rr não conseguia reproduzir o problema
Ainda assim, seria muito bom se algumas linguagens colocassem o rr diretamente na cadeia de ferramentas. Claro, sempre dá para usar rr/gdb “simplesmente”, mas imagine se configurar e usar o rr fosse tão fácil quanto o pdb do Python
Basicamente ele troca com frequência a thread em execução para imitar uma situação em que vários núcleos estão rodando ao mesmo tempo. Isso de fato já encontrou algumas condições de corrida para mim, embora obviamente tenha limitações
Em um job MPI, anexei o rr apenas ao rank 0 e encontrei o ponto em que ordens diferentes de send/recv causavam o problema. Além disso, era um modelo em Python ligado a muita geração de código nativo, então era um problema bem complexo
Eu adoraria usar, mas a maior parte do que faço envolve GPU de alguma forma
Quando eu realmente precisei, era no Mac, mas infelizmente o rr só funciona no Linux
Existe o Undodb, que funciona no Mac e talvez também consiga lidar com multithreading, mas infelizmente custa cerca de 50 mil dólares
Também vale a pena ver o https://pernos.co/, que é baseado em rr, mas adiciona toda a execução do programa como um banco de dados consultável
Dá para fazer coisas como esta
Também existe a depuração reversa embutida no GDB: https://www.sourceware.org/gdb/wiki/ProcessRecord/Tutorial
Eu diria que o rr oferece mais recursos e flexibilidade, mas, de todo modo, o próprio GDB já consegue fazer depuração reversa há bastante tempo
A implementação do GDB é mais conveniente que a do rr porque permite iniciar/parar a gravação quando quiser, mas a eficiência é várias ordens de grandeza pior. Só serve para trechos de código muito pequenos; fora disso, na prática leva uma eternidade ou esgota os recursos
Funcionou e ajudou a rastrear o bug, mas era dolorosamente lento. Tive que reduzir o tamanho da entrada para ficar minimamente utilizável e, felizmente, mesmo depois disso ainda foi possível reproduzir o problema
Se você consegue fazer busca binária até o estado desejado do programa com menos de 10000 reinicializações, isso consome menos tempo de máquina do que usar execução reversa. Na prática, a lentidão é tão grande que muitas vezes já basta mover o depurador interativamente várias vezes até ficar quase no ponto certo e então reiniciar o programa
A única vez em que consegui usar o recurso de replay do gdb de forma útil foi quando eu tinha um arquivo de entrada que fazia o programa travar em menos de 1 segundo após iniciar. Assim, bastava esperar alguns minutos para voltar de “esta variável está errada” até “como esta variável recebeu esse valor incorreto?”
No Windows, também é possível usar o WinDbg para o mesmo propósito
O suporte para depurar problemas de multithreading é melhor
https://www.forrestthewoods.com/blog/windbg-time-travelling-...
O rr usa uma implementação de depuração de viagem no tempo por gravação e replay e, quando bem implementada, isso pode ter um overhead muito menor. Da última vez que vi, o overhead do rr era algo como 2x de lentidão e, se minha memória não falha, já vi outros depuradores de viagem no tempo por gravação e replay na faixa de 10%
10% é 100 vezes mais barato que o WinDbg e baixo o bastante para deixar sempre ligado em produção. É uma diferença que muda o jogo
Há muito tempo o VMWare Workstation também suportava esse tipo de recurso
Funcionava não só para programas em espaço de usuário, mas também para o kernel e drivers dentro da VM. O recurso foi lançado, existiu por algumas versões e depois foi removido por causa de política interna
Ele podia ser estendido com plugins, e os plugins podiam controlar completamente a CPU emulada. Fico curioso se ainda existe algo parecido em algum lugar
Fico curioso se o problema do rr com CPUs Ryzen agora já ficou definitivamente no passado
Estou usando rr o dia todo, todos os dias, em um Threadripper Pro 7950 bem recente para gravar a execução do Firefox, e também junto com o Pernosco. O wiki do rr no GitHub explica como fazer funcionar. Com apenas um pequeno contorno, ele roda de forma muito estável
Também há discussões antigas
https://news.ycombinator.com/item?id=31617600 (junho de 2022)
https://news.ycombinator.com/item?id=18388879 (novembro de 2018)