Strace operētājsistēmā Linux: vēsture, dizains un lietošana

Strace operētājsistēmā Linux: vēsture, dizains un lietošana

Unix līdzīgās operētājsistēmās programmas saziņa ar ārpasauli un operētājsistēmu notiek caur nelielu funkciju kopumu – sistēmas izsaukumiem. Tas nozīmē, ka atkļūdošanas nolūkos var būt noderīgi izspiegot sistēmas izsaukumus, ko izpilda procesi.

Lietderība palīdz pārraudzīt programmu “intīmo dzīvi” operētājsistēmā Linux strace, kas ir šī raksta tēma. Spiegu aprīkojuma izmantošanas piemēriem ir pievienota īsa vēsture strace un šādu programmu dizaina apraksts.

saturs

Sugas izcelsme

Galvenā saskarne starp programmām un OS kodolu Unix ir sistēmas izsaukumi. sistēmas zvani, sistēmas zvani), programmu mijiedarbība ar ārpasauli notiek tikai caur tām.

Bet pirmajā publiskajā Unix versijā (Unix versija 6, 1975) nebija ērtu veidu, kā izsekot lietotāju procesu uzvedībai. Lai atrisinātu šo problēmu, Bell Labs atjauninās uz nākamo versiju (Unix versija 7, 1979) ierosināja jaunu sistēmas izsaukumu - ptrace.

ptrace tika izstrādāta galvenokārt interaktīvajiem atkļūdotājiem, bet līdz 80. gadu beigām (komerciālā laikmetā System V 4. izlaidums), pamatojoties uz to, parādījās un tika plaši izmantoti šauri vērsti atkļūdotāji — sistēmas izsaukumu izsekotāji.

Pirmais to pašu strace versiju Pols Kronenburgs publicēja comp.sources.sun adresātu sarakstā 1992. gadā kā alternatīvu slēgtai utilītai. trace no Sv. Gan klons, gan oriģināls bija paredzēti SunOS, taču līdz 1994. gadam strace tika pārnesta uz System V, Solaris un arvien populārāko Linux.

Mūsdienās strace atbalsta tikai Linux un paļaujas uz to pašu ptrace, apaudzis ar daudziem pagarinājumiem.

Mūsdienīgs (un ļoti aktīvs) uzturētājs strace Sākot no Dmitrijs Levins. Pateicoties viņam, utilīta ieguva uzlabotas funkcijas, piemēram, kļūdu ievadīšanu sistēmas zvanos, atbalstu plašam arhitektūru klāstam un, pats galvenais, talismans. Neoficiāli avoti apgalvo, ka izvēle uz strausu kritusi saskaņas dēļ starp krievu vārdu “ostrich” un angļu vārdu “strace”.

Ir arī svarīgi, ka ptrace sistēmas izsaukums un izsekotāji nekad netika iekļauti POSIX, neskatoties uz ilgu vēsturi un ieviešanu Linux, FreeBSD, OpenBSD un tradicionālajā Unix.

Strace ierīce īsumā: Piglet Trace

"Jūs to nesapratīsit" (Deniss Ričijs, komentārs 6. versijas Unix avota kodā)

Kopš agras bērnības es nevaru ciest melnās kastes: es nespēlējos ar rotaļlietām, bet centos izprast to struktūru (pieaugušie lietoja vārdu “salauza”, bet netic ļaunajām mēlēm). Varbūt tāpēc pirmā Unix neformālā kultūra un modernā atvērtā pirmkoda kustība man ir tik tuva.

Šī raksta vajadzībām nav saprātīgi izjaukt strace avota kodu, kas gadu desmitiem ir audzis. Bet lasītājiem nevajadzētu atstāt noslēpumus. Tāpēc, lai parādītu šādu strace programmu darbības principu, es norādīšu miniatūras marķiera kodu - Sivēnu pēda (ptr). Tas nezina, kā darīt neko īpašu, bet galvenais ir programmas sistēmas izsaukumi - tā izvada:

$ 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 aptuveni simtiem Linux sistēmas zvanu (sk. tabulā) un darbojas tikai x86-64 arhitektūrā. Izglītības nolūkos tas ir pietiekami.

