1 pontos por GN⁺ 2024-07-22 | 1 comentários | Compartilhar no WhatsApp
  • 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 run do 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 this de 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.width estava errado, usa-se watch -l mRect.width e reverse-cont
  • Combinando watchpoint de hardware e execução reversa, encontra-se o ponto em que o valor mudou de 11220 para 12000
  • 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).rpm e instala com sudo dnf install
  • O exemplo de instalação no Ubuntu baixa rr-5.9.0-Linux-$(uname -m).deb e instala com sudo 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
    • Video 7: demonstração rápida de gravação e reprodução no Firefox
    • Video 8: demonstração detalhada dos recursos básicos do rr
    • Video 9: apresentação técnica de alto nível sobre rr por Robert O'Callahan

1 comentários

 
GN⁺ 2024-07-22
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

    • Parece interessante, queria saber se existe algum texto resumindo isso que dê para compartilhar
    • Fico curioso se essa grande base de código poderia ser considerada, ainda que não seja a melhor forma de dizer, como escrita em uma linguagem de tipagem dinâmica
  • 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/

    • Do ponto de vista dos mantenedores do rr, o trabalho do Sid foi bom e nós também o apoiamos
      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 entendo muito bem essa onda de alguns anos para cá de “vamos reescrever X/Y/Z em Rust”
      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?
    • Seria uma boa análise, mas não parece fácil
      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#

    • Eu uso com Zig
      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
    • Uma vez consegui fazer o rr funcionar em um build bem específico de rpython, então pode surpreender que ainda exista interesse nisso
      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...
    • Uso bastante RR com Julia
      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
    • O rr usa o gdb para a parte de depuração propriamente dita, então o que funciona no gdb funciona no rr
      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
    • Também uso com Rust: https://bitshifter.github.io/rr+rust/index.html#1
  • 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

    • Ao executar o rr, usar a opção de chaos mode pode revelar alguns problemas de concorrência
      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
    • Na prática, já tive um bug de concorrência que consegui capturar com o rr
      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
    • Outra coisa que o rr infelizmente não suporta é GPU
      Eu adoraria usar, mas a maior parte do que faço envolve GPU de alguma forma
    • Comigo é parecido
      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

    [...] basta clicar no valor incorreto. Como há todo o histórico do programa, o Pernosco pode explicar imediatamente de onde esse valor veio. O valor é rastreado de volta através de eventos como memcpy ou movimentações para dentro e para fora de registradores, e cada etapa até chegar ao ponto em que ele se “originated” é mostrada em um painel “Dataflow” que se abre automaticamente. Não é preciso ler ou entender o código e imaginar o que pode ter acontecido; você pode simplesmente perguntar ao depurador o que realmente aconteceu.
    https://pernos.co/examples/race-condition

  • 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

    • Se alguém quiser mencionar isso, quase certamente é porque não chegou a usar de verdade
      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
    • Usei uma vez com sucesso a depuração reversa embutida do GDB em uma plataforma que o rr ainda não suportava na época
      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
    • A implementação de replay embutida no gdb impõe ao programa uma lentidão de cerca de 10000x
      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?”
    • Se minha memória estiver correta, o rr surgiu antes da solução do gdb
  • 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 WinDbg usa uma implementação de viagem no tempo baseada em emulação no nível de instrução, então vem com a lentidão de 10 a 20x típica dessa técnica
      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

    • Antes disso, acho que existia o AMD SimNow
      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

    • Sim
      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)