Strace unter Linux: Geschichte, Aufbau und Verwendung

Strace unter Linux: Geschichte, Aufbau und Verwendung

In Unix-ähnlichen Betriebssystemen erfolgt die Kommunikation eines Programms mit der Außenwelt und dem Betriebssystem über eine kleine Menge an Funktionen – Systemaufrufen. Daher kann es zu Debugging-Zwecken nützlich sein, die von den Prozessen ausgeführten Systemaufrufe zu überwachen.

Dabei hilft das Dienstprogramm strace, dem sich dieser Artikel widmet. In den Beispielen für die Verwendung von „Spionage“-Hardware sind eine kurze Geschichte strace und eine Beschreibung des Aufbaus solcher Programme enthalten.

Inhalt

Die Entstehung der Arten

Das Hauptinterface zwischen Programmen und dem Kernel des Betriebssystems in Unix sind die Systemaufrufe (engl. system calls, syscalls), die Interaktion der Programme mit der Außenwelt erfolgt 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 überwachen. Um dieses Problem zu lösen, schlugen die Bell Labs in der nächsten Version (Version 7 Unix, 1979) einen neuen Systemaufruf vor – ptrace.

Ptrace wurde zunächst hauptsächlich für interaktive Debugger entwickelt, aber Mitte der 80er Jahre (in der bereits kommerziellen Ära von System V Release 4) entstanden auf dieser Basis spezialisierte Debugger – Systemaufruf-Tracker, die weit verbreitet wurden.

Erster Selbe Version von strace wurde 1992 von Paul Kronenburg in der Mailingliste comp.sources.sun als Alternative zum proprietären Dienstprogramm trace von Sun veröffentlicht. Sowohl der Klon als auch das Original waren für SunOS vorgesehen, aber bis 1994 strace wurde es auf System V, Solaris und das zunehmend beliebte Linux portiert.

Heute unterstützt strace nur Linux und basiert weiterhin auf demselben ptrace, das mit zahlreichen Erweiterungen versehen ist.

Der moderne (und sehr aktive) Hauptentwickler straceDmitri Levin. Dank ihm hat das Dienstprogramm fortschrittliche Funktionen wie Fehlerinjektion in Systemaufrufe, Unterstützung für eine breite Palette von Architekturen und vor allem das Maskottchen. Unofficiale Quellen behaupten, dass die Wahl auf den Strauß aufgrund der Klangähnlichkeit des russischen Wortes "страус" und des englischen "strace" gefallen sei.

Nicht weniger wichtig ist, dass der Systemaufruf ptrace und die Tracer trotz ihrer langen Geschichte und ihrer Implementierung in Linux, FreeBSD, OpenBSD und traditionellen Unix nicht in POSIX aufgenommen wurden.

Der Aufbau von strace in kurzen Worten: Piglet Trace

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

Seit frühester Kindheit kann ich schwarze Kästen nicht ausstehen: Mit Spielzeug habe ich nie gespielt, sondern versucht, ihre Funktionsweise zu verstehen (Erwachsene verwendeten das Wort "zerstört", aber glauben Sie nicht den bösen Zungen). Vielleicht ist das der Grund, warum mir die informelle Kultur der frühen Unix und der modernen Open-Source-Bewegung so nahesteht.

Im Rahmen dieses Artikels ist es nicht sinnvoll, den Quellcode von strace zu zerlegen, der über Jahrzehnte hinweg gereift ist. Aber es sollten auch keine Geheimnisse für die Leser bleiben. Daher werde ich, um das Funktionsprinzip ähnlicher strace-Programme zu demonstrieren, den Code eines Mini-Tracers zeigen — Piglet Trace (ptr). Er kann nichts Besonderes tun, 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 terminated

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

Lassen Sie uns die Funktionsweise unseres Klons untersuchen. Im Fall von Linux verwendet man für Debugger und Tracer, wie oben erwähnt, den Systemaufruf ptrace. Dieser funktioniert, indem er im ersten Argument die Identifikatoren übergibt, von denen wir nur PTRACE_TRACEME, PTRACE_SYSCALL und PTRACE_GETREGS.

Der Tracer beginnt im traditionellen Unix-Stil: fork(2) startet einen Kindprozess, der wiederum mit Hilfe von exec(3) das zu untersuchende Programm ausführt. Die einzige Besonderheit hier ist der Aufruf ptrace(PTRACE_TRACEME) vor exec: der Kindprozess erwartet, dass der Elternprozess ihn überwacht:

pid_t child_pid = fork();
switch (child_pid) {
case -1:
    err(EXIT_FAILURE, "fork");
case 0:
     Kind hier 
     Ein Trace-Modus muss aktiviert werden. Ein Elternteil muss warten(2), bis dies passiert.
    ptrace(PTRACE_TRACEME, 0, NULL, NULL);
     Ersetzt sich selbst durch ein auszuffhrendes Programm.
    execvp(argv[1], argv + 1);
    err(EXIT_FAILURE, "exec");
}

Der Elternprozess muss jetzt aufrufen wait(2) im Kindprozess, d.h. sicherstellen, 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");

Damit sind die Vorbereitungen abgeschlossen und es kann mit dem Überwachen von Systemaufrufen in einer Endlosschleife begonnen werden.

Aufruf ptrace(PTRACE_SYSCALL) garantiert, dass der folgende warte Elternprozess entweder vor der Ausfchrung des Systemaufrufs oder direkt nach dessen Abschluss endet. Zwischen zwei Aufrufen sind verschiedene Aktionen m4glich: den Aufruf durch eine Alternative ersetzen, die Argumente oder den Rückgabewert ändern.

Wir müssen lediglich zweimal den Befehl ptrace(PTRACE_GETREGS), um den Zustand des Registers rax vor dem Aufruf (Nummer des Systemaufrufs) und sofort danach (Rückgabewert) zu erhalten.

Das ist eigentlich die Schleife:

/* 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 der nächsten Portierung beginnen können DTrace auf Linux.

Grundlagen: Ausführen eines Programms unter der Kontrolle von strace

Als erstes Beispiel für die Nutzung strace, sollten wir wohl die einfachste Methode anführen — die Ausführung einer Anwendung unter strace.

Um sich nicht in der endlosen Liste von Aufrufen eines typischen Programms zu verlieren, schreiben wir ein minimalistisches Programm um schreiben:

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

Komplizieren wir das Programm und stellen sicher, dass 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
pexecve(".\/write", [".\/write"], 0x7ffebd6145b0  71 vars ) = 0
brk(NULL)                               = 0x55ff5489e000
access("\/etc\/ld.so.nohwcap", F_OK)      = -1 ENOENT (Datei oder Verzeichnis nicht gefunden)
access("\/etc\/ld.so.preload", R_OK)      = -1 ENOENT (Datei oder Verzeichnis nicht gefunden)
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 (Datei oder Verzeichnis nicht gefunden)
openat(AT_FDCWD, "\/lib\/x86_64-linux-gnu\/libc.so.6", O_RDONLY|O_CLOEXEC) = 3
read(3, "177ELF2113        3 >
60GB SSD 8Gb DDR4