Apskatīsim mūsu klona darbu. Linux gadījumā atkļūdotāji un izsekotāji izmanto, kā minēts iepriekš, ptrace sistēmas izsaukumu. Tas darbojas, pirmajā argumentā nododot komandu identifikatorus, kas mums ir nepieciešami PTRACE_TRACEME, PTRACE_SYSCALL и PTRACE_GETREGS.

Izsekotājs sākas parastajā Unix stilā: fork(2) palaiž bērnu procesu, kas savukārt izmanto exec(3) uzsāk pētāmo programmu. Vienīgais smalkums šeit ir izaicinājums ptrace(PTRACE_TRACEME) pirms exec: pakārtotais process sagaida, ka vecāku process to pārraudzī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");
}

Vecāku procesam tagad vajadzētu izsaukt wait(2) bērna procesā, tas ir, pārliecinieties, vai ir notikusi pārslēgšanās uz izsekošanas režīmu:

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

Šajā brīdī sagatavošanās darbi ir pabeigti, un jūs varat pāriet tieši uz sistēmas zvanu izsekošanu bezgalīgā ciklā.

Zvans ptrace(PTRACE_SYSCALL) garantē, ka turpmāk wait vecāks pabeigs vai nu pirms sistēmas izsaukuma izpildes, vai tūlīt pēc tā pabeigšanas. Starp diviem izsaukumiem varat veikt jebkādas darbības: aizstāt zvanu ar alternatīvu, mainīt argumentus vai atgriešanas vērtību.

Mums vienkārši divreiz jāizsauc komanda ptrace(PTRACE_GETREGS)lai iegūtu reģistra stāvokli rax pirms zvana (sistēmas izsaukuma numurs) un tūlīt pēc (atgriešanās vērtība).

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

Tas ir viss izsekotājs. Tagad jūs zināt, kur sākt nākamo pārnešanu DTrace operētājsistēmā Linux.

Pamati: palaist programmu, kas darbojas strace

Kā pirmais lietošanas gadījums strace, iespējams, ir vērts minēt vienkāršāko metodi - palaist lietojumprogrammu strace.

Lai neiedziļinātos tipiskas programmas nebeidzamajā zvanu sarakstā, mēs rakstām minimālā programma apkārt write:

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

Izveidosim programmu un pārliecināsimies, ka tā darbojas:

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

Un visbeidzot, izpildīsim to zem strace kontroles:

$ 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, "177ELF21133>1260342"..., 832) = 832
fstat(3, {st_mode=S_IFREG|0755, st_size=2030544, ...}) = 0
mmap(NULL, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f7a2a631000
mmap(NULL, 4131552, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_DENYWRITE, 3, 0) = 0x7f7a2a04c000
mprotect(0x7f7a2a233000, 2097152, PROT_NONE) = 0
mmap(0x7f7a2a433000, 24576, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x1e7000) = 0x7f7a2a433000
mmap(0x7f7a2a439000, 15072, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0) = 0x7f7a2a439000
close(3)                                = 0
arch_prctl(ARCH_SET_FS, 0x7f7a2a6324c0) = 0
mprotect(0x7f7a2a433000, 16384, PROT_READ) = 0
mprotect(0x55ff52b52000, 4096, PROT_READ) = 0
mprotect(0x7f7a2a664000, 4096, PROT_READ) = 0
munmap(0x7f7a2a633000, 197410)          = 0
write(1, "write me to stdoutn", 20write me to stdout
)  = 20
exit_group(0)                           = ?

Ļoti “vārdi” un ne pārāk izglītojoši. Šeit ir divas problēmas: programmas izvade ir sajaukta ar izvadi strace un daudz sistēmas zvanu, kas mūs neinteresē.

Varat atdalīt programmas standarta izvades straumi un strace kļūdu izvadi, izmantojot slēdzi -o, kas novirza sistēmas izsaukumu sarakstu uz argumentu failu.

Atliek risināt “papildu” zvanu problēmu. Pieņemsim, ka mūs interesē tikai zvani write. Atslēga -e ļauj norādīt izteiksmes, pēc kurām tiks filtrēti sistēmas izsaukumi. Populārākā nosacījumu iespēja, protams, ir trace=*, ar kuru varat atstāt tikai mūs interesējošos zvanus.

Lietojot vienlaikus -o и -e mēs iegūsim:

$ strace -e trace=write -owrite-simple.log ./write-simple
write me to stdout
$ cat write-simple.log
write(1, "write me to stdoutn", 20
)  = 20
+++ exited with 0 +++

Tātad, redziet, to ir daudz vieglāk lasīt.

Varat arī noņemt sistēmas zvanus, piemēram, tos, kas saistīti ar atmiņas piešķiršanu un atbrīvošanu:

$ strace -e trace=!brk,mmap,mprotect,munmap -owrite-simple.log ./write-simple
write me to stdout
$ cat write-simple.log
execve("./write-simple", ["./write-simple"], 0x7ffe9972a498 /* 69 vars */) = 0
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=124066, ...}) = 0
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, "177ELF21133>1260342"..., 832) = 832
fstat(3, {st_mode=S_IFREG|0755, st_size=2030544, ...}) = 0
close(3)                                = 0
arch_prctl(ARCH_SET_FS, 0x7f00f0be74c0) = 0
write(1, "write me to stdoutn", 20)  = 20
exit_group(0)                           = ?
+++ exited with 0 +++

