Strace en Linukso: historio, dezajno kaj uzo

Strace en Linukso: historio, dezajno kaj uzo

En Unikso-similaj operaciumoj, la komunikado de programo kun la ekstera mondo kaj la operaciumo okazas per malgranda aro de funkcioj - sistemaj vokoj. Ĉi tio signifas, ke por sencimigaj celoj povas esti utile spioni sistemajn vokojn ekzekutajn de procezoj.

Ilo helpas vin kontroli la "intima vivo" de programoj en Linukso strace, kiu estas la temo de ĉi tiu artikolo. Ekzemploj de la uzo de spiona ekipaĵo estas akompanitaj de mallonga historio strace kaj priskribo de la dezajno de tiaj programoj.

Enhavo

Origino de specioj

La ĉefa interfaco inter programoj kaj la OS-kerno en Unikso estas sistemaj vokoj. sistemaj vokoj, syscalls), la interago de programoj kun la ekstera mondo okazas ekskluzive per ili.

Sed en la unua publika versio de Unikso (Versio 6 Unikso, 1975) ekzistis neniuj oportunaj manieroj spuri la konduton de uzantprocezoj. Por solvi ĉi tiun problemon, Bell Labs ĝisdatigos al la sekva versio (Versio 7 Unikso, 1979) proponis novan sistemvokon - ptrace.

ptrace estis evoluigita ĉefe por interagaj erarserĉiloj, sed antaŭ la fino de la 80-aj jaroj (en la epoko de komerca Sistemo V Eldono 4) sur tiu bazo, mallarĝe fokusitaj erarserĉiloj - sistemvokospuriloj - aperis kaj iĝis vaste uzataj.

La unua la sama versio de strace estis publikigita fare de Paul Cronenburg en la comp.sources.sun dissendolisto en 1992 kiel alternativo al fermita servaĵo trace de Suno. Kaj la klono kaj la originalo estis destinitaj por SunOS, sed antaŭ 1994 strace estis adaptita al System V, Solaris kaj la ĉiam pli populara Linukso.

Hodiaŭ strace nur subtenas Linukson kaj dependas de la sama ptrace, superkreskita per multaj etendaĵoj.

Moderna (kaj tre aktiva) prizorganto strace - Dmitrij Levin. Danke al li, la utileco akiris altnivelajn funkciojn kiel eraran injekton en sistemajn vokojn, subtenon por ampleksa gamo de arkitekturoj kaj, plej grave, maskoto. Neoficialaj fontoj asertas, ke la elekto falis sur la struton pro la konsonanco inter la rusa vorto "struto" kaj la angla vorto "strace".

Ankaŭ gravas, ke la ptrace-sistemvoko kaj spuriloj neniam estis inkluditaj en POSIX, malgraŭ longa historio kaj efektivigo en Linukso, FreeBSD, OpenBSD kaj tradicia Unikso.

Strace-aparato en malmultaj vortoj: Porkido-Spuro

"Vi ne atendas tion kompreni" (Dennis Ritchie, komento en Versio 6 Unix fontkodo)

Ekde la frua infanaĝo, mi ne eltenas nigrajn skatolojn: mi ne ludis per ludiloj, sed provis kompreni ilian strukturon (plenkreskuloj uzis la vorton "rompis", sed ne kredas la malbonajn langojn). Eble tial la neformala kulturo de la unua Unikso kaj la moderna malfermfonta movado estas tiel proksimaj al mi.

Por la celoj de ĉi tiu artikolo, estas neracie malmunti la fontkodon de strace, kiu kreskis dum jardekoj. Sed ne restu sekretoj por legantoj. Tial, por montri la principon de funkciado de tiaj strekprogramoj, mi provizos la kodon por miniatura spurilo - Porkido-Spuro (ptr). Ĝi ne scias kiel fari ion specialan, sed la ĉefa afero estas la sistemaj vokoj de la programo - ĝi eligas:

$ 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 rekonas ĉirkaŭ centojn da Linuksaj sistemvokoj (vidu. la tablo) kaj funkcias nur sur x86-64-arkitekturo. Ĉi tio sufiĉas por edukaj celoj.

Ni rigardu la laboron de nia klono. En la kazo de Linukso, erarserĉiloj kaj spuriloj uzas, kiel menciite supre, la ptrace-sistemvoko. Ĝi funkcias pasante en la unua argumento la komandidentigilojn, el kiuj ni nur bezonas PTRACE_TRACEME, PTRACE_SYSCALL и PTRACE_GETREGS.

