Strace Linux: istorija, dizainas ir naudojimas

Strace Linux: istorija, dizainas ir naudojimas

„Unix“ tipo operacinėse sistemose programos ryšys su išoriniu pasauliu ir operacine sistema vyksta per nedidelį funkcijų rinkinį – sistemos iškvietimus. Tai reiškia, kad derinimo tikslais gali būti naudinga šnipinėti procesų vykdomus sistemos iškvietimus.

Priemonė padeda stebėti „Linux“ programų „intymų gyvenimą“. strace, kuri yra šio straipsnio tema. Prie šnipinėjimo įrangos naudojimo pavyzdžių pateikiama trumpa istorija strace ir tokių programų dizaino aprašymas.

Turinys

Rūšių kilmė

Pagrindinė sąsaja tarp programų ir OS branduolio Unix yra sistemos skambučiai. sistemos skambučiai, sistemos skambučiai), programų sąveika su išoriniu pasauliu vyksta tik per jas.

Tačiau pirmojoje viešoje Unix versijoje (6 versija Unix, 1975) nebuvo patogių būdų sekti vartotojų procesų elgesį. Norėdami išspręsti šią problemą, Bell Labs atnaujins į kitą versiją (7 versija Unix, 1979) pasiūlė naują sistemos iškvietimą - ptrace.

ptrace buvo sukurta daugiausia interaktyviems derintojams, tačiau iki devintojo dešimtmečio pabaigos (komercinės eros System V 4 leidimas) šiuo pagrindu atsirado ir buvo plačiai naudojami siaurai orientuoti derintuvai – sistemos iškvietimo žymekliai.

pirmas tą pačią strace versiją 1992 m. comp.sources.sun adresų sąraše paskelbė Paulas Cronenburgas kaip alternatyva uždarajai programai trace nuo Saulės. Tiek klonas, tiek originalas buvo skirti SunOS, tačiau iki 1994 m strace buvo perkelta į System V, Solaris ir vis populiarėjančią Linux.

Šiandien „strace“ palaiko tik „Linux“ ir remiasi tuo ptrace, apaugę daugybe priestatų.

Šiuolaikinis (ir labai aktyvus) prižiūrėtojas strace - Dmitrijus Levinas. Jo dėka programa įgijo pažangių funkcijų, tokių kaip klaidų įvedimas į sistemos skambučius, įvairių architektūrų palaikymas ir, svarbiausia, talismanas. Neoficialūs šaltiniai teigia, kad strutį pasirinko dėl rusiško žodžio „strich“ ir angliško „strace“ sąskambio.

Taip pat svarbu, kad ptrace sistemos iškvietimas ir tracers niekada nebuvo įtraukti į POSIX, nepaisant ilgos istorijos ir įdiegimo Linux, FreeBSD, OpenBSD ir tradicinėse Unix sistemose.

„Strace“ prietaisas trumpai: „Piglet Trace“.

„Jūs to nesuprasite“ (Dennisas Ritchie, komentaras 6 versijos Unix šaltinio kode)

Nuo ankstyvos vaikystės negaliu pakęsti juodųjų dėžių: nežaidžiau su žaislais, o bandžiau suprasti jų struktūrą (suaugusieji vartojo žodį „sugedo“, bet netiki piktomis kalbomis). Galbūt todėl man tokia artima neformali pirmojo Unix kultūra ir šiuolaikinis atvirojo kodo judėjimas.

Šiame straipsnyje neprotinga išardyti strace šaltinio kodą, kuris išaugo per dešimtmečius. Tačiau skaitytojams neturėtų likti paslapčių. Todėl, norėdamas parodyti tokių strace programų veikimo principą, pateiksiu miniatiūrinio sekimo kodą - Paršelio pėdsakas (ptr). Jis nežino, kaip padaryti nieko ypatingo, bet pagrindinis dalykas yra programos sistemos iškvietimai - ji išveda:

$ 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“ atpažįsta apie šimtus „Linux“ sistemos iškvietimų (žr. stalas) ir veikia tik x86-64 architektūroje. To pakanka švietimo tikslams.

Pažvelkime į mūsų klono darbą. „Linux“ atveju derintojai ir sekimo priemonės naudoja, kaip minėta aukščiau, sistemos „ptrace“ iškvietimą. Tai veikia pirmame argumente perduodant komandų identifikatorius, kurių mums reikia tik PTRACE_TRACEME, PTRACE_SYSCALL и PTRACE_GETREGS.

Tracer prasideda įprastu Unix stiliumi: fork(2) paleidžia vaiko procesą, kuris savo ruožtu naudoja exec(3) pradeda studijuojamą programą. Vienintelis subtilumas čia yra iššūkis ptrace(PTRACE_TRACEME) prieš exec: antrinis procesas tikisi, kad tėvų procesas jį stebė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ėvų procesas dabar turėtų skambinti wait(2) antriniame procese, ty įsitikinkite, kad įvyko perjungimas į sekimo režimą:

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