Ņemiet vērā izslēgto izsaukumu sarakstā izmantoto izsaukuma zīmi: to pieprasa komandas apvalks. apvalks).

Manā glibc versijā sistēmas izsaukums pārtrauc procesu exit_group, nav tradicionāls _exit. Šīs ir grūtības strādāt ar sistēmas zvaniem: saskarne, ar kuru strādā programmētājs, nav tieši saistīta ar sistēmas zvaniem. Turklāt tas regulāri mainās atkarībā no ieviešanas un platformas.

Pamati: pievienošanās procesam lidojumā

Sākotnēji ptrace sistēmas izsaukums, uz kura tas tika izveidots strace, var izmantot tikai, palaižot programmu īpašā režīmā. Šis ierobežojums, iespējams, izklausījās saprātīgs Unix 6. versijas laikā. Mūsdienās ar to vairs nepietiek: dažreiz ir jāizpēta kādas darba programmas problēmas. Tipisks piemērs ir process, kas bloķēts uz roktura vai guļ. Tāpēc moderns strace var pievienoties procesiem lidojumā.

Saldēšanas piemērs programmas:

int main(int argc, char *argv[])
{
    (void) argc; (void) argv;

    char str[] = "write men";

    write(STDOUT_FILENO, str, sizeof(str));

    /* Sleep indefinitely or until a signal arrives */
    pause();

    write(STDOUT_FILENO, str, sizeof(str));

    return EXIT_SUCCESS;
}

Izveidosim programmu un pārliecināsimies, ka tā ir iesaldēta:

$ gcc examples/write-sleep.c -o write-sleep
$ ./write-sleep
./write-sleep
write me
^C
$

Tagad mēģināsim tai pievienoties:

