Los errores son un fenómeno colateral (e indeseado) que se produce al escribir código. Algunos, como los errores sintácticos, son fáciles de detectar y corregir ya que normalmente el compilador, el intérprete o el propio IDE nos avisará de que se nos ha olvidado cerrar un paréntesis, por ejemplo. Sin embargo, otros errores pueden producir intensos dolores de cabeza, como un fallo de segmentación en algún lugar dentro de miles de líneas de código que se desencadena bajo una serie de condiciones específicas.
Existen varias formas de lidiar con los errores, probablemente el método más utilizado es añadir llamadas a la función print() (en todas sus variantes) en distintas partes del código para poder seguir la ejecución del programa y comprobar que valores toman las variables en determinados momentos. Pero también existen herramientas conocidas como depuradores que, a pesar de que a personas como Linus Torvalds no le entusiasmen demasiado, ofrecen funcionalidad útil para combatir errores complejos, y no solo eso, sino que también son una herramienta potente en el análisis de programas con distintos objetivos. En sistemas Unix, el depurador por excelencia es GDB, que permite hacer cuatro cosas principalmente: iniciar un programa especificando parámetros que puedan afectar su comportamiento, detener un programa en determinadas condiciones y momentos, inspeccionar que está ocurriendo dentro del programa, y modificar distintos valores del programa para observar como afecta a su comportamiento. Pero, ¿como puede un depurador como GDB tener control sobre otro proceso?
La funcionalidad de observar y controlar la ejecución de otro proceso se proporciona mediante la llamada al sistema ptrace(), pero para ello primero es necesario adjuntar el proceso «rastreador» (el depurador GDB en este caso) al proceso «rastreado«. A grandes rasgos, la primera forma de conseguirlo es ejecutando el programa desde GDB, de forma que se haga un fork() del proceso y se establezca una relación directa padre-hijo. Por otra parte, también existe la posibilidad de asumir el rol de proceso padre y adjuntarse a un proceso en ejecución mediante la operación PTRACE_ATTACH. Las principales operaciones (entre otras) que ptrace permite realizar sobre el proceso rastreado son:
- Leer contenidos de la memoria (PTRACE_PEEKTEXT, PTRACE_PEEKDATA, PTRACE_PEEKUSER) y escribir en memoria (PTRACE_POKETEXT, PTRACE_POKEDATA, PTRACE_POKEUSER).
- Obtener/leer contenidos en los registros (PTRACE_GETREGS, PTRACE_GETFPREGS, PTRACE_GETREGSET) y escribir en los registros (PTRACE_SETREGS, PTRACE_SETFPREGS, PTRACE_SETREGSET).
- Detener la ejecución (PTRACE_INTERRUPT) y forzar la ejecución de instrucciones una por una (PTRACE_SINGLESTEP).