La spurilo komenciĝas en la kutima Unikso-stilo: fork(2) lanĉas infanan procezon, kiu siavice uzas exec(3) lanĉas la studatan programon. La sola subtileco ĉi tie estas la defio ptrace(PTRACE_TRACEME) antaŭe exec: La infana procezo atendas, ke la gepatra procezo kontrolas ĝin:

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

La gepatra procezo nun devus voki wait(2) en la infana procezo, tio estas, certigu, ke ŝanĝado al spurreĝimo okazis:

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

Je ĉi tiu punkto, la preparoj estas kompletaj kaj vi povas rekte sekvi sistemajn vokojn en senfina buklo.

Defio ptrace(PTRACE_SYSCALL) garantias ke posta wait gepatro kompletigos aŭ antaŭ ol la sistemvoko estas efektivigita aŭ tuj post kiam ĝi finiĝos. Inter du alvokoj vi povas fari ajnajn agojn: anstataŭigi la alvokon per alternativa, ŝanĝi la argumentojn aŭ la revenan valoron.

Ni nur bezonas voki la komandon dufoje ptrace(PTRACE_GETREGS)akiri la registron staton rax antaŭ la voko (sistema voknumero) kaj tuj post (revenvaloro).

Fakte, la ciklo:

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

Tio estas la tuta spurilo. Nun vi scias, kie komenci la sekvan portadon DTrace sur Linukso.

Bazoj: ruli programon kurante strace

Kiel unua uzokazo strace, eble indas citi la plej simplan metodon - lanĉi aplikaĵon funkciantan strace.

Por ne enprofundiĝi en la senfinan liston de alvokoj de tipa programo, ni skribas minimuma programo ĉirkaŭe 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;
}

Ni konstruu la programon kaj certigu, ke ĝi funkcias:

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

Kaj finfine, ni rulu ĝin sub kontrolo:

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

Tre "vortema" kaj ne tre eduka. Estas du problemoj ĉi tie: la programa eligo estas miksita kun la eligo strace kaj abundo da sistemvokoj kiuj ne interesas nin.

Vi povas apartigi la norman eligfluon de la programo kaj trace-eraran eligon per la -o ŝaltilo, kiu redirektas la liston de sistemvokoj al argumentdosiero.

Restas trakti la problemon de "krom" vokoj. Ni supozu, ke ni interesiĝas nur pri vokoj write. Ŝlosilo -e permesas al vi specifi esprimojn per kiuj sistemaj vokoj estos filtritaj. La plej populara kondiĉa opcio estas, nature, trace=*, per kiu vi povas lasi nur la alvokojn, kiuj interesas nin.

Kiam uzata samtempe -o и -e ni ricevos:

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

Do, vi vidas, ĝi estas multe pli facile legebla.

Vi ankaŭ povas forigi sistemajn vokojn - ekzemple tiujn asociitajn kun asignado kaj liberigo de memoro:

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

Notu la eskapintan ekkrion en la listo de ekskluditaj vokoj: tio estas postulata de la komanda ŝelo. ŝelo).

En mia versio de glibc, sistemvoko finas la procezon exit_group, ne tradicia _exit. Jen la malfacileco labori kun sistemvokoj: la interfaco kun kiu la programisto laboras ne rekte rilatas al sistemaj vokoj. Krome, ĝi ŝanĝiĝas regule depende de la efektivigo kaj platformo.

Bazoj: aliĝi al la procezo sur la flugo

Komence, la ptrace-sistemvoko sur kiu ĝi estis konstruita strace, povus esti uzata nur dum rulado de la programo en speciala reĝimo. Ĉi tiu limigo eble sonis racie en la tagoj de Versio 6 Unikso. Nuntempe tio ne plu sufiĉas: foje necesas esplori la problemojn de laborprogramo. Tipa ekzemplo estas procezo blokita sur tenilo aŭ dormanta. Tial moderna strace povas aliĝi al procezoj sur la flugo.

Frosta ekzemplo programoj:

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

Ni konstruu la programon kaj certigu, ke ĝi estas frostigita:

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

Nun ni provu aliĝi al ĝi:

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

Programo blokita per voko pause. Ni vidu kiel ŝi reagas al la signaloj:

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

Ni lanĉis la frostan programon kaj aliĝis al ĝi uzante strace. Du aferoj evidentiĝis: la paŭza sistemvoko ignoras signalojn sen pritraktiloj kaj, pli interese, strace monitoras ne nur sistemajn vokojn, sed ankaŭ envenantajn signalojn.

Ekzemplo: Spurado de Infanaj Procezoj

Laborante kun procezoj per voko fork - la bazo de ĉiuj Uniksoj. Ni vidu kiel strace funkcias kun proceza arbo uzante la ekzemplon de simpla "bredado" programoj:

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

Ĉi tie la origina procezo kreas infanan procezon, ambaŭ skribante al norma eligo:

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

