Strace Linux: vēsture, uzbūve un lietojums

Strace Linux: vēsture, uzbūve un lietojums

Unix līdzīgās operētājsistēmās programmu mijiedarbība ar ārējo pasauli un operētājsistēmu notiek caur nelielu funkciju kopumu — sistēmas izsaukumiem. Tas nozīmē, ka debugēšanas nolūkos ir noderīgi vērot, kādas sistēmas izsaukumus izpilda procesi.

Programmu "intīmās dzīves" uzraudzībā Linux palīdz rīks strace, kuram šis raksts ir veltīts. Tiek sniegta īsa vēsture strace un līdzīgu programmu apraksts.

Satelīte

Suatu veidu izcelsme

Galvenā saskarne starp programmām un OS kodolu Unix ir sistēmas izsaukumi (angl. system calls, syscalls), programmu mijiedarbība ar ārējo pasauli notiek tikai caur tiem.

Tomēr pirmajā publiskajā Unix versijā (Version 6 Unix, 1975. gads) nebija ērti veidi, kā uzraudzīt lietotāja procesu uzvedību. Lai atrisinātu šo problēmu, Bell Labs nākamajā versijā (Version 7 Unix, 1979. gads) piedāvāja jaunu sistēmas izsaukumu — ptrace.

ptrace tika izstrādāts galvenokārt interaktīvajiem debugeriem, taču 80. gadu beigās (komerciālajā System V Release 4) uz šī pamata parādījās un guva plašu popularitāti specializēti debugeri — sistēmas izsaukumu izsekotāji.

Pirmais Strace versiju 1992. gadā publicēja Pols Kronenbergs e-pasta sarakstā comp.sources.sun kā alternatīvu slēgtajam rīkam trace no Sun. Gan klons, gan oriģināls bija paredzēti SunOS, taču līdz 1994. gadam strace tika pārtulkots uz System V, Solaris un arvien populārāku Linux.

Šodien strace atbalsta tikai Linux un balstās uz ptrace, kas ir kļuvusi plašāk pazīstama ar vairākiem paplašinājumiem.

Mūsdienu (un diezgan aktīvais) uzturētājs strace — Dmitrijs Levins. Pateicoties viņam, rīks ir ieguvis uzlabotas iespējas, piemēram, kļūdu injekcijas sistēmas izsaukumos, plaša spektra arhitektūru atbalstu un, galvenais, mascots. Neoficiālie avoti apgalvo, ka izvēle kritusi uz strausu sakarā ar krievu vārda "страус" līdzību ar angļu "strace".

Tas ir arī ļoti svarīgi, ka sistēmas izsaukums ptrace un pārraudzītāji vēl joprojām nav iekļauti POSIX, neskatoties uz ilgo vēsturi un to realizāciju Linux, FreeBSD, OpenBSD un tradicionālajā Unix.

Strace struktūra divos vārdos: Piglet Trace

"Tu neesi gaidījis, ka tu to sapratīsi" (Deniss Riči, komentārs Version 6 Unix avota kodā)

Kopš bērnības es nevarēju ciest melnus kastītes: ar rotaļlietām es nespēlēju, bet mēģināju izprast to uzbūvi (pieaugušie lietoja vārdu "salauzu", bet neticiet ļaunajiem valodām). Iespējams, tādēļ man ir tik tuva neformālā kultūra no pirmajiem Unix un mūsdienu open-source kustības.

Šajā rakstā nav saprātīgi analizēt avota kodu, kas ir ieguvis gadu gaitā strace. Bet arī noslēpumiem nevajadzētu palikt lasītājiem. Tādēļ, lai parādītu līdzīgu strace programmu darbības principu, es sniegšu neliela izsekošanas rīka kodu — Piglet Trace (ptr). Tam nav īpašu spēju, bet galvenais — tas izvada programmas sistēmas izsaukumus:

