Wie verwende ich PTRACE, um eine konsistente Ansicht mehrerer Threads zu erhalten?

8

Während ich zu dieser Frage gearbeitet habe, bin ich auf eine mögliche Idee gestoßen, die ptrace verwendet, aber Ich kann nicht richtig verstehen, wie ptrace mit Threads interagiert.

Angenommen, ich habe einen bestimmten Multithread-Hauptprozess und möchte einen bestimmten Thread darin anhängen (möglicherweise von einem gegabelten Kind).

  1. Kann ich an einen bestimmten Thread anhängen? (Die Anleitungen unterscheiden sich in dieser Frage.)

  2. Wenn ja, bedeutet das, dass Single-Stepping nur durch die Anweisungen dieses einen Threads geht? Stoppt es alle Threads des Prozesses?

  3. Wenn ja, bleiben alle anderen Threads stehen, während ich PTRACE_SYSCALL oder PTRACE_SINGLESTEP aufruft, oder werden alle Threads fortgesetzt? Gibt es eine Möglichkeit, nur in einem einzigen Thread vorzugehen, aber sicherzustellen, dass die anderen Threads gestoppt bleiben?

Grundsätzlich möchte ich das ursprüngliche Programm synchronisieren, indem Sie alle Threads zwingen zu stoppen, und dann nur eine kleine Menge von single-threaded Anweisungen ausführen, indem Sie den verfolgten Thread einzeln schrittweise ausführen.

Meine bisherigen persönlichen Versuche sehen ein bisschen so aus:

%Vor%

Allerdings bin ich mir nicht sicher, und kann keine passenden Referenzen finden, dass dies gleichzeitig korrekt ist und dass important_instruction() garantiert nur ausgeführt wird, wenn alle anderen Threads gestoppt sind. Ich verstehe auch, dass es Rassenbedingungen geben kann, wenn das Elternteil Signale von anderswo empfängt, und ich hörte, dass ich stattdessen PTRACE_SEIZE verwenden sollte, aber das scheint nicht überall zu existieren.

Jede Klärung oder Referenzen würden sehr geschätzt werden!

    
Kerrek SB 02.09.2013, 17:07
quelle

4 Antworten

18

Ich habe einen zweiten Testfall geschrieben. Ich musste eine separate Antwort hinzufügen, da es zu lang war, um in die erste zu passen, mit Beispielausgabe eingeschlossen.

Erstens, hier ist tracer.c :

%Vor%

tracer.c führt den angegebenen Befehl aus und wartet darauf, dass der Befehl ein SIGSTOP -Signal empfängt. ( tracer.c sendet es nicht selbst; Sie können entweder das Tracee selbst stoppen lassen oder das Signal extern senden.)

Wenn der Befehl beendet wurde, fügt tracer.c jedem Thread einen ptrace-Befehl bei und führt einen der Threads in einer festen Anzahl von Schritten durch ( SINGLESTEPS Kompilierzeitkonstante) und zeigt den zugehörigen Registerstatus für jeden Thread an .

Danach trennt es sich von dem Befehl und sendet ihm ein Signal SIGCONT , damit es normal weiterarbeitet.

Hier ist ein einfaches Testprogramm, worker.c , das ich zum Testen benutzt habe:

%Vor%

Kompilieren Sie beide mit z. B.

%Vor%

und führen Sie entweder in einem separaten Terminal oder im Hintergrund mit z. B.

%Vor%

Der Tracer zeigt die PID des Arbeiters:

%Vor%

An diesem Punkt läuft das Kind normal. Die Aktion wird gestartet, wenn Sie SIGSTOP an das untergeordnete Element senden. Der Tracer erkennt es, macht die gewünschte Verfolgung, löst sich dann ab und lässt das Kind normal weiterfahren:

%Vor%

