Strace në Linux: historia, dizajni dhe përdorimi

Strace në Linux: historia, dizajni dhe përdorimi

Në sistemet operative të ngjashme me Unix, komunikimi i një programi me botën e jashtme dhe sistemin operativ ndodh përmes një grupi të vogël funksionesh - thirrjet e sistemit. Kjo do të thotë që për qëllime korrigjimi mund të jetë e dobishme të spiunohen thirrjet e sistemit që ekzekutohen nga proceset.

Një mjet ndihmës ju ndihmon të monitoroni "jetën intime" të programeve në Linux strace, e cila është objekt i këtij artikulli. Shembujt e përdorimit të pajisjeve të spiunazhit shoqërohen me një histori të shkurtër strace dhe një përshkrim të dizajnit të programeve të tilla.

Përmbajtje

Origjina e specieve

Ndërfaqja kryesore midis programeve dhe kernelit OS në Unix janë thirrjet e sistemit. thirrjet e sistemit, sycalls), ndërveprimi i programeve me botën e jashtme ndodh ekskluzivisht përmes tyre.

Por në versionin e parë publik të Unix (Versioni 6 Unix, 1975) nuk kishte mënyra të përshtatshme për të gjurmuar sjelljen e proceseve të përdoruesit. Për të zgjidhur këtë problem, Bell Labs do të përditësohet në versionin tjetër (Versioni 7 Unix, 1979) propozoi një thirrje të re të sistemit - ptrace.

ptrace u zhvillua kryesisht për korrigjuesit interaktivë, por nga fundi i viteve '80 (në epokën e komercialeve Publikimi i Sistemit V 4) mbi këtë bazë, u shfaqën dhe u përdorën gjerësisht korrigjuesit me fokus të ngushtë - gjurmuesit e thirrjeve të sistemit.

Первая i njëjti version i strace u botua nga Paul Cronenburg në listën e postimeve comp.sources.sun në 1992 si një alternativë ndaj një shërbimi të mbyllur. trace nga Dielli. Si kloni ashtu edhe origjinali ishin të destinuara për SunOS, por deri në vitin 1994 strace u transferua në System V, Solaris dhe Linux gjithnjë e më popullor.

Sot strace mbështet vetëm Linux dhe mbështetet në të njëjtën gjë ptrace, i tejmbushur me shumë zgjatime.

Mirëmbajtës modern (dhe shumë aktiv). strace - Dmitry Levin. Falë tij, shërbimi fitoi veçori të avancuara si injektimi i gabimeve në thirrjet e sistemit, mbështetje për një gamë të gjerë arkitekturash dhe, më e rëndësishmja, nuskë. Burimet jozyrtare pohojnë se zgjedhja ra mbi strucin për shkak të bashkëtingëllimit midis fjalës ruse "struc" dhe fjalës angleze "strace".

Është gjithashtu e rëndësishme që thirrja e sistemit ptrace dhe gjurmuesit nuk janë përfshirë kurrë në POSIX, megjithë një histori të gjatë dhe zbatim në Linux, FreeBSD, OpenBSD dhe Unix tradicionale.

Pajisja Strace me pak fjalë: Piglet Trace

"Nuk pritet ta kuptoni këtë" (Dennis Ritchie, koment në kodin burimor të Unix Version 6)

Që nga fëmijëria e hershme, nuk i duroj dot kutitë e zeza: nuk luaja me lodra, por u përpoqa të kuptoja strukturën e tyre (të rriturit përdorën fjalën "thyer", por mos u besoni gjuhëve të liga). Ndoshta kjo është arsyeja pse kultura informale e Unix-it të parë dhe e lëvizjes moderne me burim të hapur është kaq afër meje.

Për qëllimet e këtij neni, është e paarsyeshme të çmontohet kodi burimor i strace, i cili është rritur me dekada. Por për lexuesit nuk duhet të mbeten sekrete. Prandaj, për të treguar parimin e funksionimit të programeve të tilla strace, unë do të jap kodin për një gjurmues miniaturë - Gjurma e derrit (ptr). Nuk di të bëjë asgjë të veçantë, por gjëja kryesore janë thirrjet e sistemit të programit - ai del:

$ 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 njeh rreth qindra thirrje të sistemit Linux (shih. tryezë) dhe punon vetëm në arkitekturën x86-64. Kjo është e mjaftueshme për qëllime edukative.

Le të shohim punën e klonit tonë. Në rastin e Linux-it, korrigjuesit dhe gjurmuesit përdorin, siç u përmend më lart, thirrjen e sistemit ptrace. Funksionon duke kaluar në argumentin e parë identifikuesit e komandës, prej të cilëve na duhen vetëm PTRACE_TRACEME, PTRACE_SYSCALL и PTRACE_GETREGS.

Gjurmuesi fillon në stilin e zakonshëm Unix: fork(2) nis një proces fëmijësh, i cili nga ana tjetër përdor exec(3) nis programin në studim. E vetmja hollësi këtu është sfida ptrace(PTRACE_TRACEME) para exec: Procesi i fëmijës pret që procesi prind ta monitorojë atë:

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

