Strace unter Linux: Geschichte, Design und Verwendung

Strace unter Linux: Geschichte, Design und Verwendung

In Unix-ähnlichen Betriebssystemen erfolgt die Kommunikation eines Programms mit der Außenwelt und dem Betriebssystem über eine kleine Reihe von Funktionen – Systemaufrufe. Das bedeutet, dass es für Debugging-Zwecke nützlich sein kann, Systemaufrufe auszuspionieren, die von Prozessen ausgeführt werden.

Ein Dienstprogramm hilft Ihnen, das „intime Leben“ von Programmen unter Linux zu überwachen strace, das Gegenstand dieses Artikels ist. Beispiele für den Einsatz von Spionageausrüstung werden von einer kurzen Geschichte begleitet strace und eine Beschreibung des Designs solcher Programme.

Inhalt

Entstehung der Arten

Die Hauptschnittstelle zwischen Programmen und dem Betriebssystemkernel in Unix sind Systemaufrufe. Systemaufrufe, Systemaufrufe) erfolgt die Interaktion von Programmen mit der Außenwelt ausschließlich über sie.

Aber in der ersten öffentlichen Version von Unix (Version 6 Unix, 1975) gab es keine bequemen Möglichkeiten, das Verhalten von Benutzerprozessen zu verfolgen. Um dieses Problem zu beheben, wird Bell Labs auf die nächste Version aktualisieren (Version 7 Unix, 1979) schlug einen neuen Systemaufruf vor - ptrace.

Ptrace wurde hauptsächlich für interaktive Debugger entwickelt, aber Ende der 80er Jahre (im Zeitalter der kommerziellen System V-Release 4) Auf dieser Grundlage entstanden eng fokussierte Debugger – Systemaufruf-Tracer – und wurden weit verbreitet.

erste Dieselbe Version von Strace wurde 1992 von Paul Cronenburg auf der Mailingliste comp.sources.sun als Alternative zu einem geschlossenen Dienstprogramm veröffentlicht trace von Sun. Sowohl der Klon als auch das Original waren für SunOS gedacht, allerdings schon seit 1994 strace wurde auf System V, Solaris und das immer beliebter werdende Linux portiert.

Heute unterstützt Strace nur Linux und verlässt sich auf dasselbe ptrace, überwuchert mit vielen Erweiterungen.

Moderner (und sehr aktiver) Betreuer strace - Dmitry Lewin. Dank ihm erhielt das Dienstprogramm erweiterte Funktionen wie Fehlerinjektion in Systemaufrufe, Unterstützung für eine breite Palette von Architekturen und, was am wichtigsten ist, Maskottchen. Inoffizielle Quellen behaupten, dass die Wahl aufgrund der Übereinstimmung zwischen dem russischen Wort „ostrich“ und dem englischen Wort „strace“ auf den Strauß fiel.

Es ist auch wichtig, dass der Ptrace-Systemaufruf und die Tracer trotz einer langen Geschichte und Implementierung in Linux, FreeBSD, OpenBSD und traditionellem Unix nie in POSIX enthalten waren.

Das Strace-Gerät auf den Punkt gebracht: Piglet Trace

„Von Ihnen wird nicht erwartet, dass Sie das verstehen“ (Dennis Ritchie, Kommentar im Unix-Quellcode der Version 6)

Seit meiner frühen Kindheit kann ich Blackboxen nicht ausstehen: Ich habe nicht mit Spielzeug gespielt, sondern versucht, ihre Struktur zu verstehen (Erwachsene benutzten das Wort „kaputt“, glauben aber den bösen Zungen nicht). Vielleicht liegt mir deshalb die informelle Kultur des ersten Unix und der modernen Open-Source-Bewegung so nahe.

Für die Zwecke dieses Artikels ist es unzumutbar, den über Jahrzehnte gewachsenen Quellcode von Strace zu zerlegen. Aber es sollten keine Geheimnisse für die Leser übrig bleiben. Um das Funktionsprinzip solcher Strace-Programme zu zeigen, werde ich daher den Code für einen Miniatur-Tracer bereitstellen - Ferkelspur (ptr). Es weiß nicht, wie man etwas Besonderes macht, aber die Hauptsache sind die Systemaufrufe des Programms – es gibt Folgendes aus:

