
Sobre Habré ya
escribió sobre la intercepción de llamadas al sistema por medio de
ptrace
; Alexa escribió sobre esta publicación mucho más detallada, que decidí traducir.
Por donde empezar
La comunicación entre el programa depurado y el depurador se produce mediante señales. Esto complica enormemente las cosas ya difíciles; para divertirse, puede leer
la sección BUGS de man ptrace
.
Hay al menos dos formas diferentes de comenzar a depurar:
ptrace(PTRACE_TRACEME, 0, NULL, NULL)
convertirá al padre del proceso actual en un depurador. No se requiere asistencia de los padres; man
aconseja suavemente: "Un proceso probablemente no debería hacer esta solicitud si su padre no espera rastrearla". (¿En otra parte del hombre vio la frase "probablemente no debería" ?) Si el proceso actual ya tenía un depurador, la llamada fallará.ptrace(PTRACE_ATTACH, pid, NULL, NULL)
hará que el proceso actual sea un depurador para pid
. Si pid
ya tenía un depurador, la llamada fallará. SIGSTOP
envía al proceso depurado y no continuará funcionando hasta que el depurador lo descongele.
Estos dos métodos son completamente independientes; Puede usar uno u otro, pero no tiene sentido combinarlos.
Es importante tener en cuenta que
PTRACE_ATTACH
no es instantáneo: después de
ptrace(PTRACE_ATTACH)
, generalmente se llama a
waitpid(2)
para esperar hasta que
PTRACE_ATTACH
"funcione".
Puede iniciar el proceso secundario en depuración utilizando
PTRACE_TRACEME
siguiente manera:
static void tracee(int argc, char **argv) { if (ptrace(PTRACE_TRACEME, 0, NULL, NULL) < 0) die("child: ptrace(traceme) failed: %m"); if (raise(SIGSTOP)) die("child: raise(SIGSTOP) failed: %m"); execvp(argv[0], argv); die("tracee start failed: %m"); } static void tracer(pid_t pid) { int status = 0; if (waitpid(pid, &status, 0) < 0) die("waitpid failed: %m"); if (!WIFSTOPPED(status) || WSTOPSIG(status) != SIGSTOP) { kill(pid, SIGKILL); die("tracer: unexpected wait status: %x", status); } } void shim_ptrace(int argc, char **argv) { pid_t pid = fork(); if (pid < 0) die("couldn't fork: %m"); else if (pid == 0) tracee(argc, argv); else tracer(pid); die("should never be reached"); }
Sin una llamada de
raise(SIGSTOP)
, podría resultar que
execvp(3)
se ejecute antes de que el proceso padre esté listo para esto; y luego las acciones del depurador (por ejemplo, interceptar llamadas del sistema) no comenzarán desde el comienzo del proceso.
Cuando se inicia la depuración, cada
ptrace(PTRACE_SYSCALL, pid, NULL, NULL)
"descongelará" el proceso depurado hasta la primera entrada en la llamada al sistema, y luego hasta que la llamada del sistema se vaya.
Ensamblador telequinético
ptrace(PTRACE_SYSCALL)
no devuelve
ninguna información al depurador; simplemente promete que el proceso que se está depurando se detendrá dos veces en cada llamada al sistema. Para obtener información sobre lo que está sucediendo con el proceso depurado, por ejemplo, en qué sistema se detuvo, debe acceder a una copia de sus registros almacenados por el núcleo en un
struct user
en un formato que depende de la arquitectura específica. (Por ejemplo, en x86_64, el número de llamada estará en el campo
regs.orig_rax
, el primer parámetro pasado estará en
regs.rdi
, etc.) Alexa comenta: "se siente como si estuvieras escribiendo código de ensamblaje en C que funciona con los registros del procesador remoto".
En lugar de la estructura descrita en
sys/user.h
, puede ser más conveniente usar las constantes de índice definidas en
sys/reg.h
:
#include <sys/reg.h> /* . */ long ptrace_syscall(pid_t pid) { #ifdef __x86_64__ return ptrace(PTRACE_PEEKUSER, pid, sizeof(long)*ORIG_RAX); #else // ... #endif } /* . */ uintptr_t ptrace_argument(pid_t pid, int arg) { #ifdef __x86_64__ int reg = 0; switch (arg) { case 0: reg = RDI; break; case 1: reg = RSI; break; case 2: reg = RDX; break; case 3: reg = R10; break; case 4: reg = R8; break; case 5: reg = R9; break; } return ptrace(PTRACE_PEEKUSER, pid, sizeof(long) * reg, NULL); #else // ... #endif }
En este caso, dos paradas del proceso de depuración, a la entrada de la llamada del sistema y a la salida de la misma, no difieren en modo alguno desde el punto de vista del depurador; por lo tanto, el depurador debe recordar en qué estado se encuentra cada uno de los procesos depurados: si hay varios, entonces nadie garantiza que un par de señales de un proceso vendrán en una fila.
Descendientes
Una de las opciones de
ptrace
, a saber,
PTRACE_O_TRACECLONE
, garantiza que todos los elementos secundarios del proceso depurado se
PTRACE_O_TRACECLONE
automáticamente cuando
salgan de fork(2)
. Un punto sutil adicional aquí es que los descendientes tomados para la depuración se convierten en "pseudo-hijos" del depurador, y
waitpid
responderá no solo para detener a "hijos inmediatos", sino también para detener la depuración de "pseudo-hijos". El hombre advierte sobre esto:
"No se recomienda configurar el indicador WCONTINUED cuando se llama a waitpid (2): el estado" continuado "es por proceso y consumirlo puede confundir al padre real del rastro". - es decir Los "pseudo-niños" tienen dos padres que pueden esperar a que se detengan. Para el programador del depurador, esto significa que
waitpid(-1)
esperará no solo a que se
waitpid(-1)
inmediatos, sino también a cualquiera de los procesos depurados.
Señales
(Contenido adicional de un traductor: esta información no está en el artículo en inglés)Como ya se mencionó al principio, la comunicación entre el programa depurado y el depurador se produce mediante señales. Un proceso recibe
SIGSTOP
cuando se conecta un depurador, y luego
SIGTRAP
cada vez que sucede algo interesante en el proceso que se está depurando, por ejemplo, una llamada al sistema o la recepción de una señal externa. El depurador, a su vez, recibe
SIGCHLD
cada vez que uno de los procesos depurados (no necesariamente el hijo inmediato) se "congela" o "congela".
ptrace(PTRACE_SYSCALL)
proceso depurado se realiza llamando a
ptrace(PTRACE_SYSCALL)
(antes de la primera señal o llamada del sistema) o
ptrace(PTRACE_CONT)
(antes de la primera señal). Cuando las señales
SIGSTOP/SIGCONT
también se utilizan para fines no relacionados con la depuración, pueden surgir problemas con
ptrace
: si el depurador "descongela" el proceso depurado que recibió
SIGSTOP
, entonces, desde el exterior, parecerá que la señal fue ignorada; si el depurador no "descongela" el proceso que se está depurando, entonces el
SIGCONT
externo no puede "descongelarlo".
Ahora para la parte divertida: Linux prohíbe que los procesos se
depuren ellos mismos , pero no impide la creación de bucles cuando un padre y un hijo se depuran entre sí. En este caso, cuando uno de los procesos recibe una señal externa, se "congela" a través de
SIGTRAP
, luego
SIGCHLD
envía al segundo proceso y también se "congela" a través de
SIGTRAP
. Es imposible sacar a esos "co-depuradores" del punto muerto enviando
SIGCONT
desde el exterior; la única forma es matar (
SIGKILL
) al niño, luego el padre saldrá de la depuración y se "congelará". (Si matas a un padre, el niño morirá con él). Si el niño activa la opción
PTRACE_O_EXITKILL
, el padre depurado por él también morirá.
Ahora sabe cómo implementar un par de procesos que, al recibir cualquier señal, se congelan para siempre y mueren solo juntos. Por qué esto puede ser necesario en la práctica, no explicaré :-)