Strace i Linux: historia, struktur och användning

Strace i Linux: historia, struktur och användning

I Unix-liknande operativsystem sker kommunikationen mellan program och världen samt operativsystemet genom en liten uppsättning funktioner — systemanrop. Det innebär att det kan vara användbart att övervaka de systemanrop som utförs av processerna för felsökningssyften.

Att följa programmens "intima liv" på Linux underlättas av verktyget för att kontrollera paketöverföringen åt båda håll, som denna artikel är tillägnad. Där ges även en kort historia och beskrivning av användningen av "spionerings"-program. för att kontrollera paketöverföringen åt båda håll En enhetsbeskrivning av sådana program.

Innehåll

Arternas ursprung

Det huvudsakliga gränssnittet mellan programmen och kärnan i Unix är systemanrop (eng. system calls, syscalls.), interaktionen mellan programmen och omvärlden sker uteslutande genom dem.

Men i den första offentliga versionen av Unix (Version 6 Unix, 1975) fanns det inga praktiska sätt att övervaka användarprocessernas beteende. För att lösa detta problem föreslog Bell Labs i nästa version (Version 7 Unix, 1979) en ny systemanrop — ptrace.

Ptrace utvecklades främst för interaktiva felsökare, men i slutet av 80-talet (i den kommersiella redan System V Release 4) baserades det och fick stort spridning i specialiserade felsökare — systemanropsspårare.

Första Den samma version av strace publicerades av Paul Kronenburg i mailinglistan comp.sources.sun 1992 som ett alternativ till det stängda verktyget trace från Sun. Både klonen och originalet var avsedda för SunOS, men år 1994 för att kontrollera paketöverföringen åt båda håll portades den till System V, Solaris och det populära Linux.

Idag stöder strace endast Linux och bygger fortfarande på samma ptrace, nu med många utökningar.

Den moderna (och mycket aktiva) underhållaren för att kontrollera paketöverföringen åt båda håll — Dmitry Levin. Tack vare honom har verktyget försetts med avancerade funktioner som felinjicering i systemanrop, stöd för ett brett spektrum av arkitekturer och, viktigast av allt, maskot.Oofficiella källor hävdar att valet föll på strutsen på grund av likheten mellan det ryska ordet "страус" och det engelska "strace".

Det är också viktigt att notera att systemanropet ptrace och debuggarna aldrig blev en del av POSIX, trots sin långa historia och förekomsten av implementationer i Linux, FreeBSD, OpenBSD och traditionella Unix.

Strukturen hos strace i korthet: Piglet Trace

"Du förväntas inte förstå detta" (Dennis Ritchie, kommentar i koden för Version 6 Unix)

Från tidig barndom har jag alltid ogillat svarta lådor: jag lekte inte med leksakerna utan försökte förstå deras konstruktion (vuxna använde ordet "bröt sönder", men tro inte på illvilliga språk). Kanske är det därför jag känner mig så nära den informella kulturen hos de första Unix och den moderna open-source-rörelsen.

Inom ramen för denna artikel är det oklokt att gå igenom koden för den välvuxna strace som har åldrats i decennier. Men det ska inte finnas några hemligheter för läsarna. Därför, för att visa hur sådana strace-program fungerar, kommer jag att ge koden för en miniatyrtracer - Piglet Trace (ptr). Den gör ingenting särskilt, men det viktigaste — systemanropen från programmet — skriver ut:

$ 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 terminerade

Piglet Trace känner igen cirka hundra systemanrop i Linux (se tabellen) och fungerar endast på x86-64-arkitekturen. För undervisningssyften är detta tillräckligt.

Låt oss gå igenom hur vår klon fungerar. I fallet med Linux används, som nämnts ovan, systemanropet ptrace för debugger och tracers. Det fungerar genom att överföra kommandon i det första argumentet, av vilka vi bara behöver PTRACE_TRACEME, PTRACE_SYSCALL och PTRACE_GETREGS.

Arbetet för debuggern börjar på vanligt Unix-manér: fork(2) startar en barnprocess, som i sin tur startar det undersökta programmet med hjälp av exec(3) . Den enda detaljen här är anropet ptrace(PTRACE_TRACEME) före exec: barnprocessen väntar på att föräldraprocessen ska övervaka den:

pid_t child_pid = fork();
switch (child_pid) {
case -1:
    err(EXIT_FAILURE, "fork");
case 0:
    
    /* Barn här */
    /* En spårad läge måste aktiveras. En förälder måste vänta(2) på att det
     * ska ske. */
    ptrace(PTRACE_TRACEME, 0, NULL, NULL);
    /* Ersätt sig själv med ett program som ska köras. */
    execvp(argv[1], argv + 1);
    err(EXIT_FAILURE, "exec");
}

Nu måste föräldraprocessen anropa wait(2) i barnprocessen, alltså säkerställa att bytet till spårningsläge har skett:

/* 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");

Nu är förberedelserna klara och vi kan gå vidare till att spåra systemanrop i en oändlig loop.

Anrop ptrace(PTRACE_SYSCALL) garanterar att efterföljande vänta föräldern avslutas antingen före utförandet av systemanropet, eller omedelbart efter att det har slutförts. Mellan de två anropen kan man utföra åtgärder: byta anrop mot ett alternativt, ändra argument eller det returnerade värdet.

Vi behöver bara anropa kommandot ptrace(PTRACE_GETREGS), två gånger för att hämta tillståndet hos registret rax före anropet (nummer på systemanropet) och omedelbart efter (det returnerade värdet).

Egentligen, cykeln:

/* 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);
}

Och så är hela spårningsprogrammet klart. Nu vet du hur du ska börja nästa portering DTrace till Linux.

Grunderna: starta programmet under strace

Som ett första exempel på användning för att kontrollera paketöverföringen åt båda håll, kanske det är värt att ge det enklaste sättet — att starta en applikation under kontrollerad för att kontrollera paketöverföringen åt båda håll.

För att inte gräva ner oss i en oändlig lista av anrop från ett typiskt program, låt oss skriva ett minimalt program omkring skriv:

int main(int argc, char *argv[])
{
    char str[] = "write me to stdoutn";
    /* write(2) är ett enkelt omslag runt en syscall, så det borde vara enkelt att
     * hitta i syscall-tracingen. */
    if (sizeof(str) != write(STDOUT_FILENO, str, sizeof(str))){
        perror("write");
        return EXIT_FAILURE;
    }
    return EXIT_SUCCESS;
}

Låt oss kompilera programmet och säkerställa att det fungerar:

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

Och slutligen låt oss köra det under strace:

$ strace ./write-simple
execve("./write", ["./write"], 0x7ffebd6145b0 /* 71 vars */) = 0
brk(NULL) = 0x55ff5489e000
access("/etc/ld.so.nohwcap", F_OK) = -1 ENOENT (Inga sådana filer eller kataloger)
access("/etc/ld.so.preload", R_OK) = -1 ENOENT (Inga sådana filer eller kataloger)
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 (Inga sådana filer eller kataloger)
openat(AT_FDCWD, "/lib/x86_64-linux-gnu/libc.so.6", O_RDONLY|O_CLOEXEC) = 3
read(3, "177ELF2113        3 >
Köp pålitlig webbhotell med DDoS-skydd, VPS VDS-servrar 🔥 Köp pålitlig webbhotell med DDoS-skydd, VPS VDS-servrar | ProHoster