Tani duhet të telefonojë procesi prind wait(2) në procesin e fëmijës, domethënë, sigurohuni që të ketë ndodhur kalimi në modalitetin e gjurmimit:

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

Në këtë pikë, përgatitjet kanë përfunduar dhe ju mund të vazhdoni drejtpërdrejt me ndjekjen e thirrjeve të sistemit në një qark të pafund.

Вызов ptrace(PTRACE_SYSCALL) garanton që më pas wait prindi do të përfundojë ose përpara se thirrja e sistemit të ekzekutohet ose menjëherë pasi të përfundojë. Midis dy thirrjeve mund të kryeni çdo veprim: zëvendësoni thirrjen me një alternative, ndryshoni argumentet ose vlerën e kthimit.

Thjesht duhet të thërrasim dy herë komandën ptrace(PTRACE_GETREGS)për të marrë gjendjen e regjistrit rax para thirrjes (numri i thirrjes së sistemit) dhe menjëherë pas (vlera e kthimit).

Në fakt, cikli:

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

Ky është i gjithë gjurmuesi. Tani e dini se ku të filloni transferimin e ardhshëm DGjurmë në Linux.

Bazat: ekzekutimi i një programi që ekzekuton strace

Si rast përdorimi i parë strace, ndoshta ia vlen të përmendet metoda më e thjeshtë - nisja e një aplikacioni të ekzekutohet strace.

Për të mos u thelluar në listën e pafund të thirrjeve të një programi tipik, ne shkruajmë program minimal вокруг 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;
}

Le të ndërtojmë programin dhe të sigurohemi që funksionon:

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

Dhe së fundi, le ta drejtojmë atë nën kontrollin e rrugës:

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

Shumë "fjalë" dhe jo shumë edukative. Këtu ka dy probleme: dalja e programit është e përzier me daljen strace dhe një bollëk telefonatash sistemore që nuk na interesojnë.

Ju mund të ndani rrymën standarde të daljes së programit dhe daljen e gabimit të brezit duke përdorur çelësin -o, i cili ridrejton listën e thirrjeve të sistemit në një skedar argumenti.

Mbetet për t'u marrë me problemin e thirrjeve "ekstra". Le të supozojmë se ne jemi të interesuar vetëm për telefonata write. Celës -e ju lejon të specifikoni shprehjet me të cilat do të filtrohen thirrjet e sistemit. Opsioni më i popullarizuar i gjendjes është, natyrisht, trace=*, me të cilin mund të lini vetëm telefonatat që na interesojnë.

Kur përdoret njëkohësisht -o и -e do të marrim:

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

Pra, e shihni, është shumë më e lehtë për t'u lexuar.

Ju gjithashtu mund të hiqni thirrjet e sistemit, për shembull ato që lidhen me shpërndarjen dhe lirimin e memories:

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

Vini re pikëçuditjen e ikur në listën e thirrjeve të përjashtuara: kjo kërkohet nga guaska e komandës. predhë).

Në versionin tim të glibc, një thirrje sistemi përfundon procesin exit_group, jo tradicionale _exit. Kjo është vështirësia e punës me thirrjet e sistemit: ndërfaqja me të cilën punon programuesi nuk lidhet drejtpërdrejt me thirrjet e sistemit. Për më tepër, ai ndryshon rregullisht në varësi të zbatimit dhe platformës.

Bazat: bashkimi i procesit në fluturim

Fillimisht, thirrja e sistemit ptrace në të cilën u ndërtua strace, mund të përdoret vetëm kur programi ekzekutohet në një modalitet të veçantë. Ky kufizim mund të ketë tingëlluar i arsyeshëm në ditët e Versionit 6 Unix. Në ditët e sotme, kjo nuk mjafton më: ndonjëherë ju duhet të hetoni problemet e një programi pune. Një shembull tipik është një proces i bllokuar në një dorezë ose gjumi. Prandaj moderne strace mund të bashkohet me proceset në fluturim.

Shembull ngrirës programet:

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

Le të ndërtojmë programin dhe të sigurohemi që është i ngrirë:

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

Tani le të përpiqemi t'i bashkohemi:

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

Programi u bllokua me thirrje pause. Le të shohim se si reagon ajo ndaj sinjaleve:

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

Ne nisëm programin e ngrirë dhe u bashkuam duke përdorur strace. Dy gjëra u bënë të qarta: thirrja e sistemit pauzë injoron sinjalet pa mbajtës dhe, më interesante, monitoron jo vetëm thirrjet e sistemit, por edhe sinjalet hyrëse.

Shembull: Ndjekja e proceseve të fëmijëve

Puna me proceset përmes një thirrjeje fork - baza e të gjitha Unix-eve. Le të shohim se si funksionon strace me një pemë procesi duke përdorur shembullin e një "mbareshtimi" të thjeshtë programet:

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

Këtu procesi origjinal krijon një proces fëmijësh, të dy duke shkruar në dalje standarde:

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

Si parazgjedhje, ne do të shohim vetëm thirrjet e sistemit nga procesi prind:

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

