Strace v Linuxe: história, zariadenie a použitie

Strace v Linuxe: história, zariadenie a použitie

V Unixových operačných systémoch prebieha komunikácia programu s vonkajším svetom a operačným systémom prostredníctvom malej sady funkcií – systémových volaní. To znamená, že pre účely ladenia môže byť užitočné sledovať vykonávané procesy pomocou systémových volaní.

Sledovať „intímny život“ programov na Linuxe pomáha utilita strace, ktorej je venovaný tento článok. K príkladom použitia „špionážneho“ vybavenia je priložený krátky príbeh strace a opis zariadenia podobných programov.

Obsah

Pôvod druhov

Hlavné rozhranie medzi programami a jadrom OS v Unixe sú systémové volania (angl. system calls, syscalls), interakcia programov s vonkajším svetom prebieha výhradne prostredníctvom nich.

No v prvej verejnej verzii Unixu (Version 6 Unix, 1975 rok) neexistovali pohodlné spôsoby sledovania správania používateľských procesov. Na vyriešenie tohto problému navrhla Bell Labs v nasledujúcej verzii (Version 7 Unix, 1979 rok) nové systémové volanie – ptrace.

Ptrace bol vyvíjaný predovšetkým pre interaktívne ladice, ale koncom 80. rokov (v ére komerčného už System V Release 4) na jeho základe sa objavili a získali široké rozšírenie úzko špecializované ladice – trasery systémových volaní.

Prvá Táto verzia strace bola publikovaná Paulom Kronenburgom v distribúcii comp.sources.sun v roku 1992 ako alternatíva k uzavretej utilite trace od Sun. Ako klon, tak aj originál boli určené pre SunOS, ale do roku 1994 strace bola portovaná na System V, Solaris a na čoraz populárnejší Linux.

Dnes strace podporuje iba Linux a opiera sa o ten istý ptrace, ktorý sa obohatil o množstvo rozšírení.

Moderný (a veľmi aktívny) správcovia strace — Dmitrij Levin. Vďaka nemu utilita získala pokročilé možnosti ako injekcia chýb do systémových volaní, podporu širokého spektra architektúr a najmä maskota. Neoficiálne zdroje tvrdia, že voľba padla na pštrosa kvôli zvukovej podobnosti ruského slova „петух“ a anglického "strace".

Nie je bez významu ani to, že systémový hovor ptrace a ladenie neboli zahrnuté v POSIX, napriek dlhému príbehu a existencii implementácie v Linuxe, FreeBSD, OpenBSD a tradičných Unix.

Zariadenie strace v dvoch slovách: Piglet Trace

"Nie je od vás očakávané, že to pochopíte" (Dennis Ritchie, poznámka v zdrojovom kóde verzie 6 Unixu)

Od detstva nenávidím čierne skrinky: s hračkami som si nehral, ale snažil som sa pochopiť ich fungovanie (dospelí používali slovo „rozbíjal“, ale neverte zlým jazykům). Možno aj preto mi je neformálna kultúra prvých systémov Unix a súčasného open-source hnutia tak blízka.

V rámci tohto článku nie je rozumné analyzovať zdrojový kód za desaťročia vyvinutého strace. Ale ani tajomstvá by nemali čitateľom ostať. Preto, aby som ukázal princíp, ako takéto programy ako strace fungujú, uvediem kód mini ladíča — Piglet Trace (ptr). Nič zvláštne robiť nevie, ale hlavná vec — systémové volania programu — vypisuje:

$ 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 rozpoznáva približne sto systémových volaní Linuxu (pozri tabuľku) a funguje len na architektúre x86-64. Pre vzdelávacie účely to stačí.

Pozrime sa na fungovanie nášho klonu. V prípade Linuxu sa pre ladíče a trasovanie používa, ako bolo spomenuté vyššie, systémový hovor ptrace. Funguje prenášaním identifikátorov príkazov ako prvý argument, z ktorých potrebujeme len PTRACE_TRACEME, PTRACE_SYSCALL a PTRACE_GETREGS.

Fungovanie ladíča začína v bežnom štýle Unixu: fork(2) spúšťa dieťa proces, a ten zase pomocou exec(3) spúšťa skúmaný program. Jediná nuansa tu — hovor ptrace(PTRACE_TRACEME) pred exec: proces-dieťa očakáva, že proces-rodič bude jeho sledovať:

pid_t child_pid = fork();
switch (child_pid) {
case -1:
    err(EXIT_FAILURE, "fork");
case 0:
    /* Dieťa tu */
    /* Mód sledovania musí byť povolený. Rodič bude musieť čakať(2) kým sa to
     * stane. */
    ptrace(PTRACE_TRACEME, 0, NULL, NULL);
    /* Nahradiť sa programom, ktorý sa má spustiť. */
    execvp(argv[1], argv + 1);
    err(EXIT_FAILURE, "exec");
}

Rodičovský proces teraz musí zavolať wait(2) v dieťaťu, teda zabezpečiť, že prechod do režimu sledovania sa uskutočnil:

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

Týmto sú prípravy dokončené a môžeme pristúpiť priamo k sledovaniu systémových volaní v nekonečnom cykle.

Vyvolanie ptrace(PTRACE_SYSCALL) zaručuje, že nasledujúci čakať rodiča skončí buď pred vykonaním systémového volania, alebo hneď po jeho dokončení. Medzi dvoma volaniami môžeme vykonať nejaké akcie: nahradiť volanie alternatívnym, zmeniť argumenty alebo návratovú hodnotu.

Pre nás stačí dvakrát zavolať príkaz ptrace(PTRACE_GETREGS), aby sme získali stav registra rax pred volaním (číslo systémového volania) a hneď po (návratová hodnota).

Vlastne, cyklus:

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

A to je celý sledovač. Teraz viete, ako začať ďalšie portovanie DTrace na Linux.

Základy: spustenie programu pod správou strace

Ako prvý príklad použitia strace, pravdepodobne stojí za to uviesť najjednoduchší spôsob — spustiť aplikáciu pod správou strace.

Aby sme nemuseli prehľadávať nekonečný zoznam volaní typického programu, napíšeme minimálny program okolo write:

int main(int argc, char *argv[])
{
    char str[] = "write me to stdoutn";
    /* write(2) je jednoduchý obal okolo systémového volania, takže by ho malo byť ľahké
     * nájsť v sledovaní systémového volania. */
    if (sizeof(str) != write(STDOUT_FILENO, str, sizeof(str))){
        perror("write");
        return EXIT_FAILURE;
    }
    return EXIT_SUCCESS;
}

Zostavíme program a uistíme sa, že funguje:

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

A nakoniec ho spustíme pod správou strace:

$ 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 >
Kúpte si spoľahlivý hosting pre webové stránky s ochranou proti DDoS, VPS VDS servery 🔥 Kúpte si spoľahlivý hosting pre webové stránky s ochranou proti DDoS, VPS VDS servery | ProHoster