Strace i Linux: historie, struktur og bruk

Strace i Linux: historie, struktur og bruk

I Unix-liknende operativsystemer skjer kommunikasjonen mellom programmet og den eksterne verden samt operativsystemet gjennom et lite sett av funksjoner – systemkall. Dette betyr at det kan være nyttig å se etter systemkallene for debug-formål.

Å overvåke det «intime livet» til programmer på Linux hjelper verktøyet strace, som er dedikert til denne artikkelen. Eksempler på bruken av «spionutstyr» medfølger en kort historie strace og beskrivelse av slike programmer.

Innhold

Artenes opprinnelse

Hovedgrensesnittet mellom programmer og kjernen til OS i Unix er systemkall (eng. system calls, syscalls), og programmenes interaksjon med den eksterne verden skjer utelukkende gjennom dem.

Men i den første offentlige versjonen av Unix (Version 6 Unix, 1975) var det ikke praktiske metoder for å overvåke atferden til brukerprosesser. For å løse dette problemet foreslo Bell Labs i neste versjon (Version 7 Unix, 1979) et nytt systemkall – ptrace.

ptrace ble først og fremst utviklet for interaktive debuggers, men mot slutten av 80-tallet (i den kommersielle perioden allerede System V Release 4) kom det, basert på dette, smale debuggere – sporingsverktøy for systemkall.

Første Den samme versjonen av strace ble publisert av Paul Kronenburg i mailinglisten comp.sources.sun i 1992 som et alternativ til det lukkede verktøyet trace fra Sun. Både klonen og originalen var ment for SunOS, men innen 1994 strace ble portert til System V, Solaris og det stadig mer populære Linux.

I dag støtter strace kun Linux og baserer seg fortsatt på det samme ptrace, som har fått mange utvidelser.

Den moderne (og ganske aktive) vedlikeholderen strace — Dmitrij Levin. Takket være ham har verktøyet fått avanserte funksjoner som feilinjeksjon i systemkall, støtte for et bredt spekter av arkitekturer og, viktigst av alt, maskot. Uoffisielle kilder hevder at valget falt på strutsen på grunn av klangen mellom det russiske ordet «страус» og det engelske "strace".

Det er også viktig at systemanropet ptrace og sporingsverktøy aldri har blitt inkludert i POSIX, til tross for sin lange historie og eksisterende implementering i Linux, FreeBSD, OpenBSD og tradisjonell Unix.

Strukturen til strace med to ord: Piglet Trace

"You are not expected to understand this" (Dennis Ritchie, kommentar i kildekoden til Version 6 Unix)

Fra barndommen har jeg alltid mislikt svarte bokser: jeg lekte ikke med lekene, men prøvde å finne ut av hvordan de fungerte (voksne brukte ordet «ødelagte», men ikke tro på vondspråket). Kanskje er det derfor den uformelle kulturen til de tidlige Unix og den moderne open-source-bevegelsen føles så nær meg.

I denne artikkelen er det urimelig å gå gjennom kildekoden til strace, som har utviklet seg gjennom årtier. Men det skal heller ikke være noen hemmeligheter for leserne. Derfor, for å vise prinsippet bak liknende strace-programmer, vil jeg gi koden til en mini sporingsverktøy — Piglet Trace (ptr). Det gjør ikke noe spesielt, men det viktigste — det skriver ut systemanropene til programmet:

$ 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 gjenkjenner omtrent hundre systemanrop i Linux (se tabellen) og fungerer kun på x86-64-arkitekturen. For læringsformål er dette tilstrekkelig.

La oss se nærmere på hvordan vår klone fungerer. I Linux brukes, som nevnt tidligere, systemanropet ptrace for feilsøkere og sporingsverktøy. Det fungerer ved å overføre kommandidentifikatorer i første argument, hvorav vi kun trenger PTRACE_TRACEME, PTRACE_SYSCALL og PTRACE_GETREGS.

Funksjonen til sporingsverktøyet begynner på en vanlig Unix-måte: fork(2) starter en datterprosess, som igjen bruker exec(3) for å starte det undersøkte programmet. Den eneste finurligheten her er anropet ptrace(PTRACE_TRACEME) før exec: barneprosessen venter på at forelderen skal overvåke den:

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

Foreldra prosessen må nå kalle wait(2) i barneprosessen, altså sikre at overgangen til sporingsmodus har skjedd:

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

Nå er forberedelsene ferdige, og vi kan begynne selve sporing av systemanrop i en evig sløyfe.

Kall ptrace(PTRACE_SYSCALL) sikrer at den påfølgende wait forelderen vil fullføre enten før systemanropet utføres eller rett etter det er fullført. Mellom to kall kan man gjøre tiltak: erstatte anropet med et alternativ, endre argumentene eller returverdien.

Vi har mer enn nok til å kalle ptrace(PTRACE_GETREGS), to ganger for å få tilstanden til registeret rax før anropet (nummeret på systemanropet) og rett etter (returverdien).

Egentlig, sløyfen:

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

Og det er hele sporingsprogrammet. Nå vet du hvordan du skal starte porteringen DTrace til Linux.

Grunnleggende: kjøre et program under strace

Som et første eksempel på bruk strace, kan vi kanskje ta den enkleste måten - kjøre applikasjonen under ledelse av strace.

For å unngå å lete gjennom en endeløs liste av anrop fra et typisk program, la oss skrive en minimal program rundt write:

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

La oss kompilere programmet og sjekke at det fungerer:

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

Og til slutt kjøre det under ledelse av strace:

$ strace .\/write-simple
pexecve(".\/write", [".\/write"], 0x7ffebd6145b0 C!-- 71 vars --E) = 0
brk(NULL)                               = 0x55ff5489e000
access("\/etc\/ld.so.nohwcap", F_OK)      = -1 ENOENT (Ingen slik fil eller katalog)
access("\/etc\/ld.so.preload", R_OK)      = -1 ENOENT (Ingen slik fil eller katalog)
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 (Ingen slik fil eller katalog)
openat(AT_FDCWD, "\/lib\/x86_64-linux-gnu\/libc.so.6", O_RDONLY|O_CLOEXEC) = 3
read(3, "177ELF2113        3 >
Kjøp pålitelig hosting for nettsteder med DDoS-beskyttelse, VPS VDS-servere 🔥 Kjøp pålitelig hosting for nettsteder med DDoS-beskyttelse, VPS VDS-servere | ProHoster