Flamuri ju ndihmon të gjurmoni të gjithë pemën e procesit -f, e cila strace monitoron thirrjet e sistemit në proceset e fëmijëve. Kjo shton në secilën linjë të prodhimit pid proces që bën një dalje të sistemit:

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

Në këtë kontekst, filtrimi sipas grupit të thirrjeve të sistemit mund të jetë i dobishëm:

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

Nga rruga, çfarë thirrje sistemi përdoret për të krijuar një proces të ri?

Shembull: shtigjet e skedarëve në vend të dorezave

Njohja e përshkruesve të skedarëve është padyshim e dobishme, por emrat e skedarëve specifikë që një program akseson gjithashtu mund të jenë të dobishëm.

Tjetër program shkruan rreshtin në një skedar të përkohshëm:

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

Gjatë një telefonate normale strace do të tregojë vlerën e numrit të përshkruesit të kaluar në thirrjen e sistemit:

$ 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 një flamur -y Programi tregon shtegun drejt skedarit të cilit i korrespondon përshkruesi:

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

Shembull: Ndjekja e qasjes në skedar

Një veçori tjetër e dobishme: shfaq vetëm thirrjet e sistemit të lidhura me një skedar specifik. Tjetra program shton një rresht në një skedar arbitrar të kaluar si argument:

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

By default strace shfaq shumë informacione të panevojshme. Flamuri -P me një argument bën që strace të printojë vetëm thirrje në skedarin e specifikuar:

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

Shembull: Programet me shumë fije

Shërbim strace mund të ndihmojë gjithashtu kur punoni me shumë fije programi. Programi i mëposhtëm shkruan në dalje standarde nga dy rryma:

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

Natyrisht, ai duhet të përpilohet me një përshëndetje të veçantë për lidhësin - flamuri -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
$

flamur -f, si në rastin e proceseve të rregullta, do të shtojë pid-in e procesit në fillim të çdo rreshti.

Natyrisht, ne nuk po flasim për një identifikues thread në kuptimin e zbatimit të standardit POSIX Threads, por për numrin e përdorur nga planifikuesi i detyrave në Linux. Nga këndvështrimi i këtij të fundit, nuk ka procese ose fije - ka detyra që duhet të shpërndahen midis bërthamave të disponueshme të makinës.

Kur punoni në fije të shumta, thirrjet e sistemit bëhen shumë të shumta:

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

Ka kuptim të kufizoheni vetëm në menaxhimin e procesit dhe thirrjet e sistemit 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 +++

Nga rruga, pyetje. Çfarë thirrje sistemi përdoret për të krijuar një thread të ri? Si ndryshon kjo thirrje për threads nga thirrja për procese?

Klasa master: procesoni grumbullin në kohën e një thirrjeje sistemi

Një nga të shfaqurit së fundmi strace aftësitë - shfaqja e grupit të thirrjeve të funksioneve në momentin e thirrjes së sistemit. E thjeshtë shembull:

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

Natyrisht, prodhimi i programit bëhet shumë voluminoz, dhe, përveç flamurit -k (ekrani i pirgut të thirrjeve), ka kuptim të filtrosh thirrjet e sistemit me emër:

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

Klasa master: injeksion gabimi

Dhe një veçori tjetër e re dhe shumë e dobishme: injektimi i gabimeve. Këtu program, duke shkruar dy rreshta në rrjedhën e daljes:

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

Le të gjurmojmë të dyja thirrjet e shkrimit:

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

Tani përdorim shprehjen injectpër të futur një gabim EBADF në të gjitha thirrjet shkruani:

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

Është interesante se çfarë gabimesh kthehen të gjithë sfidat write, duke përfshirë thirrjen e fshehur pas mashtrimit. Ka kuptim të ktheni një gabim vetëm për thirrjet e para:

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

Ose e dyta:

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

Nuk është e nevojshme të specifikoni llojin e gabimit:

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

Në kombinim me flamuj të tjerë, ju mund të "prishni" aksesin në një skedar specifik. Shembull:

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

Përveç injektimit të gabimit, një mund të futni vonesa kur bëni thirrje ose merrni sinjale.

pasthënje

Shërbim strace - një mjet i thjeshtë dhe i besueshëm. Por përveç thirrjeve të sistemit, aspekte të tjera të funksionimit të programeve dhe sistemit operativ mund të korrigjohen. Për shembull, mund të gjurmojë thirrjet drejt bibliotekave të lidhura në mënyrë dinamike. gjurmë, ata mund të shikojnë funksionimin e sistemit operativ SystemTap и ftrace, dhe ju lejon të hulumtoni thellësisht performancën e programit perfekte. Megjithatë, është strace - linja e parë e mbrojtjes në rast të problemeve me programet e mia dhe të njerëzve të tjerë, dhe e përdor të paktën disa herë në javë.

Me pak fjalë, nëse ju pëlqen Unix, lexoni man 1 strace dhe mos ngurroni të shikoni programet tuaja!

Burimi: www.habr.com

Shto një koment