1 puntos por GN⁺ 2024-07-22 | 1 comentarios | Compartir por WhatsApp
  • rr permite grabar una vez una ejecución fallida en Linux y luego reproducir esa misma ejecución repetidamente con gdb, facilitando el rastreo de bugs de C/C++ difíciles de reproducir
  • Durante la reproducción, el espacio de direcciones, los registros, los datos de llamadas al sistema y la disposición de memoria son siempre iguales, por lo que no se pierden pistas de depuración como direcciones de objetos u orden de eventos
  • Soporta comandos comunes de gdb, scripting e integración con IDE, y combina watchpoints de datos por hardware con ejecución inversa para volver al punto donde cambió un valor
  • Maneja Firefox, Chrome, QEMU, LibreOffice, programas en Go, cargas de trabajo multiproceso e incluso contenedores, aunque tiene limitaciones en emulación de un solo núcleo, memoria compartida y cobertura de CPU/llamadas al sistema
  • Permite guardar fallas intermitentes o fallas surgidas de fuzzers e inyección aleatoria de fallos para analizarlas repetidamente, lo que ayuda a reducir el costo de corregir bugs y mejorar la calidad del software

Modelo de depuración de rr

  • rr es una herramienta de depuración de C/C++ para Linux; más que reemplazar gdb, refuerza el flujo de trabajo de gdb con funciones de grabación y reproducción
  • Una vez que se guarda una ejecución donde ocurrió una falla, se puede depurar esa misma ejecución tantas veces como se quiera
  • La reproducción no cambia de forma no determinista como una ejecución en vivo, sino que avanza siempre con el mismo estado
  • Ofrece ejecución inversa eficiente bajo gdb
    • Se pueden configurar breakpoints y data watchpoints
    • Se puede hacer reverse-execute rápidamente hasta el punto de interés
  • Se usa en aplicaciones reales, y varios desarrolladores lo emplean para corregir bugs reales

Funciones soportadas y alcance de aplicación

  • Las capacidades de rr están orientadas a aplicaciones reales y flujos de trabajo basados en gdb
    • Menor overhead que herramientas similares, especialmente en cargas de trabajo mayormente monohilo
    • Soporte de grabación y reproducción para diversas aplicaciones como Firefox, Chrome, QEMU, LibreOffice y programas en Go
    • Soporte para grabar, reproducir y depurar cargas de trabajo multiproceso, incluidos contenedores completos
    • Soporte para scripting de gdb e integración con IDE
    • Soporte para traces durables y comprimidos que pueden moverse entre máquinas
    • Incluye Chaos mode, que ayuda a exponer mejor bugs intermitentes

Flujo de grabación y reproducción

  • La aplicación se graba con rr record /your/application --args
  • Toda la ejecución, incluida la falla, se guarda en disco y luego se puede depurar con rr replay
  • Durante la reproducción no se depura una ejecución en vivo, sino el trace grabado
  • El espacio de direcciones, el contenido de los registros y los datos de llamadas al sistema se mantienen idénticos en cada reproducción
  • Se pueden usar tal cual la mayoría de los comandos comunes de gdb
  • Cuando hay que reiniciar la sesión de depuración, el comando run de gdb reproduce la grabación desde el principio
    • Incluso después de reiniciar, se vuelve a reproducir la misma ejecución
    • El estado de depuración se conserva entre reinicios
  • El puntero this de los objetos asignados dinámicamente también es el mismo en cada sesión de reproducción
    • Como las asignaciones de memoria son idénticas cada vez, se puede hardcodear la dirección a observar

Ejecución inversa y watchpoints

  • rr permite depurar retrocediendo desde el resultado hasta la causa cuando se busca por qué un valor quedó mal establecido
  • En un ejemplo de depuración de layout de Firefox, tras confirmar que el valor de mRect.width era incorrecto, se usa watch -l mRect.width y reverse-cont
  • Al combinar watchpoints por hardware con reverse execution, se encuentra el punto donde el valor cambió de 11220 a 12000
  • Este enfoque reduce el trabajo de volver desde el momento en que aparece el problema hasta el punto real del cambio