Šiuo metu parengiamieji darbai baigti ir galite pereiti tiesiai prie sistemos skambučių stebėjimo begaliniame cikle.

Вызов ptrace(PTRACE_SYSCALL) garantuoja, kad vėliau wait tėvas užbaigs arba prieš įvykdant sistemos iškvietimą, arba iškart jam pasibaigus. Tarp dviejų skambučių galite atlikti bet kokius veiksmus: pakeisti skambutį alternatyviu, pakeisti argumentus arba grąžinamąją reikšmę.

Mums tereikia du kartus iškviesti komandą ptrace(PTRACE_GETREGS)gauti registro būseną rax prieš skambutį (sistemos skambučio numeris) ir iškart po (grąžinimo vertė).

Tiesą sakant, ciklas:

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

Štai ir visas sekiklis. Dabar žinote, kur pradėti kitą perkėlimą DTrace Linux sistemoje.

Pagrindai: paleisti programą, kuri veikia strace

Kaip pirmasis naudojimo atvejis strace, galbūt verta paminėti paprasčiausią būdą – paleisti veikiančią programą strace.

Kad nesigilintume į begalinį tipinės programos skambučių sąrašą, rašome minimali programa aplink 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;
}

Sukurkime programą ir įsitikinkime, kad ji veikia:

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

Ir galiausiai paleiskite jį kontroliuojant strace:

$ strace ./write-simple
pexecve("./write", ["./write"], 0x7ffebd6145b0 /* 71 vars */) = 0
brk(NULL)                               = 0x55ff5489e000
access("/etc/ld.so.nohwcap", F_OK)      = -1 ENOENT (No such file or directory)
access("/etc/ld.so.preload", R_OK)      = -1 ENOENT (No such file or directory)
openat(AT_FDCWD, "/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3
fstat(3, {st_mode=S_IFREG|0644, st_size=197410, ...}) = 0
mmap(NULL, 197410, PROT_READ, MAP_PRIVATE, 3, 0) = 0x7f7a2a633000
close(3)                                = 0
access("/etc/ld.so.nohwcap", F_OK)      = -1 ENOENT (No such file or directory)
openat(AT_FDCWD, "/lib/x86_64-linux-gnu/libc.so.6", O_RDONLY|O_CLOEXEC) = 3
read(3, "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)                           = ?

Labai „žodingas“ ir nelabai mokomas. Čia yra dvi problemos: programos išvestis sumaišoma su išvestimi strace ir daugybė sistemos skambučių, kurios mūsų nedomina.

Galite atskirti standartinį programos išvesties srautą ir strace klaidų išvestį naudodami jungiklį -o, kuris nukreipia sistemos iškvietimų sąrašą į argumentų failą.

Belieka spręsti „papildomų“ skambučių problemą. Tarkime, kad mus domina tik skambučiai write. Raktas -e leidžia nurodyti išraiškas, pagal kurias bus filtruojami sistemos iškvietimai. Žinoma, populiariausias sąlygų variantas yra trace=*, su kuria galite palikti tik mus dominančius skambučius.

Kai naudojamas vienu metu -o и -e mes gausime:

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

Taigi, matote, daug lengviau skaityti.

Taip pat galite pašalinti sistemos skambučius, pavyzdžiui, susijusius su atminties paskirstymu ir atlaisvinimu:

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

Atkreipkite dėmesį į pašalintą šauktuką neįtrauktų skambučių sąraše: to reikalauja komandos apvalkalas. apvalkalas).

Mano glibc versijoje sistemos iškvietimas užbaigia procesą exit_group, ne tradicinis _exit. Tai yra sunkumas dirbant su sistemos skambučiais: sąsaja, su kuria dirba programuotojas, nėra tiesiogiai susijusi su sistemos skambučiais. Be to, jis reguliariai keičiasi priklausomai nuo diegimo ir platformos.

Pagrindai: prisijungimas prie proceso skrendant

Iš pradžių ptrace sistemos skambutis, ant kurio jis buvo sukurtas strace, galima naudoti tik paleidus programą specialiu režimu. Šis apribojimas 6 versijos Unix laikais galėjo atrodyti pagrįstas. Šiais laikais to jau nebeužtenka: kartais reikia patyrinėti veikiančios programos problemas. Tipiškas pavyzdys yra procesas, užblokuotas ant rankenos arba miegas. Todėl modernus strace gali prisijungti prie procesų skrydžio metu.

Užšalimo pavyzdys programos:

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

Sukurkime programą ir įsitikinkime, kad ji užšaldyta:

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

Dabar pabandykime prie jo prisijungti:

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

Programą užblokavo skambutis pause. Pažiūrėkime, kaip ji reaguoja į signalus:

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

Paleidome šaldytą programą ir prisijungėme prie jos naudodami strace. Paaiškėjo du dalykai: pause sistemos iškvietimas ignoruoja signalus be tvarkyklių ir, kas įdomiau, strace stebi ne tik sistemos skambučius, bet ir įeinančius signalus.

Pavyzdys: Vaikų procesų stebėjimas

Darbas su procesais per skambutį fork - visų unixų pagrindas. Pažiūrėkime, kaip „strace“ veikia su proceso medžiu, naudodami paprasto „veisimo“ pavyzdį. programos:

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

Čia pradinis procesas sukuria antrinį procesą, abu rašant į standartinę išvestį:

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

Pagal numatytuosius nustatymus matysime tik sistemos iškvietimus iš pirminio proceso:

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

Vėliava padeda sekti visą proceso medį -f, kuris strace stebi sistemos iškvietimus antriniuose procesuose. Tai papildo kiekvieną išvesties eilutę pid procesas, kuris sukuria sistemos išvestį:

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

Šiame kontekste gali būti naudingas filtravimas pagal sistemos skambučių grupę:

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

Beje, koks sistemos iškvietimas naudojamas kuriant naują procesą?

Pavyzdys: failo keliai, o ne rankenos

Žinoti failų aprašus tikrai naudinga, tačiau gali praversti ir konkrečių failų, kuriuos programa pasiekia, pavadinimai.

kitas programa įrašo eilutę į laikinąjį 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;
}

