Strace unter Linux: Geschichte, Funktionsweise und Anwendung

Strace unter Linux: Geschichte, Funktionsweise und Anwendung

In Unix-ähnlichen Betriebssystemen erfolgt die Kommunikation zwischen Programmen und dem Betriebssystem über eine kleine Anzahl an Funktionen — den Systemaufrufen. Daher ist es aus Debugging-Gründen oft hilfreich, einen Blick auf die von Prozessen ausgeführten Systemaufrufe zu werfen.

Um die "intimen Details" von Programmen unter Linux zu beobachten, kann das Dienstprogramm strace, dem dieser Artikel gewidmet ist, verwendet werden. Es enthält eine kurze Geschichte strace und eine Beschreibung von Programmen dieser Art.

Inhalt

Die Entstehung der Arten

Die Hauptschnittstelle zwischen Programmen und dem OS-Kernel in Unix sind die Systemaufrufe (engl. system calls, syscalls), die Interaktion der Programme mit der Außenwelt erfolgt ausschließlich durch sie.

Die erste öffentliche Version von Unix (Version 6 Unix, im Jahr 1975 gab es keine benutzerfreundlichen Methoden zur Verfolgung von Benutzerprozessverhalten. Um dieses Problem zu lösen, schlugen Bell Labs in der nächsten Version (Version 7 Unix, im Jahr 1979) einen neuen Systemaufruf vor — ptrace.

ptrace wurde hauptsächlich für interaktive Debugger entwickelt, doch bis Ende der 80er Jahre (in der kommerziellen Phase bereits System V Release 4) wurden auf dieser Basis spezialisierte Debugger — Systemaufruf-Trace-Tools — entwickelt und weit verbreitet.

Erster Die Version strace wurde von Paul Kronenberg 1992 in der Mailingliste comp.sources.sun als Alternative zu dem proprietären Tool trace von Sun veröffentlicht. Sowohl der Klon als auch das Original waren für SunOS gedacht, aber bis 1994 strace wurde es auf System V, Solaris und das aufstrebende Linux portiert.

Heute unterstützt strace nur Linux und basiert weiterhin auf dem ptrace, das mittlerweile viele Erweiterungen hat.

Der moderne (und sehr aktive) Hauptentwickler straceDmitrij Levin. Dank ihm erhielt das Tool fortschrittliche Funktionen, wie die Injektion von Fehlern in Systemaufrufe, Unterstützung für eine breite Palette von Architekturen und vor allem ein Maskottchen. Unbestätigte Quellen behaupten, dass die Wahl auf den Strauß fiel, da das russische Wort «страус» und das englische "strace" klanglich ähnlich sind.

Es ist ebenfalls bedeutend, dass der Systemaufruf ptrace und Debugger nicht in POSIX aufgenommen wurden, trotz einer langen Geschichte und der Verfügbarkeit in Linux, FreeBSD, OpenBSD und traditionellen Unix-Systemen.

Das Tool strace in Kürze: Piglet Trace

"You are not expected to understand this" (Dennis Ritchie, Kommentar im Quellcode von Version 6 Unix)

Seit meiner Kindheit habe ich eine Abneigung gegen schwarze Kästen: Mit Spielzeugen habe ich nicht gespielt, sondern versucht, ihre Funktionsweise zu verstehen (Erwachsene verwendeten das Wort „zerstören“, aber glauben Sie nicht den bösen Zungen). Vielleicht liegt es daran, dass mir die informelle Kultur der frühen Unix-Systeme und der modernen Open-Source-Bewegung so nah steht.

Im Rahmen dieses Artikels ist es unvernünftig, den Quellcode des über die Jahrzehnte gereiften strace zu analysieren. Doch auch den Lesern sollen keine Geheimnisse verborgen bleiben. Daher werde ich, um das Prinzip der Funktionsweise solcher strace-Programme zu demonstrieren, den Code eines minimalistischen Tracers präsentieren — Piglet Trace (ptr). Er kann nichts Besonderes, aber das Wichtigste — die Systemaufrufe des Programms — gibt er 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 beendet

Piglet Trace erkennt etwa einhundert Linux-Systemaufrufe (siehe Tabelle) und funktioniert nur auf der Architektur x86-64. Für Bildungszwecke ist das ausreichend.

Lassen Sie uns die Funktionsweise unseres Klons ansehen. Im Fall von Linux wird für Debugger und Tracer, wie bereits erwähnt, der Systemaufruf ptrace verwendet. Er funktioniert indem er im ersten Argument Identifikatoren von Befehlen übergibt, von denen wir nur PTRACE_TRACEME, PTRACE_SYSCALL und PTRACE_GETREGS.

Die Arbeit des Tracers beginnt im typischen Unix-Stil: fork(2) startet einen Kindprozess, der seinerseits mit Hilfe von exec(3) startet das untersuchte Programm. Die einzige Besonderheit hier ist der Aufruf ptrace(PTRACE_TRACEME) vor exec: Der Kindprozess wartet darauf, dass der Elternprozess ihn verfolgt:

pid_t child_pid = fork();
switch (child_pid) {
case -1:
    err(EXIT_FAILURE, "fork");
case 0:
    /* Kind hier */
    /* Ein Traced-Modus muss aktiviert werden. Ein Elternteil muss wait(2) aufrufen, damit dies
     * geschieht. */
    ptrace(PTRACE_TRACEME, 0, NULL, NULL);
    /* Ersetzt sich selbst durch ein auszuführendes Programm. */
    execvp(argv[1], argv + 1);
    err(EXIT_FAILURE, "exec");
}

Der Elternprozess muss jetzt wait(2) im Kindprozess aufrufen, um sicherzustellen, dass der Wechsel in den Tracemodus 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");

Damit sind die Vorbereitungen abgeschlossen, und man kann mit der Überwachung von Systemaufrufen in einer Endlosschleife beginnen.

Aufruf ptrace(PTRACE_SYSCALL) stellt sicher, dass der folgende wait Elternteil entweder vor der Ausführung des Systemaufrufs oder sofort nach dessen Abschluss beendet wird. Zwischen zwei Aufrufen können verschiedene Aktionen durchgeführt werden: den Aufruf durch einen alternativen ersetzen, Argumente oder Rückgabewerte ändern.

Es genügt, den Befehl ptrace(PTRACE_GETREGS), zweimal aufzurufen, um den Zustand des Registers rax vor dem Aufruf (Systemaufrufnummer) und sofort danach (Rückgabewert) zu erhalten.

Tatsächlich 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 gesamte Tracer. Jetzt wissen Sie, wo Sie mit dem nächsten Portierungen beginnen können. DTrace unter Linux.

Grundlagen: Ausführung eines Programms unter strace

Als erstes Beispiel für die Verwendung stracekönnte es sinnvoll sein, die einfachste Methode zu nennen – das Starten einer Anwendung, die strace.

Um uns nicht durch eine endlose Liste von Aufrufen eines typischen Programms zu wühlen, schreiben wir ein minimales Programm um write:

int main(int argc, char *argv[])
{
    char str[] = "write me to stdoutn";
    /* write(2) ist ein einfacher Wrapper um einen Systemaufruf, daher sollte er leicht zu
     * finden sein im Systemaufruf-Trace. */
    if (sizeof(str) != write(STDOUT_FILENO, str, sizeof(str))){
        perror("write");
        return EXIT_FAILURE;
    }
    return EXIT_SUCCESS;
}

Kompilieren wir das Programm und überprüfen, ob es funktioniert:

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

Und schließlich führen wir es unter strace aus:

$ strace ./write-simple
execve("./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, "177ELF2113        3 >
Zuverlässiges Webhosting mit DDoS-Schutz, VPS- und VDS-Server kaufen 🔥 Zuverlässiges Webhosting mit DDoS-Schutz, VPS- und VDS-Server kaufen | ProHoster