$ 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 atpazīst apmēram simts Linux sistēmas izsaukumu (sk. tabula) un darbojas tikai x86-64 arhitektūrā. Mācību nolūkiem tas ir pietiekami.

Paskatīsimies, kā darbojas mūsu klons. Gadījumā ar Linux, kā jau iepriekš minēts, debuggeriem un tracing rīkiem tiek izmantots sistēmas izsaukums ptrace. Tas darbojas, pārsūtot komandu identifikatorus pirmajā argumentā, no kuriem mums ir nepieciešami tikai PTRACE_TRACEME, PTRACE_SYSCALL un PTRACE_GETREGS.

Tracer darbs sākas parastajā Unix stilā: fork(2) izveido bērnu procesu, un tas savukārt, izmantojot exec(3) izsauc pārbaudāmo programmu. Vienīgā nianses šeit — izsaukums ptrace(PTRACE_TRACEME) pirms exec: bērnu process gaida, ka vecāku process to uzraudzīs:

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

Tēva process tagad jāsauc wait(2) bērnu procesā, tas ir, jāpārliecinās, ka pāreja uz izsekošanas režīmu notika:

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

Ar to sagatavošanās ir beigusies, un var ķerties pie sistēmas izsaukumu uzraudzības bezgalīgā ciklā.

Izsaukt ptrace(PTRACE_SYSCALL) garantē, ka nākamais gaidīt vecāks pabeigs vai nu pirms sistēmas izsaukuma izpildes, vai tūlīt pēc tā pabeigšanas. Starp abiem izsaukumiem var veikt jebkādas darbības: aizstāt izsaukumu ar alternatīvu, mainīt argumentus vai atgrieztās vērtības.

Mums pietiek divreiz izsaukt komandu ptrace(PTRACE_GETREGS), lai iegūtu reģistra stāvokli rax pirms izsaukuma (sistēmas izsaukuma numurs) un tūlīt pēc (atgrieztais vērtējums).

Patiesībā, cikls:

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

Un tas ir viss izsekotājs. Tagad jūs zināt, ar ko sākt nākamo portēšanu DTrace uz Linux.

Pamati: programmas palaišana, izmantojot strace

Kā pirmo izmantošanas piemēru strace, varbūt, ir vērts piesaukt visvienkāršāko veidu — palaist programmu, ko kontrolē strace.

Lai netērētu laiku bezgalīgajā izsaukumu sarakstā tipiskai programmai, uzrakstīsim minimālu programmu apņemot write:

int main(int argc, char *argv[])
{
    char str[] = "write me to stdoutn";
     write(2) ir vienkārša apvalka funkcija ap sistēmas izsaukumu, tādēļ tai vajadzētu būt viegli atrodams
     * sistēmas izsaukumu izsekojumā. 
    if (sizeof(str) != write(STDOUT_FILENO, str, sizeof(str))){
        perror("write");
        return EXIT_FAILURE;
    }
    return EXIT_SUCCESS;
}

Sanalizēsim programmu un pārliecināsimies, ka tā darbojas:

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

Un visbeidzot palaidīsim to strace vadībā:

$ strace .\/write-simple
pexecve(".\/write", [".\/write"], 0x7ffebd6145b0  71 vars ) = 0
brk(NULL)                               = 0x55ff5489e000
access("\/etc\/ld.so.nohwcap", F_OK)      = -1 ENOENT (Nav tāda faila vai direktorijas)
access("\/etc\/ld.so.preload", R_OK)      = -1 ENOENT (Nav tāda faila vai direktorijas)
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 (Nav tāda faila vai direktorijas)
openat(AT_FDCWD, "\/lib\/x86_64-linux-gnu\/libc.so.6", O_RDONLY|O_CLOEXEC) = 3
read(3, "177ELF2113        3 >
Iegādājieties uzticamu tīmekļa hostingu ar DDoS aizsardzību, VPS VDS serveriem 🔥 Iegādājieties uzticamu tīmekļa hostingu ar DDoS aizsardzību, VPS VDS serveriem | ProHoster