
 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;- manconseille 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- pidavait déjà un débogueur, l'appel échouera.- SIGSTOPenvoyé 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 :-)