Strace í Linux: saga, hönnun og notkun

Strace í Linux: saga, hönnun og notkun

Í Unix-líkum stýrikerfum eiga samskipti forrits við umheiminn og stýrikerfið sér stað í gegnum lítið safn aðgerða - kerfiskalla. Þetta þýðir að í villuleitarskyni getur verið gagnlegt að njósna um kerfissímtöl sem eru keyrð af ferlum.

Tól hjálpar þér að fylgjast með „nána lífi“ forrita á Linux strace, sem er efni þessarar greinar. Dæmi um notkun njósnabúnaðar fylgir stutt saga strace og lýsingu á hönnun slíkra forrita.

efni

Uppruni tegunda

Aðalviðmótið milli forrita og OS kjarnans í Unix er kerfissímtöl. kerfissímtöl, syscals), samskipti forrita við umheiminn eiga sér stað eingöngu í gegnum þau.

En í fyrstu opinberu útgáfunni af Unix (Útgáfa 6 Unix, 1975) voru engar þægilegar leiðir til að fylgjast með hegðun notendaferla. Til að leysa þetta mál mun Bell Labs uppfæra í næstu útgáfu (Útgáfa 7 Unix, 1979) lagði til nýtt kerfiskall - ptrace.

ptrace var fyrst og fremst þróað fyrir gagnvirka kembiforrita, en í lok níunda áratugarins (á tímum auglýsinga System V útgáfa 4) á þessum grundvelli birtust þröngt fókusar villuleitar-kerfiskallasporar og urðu mikið notaðir.

First Sama útgáfa af strace var gefin út af Paul Cronenburg á póstlistanum comp.sources.sun árið 1992 sem valkostur við lokaða þjónustu. trace frá Sun. Bæði klóninn og frumritið voru ætlaðir fyrir SunOS, en árið 1994 strace var flutt til System V, Solaris og sífellt vinsælli Linux.

Í dag styður strace aðeins Linux og treystir á það sama ptrace, gróin mörgum framlengingum.

Nútímalegur (og mjög virkur) viðhaldsaðili strace - Dmitry Levin. Þökk sé honum öðlaðist tólið háþróaða eiginleika eins og villuinnspýtingu í kerfissímtöl, stuðning við fjölbreytt úrval arkitektúra og síðast en ekki síst, lukkudýr. Óopinberar heimildir halda því fram að valið hafi fallið á strútinn vegna samhljóða rússneska orðsins „strútur“ og enska orðsins „strace“.

Það er líka mikilvægt að ptrace kerfiskallið og rekjanirnar voru aldrei með í POSIX, þrátt fyrir langa sögu og útfærslu í Linux, FreeBSD, OpenBSD og hefðbundnum Unix.

Strace tæki í hnotskurn: Piglet Trace

„Það er ekki ætlast til að þú skiljir þetta“ (Dennis Ritchie, athugasemd í útgáfu 6 Unix frumkóða)

Frá barnæsku þoli ég ekki svarta kassa: Ég lék mér ekki með leikföng heldur reyndi að skilja uppbyggingu þeirra (fullorðnir notuðu orðið „brotinn“ en trúa ekki illu tungunum). Kannski er þetta ástæðan fyrir því að óformleg menning fyrsta Unix og nútíma opinn-uppspretta hreyfingarinnar er mér svo nálægt.

Í tilgangi þessarar greinar er óeðlilegt að taka í sundur frumkóðann fyrir strace, sem hefur vaxið í áratugi. En það ættu ekki að vera nein leyndarmál fyrir lesendur. Þess vegna, til að sýna meginregluna um notkun slíkra strace-forrita, mun ég útvega kóðann fyrir smækkað sporefni - Grísaspor (ptr). Það veit ekki hvernig á að gera neitt sérstakt, en aðalatriðið er kerfiskall forritsins - það gefur út:

$ 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 þekkir um hundruð Linux kerfiskalla (sjá. borð) og virkar aðeins á x86-64 arkitektúr. Þetta nægir í fræðsluskyni.

Við skulum skoða verk klónsins okkar. Þegar um er að ræða Linux nota villuleitar- og rekjaforritarar, eins og getið er hér að ofan, ptrace kerfiskallið. Það virkar með því að senda í fyrstu röksemdafærsluna skipanaauðkenni, sem við þurfum aðeins af PTRACE_TRACEME, PTRACE_SYSCALL и PTRACE_GETREGS.

Sporefnið byrjar í venjulegum Unix stíl: fork(2) setur af stað barnaferli, sem aftur notar exec(3) hleypir af stokkunum náminu sem er í námi. Eina lúmskan hér er áskorunin ptrace(PTRACE_TRACEME) áður exec: Barnaferlið gerir ráð fyrir að foreldraferlið fylgist með því:

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

Foreldraferlið ætti nú að hringja wait(2) í undirferlinu, það er að segja, ganga úr skugga um að skipt hafi verið yfir í rekjastillingu:

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

