Strace Linuxban: történelem, felépítés és használat.

Strace Linuxban: történelem, felépítés és használat.

Unix-alapú operációs rendszerekben a programok és a külső világ, valamint az operációs rendszer közötti kommunikáció egy kis funkcióhalmazon — rendszerhívásokon keresztül történik. Ezért hibakeresési célokra hasznos lehet figyelni a végrehajtott folyamatokat a rendszerhívásokon.

A Linux programok "intim életének" nyomon követésére a következő segédprogram segít strace, aminek ez a cikk szenteltetett. Az „kémes” berendezések használatának példáihoz egy rövid történet is járul. strace és az ilyen programok eszközeinek leírása.

Tartalom

A fajok eredete

A programok és az Unix operációs rendszer magja közötti fő interfész a rendszerhívások (angolul system calls, syscalls), a programok külső világgal való kölcsönhatása kizárólag ezen keresztül valósul meg.

Azonban az első nyilvános Unix verzióban (Version 6 Unix, 1975) kényelmes módok nem álltak rendelkezésre a felhasználói folyamatok viselkedésének nyomon követésére. E problémára a Bell Labs a következő verzióra (Version 7 Unix, 1979) új rendszerhívást javasolt — ptrace.

A ptrace-t elsősorban interaktív hibakeresők számára fejlesztették ki, de a 80-as évek végére (a már kereskedelmi System V Release 4) ezen alapul új, speciális hibakeresők — a rendszerhívások nyomozói — jelentek meg, és széles körben elterjedtek.

Első A strace ezen verzióját Paul Kronenburg publikálta a comp.sources.sun hírlevélben 1992-ben, mint alternatíváját a Sun zárt segédprogramjának. trace Mind a klón, mind az eredeti a SunOS-hoz készült, de 1994-re strace portálták a System V-re, Solarisra, és a népszerű Linuxra.

Ma már a strace csak Linuxot támogat és az összes ptrace, amely számos kiterjesztéssel bővült.

A modern (és meglehetősen aktív) karbantartó strace — Dmitrij Leven. Kizárólag ennek köszönhetően a segédprogram fejlettebb funkciókkal gazdagodott, mint a rendszerhívások hibáinak injektálása, széles architektúrák támogatása és, ami a legfontosabb, mascotjaNem hivatalos források szerint a választás a struccra esett, mivel a "strucc" orosz szó és az angol "strace" hasonlósága miatt.

Fontos megemlíteni, hogy a ptrace rendszerhívás és a nyomozók nem kerültek be a POSIX szabványba, annak ellenére, hogy hosszú múltra tekintenek vissza, és megvalósításuk elérhető Linuxban, FreeBSD-ben, OpenBSD-ben és a hagyományos Unix rendszerekben.

A strace röviden: Piglet Trace

"Nem várják el, hogy megértsd ezt" (Dennis Ritchie, megjegyzés a Version 6 Unix forráskódjában)

Gyermekkorom óta ki nem állhatom a fekete dobozokat: a játékokkal nem játszottam, hanem próbáltam megérteni, hogyan működnek (a felnőttek azt mondták, hogy "törtem őket", de ne higgy a gonosz nyelveknek). Talán ezért érzem magam közel a korai Unix informális kultúrájához és a modern nyílt forráskódú mozgalomhoz.

Ennek a cikknek a keretein belül nem érdemes a strace évtizedek alatt kifejlesztett forráskódját elemezni. De titkok sem maradhatnak az olvasók előtt. Ezért, hogy bemutassam az ilyen strace programok működési elvét, elővezetem egy apró nyomozó kódját — Piglet Trace (ptr). Nem csinál különösebb dolgokat, de a lényeg, hogy a program rendszerszintű hívásait kiírja:

$ 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

A Piglet Trace nagyjából száz Linux rendszerhívást ismer fel (lásd a táblázatot). Ez kizárólag x86-64 architektúrán működik. Tanulmányi célokra ez elegendő.

Nézzük meg a klónunk működését. A Linux esetében a hibakeresők és nyomozók a fent említett ptrace rendszerhívást használják. Ez az első argumentumban parancsazonosítók átadásával működik, amelyekből nekünk csak a következők szükségesek: PTRACE_TRACEME, PTRACE_SYSCALL és PTRACE_GETREGS.

A nyomozó munkája szokásos Unix-stílusban kezdődik: fork(2) elindít egy gyermek folyamatot, ez pedig a exec(3) hívással elindítja a vizsgált programot. Az egyetlen kis finomság itt a ptrace(PTRACE_TRACEME) előhívása: a gyermek folyamat várja, hogy a szülő folyamat nyomon kövesse őt: exec: процесс-потомок ожидает, что процесс-родитель будет его отслеживать:

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");
}

A szülő folyamatnak most meg kell hívnia wait(2) a gyermek folyamatban, azaz biztosítania kell, hogy a nyomozási módra való váltás megtörtént:

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

Ezzel a készülődés befejeződött, és most már közvetlenül elkezdhetjük a rendszerhívások nyomozását végtelen ciklusban.

Hívás ptrace(PTRACE_SYSCALL) biztosítja, hogy a következő várakozás szülő befejeződik vagy a rendszerhívás végrehajtása előtt, vagy közvetlenül utána. Két hívás között bármilyen műveletet végezhetünk: helyettesíthetjük a hívást egy alternatívával, megváltoztathatjuk az argumentumokat vagy a visszatérési értéket.

Nekünk elegendő kétszer meghívni a parancsot ptrace(PTRACE_GETREGS), hogy megkapjuk a regiszter állapotát rax a hívás előtt (a rendszerhívás száma) és közvetlenül utána (a visszatérési érték).

Valójában a ciklus:

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

Íme az egész nyomozó. Most már tudja, honnan kezdje a következő portálást DTrace Linuxon.

Alapok: program indítása strace kezelésével

Az első példájaként feltehetően a legegyszerűbb módszert érdemes bemutatni — egy alkalmazás indítása a straceFelesleges tovább kutatni egy tipikus program hívásainak végtelen listájában, írjunk strace.

egy minimális programot köré 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; } write:

Fordítsuk le a programot, és győződjünk meg róla, hogy működik:

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

És végül indítsuk el a strace alá:

$ 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, "177ELF2113 3 >

Unix-szerű rendszerekben.
Megbízható hosting vásárlása weboldalak számára, DDoS védelemmel, VPS VDS szerverek. 🔥 Megbízható webtárhely vásárlása DDoS védelemmel, VPS VDS szerverek | ProHoster