Defaŭlte, ni nur vidos sistemajn vokojn de la gepatra procezo:

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

La flago helpas vin spuri la tutan procezan arbon -f, kiu strace monitoras sistemajn vokojn en infanaj procezoj. Ĉi tio aldonas al ĉiu linio de eligo pid procezo kiu faras sisteman eligon:

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

En ĉi tiu kunteksto, filtri laŭ grupo de sistemvokoj povas esti utila:

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

Cetere, kia sistemvoko estas uzata por krei novan procezon?

Ekzemplo: dosiervojoj anstataŭ teniloj

Koni dosierpriskribilojn certe estas utila, sed la nomoj de la specifaj dosieroj, kiujn programo aliras, povas ankaŭ esti utilaj.

La sekva la programo skribas la linion al provizora dosiero:

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

Dum normala voko strace montros la valoron de la priskriba nombro transdonita al la sistema voko:

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

Kun flago -y La ilo montras la vojon al la dosiero al kiu la priskribilo respondas:

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

Ekzemplo: Dosiera Aliro Spurado

Alia utila funkcio: montri nur sistemajn vokojn asociitajn kun specifa dosiero. Poste la programo aldonas linion al arbitra dosiero pasigita kiel argumento:

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

defaŭlte strace montras multajn nenecesajn informojn. Flago -P kun argumento igas strace presi nur vokojn al la specifita dosiero:

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

Ekzemplo: Plurfadenaj Programoj

Utileco strace povas ankaŭ helpi kiam vi laboras kun multfadena la programo. La sekva programo skribas al norma eligo de du fluoj:

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

Kompreneble, ĝi devas esti kompilita kun speciala saluto al la ligilo - la flago -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
$

Flago -f, kiel en la kazo de regulaj procezoj, aldonos la pid de la procezo al la komenco de ĉiu linio.

Nature, ni ne parolas pri fadenidentigilo en la senco de la efektivigo de la normo POSIX Threads, sed pri la nombro uzata de la taskoplanilo en Linukso. De ĉi-lasta vidpunkto, ne ekzistas procezoj aŭ fadenoj - estas taskoj, kiuj devas esti distribuitaj inter la disponeblaj kernoj de la maŝino.

Kiam vi laboras en pluraj fadenoj, sistemaj vokoj fariĝas tro multaj:

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

Estas senco limigi vin nur al procezadministrado kaj sistemaj vokoj 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 +++

Cetere, demandoj. Kia sistemvoko estas uzata por krei novan fadenon? Kiel ĉi tiu alvoko por fadenoj diferencas de la alvoko por procezoj?

Majstra klaso: proceza stako dum sistema voko

Unu el la lastatempe aperis strace kapabloj - montrante la stakon de funkciovokoj dum la sistemvoko. Simpla ekzemplo:

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

Nature, la programo eligo fariĝas tre volumena, kaj, krom la flago -k (voka stakmontro), estas senco filtri sistemajn vokojn laŭ nomo:

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

Majstra klaso: erara injekto

Kaj unu pli nova kaj tre utila trajto: erara injekto. Jen la programo, skribante du liniojn al la eligfluo:

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

Ni spuru ambaŭ skribajn vokojn:

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

Nun ni uzas la esprimon injectpor enmeti eraron EBADF en ĉiuj skribvokoj:

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

Estas interese, kiaj eraroj estas redonitaj ĉiuj defioj write, inkluzive de la voko kaŝita malantaŭ peroro. Estas nur senco resendi eraron por la unua el la vokoj:

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

Aŭ la dua:

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

Ne necesas specifi la erarspecon:

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

En kombinaĵo kun aliaj flagoj, vi povas "rompi" aliron al specifa dosiero. Ekzemplo:

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

Krom erara injekto, povas enkonduku prokrastojn dum vokoj aŭ ricevado de signaloj.

Antaŭparolo

Utileco strace - simpla kaj fidinda ilo. Sed krom sistemvokoj, aliaj aspektoj de la funkciado de programoj kaj la operaciumo povas esti elpurigitaj. Ekzemple, ĝi povas spuri vokojn al dinamike ligitaj bibliotekoj. lspuro, ili povas rigardi la funkciadon de la operaciumo SystemTap и ftrace, kaj permesas vin profunde esplori program-efikecon perfekta. Tamen, ĝi estas strace - la unua defendlinio en kazo de problemoj kun miaj propraj kaj aliulaj programoj, kaj mi uzas ĝin almenaŭ kelkajn fojojn semajne.

Resume, se vi amas Unikson, legu man 1 strace kaj bonvolu rigardi viajn programojn!

fonto: www.habr.com

Aldoni komenton