Á þessum tímapunkti er undirbúningi lokið og þú getur haldið beint áfram í að rekja kerfissímtöl í endalausri lykkju.

Hringdu ptrace(PTRACE_SYSCALL) tryggir að síðari wait foreldri mun ljúka annað hvort áður en kerfiskallið er keyrt eða strax eftir að því lýkur. Á milli tveggja símtala geturðu framkvæmt hvaða aðgerðir sem er: skiptu símtalinu út fyrir aðra, breyttu rökum eða skilgildi.

Við þurfum bara að hringja í skipunina tvisvar ptrace(PTRACE_GETREGS)til að fá skráarríkið rax fyrir símtalið (kerfissímtalsnúmer) og strax á eftir (skilagildi).

Í raun, hringrásin:

/* 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ð er allt rakið. Nú veistu hvar þú átt að byrja næstu flutning DTrace á Linux.

Grunnatriði: keyra forrit sem keyrir strace

Sem fyrsta notkunartilvik strace, kannski er það þess virði að vitna í einföldustu aðferðina - að ræsa forrit í gangi strace.

Til þess að kafa ekki í endalausan lista yfir símtöl dæmigerðs forrits skrifum við lágmarks prógramm í kring 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;
}

Við skulum byggja forritið og ganga úr skugga um að það virki:

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

Og að lokum skulum við keyra það undir strace control:

$ 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)                           = ?

Mjög „orðalegt“ og ekki mjög fræðandi. Það eru tvö vandamál hér: úttak forritsins er blandað saman við úttakið strace og gnægð af kerfissímtölum sem vekja ekki áhuga okkar.

Þú getur aðskilið venjulegt úttaksstraum forritsins og úttaksvilluúttak með því að nota -o rofann, sem vísar listanum yfir kerfissímtöl yfir í rökfærsluskrá.

Það á eftir að takast á við vandamálið við „auka“ símtöl. Gerum ráð fyrir að við höfum aðeins áhuga á símtölum write. Lykill -e gerir þér kleift að tilgreina tjáningar sem kerfissímtöl verða síuð eftir. Vinsælasti ástandsvalkosturinn er náttúrulega, trace=*, þar sem þú getur aðeins skilið eftir þau símtöl sem hafa áhuga á okkur.

Þegar það er notað samtímis -o и -e við fáum:

$ 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 +++

Svo þú sérð, það er miklu auðveldara að lesa.

Þú getur líka fjarlægt kerfissímtöl, til dæmis þau sem tengjast minnisúthlutun og losun:

$ 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 +++

Taktu eftir upphrópunarmerkinu sem er sloppið á listanum yfir útilokuð símtöl: þetta er krafist af skipanaskelinni. skel).

Í minni útgáfu af glibc lýkur kerfiskall ferlinu exit_group, ekki hefðbundið _exit. Þetta er erfiðleikinn við að vinna með kerfissímtöl: viðmótið sem forritarinn vinnur með er ekki beint tengt kerfissímtölum. Þar að auki breytist það reglulega eftir útfærslu og vettvangi.

Grunnatriði: að taka þátt í ferlinu á flugu

Upphaflega kallar ptrace kerfið sem það var byggt á strace, var aðeins hægt að nota þegar forritið er keyrt í sérstökum ham. Þessi takmörkun gæti hafa hljómað skynsamleg á dögum útgáfu 6 Unix. Nú á dögum er þetta ekki lengur nóg: stundum þarftu að rannsaka vandamálin í starfandi forriti. Dæmigerð dæmi er ferli sem er stíflað á handfangi eða sofandi. Því nútímalegt strace geta sameinast ferlum á flugu.

Frostdæmi forrit:

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

Við skulum byggja forritið og ganga úr skugga um að það sé frosið:

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

Nú skulum við reyna að taka þátt í því:

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

Forrit lokað með símtali pause. Við skulum sjá hvernig hún bregst við merkjunum:

$ 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 +++

Við ræstum frysta forritið og gengum í það með því að nota strace. Tvennt varð ljóst: hlé kerfiskallið hunsar merki án meðhöndlunar og það sem meira er athyglisvert, Strace fylgist ekki aðeins með kerfissímtölum, heldur einnig innkomnum merkjum.

Dæmi: Rekja barnaferla

Vinna með ferla í gegnum símtal fork - grunnur allra Unixa. Við skulum sjá hvernig strace virkar með vinnslutré með því að nota dæmi um einfalda „ræktun“ forrit:

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

Hér býr upprunalega ferlið til barnaferli, bæði skrifar í staðlað úttak:

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

Sjálfgefið er að við sjáum aðeins kerfissímtöl frá foreldraferlinu:

$ 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 +++

Fáninn hjálpar þér að fylgjast með öllu ferlitrénu -f, sem strace fylgist með kerfissímtölum í barnaferlum. Þetta bætist við hverja framleiðslulínu pid ferli sem gerir kerfisúttak:

$ 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 +++

Í þessu samhengi getur síun eftir hópi kerfiskalla verið gagnleg:

$ 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 +++

Við the vegur, hvaða kerfiskall er notað til að búa til nýtt ferli?

Dæmi: skráarslóðir í stað handfanga

Það er vissulega gagnlegt að þekkja skráarlýsingar, en nöfn tiltekinna skráa sem forrit hefur aðgang að geta líka komið sér vel.

næsta program skrifar línuna í bráðabirgðaskrána:

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

Meðan á venjulegu símtali stendur strace mun sýna gildi lýsingarnúmersins sem sent er í kerfiskallið:

$ 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 +++

Með fána -y Tækið sýnir slóðina að skránni sem lýsingin samsvarar:

$ 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 +++

Dæmi: Skráaaðgangsmæling

Annar gagnlegur eiginleiki: birta aðeins kerfissímtöl sem tengjast tiltekinni skrá. Næst program bætir línu við handahófskennda skrá sem send er sem rök:

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

Sjálfgefið strace sýnir mikið af óþarfa upplýsingum. Fáni -P með argument veldur því að strace prentar aðeins símtöl í tilgreinda skrá:

$ 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 +++

Dæmi: Fjölþráða forrit

Gagnsemi strace getur líka hjálpað þegar unnið er með fjölþráða forritið. Eftirfarandi forrit skrifar á staðlað úttak úr tveimur straumum:

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

Auðvitað verður það að vera tekið saman með sérstakri kveðju til tengiliðsins - -pthread fánann:

$ 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
$

Flagga -f, eins og í tilviki venjulegra ferla, mun bæta pid ferlisins við upphaf hverrar línu.

Við erum náttúrulega ekki að tala um þráðaauðkenni í skilningi innleiðingar POSIX Threads staðalsins, heldur töluna sem verkefnaáætlunarmaðurinn notar í Linux. Frá sjónarhóli þess síðarnefnda eru engir ferlar eða þræðir - það eru verkefni sem þarf að dreifa á tiltæka kjarna vélarinnar.

Þegar unnið er í mörgum þráðum verða kerfissímtöl of mörg:

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

Það er skynsamlegt að takmarka þig við vinnslustjórnun og kerfissímtöl eingöngu 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 +++

Við the vegur, spurningar. Hvaða kerfiskall er notað til að búa til nýjan þráð? Hvernig er þetta ákall um þræði frábrugðið ákalli um ferla?

Meistaranámskeið: vinnslustafla við kerfiskall

Einn þeirra birtist nýlega strace getu - sýnir stafla af aðgerðasímtölum á þeim tíma sem kerfiskallið var gert. Einfalt Dæmi:

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

Auðvitað, the program framleiðsla verður mjög fyrirferðarmikill, og, auk fána -k (símtalsskjár), það er skynsamlegt að sía kerfissímtöl eftir nafni:

$ 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 +++

Meistaraflokkur: villuinnspýting

Og enn einn nýr og mjög gagnlegur eiginleiki: villuinnspýting. Hérna program, skrifa tvær línur í úttaksstrauminn:

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

Við skulum rekja bæði skrifa símtölin:

$ 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 +++

Nú notum við orðatiltækið injecttil að setja inn villu EBADF í öllum skrifa símtölum:

$ 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 +++

Það er athyglisvert hvaða villum er skilað allt áskoranir write, þar á meðal símtalið sem er falið á bak við perror. Það er bara skynsamlegt að skila villu fyrir fyrsta símtalið:

$ 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 +++

Eða sá seinni:

$ 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 +++

Það er ekki nauðsynlegt að tilgreina villutegundina:

$ 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 +++

Í samsettri meðferð með öðrum fánum geturðu „brotið“ aðgang að tiltekinni skrá. Dæmi:

$ 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 +++

Fyrir utan villuinnspýtingu, maður getur kynna tafir þegar hringt er eða tekið á móti merki.

Eftirsögn

Gagnsemi strace - einfalt og áreiðanlegt tæki. En auk kerfiskalla er hægt að kemba aðra þætti í rekstri forrita og stýrikerfisins. Til dæmis getur það fylgst með símtölum í virkt tengd bókasöfn. ltrace, geta þeir skoðað rekstur stýrikerfisins SystemTap и ftrace, og gerir þér kleift að rannsaka árangur forritsins djúpt fullkominn. Engu að síður er það strace - fyrsta varnarlínan ef upp koma vandamál með mín eigin og annarra forrit, og ég nota hana að minnsta kosti nokkrum sinnum í viku.

Í stuttu máli, ef þú elskar Unix, lestu man 1 strace og ekki hika við að kíkja á forritin þín!

Heimild: www.habr.com

Bæta við athugasemd