Instalación y primeros pasos

  • Para compilar desde el código fuente, hay que seguir las instrucciones de Building And Installing
  • Cuando el paquete no sea adecuado, se recomienda compilar desde el código fuente
    • A veces, cambios del kernel o actualizaciones del sistema operativo requieren cambios en rr
  • En Fedora, el ejemplo de instalación consiste en descargar rr-5.9.0-Linux-$(uname -m).rpm e instalarlo con sudo dnf install
  • En Ubuntu, el ejemplo de instalación consiste en descargar rr-5.9.0-Linux-$(uname -m).deb e instalarlo con sudo dpkg -i

Cómo maneja fallas intermitentes

  • La motivación original de rr era facilitar la depuración de fallas intermitentes
    • Las fallas intermitentes son difíciles de depurar porque en algunas ejecuciones no aparecen
    • Se pueden grabar ejecuciones de prueba con bajo overhead y, cuando ocurre una falla, reproducir esa ejecución repetidamente en el depurador
  • La reproducción determinista permite seguir acumulando pistas incluso al depurar bugs comunes
    • En un depurador normal, al volver a ejecutar pueden cambiar la dirección del objeto de interés o el orden de eventos importantes, haciendo inútil la información previa
    • En rr, el conocimiento sobre la ejecución fallida se mantiene aunque se repita la reproducción
  • Como la depuración suele ser un proceso de ir desde el resultado hacia la causa, poder ejecutar hacia atrás en el tiempo facilita rastrear el origen
  • rr ofrece un sistema de grabación y reproducción de bajo overhead y un backend práctico para los comandos de reverse execution de gdb
  • Es una herramienta funcional que desarrolladores de muchos proyectos grandes y pequeños usan regularmente

Estado garantizado por la reproducción determinista

  • rr graba grupos de procesos de espacio de usuario en Linux
  • Captura todas las entradas que los procesos grabados reciben del kernel y los efectos no deterministas de la CPU
  • La reproducción garantiza la preservación del flujo de control a nivel de instrucciones, la memoria y el contenido de registros
  • La disposición de memoria siempre es la misma, las direcciones de objetos no cambian, los valores de registros son idénticos y las llamadas al sistema devuelven los mismos datos
  • Los fuzzers y las herramientas de inyección aleatoria de fallos se vuelven más potentes cuando se usan con rr
    • Estas herramientas son buenas para provocar fallas intermitentes, pero puede ser difícil reproducir la misma falla otra vez
    • rr graba la ejecución aleatoria y, si falla, permite depurar el problema de forma determinista usando la grabación guardada

Diferencias frente a herramientas existentes de grabación y reproducción

  • rr implementa la vieja idea de record-and-replay debugging con objetivos de diseño específicos
  • El foco inicial fue Firefox
    • Muchas técnicas de grabación y reproducción requieren un lenguaje específico o no escalan bien, lo que dificulta manejar Firefox
    • Como Firefox es una aplicación compleja, si algo resulta útil para depurar Firefox, probablemente también sea útil en general
  • Da prioridad a la posibilidad de distribución
    • rr funciona sobre kernels Linux comunes y hardware de propósito general
    • No exige cambiar la configuración del sistema
    • Muchas otras técnicas requieren modificar el kernel o ejecutar el sistema operativo dentro de una máquina virtual
  • Apunta a bajo overhead en tiempo de ejecución
    • Para reemplazar el flujo de trabajo con gdb, debe entregar resultados a una velocidad similar a la de usar gdb
    • El bajo overhead también reduce la interferencia con las pruebas
  • Prioriza la simplicidad del diseño
    • Evita enfoques que dependen de técnicas complejas como instrumentación binaria dinámica
    • La simplicidad también contribuye a la robustez y bajo overhead de rr

