
On Habré a déjà
écrit sur l'interception d'appels système au moyen de
ptrace
; Alexa a écrit sur ce post beaucoup plus détaillé, que j'ai décidé de traduire.
Par où commencer
La communication entre le programme débogué et le débogueur se fait à l'aide de signaux. Cela complique grandement les choses déjà difficiles; pour le plaisir, vous pouvez lire
la section BUGS de man ptrace
.
Il existe au moins deux façons différentes de démarrer le débogage:
ptrace(PTRACE_TRACEME, 0, NULL, NULL)
fera du parent du processus actuel un débogueur pour lui. Aucune assistance n'est requise du parent; man
conseille doucement: "Un processus ne devrait probablement pas faire cette demande si son parent ne s'attend pas à la retrouver." (Ailleurs dans le Mans, avez-vous vu la phrase «ne devrait probablement pas» ?) Si le processus actuel avait déjà un débogueur, alors l'appel échouera.ptrace(PTRACE_ATTACH, pid, NULL, NULL)
fera du processus actuel un débogueur pour pid
. Si pid
avait déjà un débogueur, l'appel échouera. SIGSTOP
envoyé au processus débogué et il ne continuera pas à fonctionner jusqu'à ce que le débogueur le dégivre.
Ces deux méthodes sont complètement indépendantes; Vous pouvez utiliser l'un ou l'autre, mais il est inutile de les combiner.
Il est important de noter que
PTRACE_ATTACH
n'est pas instantané: après l'
ptrace(PTRACE_ATTACH)
de
ptrace(PTRACE_ATTACH)
,
waitpid(2)
est généralement
PTRACE_ATTACH
pour attendre que
PTRACE_ATTACH
"fonctionne".
Vous pouvez démarrer le processus enfant sous débogage à l'aide de
PTRACE_TRACEME
comme suit:
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"); }
Sans appel de
raise(SIGSTOP)
, il se peut que
execvp(3)
s'exécute avant que le processus parent ne soit prêt pour cela; puis les actions du débogueur (par exemple, intercepter les appels système) ne démarreront pas depuis le début du processus.
Lorsque le débogage est démarré, chaque
ptrace(PTRACE_SYSCALL, pid, NULL, NULL)
«décongèle» le processus débogué jusqu'à la première entrée dans l'appel système, puis jusqu'à ce que l'appel système quitte.
Assembleur télékinétique
ptrace(PTRACE_SYSCALL)
ne renvoie
aucune information au débogueur; il promet simplement que le processus débogué s'arrêtera deux fois à chaque appel système. Pour obtenir des informations sur ce qui se passe avec le processus débogué - par exemple, quel système l'appelle arrêté - vous devez monter dans une copie de ses registres stockée par le noyau dans un
struct user
dans un format qui dépend de l'architecture spécifique. (Par exemple, sur x86_64, le numéro d'appel sera dans le champ
regs.orig_rax
, le premier paramètre transmis sera dans
regs.rdi
, etc.) Alexa commente: "on dirait que vous écrivez du code assembleur en C qui fonctionne avec les registres du processeur distant."
Au lieu de la structure décrite dans
sys/user.h
, il peut être plus pratique d'utiliser les constantes d'index définies dans
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 }
Dans ce cas, deux arrêts du processus débogué - à l'entrée de l'appel système et à la sortie de celui-ci - ne diffèrent en aucune manière du point de vue du débogueur; de sorte que le débogueur lui-même doit se souvenir de l'état dans lequel se trouve chacun des processus débogués: s'il y en a plusieurs, alors personne ne garantit qu'une paire de signaux d'un processus viendra d'affilée.
Descendants
L'une des options
ptrace
, à savoir
PTRACE_O_TRACECLONE
, garantit que tous les enfants du processus débogué seront automatiquement débogués lorsqu'ils quitteront
fork(2)
. Un point subtil supplémentaire ici est que les descendants pris pour le débogage deviennent des «pseudo-enfants» du débogueur, et
waitpid
répondra non seulement à l'arrêt des «enfants immédiats», mais aussi à l'arrêt du débogage des «pseudo-enfants». Un homme met en garde contre ceci:
"La définition de l'indicateur WCONTINUED lors de l'appel de waitpid (2) n'est pas recommandée: l'état" continu "est par processus et sa consommation peut confondre le vrai parent du suivi." - c'est-à-dire Les «pseudo-enfants» ont deux parents qui peuvent attendre qu'ils s'arrêtent. Pour le programmeur du débogueur, cela signifie que
waitpid(-1)
attendra non seulement l'arrêt immédiat des enfants, mais également tout processus débogué.
Signaux
(Contenu bonus d'un traducteur: cette information n'est pas dans l'article en anglais)Comme déjà mentionné au tout début, la communication entre le programme débogué et le débogueur se fait à l'aide de signaux. Un processus reçoit
SIGSTOP
lorsqu'un débogueur lui est connecté, puis
SIGTRAP
chaque fois que quelque chose d'intéressant se produit dans le processus en cours de débogage - par exemple, un appel système ou la réception d'un signal externe. Le débogueur, à son tour, reçoit
SIGCHLD
chaque fois que l'un des processus débogués (pas nécessairement l'enfant immédiat) se "fige" ou "se fige".
ptrace(PTRACE_SYSCALL)
processus débogué est effectuée en appelant
ptrace(PTRACE_SYSCALL)
(avant le premier signal ou appel système) ou
ptrace(PTRACE_CONT)
(avant le premier signal). Lorsque les signaux
SIGSTOP/SIGCONT
sont également utilisés à des fins non liées au débogage, des problèmes avec
ptrace
peuvent survenir: si le débogueur «débloque» le processus débogué qui a reçu
SIGSTOP
, de l'extérieur, il semblera que le signal a été ignoré; si le débogueur ne «dégivre» pas le processus en cours de débogage, le
SIGCONT
externe ne peut pas le «dégivrer».
Maintenant, pour la partie amusante: Linux interdit aux processus de se
déboguer eux -
mêmes , mais n'empêche pas la création de boucles lorsqu'un parent et un enfant se déboguent mutuellement. Dans ce cas, lorsqu'un des processus reçoit un signal externe, il "se fige" via
SIGTRAP
- puis
SIGCHLD
envoyé au deuxième processus, et il "se fige" également via
SIGTRAP
. Il est impossible de sortir de tels «co-débogueurs» de l'impasse en envoyant
SIGCONT
de l'extérieur; la seule façon est de tuer (
SIGKILL
) l'enfant, puis le parent quittera le débogage et «se fige». (Si vous tuez un parent, l'enfant mourra avec lui.) Si l'enfant active l'option
PTRACE_O_EXITKILL
, le parent débogué par lui mourra également.
Vous savez maintenant comment mettre en œuvre une paire de processus qui, lors de la réception d'un signal, se figent pour toujours et ne meurent qu'ensemble. Pourquoi cela peut être nécessaire dans la pratique, je ne vais pas expliquer :-)