$ ./write-sleep &
[1] 15329
write me
$ strace -p 15329
strace: Process 15329 attached
pause(
^Cstrace: Process 15329 detached
 <detached ...>

Programma bloķēta zvana dēļ pause. Apskatīsim, kā viņa reaģē uz signāliem:

$ strace -o write-sleep.log -p 15329 &
strace: Process 15329 attached
$
$ kill -CONT 15329
$ cat write-sleep.log
pause()                                 = ? ERESTARTNOHAND (To be restarted if no handler)
--- SIGCONT {si_signo=SIGCONT, si_code=SI_USER, si_pid=14989, si_uid=1001} ---
pause(
$
$ kill -TERM 15329
$ cat write-sleep.log
pause()                                 = ? ERESTARTNOHAND (To be restarted if no handler)
--- SIGCONT {si_signo=SIGCONT, si_code=SI_USER, si_pid=14989, si_uid=1001} ---
pause()                                 = ? ERESTARTNOHAND (To be restarted if no handler)
--- SIGTERM {si_signo=SIGTERM, si_code=SI_USER, si_pid=14989, si_uid=1001} ---
+++ killed by SIGTERM +++

Mēs palaidām iesaldēto programmu un pievienojāmies tai, izmantojot strace. Kļuva skaidras divas lietas: pauzes sistēmas izsaukums ignorē signālus bez apstrādātājiem un, kas vēl interesantāk, strace uzrauga ne tikai sistēmas zvanus, bet arī ienākošos signālus.

Piemērs: bērnu procesu izsekošana

Darbs ar procesiem caur zvanu fork - visu Unix pamats. Apskatīsim, kā strace darbojas ar procesa koku, izmantojot vienkāršas “selekcijas” piemēru. programmas:

int main(int argc, char *argv[])
{
    pid_t parent_pid = getpid();
    pid_t child_pid = fork();
    if (child_pid == 0) {
        /* A child is born! */
        child_pid = getpid();

        /* In the end of the day printf is just a call to write(2). */
        printf("child (self=%d)n", child_pid);
        exit(EXIT_SUCCESS);
    }

    printf("parent (self=%d, child=%d)n", parent_pid, child_pid);

    wait(NULL);

    exit(EXIT_SUCCESS);
}

Šeit sākotnējais process izveido atvasināto procesu, abi rakstot standarta izvadē:

$ gcc examples/fork-write.c -o fork-write
$ ./fork-write
parent (self=11274, child=11275)
child (self=11275)

Pēc noklusējuma mēs redzēsim tikai sistēmas zvanus no vecākprocesa:

$ strace -e trace=write -ofork-write.log ./fork-write
child (self=22049)
parent (self=22048, child=22049)
$ cat fork-write.log
write(1, "parent (self=22048, child=22049)"..., 33) = 33
--- SIGCHLD {si_signo=SIGCHLD, si_code=CLD_EXITED, si_pid=22049, si_uid=1001, si_status=0, si_utime=0, si_stime=0} ---
+++ exited with 0 +++

Karogs palīdz izsekot visam procesa kokam -f, kas strace uzrauga sistēmas izsaukumus bērnu procesos. Tas papildina katru izvades rindu pid process, kas veido sistēmas izvadi:

$ strace -f -e trace=write -ofork-write.log ./fork-write
parent (self=22710, child=22711)
child (self=22711)
$ cat fork-write.log
22710 write(1, "parent (self=22710, child=22711)"..., 33) = 33
22711 write(1, "child (self=22711)n", 19) = 19
22711 +++ exited with 0 +++
22710 --- SIGCHLD {si_signo=SIGCHLD, si_code=CLD_EXITED, si_pid=22711, si_uid=1001, si_status=0, si_utime=0, si_stime=0} ---
22710 +++ exited with 0 +++

Šajā kontekstā var būt noderīga filtrēšana pēc sistēmas zvanu grupas:

$ strace -f -e trace=%process -ofork-write.log ./fork-write
parent (self=23610, child=23611)
child (self=23611)
$ cat fork-write.log
23610 execve("./fork-write", ["./fork-write"], 0x7fff696ff720 /* 63 vars */) = 0
23610 arch_prctl(ARCH_SET_FS, 0x7f3d03ba44c0) = 0
23610 clone(child_stack=NULL, flags=CLONE_CHILD_CLEARTID|CLONE_CHILD_SETTID|SIGCHLD, child_tidptr=0x7f3d03ba4790) = 23611
23610 wait4(-1,  <unfinished ...>
23611 exit_group(0)                     = ?
23611 +++ exited with 0 +++
23610 <... wait4 resumed> NULL, 0, NULL) = 23611
23610 --- SIGCHLD {si_signo=SIGCHLD, si_code=CLD_EXITED, si_pid=23611, si_uid=1001, si_status=0, si_utime=0, si_stime=0} ---
23610 exit_group(0)                     = ?
23610 +++ exited with 0 +++

Starp citu, kāds sistēmas izsaukums tiek izmantots, lai izveidotu jaunu procesu?

Piemērs: faila ceļi, nevis rokturi

Zināt failu deskriptorus noteikti ir noderīgi, taču var noderēt arī konkrēto failu nosaukumi, kuriem programma piekļūst.

nākamā programma ieraksta rindiņu pagaidu failā:

void do_write(int out_fd)
{
    char str[] = "write me to a filen";

    if (sizeof(str) != write(out_fd, str, sizeof(str))){
        perror("write");
        exit(EXIT_FAILURE);
    }
}

int main(int argc, char *argv[])
{
    char tmp_filename_template[] = "/tmp/output_fileXXXXXX";

    int out_fd = mkstemp(tmp_filename_template);
    if (out_fd == -1) {
        perror("mkstemp");
        exit(EXIT_FAILURE);
    }

    do_write(out_fd);

    return EXIT_SUCCESS;
}

Parastas sarunas laikā strace parādīs sistēmas zvanam nodotā ​​deskriptora numura vērtību:

$ strace -e trace=write -o write-tmp-file.log ./write-tmp-file
$ cat write-tmp-file.log
write(3, "write me to a filen", 20)  = 20
+++ exited with 0 +++

Ar karogu -y Lietderība parāda ceļu uz failu, kuram atbilst deskriptors:

$ strace -y -e trace=write -o write-tmp-file.log ./write-tmp-file
$ cat write-tmp-file.log
write(3</tmp/output_fileCf5MyW>, "write me to a filen", 20) = 20
+++ exited with 0 +++

Piemērs: Failu piekļuves izsekošana

Vēl viena noderīga funkcija: parādīt tikai sistēmas zvanus, kas saistīti ar konkrētu failu. Nākamais programma pievieno rindiņu patvaļīgam failam, kas nodots kā arguments:

void do_write(int out_fd)
{
    char str[] = "write me to a filen";

    if (sizeof(str) != write(out_fd, str, sizeof(str))){
        perror("write");
        exit(EXIT_FAILURE);
    }
}

int main(int argc, char *argv[])
{
    /*
     * Path will be provided by the first program argument.
     *  */
    const char *path = argv[1];

    /*
     * Open an existing file for writing in append mode.
     *  */
    int out_fd = open(path, O_APPEND | O_WRONLY);
    if (out_fd == -1) {
        perror("open");
        exit(EXIT_FAILURE);
    }

    do_write(out_fd);

    return EXIT_SUCCESS;
}

Pēc noklusējuma strace parāda daudz nevajadzīgas informācijas. Karogs -P ar argumentu liek strace drukāt tikai izsaukumus uz norādīto failu:

$ strace -y -P/tmp/test_file.log -o write-file.log ./write-file /tmp/test_file.log
$ cat write-file.log
openat(AT_FDCWD, "/tmp/test_file.log", O_WRONLY|O_APPEND) = 3</tmp/test_file.log>
write(3</tmp/test_file.log>, "write me to a filen", 20) = 20
+++ exited with 0 +++

Piemērs: daudzpavedienu programmas

Lietderība strace var palīdzēt arī strādājot ar daudzvītnēm programma. Šāda programma raksta uz standarta izvadi no divām straumēm:

void *thread(void *arg)
{
    (void) arg;

    printf("Secondary thread: workingn");
    sleep(1);
    printf("Secondary thread: donen");

    return NULL;
}

int main(int argc, char *argv[])
{
    printf("Initial thread: launching a threadn");

    pthread_t thr;
    if (0 != pthread_create(&thr, NULL, thread, NULL)) {
        fprintf(stderr, "Initial thread: failed to create a thread");
        exit(EXIT_FAILURE);
    }

    printf("Initial thread: joining a threadn");
    if (0 != pthread_join(thr, NULL)) {
        fprintf(stderr, "Initial thread: failed to join a thread");
        exit(EXIT_FAILURE);
    };

    printf("Initial thread: done");

    exit(EXIT_SUCCESS);
}

Protams, tas ir jāapkopo ar īpašu sveicienu linkerim - karodziņu -pthread:

$ gcc examples/thread-write.c -pthread -o thread-write
$ ./thread-write
/thread-write
Initial thread: launching a thread
Initial thread: joining a thread
Secondary thread: working
Secondary thread: done
Initial thread: done
$

Atzīmēt -f, tāpat kā parasto procesu gadījumā, katras rindas sākumā pievienos procesa pid.

Protams, mēs nerunājam par pavedienu identifikatoru POSIX Threads standarta ieviešanas izpratnē, bet gan par numuru, ko Linux izmanto uzdevumu plānotājs. No pēdējā viedokļa nav procesu vai pavedienu - ir uzdevumi, kas jāsadala starp pieejamajiem iekārtas kodoliem.

Strādājot vairākos pavedienos, sistēmas zvanu kļūst pārāk daudz:

$ strace -f -othread-write.log ./thread-write
$ wc -l thread-write.log
60 thread-write.log

Ir lietderīgi aprobežoties tikai ar procesu pārvaldību un sistēmas izsaukumiem write:

$ strace -f -e trace="%process,write" -othread-write.log ./thread-write
$ cat thread-write.log
18211 execve("./thread-write", ["./thread-write"], 0x7ffc6b8d58f0 /* 64 vars */) = 0
18211 arch_prctl(ARCH_SET_FS, 0x7f38ea3b7740) = 0
18211 write(1, "Initial thread: launching a thre"..., 35) = 35
18211 clone(child_stack=0x7f38e9ba2fb0, flags=CLONE_VM|CLONE_FS|CLONE_FILES|CLONE_SIGHAND|CLONE_THREAD|CLONE_SYSVSEM|CLONE_SETTLS|CLONE_PARENT_SETTID|CLONE_CHILD_CLEARTID, parent_tidptr=0x7f38e9ba39d0, tls=0x7f38e9ba3700, child_tidptr=0x7f38e9ba39d0) = 18212
18211 write(1, "Initial thread: joining a thread"..., 33) = 33
18212 write(1, "Secondary thread: workingn", 26) = 26
18212 write(1, "Secondary thread: donen", 23) = 23
18212 exit(0)                           = ?
18212 +++ exited with 0 +++
18211 write(1, "Initial thread: done", 20) = 20
18211 exit_group(0)                     = ?
18211 +++ exited with 0 +++

Starp citu, jautājumi. Kāds sistēmas izsaukums tiek izmantots, lai izveidotu jaunu pavedienu? Kā šis pavedienu aicinājums atšķiras no procesu aicinājuma?

Meistarklase: procesa steks sistēmas izsaukuma laikā

Viens no nesen parādījās strace iespējas — funkciju izsaukumu kaudzes parādīšana sistēmas izsaukuma brīdī. Vienkārši piemērs:

void do_write(void)
{
    char str[] = "write me to stdoutn";
    if (sizeof(str) != write(STDOUT_FILENO, str, sizeof(str))){
        perror("write");
        exit(EXIT_FAILURE);
    }
}

int main(int argc, char *argv[])
{
    do_write();
    return EXIT_SUCCESS;
}

Protams, programmas izvade kļūst ļoti apjomīga, un papildus karogam -k (zvanu steka displejs), ir lietderīgi filtrēt sistēmas zvanus pēc nosaukuma:

$ gcc examples/write-simple.c -o write-simple
$ strace -k -e trace=write -o write-simple.log ./write-simple
write me to stdout
$ cat write-simple.log
write(1, "write me to stdoutn", 20)  = 20
 > /lib/x86_64-linux-gnu/libc-2.27.so(__write+0x14) [0x110154]
 > /home/vkazanov/projects-my/strace-post/write-simple(do_write+0x50) [0x78a]
 > /home/vkazanov/projects-my/strace-post/write-simple(main+0x14) [0x7d1]
 > /lib/x86_64-linux-gnu/libc-2.27.so(__libc_start_main+0xe7) [0x21b97]
 > /home/vkazanov/projects-my/strace-post/write-simple(_start+0x2a) [0x65a]
+++ exited with 0 +++

Meistarklase: kļūdu ievadīšana

Un vēl viena jauna un ļoti noderīga funkcija: kļūdu ievadīšana. Šeit programma, ierakstot divas rindiņas izvades straumē:

#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>

void do_write(const char *str, ssize_t len)
{
    if (len != write(STDOUT_FILENO, str, (size_t)len)){
        perror("write");
        exit(EXIT_FAILURE);
    }
}

int main(int argc, char *argv[])
{
    (void) argc; (void) argv;

    char str1[] = "write me 1n";
    do_write(str1, sizeof(str1));

    char str2[] = "write me 2n";
    do_write(str2, sizeof(str2));

    return EXIT_SUCCESS;
}

Izsekosim abus rakstīšanas zvanus:

$ gcc examples/write-twice.c -o write-twice
$ ./write-twice
write me 1
write me 2
$ strace -e trace=write -owrite-twice.log ./write-twice
write me 1
write me 2
$ cat write-twice.log
write(1, "write me 1n", 12)          = 12
write(1, "write me 2n", 12)          = 12
+++ exited with 0 +++

Tagad mēs izmantojam izteicienu injectlai ievietotu kļūdu EBADF visos zvanos rakstiet:

$ strace -e trace=write -e inject=write:error=EBADF -owrite-twice.log ./write-twice
$ cat write-twice.log
write(1, "write me 1n", 12)          = -1 EBADF (Bad file descriptor) (INJECTED)
write(3, "write: Bad file descriptorn", 27) = -1 EBADF (Bad file descriptor) (INJECTED)
+++ exited with 1 +++

Interesanti, kādas kļūdas tiek atgrieztas viss izaicinājumiem write, ieskaitot zvanu, kas paslēpts aiz kļūdas. Ir jēga atgriezt kļūdu tikai pirmajam zvanam:

$ strace -e trace=write -e inject=write:error=EBADF:when=1 -owrite-twice.log ./write-twice
write: Bad file descriptor
$ cat write-twice.log
write(1, "write me 1n", 12)          = -1 EBADF (Bad file descriptor) (INJECTED)
write(3, "write: Bad file descriptorn", 27) = 27
+++ exited with 1 +++

Vai arī otrais:

$ strace -e trace=write -e inject=write:error=EBADF:when=2 -owrite-twice.log ./write-twice
write me 1
write: Bad file descriptor
$ cat write-twice.log
write(1, "write me 1n", 12)          = 12
write(1, "write me 2n", 12)          = -1 EBADF (Bad file descriptor) (INJECTED)
write(3, "write: Bad file descriptorn", 27) = 27
+++ exited with 1 +++

Nav nepieciešams norādīt kļūdas veidu:

$ strace -e trace=write -e fault=write:when=1 -owrite-twice.log ./write-twice
$ cat write-twice.log
write(1, "write me 1n", 12)          = -1 ENOSYS (Function not implemented) (INJECTED)
write(3, "write: Function not implementedn", 32) = 32
+++ exited with 1 +++

Kombinācijā ar citiem karodziņiem varat “pārtraukt” piekļuvi noteiktam failam. Piemērs:

$ strace -y -P/tmp/test_file.log -e inject=file:error=ENOENT -o write-file.log ./write-file /tmp/test_file.log
open: No such file or directory
$ cat write-file.log
openat(AT_FDCWD, "/tmp/test_file.log", O_WRONLY|O_APPEND) = -1 ENOENT (No such file or directory) (INJECTED)
+++ exited with 1 +++

Papildus kļūdu ievadīšanai, viens var zvanot vai saņemot signālus, aizkavējas.

Pēcvārds

Lietderība strace - vienkāršs un uzticams rīks. Bet papildus sistēmas izsaukumiem var atkļūdot citus programmu un operētājsistēmas darbības aspektus. Piemēram, tas var izsekot zvanus uz dinamiski saistītām bibliotēkām. lttrace, viņi var izpētīt operētājsistēmas darbību SystemTap и ftraceun ļauj padziļināti izpētīt programmas veiktspēju ideāls. Tomēr tā ir strace - pirmā aizsardzības līnija, ja rodas problēmas ar savām un citu programmām, un es to izmantoju vismaz pāris reizes nedēļā.

Īsāk sakot, ja jums patīk Unix, lasiet man 1 strace un nekautrējieties ieskatīties savās programmās!

Avots: www.habr.com

Pievieno komentāru