Sie können das oben genannte so oft wiederholen wie Sie möchten. Beachten Sie, dass ich das SIGSTOP -Signal als Trigger ausgewählt habe, weil auf diese Weise tracer.c auch als Grundlage für das Generieren komplexer Multithread-Core-Dumps pro Anfrage nützlich ist (da der Multithread-Prozess einfach durch Senden einer SIGSTOP ausgelöst werden kann) .

Die Zerlegung der worker() -Funktion, die alle Threads im obigen Beispiel drehen:

%Vor%

Nun zeigt dieses Testprogramm nur, wie man einen Prozess stoppt, an alle seine Threads anfügt, einen Schritt an einen der Threads mit einer gewünschten Anzahl von Anweisungen anschließt und dann alle Threads normal weiterlaufen lässt; es beweist nicht noch , dass das gleiche gilt, wenn bestimmte Threads normal weiterlaufen (über PTRACE_CONT ). Das Detail, das ich unten beschreibe, weist jedoch darauf hin, dass derselbe Ansatz für PTRACE_CONT funktionieren sollte.

Das Hauptproblem oder die Überraschung, die ich beim Schreiben der obigen Testprogramme antraf, war die Notwendigkeit des

%Vor%

Schleife, vor allem für die ESRCH case (die anderen habe ich nur wegen der ptrace hinzugefügt man page Beschreibung).

Sie sehen, die meisten ptrace-Befehle sind nur erlaubt, wenn die Aufgabe gestoppt wird. Die Aufgabe wird jedoch nicht gestoppt, wenn sie z. ein Einzelschritt-Befehl. Wenn Sie also die obige Schleife verwenden - indem Sie möglicherweise einen Millisekunden-Nanoschlaf oder ähnliches hinzufügen, um CPU-Verluste zu vermeiden -, stellen Sie sicher, dass der vorherige ptrace-Befehl abgeschlossen ist (und damit die Aufgabe gestoppt wurde), bevor wir versuchen, den neuen zu liefern.

Kerrek SB, ich glaube, dass zumindest einige der Probleme, die Sie mit Ihren Testprogrammen hatten, auf dieses Problem zurückzuführen sind? Für mich persönlich war es eine Art von Moment, um zu erkennen, dass dies natürlich notwendig ist, da ptracing von Natur aus asynchron und nicht synchron ist.

(Diese Asynchronität ist auch die Ursache für die oben erwähnte Wechselwirkung). Ich glaube, dass die Interaktion mit der oben gezeigten Schleife kein Problem mehr darstellt - und eigentlich verständlich ist .)

Hinzufügen der Kommentare zu dieser Antwort:

Der Linux-Kernel verwendet eine Reihe von Taskstatus-Flags in der task_struct-Struktur (siehe SIGCONT zur Definition hinzu, um den Status jeder Aufgabe zu verfolgen. Die nutzerseitige Seite von PTRACE_CONT ist in definiert include/linux/sched.h .

Wenn ptrace() oder kernel/ptrace.c aufgerufen wird, PTRACE_SINGLESTEP : PTRACE_CONT behandelt die meisten Details. Es endet mit dem Aufruf von kernel/ptrace.c ( ptrace_continue() ).

Wenn ein Prozess über wake_up_state(child, __TASK_TRACED) signal gestoppt wird, werden alle Tasks gestoppt und landen im Status "stopped, not traced" .

An jede Aufgabe anhängen (über PTRACE_ATTACH oder PTRACE_SEIZE, siehe kernel/sched/core.c::try_to_wake_up(child, __TASK_TRACED, 0) : SIGSTOP ) ändert den Aufgabenstatus. Ptrace-Status-Bits (siehe kernel/ptrace.c Konstanten ) sind getrennt von den runnbaren Status-Bits der Task (siehe ptrace_attach() Konstanten ).

Nach dem Anhängen an die Aufgaben und Senden des Prozesses ein include/linux/ptrace.h:PT_ -Signal, wird der gestoppte Zustand nicht sofort geändert (ich glaube), da die Aufgabe auch verfolgt wird. Das Ausführen von PTRACE_SINGLESTEP oder PTRACE_CONT endet in include/linux/sched.h:TASK_ , das den Aufgabenstatus aktualisiert und die Aufgabe in die Ausführungswarteschlange verschiebt.

Jetzt ist der komplizierte Teil, den ich noch nicht gefunden habe, der Code-Pfad, wie der Task-Status im Kernel aktualisiert wird, wenn der Task als nächstes geplant wird. Meine Tests zeigen an, dass mit Single-Stepping (welches ein weiteres Task-Status-Flag ist) nur der Task-Status aktualisiert wird, wobei das Einzelschritt-Flag gelöscht wird. Es scheint, dass PTRACE_CONT nicht so zuverlässig ist; Ich glaube, das liegt daran, dass das Einzelschritt-Flag diesen Taskstatus "zwingt". Vielleicht gibt es eine "Race Condition". die Fortsetzung Signallieferung und Zustandsänderung?

(Weiterführende Bearbeitung: Die Kernel-Entwickler erwarten definitiv, dass SIGCONT aufgerufen wird, siehe zB Dieser Thread .)

Mit anderen Worten, nachdem Sie bemerkt haben, dass der Prozess beendet wurde (beachten Sie, dass Sie kernel/sched/core.c::try_to_wake_up(child, __TASK_TRACED, 0) oder wait() verwenden können, wenn der Prozess kein untergeordnetes Objekt ist und noch nicht angefügt ist), glaube ich, dass das folgende Verfahren das meiste ist robustes:

%Vor%

Nach dem obigen sollten alle Aufgaben angefügt werden und in dem erwarteten Zustand, so dass z.B. PTRACE_CONT funktioniert ohne weitere Tricks.

Wenn sich das Verhalten in zukünftigen Kernels ändert - ich glaube, dass sich die Interaktion zwischen den STOP / CONT-Signalen und dem Ptracing ändern kann; zumindest eine Frage an die LKML-Entwickler zu diesem Verhalten wäre gerechtfertigt! -, das obige Verfahren wird immer noch robust arbeiten . (Es kann auch eine gute Idee sein, auf der sicheren Seite zu bleiben, indem Sie einige Male eine Schleife zu PTRACE_SINGLESTEP verwenden.)

Der Unterschied zu PTRACE_CONT besteht darin, dass das anfängliche PTRACE_CONT den Prozess tatsächlich fortführen kann, wenn sich das Verhalten in der Zukunft ändert, wodurch der darauf folgende /proc/PID/stat fehlschlägt. Mit PTRACE_SINGLESTEP wird der Prozess beendet , sodass weitere /proc/PID/status -Aufrufe erfolgreich ausgeführt werden können.

Fragen?

    
Nominal Animal 04.09.2013, 00:56
quelle
5
  

Kann ich an einen bestimmten Thread anhängen?

Ja, zumindest für aktuelle Kernel.

  

Bedeutet das, dass Single-Stepping nur durch die Anweisungen dieses einen Threads geht? Stoppt es alle Threads des Prozesses?

Ja. Es stoppt nicht die anderen Threads, nur den angehängten.

  

Gibt es eine Möglichkeit, nur in einem einzelnen Thread vorzugehen, aber zu garantieren, dass die anderen Threads gestoppt bleiben?

Ja. Sende SIGSTOP an den Prozess (verwende waitpid(PID,,WUNTRACED) , um darauf zu warten, dass der Prozess gestoppt wird), dann PTRACE_ATTACH für jeden Thread im Prozess. Sende SIGCONT (benutze waitpid(PID,,WCONTINUED) , um darauf zu warten, dass der Prozess fortgesetzt wird).

Da alle Threads beim Anhängen gestoppt wurden und das Anhängen den Thread stoppt, bleiben alle Threads nach dem Senden des Signals SIGCONT stehen. Sie können die Threads in beliebiger Reihenfolge in einem Schritt ausführen.

Ich fand das interessant genug, um einen Testfall zu erstellen. (Okay, eigentlich vermute ich, dass niemand mein Wort dafür nehmen wird, also entschied ich, dass es besser ist Beweise zu zeigen, die du selbst duplizieren kannst.)

Mein System scheint dem man 2 ptrace zu folgen, wie in Linux-Man-Pages-Projekt , und Kerrisk scheint ziemlich gut darin zu sein, sie mit dem Kernel-Verhalten synchron zu halten. Im Allgemeinen bevorzuge ich kernel.org Quellen sehr. der Linux-Kernel zu anderen Quellen.

Zusammenfassung:

  • Das Anhängen an den Prozess selbst (TID == PID) stoppt nur den ursprünglichen Thread, nicht alle Threads.

  • Das Anhängen an einen bestimmten Thread (mit TIDs von /proc/PID/task/ ) stoppt diesen Thread. (Mit anderen Worten, der Thread mit TID == PID ist nicht speziell.)

  • Wenn Sie SIGSTOP an den Prozess senden, werden alle Threads gestoppt, aber ptrace() funktioniert immer noch einwandfrei.

  • Wenn Sie eine SIGSTOP an den Prozess gesendet haben, rufen Sie nicht ptrace(PTRACE_CONT, TID) vor dem Trennen auf. PTRACE_CONT scheint das Signal SIGCONT zu stören.

    Sie können zuerst SIGSTOP , dann PTRACE_ATTACH und dann SIGCONT ohne Probleme senden. Der Thread bleibt gestoppt (aufgrund des Ptrace). Mit anderen Worten, PTRACE_ATTACH und PTRACE_DETACH mischen sich gut mit SIGSTOP und SIGCONT , ohne irgendwelche Nebenwirkungen, die ich sehen könnte.

  • SIGSTOP und SIGCONT wirken sich auf den gesamten Prozess aus, auch wenn Sie tgkill() (oder pthread_kill() ) verwenden, um das Signal an einen bestimmten Thread zu senden.

  • Um einen bestimmten Thread zu stoppen und fortzusetzen, PTHREAD_ATTACH it; Um alle Threads eines Prozesses anzuhalten und fortzusetzen, senden Sie die Signale SIGSTOP und SIGCONT an den Prozess.

Persönlich glaube ich, dass dies den Ansatz bestätigt, den ich in dieser anderen Frage vorgeschlagen habe.

Hier ist der hässliche Testcode, den Sie kompilieren und ausführen können, um ihn selbst zu testen, traces.c :

%Vor%

Kompilieren Sie und führen Sie z. B. mit

aus %Vor%

Die Ausgabe ist ein Speicherauszug der untergeordneten Prozessindikatoren (jeweils in einem separaten Thread inkrementiert, einschließlich des ursprünglichen Threads, der den letzten Zähler verwendet). Vergleichen Sie die Zähler über die kurze Wartezeit hinweg. Zum Beispiel:

%Vor%

Wie Sie oben sehen können, wird nur der erste Thread (dessen TID == PID) gestoppt, der den letzten Zähler verwendet. Dasselbe gilt auch für die anderen drei Threads, die die ersten drei Zähler der Reihe nach verwenden:

%Vor%

Die nächsten zwei Fälle untersuchen die SIGCONT / SIGSTOP wrt. der gesamte Prozess:

%Vor%

Wie Sie sehen können, wird das Senden von SIGSTOP alle Threads stoppen, aber nicht mit ptrace() . Nach SIGCONT werden die Threads weiterhin normal ausgeführt.

Die letzten beiden Fälle untersuchen die Auswirkungen der Verwendung von tgkill() zum Senden der SIGSTOP / SIGCONT an einen bestimmten Thread (der, der dem ersten Zähler entspricht), während er an einen anderen Thread angehängt wird:

%Vor%

Leider, aber wie erwartet, ist die Disposition (gestoppt / ausgeführt) prozessweit und nicht threadspezifisch, wie Sie oben sehen können. Das bedeutet, dass Sie, um bestimmte Threads zu stoppen und die anderen Threads normal laufen zu lassen, PTHREAD_ATTACH separat zu den Threads setzen müssen, die Sie stoppen möchten.

Um alle meine obigen Aussagen zu beweisen, müssen Sie möglicherweise Testfälle hinzufügen; Ich hatte am Ende einige Kopien des Codes, alle leicht bearbeitet, um alles zu testen, und ich bin mir nicht sicher, dass ich den komplettesten Satz ausgewählt habe. Ich würde gerne das Testprogramm erweitern, wenn Sie Auslassungen finden.

Fragen?

    
Nominal Animal 03.09.2013 06:33
quelle
1

Jeder Thread in dem Prozess wird einzeln verfolgt (und jeder Thread kann möglicherweise durch einen anderen Ablaufverfolgungsprozess verfolgt werden oder nicht erkannt werden). Wenn Sie ptrace attach aufrufen, fügen Sie immer nur einen einzelnen Thread hinzu. Nur dieser Thread wird gestoppt - die anderen Threads werden weiterhin so ausgeführt, wie sie waren.

Aktuelle Versionen der ptrace() man-Seite machen dies sehr deutlich:

  

Anhang und nachfolgende Befehle sind pro Thread: in einem Multithread   Prozess, jeder Thread kann individuell an eine (möglicherweise   verschiedene) Tracer, oder links nicht angebracht und damit nicht debugged.   "Tracee" bedeutet daher immer "(ein) Thread", niemals "ein (möglicherweise)   Multithread - Prozess ". Ptrace - Befehle werden immer an a gesendet   spezifische Tracee mit einem Aufruf des Formulars

%Vor%      

Dabei ist pid die Thread-ID des entsprechenden Linux-Threads.

     

(Beachten Sie, dass auf dieser Seite ein "Multithread-Prozess" einen Thread bedeutet   Gruppe bestehend aus Threads, die mit clone(2) erstellt wurden    CLONE_THREAD flag.)

Single-stepping wirkt sich nur auf den Thread aus, auf den Sie es richten. Wenn die anderen Threads ausgeführt werden, werden sie weiter ausgeführt, und wenn sie sich in der Ablaufverfolgung befinden, bleiben sie im Tracing-Stopp. (Das bedeutet, wenn der Thread, den Sie Single-Stepping verwenden, versucht, eine Mutex- oder ähnliche Synchronisierungsressource zu erhalten, die von einem anderen nicht laufenden Thread gehalten wird, kann dieser Mutex nicht abgerufen werden.)

Wenn Sie alle Threads des Prozesses anhalten möchten, während Sie einen Thread einzeln ausführen, müssen Sie alle Threads anfügen. Es gibt die zusätzliche Komplikation, dass wenn der Prozess ausgeführt wird, während Sie versuchen, daran anzuhängen, neue Threads erstellt werden können, während Sie sie aufzählen.

    
caf 03.09.2013 04:37
quelle
-1
  

Stoppt es alle Threads des Prozesses?

Ja Es verfolgt den Prozess, alle Fäden dieses Prozesses sind gestoppt. Stellen Sie sich vor, es wäre nicht, wie Sie den Dirfferenent-Thread in Ihrer IDE sehen könnten.

aus dem Handbuch:

  

Der Systemaufruf ptrace () stellt ein Mittel zur Verfügung, mit dem ein Prozess (der "Tracer") die Ausführung eines anderen Prozesses (des "tracee") beobachten und steuern kann

Beispielcode zum Anhängen:

%Vor%

Also ja, du wirst zu einem Thread geführt und ja, es stoppt alle Threads des Prozesses.

%Vor%

vielleicht sehen Sie hier: Ссылка

    
dzada 02.09.2013 17:10
quelle