rr para C/C++: depurador de grabación y reproducción
(rr-project.org)- 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
runde 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
thisde 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.widthera incorrecto, se usawatch -l mRect.widthyreverse-cont - Al combinar watchpoints por hardware con reverse execution, se encuentra el punto donde el valor cambió de
11220a12000 - 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).rpme instalarlo consudo dnf install - En Ubuntu, el ejemplo de instalación consiste en descargar
rr-5.9.0-Linux-$(uname -m).debe instalarlo consudo 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
1 comentarios
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.
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/
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 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.
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#.
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.
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...
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.
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.
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.
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.
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.
Me gustaría usarlo, pero la mayor parte de mi trabajo involucra una GPU de una forma u otra.
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
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
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
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
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
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-...
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
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)