Rendimiento y limitaciones

  • El overhead de rr depende de la carga de trabajo de la aplicación
  • En la suite de pruebas de Firefox, el rendimiento de grabación es práctico
    • En algunos casos, el slowdown baja hasta ≤ 1.2x
    • Una suite de pruebas que tarda 10 minutos tarda alrededor de 12 minutos al grabarse con rr
  • El overhead puede variar mucho según la carga de trabajo
  • En programas mayormente monohilo, tiene un overhead mucho menor que los sistemas de grabación y reproducción competidores conocidos
  • Las principales limitaciones son las siguientes
    • Como emula una máquina de un solo núcleo, los programas paralelos sufren slowdown por ejecutarse en un solo núcleo
    • No se pueden grabar procesos que comparten memoria con procesos fuera del árbol de grabación
    • En procesos grabados, funciones como X shared memory se desactivan automáticamente
    • Requiere una CPU x86 relativamente reciente o ciertas CPU ARM como Apple M1+
    • Debe conocer todas las llamadas al sistema que ejecutan los procesos grabados
    • Soporta una amplia gama de llamadas al sistema necesarias para Firefox y varias aplicaciones, pero no es completo
    • Las llamadas al sistema no soportadas se pueden reportar en un issue de GitHub
    • Puede requerir actualizaciones para adaptarse a cambios del kernel, actualizaciones de bibliotecas del sistema y nuevas familias de CPU

Recursos y comunidad

  • El Extended Technical Report es un material introductorio que cubre cómo funciona rr y su rendimiento
  • La rr wiki trata temas técnicos relacionados con rr
  • Las preguntas se pueden hacer en la mailing list o en #rr on chat.mozilla.org
  • También hay videos de demostración disponibles
    • Video 7: demo rápida de grabación y reproducción de Firefox
    • Video 8: demostración detallada de las funciones básicas de rr
    • Video 9: presentación técnica de alto nivel sobre rr por Robert O'Callahan