Įprasto skambučio metu strace parodys deskriptoriaus numerio, perduoto sistemos skambučiui, reikšmę:

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

Su vėliava -y Naudingumas rodo kelią į failą, kurį atitinka deskriptorius:

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

Pavyzdys: Failų prieigos stebėjimas

Kita naudinga funkcija: rodyti tik sistemos skambučius, susijusius su konkrečiu failu. Kitas programa prideda eilutę prie savavališko failo, pateikto kaip argumentas:

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

Pagal nutylėjimą strace rodo daug nereikalingos informacijos. Vėliava -P su argumentu strace spausdina tik iškvietimus į nurodytą failą:

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

Pavyzdys: kelių gijų programos

Naudingumas strace taip pat gali padėti dirbant su kelių sriegių programa. Ši programa rašo į standartinę išvestį iš dviejų srautų:

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

Natūralu, kad jis turi būti sudarytas su specialiu linkėjimu - pthread vėliava:

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

Vėliava -f, kaip ir įprastų procesų atveju, kiekvienos eilutės pradžioje pridės proceso pid.

Natūralu, kad kalbame ne apie gijos identifikatorių POSIX Threads standarto įgyvendinimo prasme, o apie skaičių, kurį naudoja užduočių planuoklis Linux sistemoje. Pastarojo požiūriu, nėra procesų ar gijų - yra užduočių, kurias reikia paskirstyti tarp turimų mašinos branduolių.

Kai dirbate keliose gijose, sistemos skambučių tampa per daug:

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

Tikslinga apsiriboti tik procesų valdymu ir sistemos iškvietimais 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 +++

Beje, klausimai. Koks sistemos iškvietimas naudojamas kuriant naują giją? Kuo šis gijų kvietimas skiriasi nuo procesų kvietimo?

Meistriškumo klasė: proceso dėklas sistemos iškvietimo metu

Vienas iš neseniai pasirodžiusių strace galimybės – rodo funkcijų iškvietimų krūvą sistemos iškvietimo metu. Paprasta pavyzdys:

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

Natūralu, kad programos išvestis tampa labai didelė, be to, vėliavėlė -k (skambučių krūvos ekranas), prasminga filtruoti sistemos skambučius pagal pavadinimą:

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

Meistriškumo klasė: klaidų įvedimas

Ir dar viena nauja ir labai naudinga funkcija: klaidų įvedimas. Čia programa, įrašydami dvi eilutes į išvesties srautą:

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

Stebėkime abu rašymo skambučius:

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

Dabar mes naudojame posakį injectįterpti klaidą EBADF visuose skambučiuose rašyti:

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

Įdomu kokios klaidos grąžinamos visi iššūkius write, įskaitant skambutį, paslėptą už klaidos. Prasminga grąžinti klaidą tik pirmam skambučiui:

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

Arba antrasis:

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

Nebūtina nurodyti klaidos tipo:

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

Kartu su kitomis vėliavėlėmis galite „nutraukti“ prieigą prie konkretaus failo. Pavyzdys:

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

Be klaidų įvedimo, vienas gali skambinant ar priimant signalus vėluojama.

Afterword

Naudingumas strace - paprastas ir patikimas įrankis. Tačiau, be sistemos iškvietimų, galima derinti ir kitus programų ir operacinės sistemos veikimo aspektus. Pavyzdžiui, jis gali sekti skambučius į dinamiškai susietas bibliotekas. lttrace, jie gali ištirti operacinės sistemos veikimą SystemTap и ftraceir leidžia nuodugniai ištirti programos našumą puikus. Nepaisant to, tai yra strace – pirmoji gynybos linija iškilus problemoms su savo ir svetimomis programomis ir naudoju ją bent porą kartų per savaitę.

Trumpai tariant, jei jums patinka Unix, skaitykite man 1 strace ir nedvejodami žiūrėkite savo programas!

Šaltinis: www.habr.com

Добавить комментарий