
Über Habré wurde bereits über das Abfangen von Systemaufrufen mittels
ptrace
; Alexa schrieb über diesen viel detaillierteren Beitrag, den ich übersetzen wollte.
Wo soll ich anfangen?
Die Kommunikation zwischen dem debuggten Programm und dem Debugger erfolgt über Signale. Dies erschwert die ohnehin schwierigen Dinge erheblich; Zum Spaß können Sie
den Abschnitt BUGS von man ptrace
.
Es gibt mindestens zwei verschiedene Möglichkeiten, um mit dem Debuggen zu beginnen:
ptrace(PTRACE_TRACEME, 0, NULL, NULL)
macht das übergeordnete ptrace(PTRACE_TRACEME, 0, NULL, NULL)
des aktuellen Prozesses zu einem Debugger dafür. Es ist keine Unterstützung durch die Eltern erforderlich. man
rät sanft: "Ein Prozess sollte diese Anfrage wahrscheinlich nicht stellen, wenn sein Elternteil nicht erwartet, sie zu verfolgen." (An anderer Stelle im Mans haben Sie den Satz "wahrscheinlich nicht" gesehen ?) Wenn der aktuelle Prozess bereits einen Debugger hatte, schlägt der Aufruf fehl.ptrace(PTRACE_ATTACH, pid, NULL, NULL)
macht den aktuellen Prozess zu einem Debugger für pid
. Wenn pid
bereits einen Debugger hatte, schlägt der Aufruf fehl. SIGSTOP
an den debuggten Prozess gesendet und funktioniert erst weiter, wenn der Debugger ihn abtaut.
Diese beiden Methoden sind völlig unabhängig; Sie können entweder das eine oder das andere verwenden, aber es macht keinen Sinn, sie zu kombinieren.
Es ist wichtig zu beachten, dass
PTRACE_ATTACH
nicht sofort erfolgt: Nach dem
ptrace(PTRACE_ATTACH)
wird normalerweise
waitpid(2)
PTRACE_ATTACH
, um zu warten, bis
PTRACE_ATTACH
„funktioniert“.
Sie können den
PTRACE_TRACEME
Prozess unter Debugging mit
PTRACE_TRACEME
wie folgt
PTRACE_TRACEME
:
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"); }
Ohne einen
raise(SIGSTOP)
Aufruf
raise(SIGSTOP)
kann sich herausstellen, dass
execvp(3)
wird, bevor der übergeordnete Prozess dafür bereit ist. und dann beginnen die Aktionen des Debuggers (zum Beispiel das Abfangen von Systemaufrufen) nicht am Anfang des Prozesses.
Wenn das Debuggen gestartet wird, "taut" jeder
ptrace(PTRACE_SYSCALL, pid, NULL, NULL)
den debuggten Prozess bis zum ersten Eintrag in den Systemaufruf und dann bis zum Verlassen des Systemaufrufs auf.
Telekinetischer Assembler
ptrace(PTRACE_SYSCALL)
gibt
keine Informationen an den Debugger zurück. Er verspricht lediglich, dass der zu debuggende Prozess bei jedem Systemaufruf zweimal gestoppt wird. Um Informationen darüber zu erhalten, was mit dem debuggten Prozess geschieht - zum Beispiel, in welchem Systemaufruf er gestoppt wurde -, müssen Sie in eine Kopie seiner Register klettern, die vom Kernel in einem
struct user
in einem Format gespeichert wird, das von der spezifischen Architektur abhängt. (Unter x86_64 befindet sich die Rufnummer beispielsweise im Feld
regs.orig_rax
, der erste übergebene Parameter befindet sich in
regs.rdi
usw.) Alexa kommentiert: „Es fühlt sich an, als würden Sie Assembler-Code in C schreiben, der mit den Registern des Remote-Prozessors funktioniert.“
Anstelle der in
sys/user.h
beschriebenen Struktur kann es bequemer sein, die in
sys/reg.h
definierten
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 }
In diesem Fall unterscheiden sich zwei Stopps des Debugging-Prozesses - am Eingang des Systemaufrufs und am Ausgang des Systemaufrufs - in keiner Weise aus Sicht des Debuggers. Damit sich der Debugger selbst daran erinnern muss, in welchem Zustand sich jeder der debuggten Prozesse befindet: Wenn mehrere vorhanden sind, garantiert niemand, dass ein Signalpaar von einem Prozess hintereinander kommt.
Nachkommen
Eine der
ptrace
Optionen, nämlich
PTRACE_O_TRACECLONE
, stellt sicher, dass alle
PTRACE_O_TRACECLONE
ptrace
des
ptrace
Prozesses beim
Beenden von fork(2)
automatisch debuggt werden. Ein weiterer subtiler Punkt hierbei ist, dass Nachkommen, die zum Debuggen
waitpid
, zu „
waitpid
“ des Debuggers werden und
waitpid
nicht nur auf das Stoppen von „unmittelbaren Kindern“ reagiert, sondern auch auf das Debuggen von „
waitpid
“. Man warnt davor:
"Das Setzen des WCONTINUED-Flags beim Aufrufen von waitpid (2) wird nicht empfohlen: Der Status" Fortsetzung "ist pro Prozess und verbraucht kann das eigentliche übergeordnete Element des Trace verwirren." - d.h. "Pseudokinder" haben zwei Eltern, die darauf warten können, dass sie aufhören. Für den Debugger-Programmierer bedeutet dies, dass
waitpid(-1)
nicht nur darauf wartet, dass unmittelbare Kinder gestoppt werden, sondern auch auf einen der debuggten Prozesse.
Signale
(Bonusinhalt eines Übersetzers: Diese Informationen sind nicht im englischsprachigen Artikel enthalten.)Wie bereits eingangs erwähnt, erfolgt die Kommunikation zwischen dem debuggten Programm und dem Debugger über Signale. Ein Prozess empfängt
SIGSTOP
wenn ein Debugger mit ihm verbunden ist, und dann
SIGTRAP
jedes Mal, wenn im Debugging-Prozess etwas Interessantes passiert - beispielsweise ein Systemaufruf oder der Empfang eines externen Signals. Der Debugger wiederum erhält
SIGCHLD
jedes Mal, wenn einer der debuggten Prozesse (nicht unbedingt das unmittelbare Kind) "einfriert" oder "einfriert".
ptrace(PTRACE_SYSCALL)
zu debuggenden Prozesses wird durch Aufrufen von
ptrace(PTRACE_SYSCALL)
(vor dem ersten Signal oder Systemaufruf) oder
ptrace(PTRACE_CONT)
(vor dem ersten Signal) durchgeführt. Wenn
SIGSTOP/SIGCONT
Signale auch für Zwecke verwendet werden, die nicht mit dem Debuggen zusammenhängen, können Probleme mit
ptrace
auftreten: Wenn der Debugger den Debug-Prozess, der
SIGSTOP
empfangen hat, „auftaut“, sieht es von außen so aus, als ob das Signal ignoriert wurde. Wenn der Debugger den zu debuggenden Prozess nicht "auftaut", kann der externe
SIGCONT
ihn nicht "auftauen".
Nun zum lustigen Teil: Linux verbietet es Prozessen, sich
selbst zu debuggen , verhindert jedoch nicht die Erstellung von Schleifen, wenn sich Eltern und Kind gegenseitig debuggen. In diesem Fall „friert“ einer der Prozesse, wenn er ein externes Signal empfängt, über
SIGTRAP
- dann wird
SIGCHLD
an den zweiten Prozess gesendet und er friert auch über
SIGTRAP
. Es ist unmöglich, solche „Co-Debugger“ aus dem Stillstand zu bringen, indem
SIGCONT
von außen
SIGCONT
wird. Die einzige Möglichkeit besteht darin, das Kind zu töten (
SIGKILL
). Dann beendet das Elternteil das Debuggen und friert ein. (Wenn Sie den Elternteil töten, stirbt das Kind mit ihm.) Wenn das Kind die Option
PTRACE_O_EXITKILL
, stirbt der von ihm
PTRACE_O_EXITKILL
Elternteil mit seinem Tod.
Jetzt wissen Sie, wie Sie zwei Prozesse implementieren, die beim Empfang eines Signals für immer einfrieren und nur zusammen sterben. Warum dies in der Praxis notwendig sein kann, werde ich nicht erklären :-)