1 comentarios

 
GN⁺ 2024-07-22
Opiniones de Hacker News
  • He usado con mucho éxito los puntos de interrupción por cambio de variable junto con reverse-continue en rr para hacer ingeniería inversa de bases de código grandes.
    Me tomó algo de tiempo extraer la lógica central que estaba muy enterrada, pero fue útil.

    • Suena interesante; me pregunto si habrá algún artículo resumido que se pueda compartir.
    • Me pregunto si esa base de código grande, aunque no sea la forma más precisa de decirlo, podría considerarse escrita en un lenguaje de tipado dinámico.
  • También vale la pena mencionar que alguien intentó portar rr a Rust y, después de escribir unas 60 mil líneas, archivó el proyecto.
    Sería un caso de estudio interesante para comparar el impacto, las ventajas, las limitaciones y las dificultades de reescribir de C++ a Rust.
    https://github.com/sidkshatriya/rd/

    • Desde la perspectiva de los mantenedores de rr, el trabajo de Sid fue bueno y lo apoyamos.
      Pero las grandes razones por las que era difícil convertirlo en la versión oficial fueron: 1) rr tiene muchas correcciones complejas acumuladas durante 10 años para lidiar con comportamientos raros del kernel/proceso, y nos preocupaba perderlas durante el port, y 2) también había que portar remix[0], un proyecto de código cerrado construido sobre rr.
      [0] https://robert.ocallahan.org/2020/12/rr-remix-efficient-repl...
    • No termino de entender la corriente de los últimos años de “reescribamos X/Y/Z en Rust”.
      No conozco bien Rust, pero imagino que su interoperabilidad con C es buena. Si el software ya funciona bien, me pregunto cuál es el beneficio de una reescritura en Rust.
    • Sería un buen análisis, pero no creo que sea fácil.
      Mirando por encima, rr 1.0 tomó 3 años, tuvo contribuciones grandes de unas 3 o 4 personas y parece que participaron al menos 5. El rr actual es el resultado de sumar 10 años de trabajo adicional a eso.
  • Me pregunto si de verdad es solo para C/C++.
    Por lo que entiendo de forma limitada, un depurador necesita una lista de símbolos, cosas como los archivos .pdb de Windows y sus equivalentes en Linux, entender las llamadas al sistema y otras cosas similares. Pensaba que no le importa demasiado qué generó el binario que se está depurando. Claro, asumiendo que sea código nativo.
    Me pregunto si rr no funciona también con lenguajes como Rust, Zig, Odin o Nim. Obviamente no esperaría que funcione con lenguajes que usan memoria administrada, como Python, JS o C#.

    • Lo estoy usando con Zig.
      Es bastante cómodo junto con los asignadores de Zig. Al liberar, escriben bytes 0xaa y no reutilizan la dirección, así que es más probable que ocurra un crash; luego se puede poner un watchpoint sobre esa memoria y retroceder hasta el momento en que fue liberada.
    • Hace un tiempo logré hacer funcionar rr con una compilación muy específica de rpython, y quizás sorprenda que siga habiendo interés en el tema.
      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...
    • En Julia usamos mucho RR.
      Aunque solo muestra el sistema desde el punto de vista de GDB, puede funcionar tanto con lenguajes interpretados como compilados.
      Donde no funciona es cuando un driver actualiza directamente una dirección mapeada. CUDA es un ejemplo de eso: para reproducirlo habría que modelar la interacción con el driver, y eso ya ocurre incluso antes de llegar a UVM.
      Otra cosa buena es que RR registra el árbol de procesos, así que permite revisar fácilmente otros procesos lanzados por el ejecutable.
    • rr usa gdb para la parte real de depuración, así que lo que funcione en gdb también funciona en rr.
      Eso sí, rr tiene que envolver todas las llamadas al sistema, por lo que es muy específico de Linux y no corre en Windows. El formato de información de símbolos en Linux es DWARF.
    • También se usa con Rust: https://bitshifter.github.io/rr+rust/index.html#1
  • rr es realmente genial, pero casi siempre que decidía sacarlo como “artillería pesada” resultaba ser un bug de concurrencia, y por eso muchas veces rr no podía reproducirlo.
    Aun así, sería buenísimo que algunos lenguajes incorporaran rr directamente en su toolchain. Claro que siempre se puede “simplemente” usar rr/gdb, pero imagina que configurar y usar rr fuera tan fácil como usar pdb en Python.

    • Al ejecutar rr, la opción chaos mode puede ayudar a exponer algunos problemas de concurrencia.
      Básicamente cambia con frecuencia el hilo en ejecución para simular una situación en la que varios núcleos ejecutan al mismo tiempo. En la práctica me ayudó a encontrar algunas condiciones de carrera, aunque por supuesto tiene límites.
    • De hecho hubo un bug de concurrencia que sí pude capturar con rr.
      En un trabajo MPI adjunté rr solo al rank 0 y encontré el punto donde distintos órdenes de send/recv causaban el problema. Además, era un problema bastante complejo porque era un modelo en Python conectado con mucha generación de código nativo.
    • Otra cosa que lamentablemente rr no soporta es la GPU.
      Me gustaría usarlo, pero la mayor parte de mi trabajo involucra una GPU de una forma u otra.
    • Me pasa algo parecido.
      Cuando realmente lo necesité fue en Mac, pero por desgracia rr solo funciona en Linux.
      Existe Undodb, que funciona en Mac y quizás también pueda con multithreading, pero lamentablemente cuesta alrededor de 50 mil dólares.
  • También vale la pena ver https://pernos.co/, que está basado en rr, pero además convierte toda la ejecución del programa en una base de datos consultable
    Permite hacer cosas como esta

    [...] solo hay que hacer clic en el valor incorrecto. Como tiene todo el historial del programa, Pernosco puede explicar de inmediato de dónde vino ese valor. El valor se rastrea hacia atrás a través de eventos como memcpy o movimientos hacia y desde registros, y cada paso hasta llegar al punto donde el valor se “originó” se muestra en un panel “Dataflow” que se abre automáticamente. Sin tener que leer ni entender el código ni imaginar qué pudo haber pasado, simplemente puedes preguntarle al depurador qué pasó realmente.
    https://pernos.co/examples/race-condition

  • También existe la depuración inversa integrada en GDB: https://www.sourceware.org/gdb/wiki/ProcessRecord/Tutorial
    Creo que rr ofrece más funciones y flexibilidad, pero de todos modos GDB en sí ya puede hacer depuración inversa desde hace un tiempo

    • Si quieres mencionar esto, es casi seguro que en realidad no lo has usado
      La implementación de GDB es más cómoda que rr porque permite iniciar/detener la grabación cuando uno quiera, pero es varios órdenes de magnitud menos eficiente. Solo sirve para fragmentos de código muy pequeños; fuera de eso, en la práctica tarda una eternidad o se queda sin recursos
    • Una vez usé con éxito la depuración inversa integrada de GDB en una plataforma que rr todavía no soportaba en ese momento
      Funcionó y ayudó a rastrear el bug, pero fue dolorosamente lenta. Tuve que reducir el tamaño de la entrada para que fuera usable, y por suerte el problema siguió reproduciéndose después de hacerlo
    • La implementación de reproducción integrada de gdb impone al programa una ralentización de aproximadamente 10000 veces
      Si puedes hacer una búsqueda binaria hasta el estado del programa que quieres con menos de 10000 reinicios, usarás menos tiempo de máquina que con ejecución inversa. En la práctica, la ralentización es tan grande que muchas veces alcanza con mover el depurador de forma interactiva varias veces hasta un estado casi correcto y luego reiniciar el programa
      La razón por la que pude usar de forma útil la reproducción de gdb fue que tenía un archivo de entrada que hacía crashear el programa en menos de 1 segundo desde el arranque. Así pude retroceder desde “esta variable está mal” hasta “¿cómo se configuró esta variable con ese valor incorrecto?” esperando solo unos minutos
    • Si mal no recuerdo, rr salió antes que lo de gdb
  • En Windows se puede usar WinDbg para el mismo propósito
    Tiene mejor soporte para depurar problemas multihilo
    https://www.forrestthewoods.com/blog/windbg-time-travelling-...

    • WinDbg usa una implementación de viaje en el tiempo basada en emulación a nivel de instrucciones, por lo que sufre una ralentización de 10 a 20 veces asociada a esa técnica
      rr usa una implementación de depuración de viaje en el tiempo basada en grabación-reproducción y, si está bien implementada, puede tener una sobrecarga mucho menor. La última vez que lo vi, la sobrecarga de rr rondaba una ralentización de 2 veces y, si recuerdo bien, también vi otros depuradores de viaje en el tiempo con grabación-reproducción alrededor del 10%
      Un 10% es 100 veces más barato que WinDbg y lo suficientemente bajo como para dejarlo siempre activado en producción. Esa diferencia cambia las reglas del juego
  • Hace mucho, VMWare Workstation también soportaba esta función
    No solo para programas en espacio de usuario, sino también para el kernel y los drivers dentro de la VM. La función se lanzó y existió durante varias versiones, pero luego fue eliminada por política interna

    • Creo que antes de eso existía AMD SimNow
      Era extensible mediante plugins, y un plugin podía tener control total de la CPU emulada. Me pregunto si todavía existe algo parecido en algún lado
  • Me pregunto si el problema de rr con CPUs Ryzen ya quedó definitivamente en el pasado


    • Uso rr todo el día, todos los días, para grabar ejecuciones de Firefox en un Threadripper Pro 7950 bastante reciente, y también lo uso con Pernosco. La wiki de rr en GitHub explica cómo hacerlo funcionar. Con una pequeña solución alternativa, funciona de forma muy estable
  • También hay discusiones anteriores
    https://news.ycombinator.com/item?id=31617600 (junio de 2022)
    https://news.ycombinator.com/item?id=18388879 (noviembre de 2018)