$ gcc examples/piglet-trace.c -o ptr
$ ptr echo test > /dev/null
BRK(12) -> 94744690540544
ACCESS(21) -> 18446744073709551614
ACCESS(21) -> 18446744073709551614
unknown(257) -> 3
FSTAT(5) -> 0
MMAP(9) -> 140694657216512
CLOSE(3) -> 0
ACCESS(21) -> 18446744073709551614
unknown(257) -> 3
READ(0) -> 832
FSTAT(5) -> 0
MMAP(9) -> 140694657208320
MMAP(9) -> 140694650953728
MPROTECT(10) -> 0
MMAP(9) -> 140694655045632
MMAP(9) -> 140694655070208
CLOSE(3) -> 0
unknown(158) -> 0
MPROTECT(10) -> 0
MPROTECT(10) -> 0
MPROTECT(10) -> 0
MUNMAP(11) -> 0
BRK(12) -> 94744690540544
BRK(12) -> 94744690675712
unknown(257) -> 3
FSTAT(5) -> 0
MMAP(9) -> 140694646390784
CLOSE(3) -> 0
FSTAT(5) -> 0
IOCTL(16) -> 18446744073709551591
WRITE(1) -> 5
CLOSE(3) -> 0
CLOSE(3) -> 0
unknown(231)
Tracee terminated

Piglet Trace erkennt etwa Hunderte von Linux-Systemaufrufen (siehe. Diagramm) und funktioniert nur auf x86-64-Architektur. Dies ist für Bildungszwecke ausreichend.

Schauen wir uns die Arbeit unseres Klons an. Im Fall von Linux verwenden Debugger und Tracer, wie oben erwähnt, den Systemaufruf ptrace. Es funktioniert, indem im ersten Argument die Befehlsbezeichner übergeben werden, die wir nur benötigen PTRACE_TRACEME, PTRACE_SYSCALL и PTRACE_GETREGS.

Der Tracer startet im üblichen Unix-Stil: fork(2) startet einen untergeordneten Prozess, der wiederum verwendet exec(3) startet das zu studierende Programm. Die einzige Feinheit hier ist die Herausforderung ptrace(PTRACE_TRACEME) vor exec: Der untergeordnete Prozess erwartet, dass der übergeordnete Prozess ihn überwacht:

pid_t child_pid = fork();
switch (child_pid) {
case -1:
    err(EXIT_FAILURE, "fork");
case 0:
    /* Child here */
    /* A traced mode has to be enabled. A parent will have to wait(2) for it
     * to happen. */
    ptrace(PTRACE_TRACEME, 0, NULL, NULL);
    /* Replace itself with a program to be run. */
    execvp(argv[1], argv + 1);
    err(EXIT_FAILURE, "exec");
}

Der übergeordnete Prozess sollte nun aufgerufen werden wait(2) Stellen Sie im untergeordneten Prozess sicher, dass der Wechsel in den Trace-Modus erfolgt ist:

/* Parent */

/* First we wait for the child to set the traced mode (see
 * ptrace(PTRACE_TRACEME) above) */
if (waitpid(child_pid, NULL, 0) == -1)
    err(EXIT_FAILURE, "traceme -> waitpid");

An diesem Punkt sind die Vorbereitungen abgeschlossen und Sie können direkt mit der Verfolgung von Systemaufrufen in einer Endlosschleife fortfahren.

Вызов ptrace(PTRACE_SYSCALL) garantiert, dass anschließend wait Das übergeordnete Element wird entweder vor der Ausführung des Systemaufrufs oder unmittelbar nach dessen Abschluss abgeschlossen. Zwischen zwei Aufrufen können Sie beliebige Aktionen ausführen: den Aufruf durch einen alternativen ersetzen, die Argumente oder den Rückgabewert ändern.

Wir müssen den Befehl nur zweimal aufrufen ptrace(PTRACE_GETREGS)um den Registerstatus zu erhalten rax vor dem Aufruf (Systemrufnummer) und unmittelbar danach (Rückgabewert).

Eigentlich ist der Zyklus:

/* A system call tracing loop, one interation per call. */
for (;;) {
    /* A non-portable structure defined for ptrace/GDB/strace usage mostly.
     * It allows to conveniently dump and access register state using
     * ptrace. */
    struct user_regs_struct registers;

    /* Enter syscall: continue execution until the next system call
     * beginning. Stop right before syscall.
     *
     * It's possible to change the system call number, system call
     * arguments, return value or even avoid executing the system call
     * completely. */
  if (ptrace(PTRACE_SYSCALL, child_pid, NULL, NULL) == -1)
      err(EXIT_FAILURE, "enter_syscall");
  if (waitpid(child_pid, NULL, 0) == -1)
      err(EXIT_FAILURE, "enter_syscall -> waitpid");

  /* According to the x86-64 system call convention on Linux (see man 2
   * syscall) the number identifying a syscall should be put into the rax
   * general purpose register, with the rest of the arguments residing in
   * other general purpose registers (rdi,rsi, rdx, r10, r8, r9). */
  if (ptrace(PTRACE_GETREGS, child_pid, NULL, &registers) == -1)
      err(EXIT_FAILURE, "enter_syscall -> getregs");

  /* Note how orig_rax is used here. That's because on x86-64 rax is used
   * both for executing a syscall, and returning a value from it. To
   * differentiate between the cases both rax and orig_rax are updated on
   * syscall entry/exit, and only rax is updated on exit. */
  print_syscall_enter(registers.orig_rax);

  /* Exit syscall: execute of the syscall, and stop on system
   * call exit.
   *
   * More system call tinkering possible: change the return value, record
   * time it took to finish the system call, etc. */
  if (ptrace(PTRACE_SYSCALL, child_pid, NULL, NULL) == -1)
      err(EXIT_FAILURE, "exit_syscall");
  if (waitpid(child_pid, NULL, 0) == -1)
      err(EXIT_FAILURE, "exit_syscall -> waitpid");

  /* Retrieve register state again as we want to inspect system call
   * return value. */
  if (ptrace(PTRACE_GETREGS, child_pid, NULL, &registers) == -1) {
      /* ESRCH is returned when a child terminates using a syscall and no
       * return value is possible, e.g. as a result of exit(2). */
      if (errno == ESRCH) {
          fprintf(stderr, "nTracee terminatedn");
          break;
      }
      err(EXIT_FAILURE, "exit_syscall -> getregs");
  }

  /* Done with this system call, let the next iteration handle the next
   * one */
  print_syscall_exit(registers.rax);
}

Das ist der ganze Tracer. Jetzt wissen Sie, wo Sie mit der nächsten Portierung beginnen müssen DTrace unter Linux.

Grundlagen: Ausführen eines Programms, das Strace ausführt

Als erster Anwendungsfall strace, vielleicht lohnt es sich, die einfachste Methode zu nennen – das Starten einer laufenden Anwendung strace.

Um nicht in die endlose Liste der Aufrufe eines typischen Programms einzutauchen, schreiben wir Mindestprogramm vokrug write:

int main(int argc, char *argv[])
{
    char str[] = "write me to stdoutn";
    /* write(2) is a simple wrapper around a syscall so it should be easy to
     * find in the syscall trace. */
    if (sizeof(str) != write(STDOUT_FILENO, str, sizeof(str))){
        perror("write");
        return EXIT_FAILURE;
    }
    return EXIT_SUCCESS;
}

Lassen Sie uns das Programm erstellen und sicherstellen, dass es funktioniert:

$ gcc examples/write-simple.c -o write-simple
$ ./write-simple
write me to stdout

Und zum Schluss lassen Sie es uns unter Strace-Kontrolle ausführen:

$ strace ./write-simple
pexecve("./write", ["./write"], 0x7ffebd6145b0 /* 71 vars */) = 0
brk(NULL)                               = 0x55ff5489e000
access("/etc/ld.so.nohwcap", F_OK)      = -1 ENOENT (No such file or directory)
access("/etc/ld.so.preload", R_OK)      = -1 ENOENT (No such file or directory)
openat(AT_FDCWD, "/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3
fstat(3, {st_mode=S_IFREG|0644, st_size=197410, ...}) = 0
mmap(NULL, 197410, PROT_READ, MAP_PRIVATE, 3, 0) = 0x7f7a2a633000
close(3)                                = 0
access("/etc/ld.so.nohwcap", F_OK)      = -1 ENOENT (No such file or directory)
openat(AT_FDCWD, "/lib/x86_64-linux-gnu/libc.so.6", O_RDONLY|O_CLOEXEC) = 3
read(3, "177ELF21133>1260342"..., 832) = 832
fstat(3, {st_mode=S_IFREG|0755, st_size=2030544, ...}) = 0
mmap(NULL, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f7a2a631000
mmap(NULL, 4131552, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_DENYWRITE, 3, 0) = 0x7f7a2a04c000
mprotect(0x7f7a2a233000, 2097152, PROT_NONE) = 0
mmap(0x7f7a2a433000, 24576, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x1e7000) = 0x7f7a2a433000
mmap(0x7f7a2a439000, 15072, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0) = 0x7f7a2a439000
close(3)                                = 0
arch_prctl(ARCH_SET_FS, 0x7f7a2a6324c0) = 0
mprotect(0x7f7a2a433000, 16384, PROT_READ) = 0
mprotect(0x55ff52b52000, 4096, PROT_READ) = 0
mprotect(0x7f7a2a664000, 4096, PROT_READ) = 0
munmap(0x7f7a2a633000, 197410)          = 0
write(1, "write me to stdoutn", 20write me to stdout
)  = 20
exit_group(0)                           = ?

Sehr „wortreich“ und nicht sehr lehrreich. Hier gibt es zwei Probleme: Die Programmausgabe wird mit der Ausgabe vermischt strace und eine Fülle von Systemaufrufen, die uns nicht interessieren.

Sie können den Standardausgabestream des Programms und die Strace-Fehlerausgabe mithilfe des Schalters -o trennen, der die Liste der Systemaufrufe in eine Argumentdatei umleitet.

Es bleibt noch, sich mit dem Problem der „zusätzlichen“ Anrufe zu befassen. Nehmen wir an, dass wir nur an Anrufen interessiert sind write. Taste -e ermöglicht Ihnen die Angabe von Ausdrücken, nach denen Systemaufrufe gefiltert werden. Die beliebteste Bedingungsoption ist natürlich trace=*, mit dem Sie nur die Anrufe hinterlassen können, die uns interessieren.

Bei gleichzeitiger Verwendung -o и -e wir bekommen:

$ strace -e trace=write -owrite-simple.log ./write-simple
write me to stdout
$ cat write-simple.log
write(1, "write me to stdoutn", 20
)  = 20
+++ exited with 0 +++

Sie sehen also, es ist viel einfacher zu lesen.

Sie können auch Systemaufrufe entfernen, beispielsweise solche im Zusammenhang mit der Speicherzuweisung und -freigabe:

$ strace -e trace=!brk,mmap,mprotect,munmap -owrite-simple.log ./write-simple
write me to stdout
$ cat write-simple.log
execve("./write-simple", ["./write-simple"], 0x7ffe9972a498 /* 69 vars */) = 0
access("/etc/ld.so.nohwcap", F_OK)      = -1 ENOENT (No such file or directory)
access("/etc/ld.so.preload", R_OK)      = -1 ENOENT (No such file or directory)
openat(AT_FDCWD, "/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3
fstat(3, {st_mode=S_IFREG|0644, st_size=124066, ...}) = 0
close(3)                                = 0
access("/etc/ld.so.nohwcap", F_OK)      = -1 ENOENT (No such file or directory)
openat(AT_FDCWD, "/lib/x86_64-linux-gnu/libc.so.6", O_RDONLY|O_CLOEXEC) = 3
read(3, "177ELF21133>1260342"..., 832) = 832
fstat(3, {st_mode=S_IFREG|0755, st_size=2030544, ...}) = 0
close(3)                                = 0
arch_prctl(ARCH_SET_FS, 0x7f00f0be74c0) = 0
write(1, "write me to stdoutn", 20)  = 20
exit_group(0)                           = ?
+++ exited with 0 +++

Beachten Sie das Ausrufezeichen mit Escapezeichen in der Liste der ausgeschlossenen Aufrufe: Dies ist für die Befehlsshell erforderlich. Schale).

In meiner Version von glibc beendet ein Systemaufruf den Prozess exit_group, nicht traditionell _exit. Das ist die Schwierigkeit bei der Arbeit mit Systemaufrufen: Die Schnittstelle, mit der der Programmierer arbeitet, steht nicht in direktem Zusammenhang mit Systemaufrufen. Darüber hinaus ändert es sich regelmäßig je nach Implementierung und Plattform.

Grundlagen: Direkter Beitritt zum Prozess

Zunächst der Ptrace-Systemaufruf, auf dem es aufgebaut wurde strace, konnte nur verwendet werden, wenn das Programm in einem speziellen Modus ausgeführt wurde. Diese Einschränkung mag in den Tagen von Version 6 Unix vernünftig geklungen haben. Heutzutage reicht das nicht mehr aus: Manchmal muss man die Probleme eines funktionierenden Programms untersuchen. Ein typisches Beispiel ist ein Prozess, der an einem Griff oder im Ruhezustand blockiert ist. Daher modern strace kann Prozesse im laufenden Betrieb verbinden.

Einfrierbeispiel Programm:

int main(int argc, char *argv[])
{
    (void) argc; (void) argv;

    char str[] = "write men";

    write(STDOUT_FILENO, str, sizeof(str));

    /* Sleep indefinitely or until a signal arrives */
    pause();

    write(STDOUT_FILENO, str, sizeof(str));

    return EXIT_SUCCESS;
}

Lassen Sie uns das Programm erstellen und sicherstellen, dass es eingefroren ist:

$ gcc examples/write-sleep.c -o write-sleep
$ ./write-sleep
./write-sleep
write me
^C
$

Versuchen wir nun, mitzumachen:

$ ./write-sleep &
[1] 15329
write me
$ strace -p 15329
strace: Process 15329 attached
pause(
^Cstrace: Process 15329 detached
 <detached ...>

Programm durch Aufruf blockiert pause. Mal sehen, wie sie auf die Signale reagiert:

$ strace -o write-sleep.log -p 15329 &
strace: Process 15329 attached
$
$ kill -CONT 15329
$ cat write-sleep.log
pause()                                 = ? ERESTARTNOHAND (To be restarted if no handler)
--- SIGCONT {si_signo=SIGCONT, si_code=SI_USER, si_pid=14989, si_uid=1001} ---
pause(
$
$ kill -TERM 15329
$ cat write-sleep.log
pause()                                 = ? ERESTARTNOHAND (To be restarted if no handler)
--- SIGCONT {si_signo=SIGCONT, si_code=SI_USER, si_pid=14989, si_uid=1001} ---
pause()                                 = ? ERESTARTNOHAND (To be restarted if no handler)
--- SIGTERM {si_signo=SIGTERM, si_code=SI_USER, si_pid=14989, si_uid=1001} ---
+++ killed by SIGTERM +++

Wir haben das eingefrorene Programm gestartet und sind mit beigetreten strace. Zwei Dinge wurden deutlich: Der Pause-Systemaufruf ignoriert Signale ohne Handler und, was noch interessanter ist, Strace überwacht nicht nur Systemaufrufe, sondern auch eingehende Signale.

Beispiel: Untergeordnete Prozesse verfolgen

Arbeiten mit Prozessen über einen Anruf fork - die Basis aller Unixe. Sehen wir uns am Beispiel eines einfachen „Züchtens“ an, wie Strace mit einem Prozessbaum funktioniert. Programm:

int main(int argc, char *argv[])
{
    pid_t parent_pid = getpid();
    pid_t child_pid = fork();
    if (child_pid == 0) {
        /* A child is born! */
        child_pid = getpid();

        /* In the end of the day printf is just a call to write(2). */
        printf("child (self=%d)n", child_pid);
        exit(EXIT_SUCCESS);
    }

    printf("parent (self=%d, child=%d)n", parent_pid, child_pid);

    wait(NULL);

    exit(EXIT_SUCCESS);
}

Hier erstellt der ursprüngliche Prozess einen untergeordneten Prozess, der beide in die Standardausgabe schreibt:

$ gcc examples/fork-write.c -o fork-write
$ ./fork-write
parent (self=11274, child=11275)
child (self=11275)

Standardmäßig sehen wir nur Systemaufrufe vom übergeordneten Prozess:

$ strace -e trace=write -ofork-write.log ./fork-write
child (self=22049)
parent (self=22048, child=22049)
$ cat fork-write.log
write(1, "parent (self=22048, child=22049)"..., 33) = 33
--- SIGCHLD {si_signo=SIGCHLD, si_code=CLD_EXITED, si_pid=22049, si_uid=1001, si_status=0, si_utime=0, si_stime=0} ---
+++ exited with 0 +++

Das Flag hilft Ihnen, den gesamten Prozessbaum zu verfolgen -f, mit dem strace Überwacht Systemaufrufe in untergeordneten Prozessen. Dies wird zu jeder Ausgabezeile hinzugefügt pid Prozess, der eine Systemausgabe erzeugt:

$ strace -f -e trace=write -ofork-write.log ./fork-write
parent (self=22710, child=22711)
child (self=22711)
$ cat fork-write.log
22710 write(1, "parent (self=22710, child=22711)"..., 33) = 33
22711 write(1, "child (self=22711)n", 19) = 19
22711 +++ exited with 0 +++
22710 --- SIGCHLD {si_signo=SIGCHLD, si_code=CLD_EXITED, si_pid=22711, si_uid=1001, si_status=0, si_utime=0, si_stime=0} ---
22710 +++ exited with 0 +++

In diesem Zusammenhang kann die Filterung nach Gruppen von Systemaufrufen sinnvoll sein:

$ strace -f -e trace=%process -ofork-write.log ./fork-write
parent (self=23610, child=23611)
child (self=23611)
$ cat fork-write.log
23610 execve("./fork-write", ["./fork-write"], 0x7fff696ff720 /* 63 vars */) = 0
23610 arch_prctl(ARCH_SET_FS, 0x7f3d03ba44c0) = 0
23610 clone(child_stack=NULL, flags=CLONE_CHILD_CLEARTID|CLONE_CHILD_SETTID|SIGCHLD, child_tidptr=0x7f3d03ba4790) = 23611
23610 wait4(-1,  <unfinished ...>
23611 exit_group(0)                     = ?
23611 +++ exited with 0 +++
23610 <... wait4 resumed> NULL, 0, NULL) = 23611
23610 --- SIGCHLD {si_signo=SIGCHLD, si_code=CLD_EXITED, si_pid=23611, si_uid=1001, si_status=0, si_utime=0, si_stime=0} ---
23610 exit_group(0)                     = ?
23610 +++ exited with 0 +++

Welcher Systemaufruf wird übrigens verwendet, um einen neuen Prozess zu erstellen?

Beispiel: Dateipfade statt Handles

Die Kenntnis der Dateideskriptoren ist sicherlich nützlich, aber auch die Namen der spezifischen Dateien, auf die ein Programm zugreift, können nützlich sein.

Die nächste Programm schreibt die Zeile in die temporäre Datei:

void do_write(int out_fd)
{
    char str[] = "write me to a filen";

    if (sizeof(str) != write(out_fd, str, sizeof(str))){
        perror("write");
        exit(EXIT_FAILURE);
    }
}

int main(int argc, char *argv[])
{
    char tmp_filename_template[] = "/tmp/output_fileXXXXXX";

    int out_fd = mkstemp(tmp_filename_template);
    if (out_fd == -1) {
        perror("mkstemp");
        exit(EXIT_FAILURE);
    }

    do_write(out_fd);

    return EXIT_SUCCESS;
}

Während eines normalen Anrufs strace zeigt den Wert der an den Systemaufruf übergebenen Deskriptornummer an:

$ strace -e trace=write -o write-tmp-file.log ./write-tmp-file
$ cat write-tmp-file.log
write(3, "write me to a filen", 20)  = 20
+++ exited with 0 +++

Mit einer Fahne -y Das Dienstprogramm zeigt den Pfad zu der Datei an, der der Deskriptor entspricht:

$ strace -y -e trace=write -o write-tmp-file.log ./write-tmp-file
$ cat write-tmp-file.log
write(3</tmp/output_fileCf5MyW>, "write me to a filen", 20) = 20
+++ exited with 0 +++

Beispiel: Dateizugriffsverfolgung

Eine weitere nützliche Funktion: Zeigt nur Systemaufrufe an, die einer bestimmten Datei zugeordnet sind. Nächste Programm hängt eine Zeile an eine beliebige Datei an, die als Argument übergeben wurde:

void do_write(int out_fd)
{
    char str[] = "write me to a filen";

    if (sizeof(str) != write(out_fd, str, sizeof(str))){
        perror("write");
        exit(EXIT_FAILURE);
    }
}

int main(int argc, char *argv[])
{
    /*
     * Path will be provided by the first program argument.
     *  */
    const char *path = argv[1];

    /*
     * Open an existing file for writing in append mode.
     *  */
    int out_fd = open(path, O_APPEND | O_WRONLY);
    if (out_fd == -1) {
        perror("open");
        exit(EXIT_FAILURE);
    }

    do_write(out_fd);

    return EXIT_SUCCESS;
}

Standardmäßig strace zeigt viele unnötige Informationen an. Flagge -P mit einem Argument bewirkt, dass Strace nur Aufrufe an die angegebene Datei ausgibt:

$ strace -y -P/tmp/test_file.log -o write-file.log ./write-file /tmp/test_file.log
$ cat write-file.log
openat(AT_FDCWD, "/tmp/test_file.log", O_WRONLY|O_APPEND) = 3</tmp/test_file.log>
write(3</tmp/test_file.log>, "write me to a filen", 20) = 20
+++ exited with 0 +++

Beispiel: Multithread-Programme

Dienstprogramm strace kann auch bei der Arbeit mit Multithreading hilfreich sein Programm. Das folgende Programm schreibt aus zwei Streams in die Standardausgabe:

void *thread(void *arg)
{
    (void) arg;

    printf("Secondary thread: workingn");
    sleep(1);
    printf("Secondary thread: donen");

    return NULL;
}

int main(int argc, char *argv[])
{
    printf("Initial thread: launching a threadn");

    pthread_t thr;
    if (0 != pthread_create(&thr, NULL, thread, NULL)) {
        fprintf(stderr, "Initial thread: failed to create a thread");
        exit(EXIT_FAILURE);
    }

    printf("Initial thread: joining a threadn");
    if (0 != pthread_join(thr, NULL)) {
        fprintf(stderr, "Initial thread: failed to join a thread");
        exit(EXIT_FAILURE);
    };

    printf("Initial thread: done");

    exit(EXIT_SUCCESS);
}

Natürlich muss es mit einer besonderen Begrüßung an den Linker kompiliert werden – dem Flag -pthread:

$ gcc examples/thread-write.c -pthread -o thread-write
$ ./thread-write
/thread-write
Initial thread: launching a thread
Initial thread: joining a thread
Secondary thread: working
Secondary thread: done
Initial thread: done
$

Flagge -ffügt wie bei regulären Prozessen die PID des Prozesses am Anfang jeder Zeile hinzu.

Dabei handelt es sich natürlich nicht um eine Thread-ID im Sinne der Implementierung des POSIX-Threads-Standards, sondern um die Nummer, die der Task-Scheduler unter Linux verwendet. Aus letzterer Sicht gibt es keine Prozesse oder Threads – es gibt Aufgaben, die auf die verfügbaren Kerne der Maschine verteilt werden müssen.

Bei der Arbeit in mehreren Threads werden die Systemaufrufe zu zahlreich:

$ strace -f -othread-write.log ./thread-write
$ wc -l thread-write.log
60 thread-write.log

Es ist sinnvoll, sich nur auf die Prozessverwaltung und Systemaufrufe zu beschränken write:

$ strace -f -e trace="%process,write" -othread-write.log ./thread-write
$ cat thread-write.log
18211 execve("./thread-write", ["./thread-write"], 0x7ffc6b8d58f0 /* 64 vars */) = 0
18211 arch_prctl(ARCH_SET_FS, 0x7f38ea3b7740) = 0
18211 write(1, "Initial thread: launching a thre"..., 35) = 35
18211 clone(child_stack=0x7f38e9ba2fb0, flags=CLONE_VM|CLONE_FS|CLONE_FILES|CLONE_SIGHAND|CLONE_THREAD|CLONE_SYSVSEM|CLONE_SETTLS|CLONE_PARENT_SETTID|CLONE_CHILD_CLEARTID, parent_tidptr=0x7f38e9ba39d0, tls=0x7f38e9ba3700, child_tidptr=0x7f38e9ba39d0) = 18212
18211 write(1, "Initial thread: joining a thread"..., 33) = 33
18212 write(1, "Secondary thread: workingn", 26) = 26
18212 write(1, "Secondary thread: donen", 23) = 23
18212 exit(0)                           = ?
18212 +++ exited with 0 +++
18211 write(1, "Initial thread: done", 20) = 20
18211 exit_group(0)                     = ?
18211 +++ exited with 0 +++

Übrigens, Fragen. Welcher Systemaufruf wird verwendet, um einen neuen Thread zu erstellen? Wie unterscheidet sich dieser Aufruf von Threads vom Aufruf von Prozessen?

Meisterklasse: Prozessstapel zum Zeitpunkt eines Systemaufrufs

Einer der kürzlich erschienenen strace Fähigkeiten – Anzeige des Stapels von Funktionsaufrufen zum Zeitpunkt des Systemaufrufs. Einfach Beispiel:

void do_write(void)
{
    char str[] = "write me to stdoutn";
    if (sizeof(str) != write(STDOUT_FILENO, str, sizeof(str))){
        perror("write");
        exit(EXIT_FAILURE);
    }
}

int main(int argc, char *argv[])
{
    do_write();
    return EXIT_SUCCESS;
}

Natürlich wird die Programmausgabe sehr umfangreich und zusätzlich zur Flagge -k (Call-Stack-Anzeige), ist es sinnvoll, Systemaufrufe nach Namen zu filtern:

$ gcc examples/write-simple.c -o write-simple
$ strace -k -e trace=write -o write-simple.log ./write-simple
write me to stdout
$ cat write-simple.log
write(1, "write me to stdoutn", 20)  = 20
 > /lib/x86_64-linux-gnu/libc-2.27.so(__write+0x14) [0x110154]
 > /home/vkazanov/projects-my/strace-post/write-simple(do_write+0x50) [0x78a]
 > /home/vkazanov/projects-my/strace-post/write-simple(main+0x14) [0x7d1]
 > /lib/x86_64-linux-gnu/libc-2.27.so(__libc_start_main+0xe7) [0x21b97]
 > /home/vkazanov/projects-my/strace-post/write-simple(_start+0x2a) [0x65a]
+++ exited with 0 +++

Meisterklasse: Fehlerinjektion

Und noch eine neue und sehr nützliche Funktion: Fehlerinjektion. Hier Programmund schreibt zwei Zeilen in den Ausgabestream:

#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>

void do_write(const char *str, ssize_t len)
{
    if (len != write(STDOUT_FILENO, str, (size_t)len)){
        perror("write");
        exit(EXIT_FAILURE);
    }
}

int main(int argc, char *argv[])
{
    (void) argc; (void) argv;

    char str1[] = "write me 1n";
    do_write(str1, sizeof(str1));

    char str2[] = "write me 2n";
    do_write(str2, sizeof(str2));

    return EXIT_SUCCESS;
}

Lassen Sie uns beide Schreibaufrufe verfolgen:

$ gcc examples/write-twice.c -o write-twice
$ ./write-twice
write me 1
write me 2
$ strace -e trace=write -owrite-twice.log ./write-twice
write me 1
write me 2
$ cat write-twice.log
write(1, "write me 1n", 12)          = 12
write(1, "write me 2n", 12)          = 12
+++ exited with 0 +++

Jetzt verwenden wir den Ausdruck injecteinen Fehler einfügen EBADF in allen Schreibaufrufen:

$ strace -e trace=write -e inject=write:error=EBADF -owrite-twice.log ./write-twice
$ cat write-twice.log
write(1, "write me 1n", 12)          = -1 EBADF (Bad file descriptor) (INJECTED)
write(3, "write: Bad file descriptorn", 27) = -1 EBADF (Bad file descriptor) (INJECTED)
+++ exited with 1 +++

Es ist interessant, welche Fehler zurückgegeben werden alle Herausforderungen write, einschließlich des hinter perror versteckten Aufrufs. Es ist nur sinnvoll, beim ersten Aufruf einen Fehler zurückzugeben:

$ strace -e trace=write -e inject=write:error=EBADF:when=1 -owrite-twice.log ./write-twice
write: Bad file descriptor
$ cat write-twice.log
write(1, "write me 1n", 12)          = -1 EBADF (Bad file descriptor) (INJECTED)
write(3, "write: Bad file descriptorn", 27) = 27
+++ exited with 1 +++

Oder der zweite:

$ strace -e trace=write -e inject=write:error=EBADF:when=2 -owrite-twice.log ./write-twice
write me 1
write: Bad file descriptor
$ cat write-twice.log
write(1, "write me 1n", 12)          = 12
write(1, "write me 2n", 12)          = -1 EBADF (Bad file descriptor) (INJECTED)
write(3, "write: Bad file descriptorn", 27) = 27
+++ exited with 1 +++

Die Angabe des Fehlertyps ist nicht erforderlich:

$ strace -e trace=write -e fault=write:when=1 -owrite-twice.log ./write-twice
$ cat write-twice.log
write(1, "write me 1n", 12)          = -1 ENOSYS (Function not implemented) (INJECTED)
write(3, "write: Function not implementedn", 32) = 32
+++ exited with 1 +++

In Kombination mit anderen Flags können Sie den Zugriff auf eine bestimmte Datei „unterbrechen“. Beispiel:

$ strace -y -P/tmp/test_file.log -e inject=file:error=ENOENT -o write-file.log ./write-file /tmp/test_file.log
open: No such file or directory
$ cat write-file.log
openat(AT_FDCWD, "/tmp/test_file.log", O_WRONLY|O_APPEND) = -1 ENOENT (No such file or directory) (INJECTED)
+++ exited with 1 +++

Neben der Fehlerinjektion kann man Dies kann zu Verzögerungen beim Tätigen von Anrufen oder beim Empfangen von Signalen führen.

Nachwort

Dienstprogramm strace - ein einfaches und zuverlässiges Werkzeug. Aber neben Systemaufrufen können auch andere Aspekte der Funktionsweise von Programmen und des Betriebssystems debuggt werden. Es kann beispielsweise Aufrufe dynamisch verknüpfter Bibliotheken verfolgen. ltracekönnen sie einen Blick in die Funktionsweise des Betriebssystems werfen SystemTap и ftraceund ermöglicht es Ihnen, die Programmleistung eingehend zu untersuchen perf. Dennoch ist es so strace - die erste Verteidigungslinie bei Problemen mit meinen eigenen Programmen und denen anderer Leute, und ich verwende es mindestens ein paar Mal pro Woche.

Kurz gesagt, wenn Sie Unix lieben, lesen Sie man 1 strace und werfen Sie gerne einen Blick auf Ihre Programme!

Source: habr.com

Kommentar hinzufügen