Linux-da Strace: tarix, dizayn və istifadə

Linux-da Strace: tarix, dizayn və istifadə

Unix-ə bənzər əməliyyat sistemlərində proqramın xarici dünya və əməliyyat sistemi ilə əlaqəsi kiçik funksiyalar toplusu - sistem çağırışları vasitəsilə baş verir. Bu o deməkdir ki, sazlama məqsədləri üçün proseslər tərəfindən icra edilən sistem zənglərinə casusluq etmək faydalı ola bilər.

Utilit sizə Linux-da proqramların "intim həyatını" izləməyə kömək edir strace, bu məqalənin mövzusudur. Casus avadanlığından istifadə nümunələri qısa tarixlə müşayiət olunur strace və bu cür proqramların dizaynının təsviri.

Məzmun

Növlərin mənşəyi

Unix-də proqramlar və OS nüvəsi arasında əsas interfeys sistem zəngləridir. sistem zəngləri, qüsurlar), proqramların xarici dünya ilə qarşılıqlı əlaqəsi yalnız onlar vasitəsilə baş verir.

Lakin Unix-in ilk ictimai versiyasında (Versiya 6 Unix, 1975) istifadəçi proseslərinin davranışını izləmək üçün əlverişli yollar yox idi. Bu problemi həll etmək üçün Bell Labs növbəti versiyaya yeniləyəcək (Versiya 7 Unix, 1979) yeni bir sistem çağırışı təklif etdi - ptrace.

ptrace ilk növbədə interaktiv sazlayıcılar üçün hazırlanmışdır, lakin 80-ci illərin sonunda (kommersiya dövründə) Sistem V Buraxılış 4) bu əsasda dar fokuslu sazlayıcılar-sistem çağırış izləyiciləri meydana çıxdı və geniş istifadə olundu.

Ilk strace-in eyni versiyası Paul Cronenburg tərəfindən 1992-ci ildə comp.sources.sun poçt siyahısında qapalı yardım proqramına alternativ olaraq dərc edilmişdir. trace günəşdən. Həm klon, həm də orijinal SunOS üçün nəzərdə tutulmuşdu, lakin 1994-cü ilə qədər strace Sistem V, Solaris və getdikcə populyarlaşan Linux-a köçürüldü.

Bu gün strace yalnız Linux-u dəstəkləyir və buna güvənir ptrace, bir çox uzantıları ilə böyüyür.

Müasir (və çox aktiv) baxıcı strace - Dmitri Levin. Onun sayəsində kommunal sistem zənglərinə xəta yeritmə, geniş arxitekturaya dəstək və ən əsası, inkişaf etmiş funksiyaları əldə etdi. maskot. Qeyri-rəsmi mənbələr iddia edirlər ki, seçim rus dilindəki “dəvəquşu” ilə ingiliscə “strace” sözü arasında uyğunluq olduğundan dəvəquşuna düşüb.

Linux, FreeBSD, OpenBSD və ənənəvi Unix-də uzun tarixə və tətbiqə baxmayaraq, ptrace sistemi çağırışı və izləyicilərinin heç vaxt POSIX-ə daxil edilməməsi də vacibdir.

Qısaca Strace cihazı: Piglet Trace

"Bunu başa düşməyiniz gözlənilmir" (Dennis Ritchie, Unix mənbə kodunun 6-cı versiyasına şərh)

Erkən uşaqlıqdan qara qutulara dözə bilmirəm: oyuncaqlarla oynamırdım, lakin onların quruluşunu başa düşməyə çalışırdım (böyüklər "sındırıldı" sözünü işlədirlər, amma pis dillərə inanmırlar). Bəlkə də buna görə ilk Unix-in qeyri-rəsmi mədəniyyəti və müasir açıq mənbə hərəkatı mənə bu qədər yaxındır.

Bu məqalənin məqsədləri üçün onilliklər ərzində böyüyən strace mənbə kodunu sökmək ağlabatan deyil. Amma oxucular üçün heç bir sirr qalmamalıdır. Buna görə də, bu cür strace proqramlarının işləmə prinsipini göstərmək üçün miniatür izləyicinin kodunu təqdim edəcəyəm - Piglet Trace (ptr). Xüsusi bir şey necə edəcəyini bilmir, amma əsas odur ki, proqramın sistem çağırışlarıdır - o çıxış edir:

$ 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 yüzlərlə Linux sistem zənglərini tanıyır (bax. masa) və yalnız x86-64 arxitekturasında işləyir. Bu təhsil məqsədləri üçün kifayətdir.

Gəlin klonumuzun işinə baxaq. Linux vəziyyətində, sazlayıcılar və izləyicilər, yuxarıda qeyd edildiyi kimi, ptrace sistem çağırışından istifadə edirlər. O, birinci arqumentdə yalnız bizə lazım olan komanda identifikatorlarını ötürməklə işləyir PTRACE_TRACEME, PTRACE_SYSCALL и PTRACE_GETREGS.

İzləyici adi Unix üslubunda başlayır: fork(2) uşaq prosesini işə salır, bu da öz növbəsində istifadə edir exec(3) tədqiq olunan proqramı işə salır. Burada yeganə incəlik problemdir ptrace(PTRACE_TRACEME) əvvəl exec: Uşaq prosesi ana prosesin ona nəzarət etməsini gözləyir:

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

Ana proses indi çağırmalıdır wait(2) uşaq prosesində, yəni izləmə rejiminə keçidin baş verdiyinə əmin olun:

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

Bu nöqtədə, hazırlıqlar tamamlandı və siz sonsuz bir dövrədə sistem zənglərini birbaşa izləməyə davam edə bilərsiniz.

Çağırış ptrace(PTRACE_SYSCALL) sonradan zəmanət verir wait valideyn ya sistem çağırışı yerinə yetirilməzdən əvvəl, ya da tamamlandıqdan dərhal sonra tamamlanacaq. İki zəng arasında istənilən hərəkəti yerinə yetirə bilərsiniz: zəngini alternativi ilə əvəz edin, arqumentləri dəyişdirin və ya dəyəri qaytarın.

Bizə sadəcə iki dəfə komanda çağırmaq lazımdır ptrace(PTRACE_GETREGS)reyestr dövlətini əldə etmək rax zəngdən əvvəl (sistem zəng nömrəsi) və dərhal sonra (qaytarma dəyəri).

Əslində, dövr:

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

Bütün izləyici budur. İndi növbəti daşınmaya haradan başlayacağınızı bilirsiniz DTrace Linux-da.

Əsaslar: strace ilə işləyən proqramı işə salmaq

İlk istifadə halı kimi strace, bəlkə də ən sadə üsulu - işləyən bir tətbiqi işə salmağa dəyər strace.

Tipik bir proqramın sonsuz zənglər siyahısına girməmək üçün yazırıq minimum proqram ətrafında 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;
}

Proqramı yaradaq və işlədiyinə əmin olaq:

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

Və nəhayət, onu strace nəzarəti altında işlədək:

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

Çox “sözlü” və çox da maarifləndirici deyil. Burada iki problem var: proqramın çıxışı çıxışla qarışdırılır strace və bizi maraqlandırmayan çoxlu sistem zəngləri.

Sistem çağırışlarının siyahısını arqument faylına yönləndirən -o keçidindən istifadə edərək proqramın standart çıxış axını və xəta çıxışını ayıra bilərsiniz.

"Əlavə" zənglər problemi ilə məşğul olmaq qalır. Tutaq ki, bizi yalnız zənglər maraqlandırır write... Açar -e sistem zənglərinin filtrlənəcəyi ifadələri təyin etməyə imkan verir. Ən populyar şərt variantı, təbii ki, trace=*, onunla yalnız bizi maraqlandıran zəngləri tərk edə bilərsiniz.

Eyni vaxtda istifadə edildikdə -o и -e alacağıq:

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

Beləliklə, görürsən, oxumaq daha asandır.

Siz həmçinin sistem zənglərini, məsələn, yaddaşın ayrılması və boşaldılması ilə bağlı olanları silə bilərsiniz:

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

İstisna edilən zənglər siyahısında qaçmış nida işarəsinə diqqət yetirin: bu, əmr qabığı tərəfindən tələb olunur. shell).

Mənim glibc versiyamda sistem çağırışı prosesi dayandırır exit_group, ənənəvi deyil _exit. Sistem zəngləri ilə işləməyin çətinliyi budur: proqramçının işlədiyi interfeys birbaşa sistem zəngləri ilə əlaqəli deyil. Üstəlik, tətbiqdən və platformadan asılı olaraq müntəzəm olaraq dəyişir.

Əsaslar: prosesə tez qoşulmaq

Əvvəlcə onun qurulduğu ptrace sistemi çağırılır strace, yalnız proqramı xüsusi rejimdə işləyərkən istifadə edilə bilər. Bu məhdudiyyət Unix 6 Versiyasının günlərində ağlabatan səslənmiş ola bilər. İndiki vaxtda bu kifayət deyil: bəzən işləyən proqramın problemlərini araşdırmaq lazımdır. Tipik bir nümunə, bir tutacaqda bloklanmış və ya yatmış bir prosesdir. Ona görə də müasir strace proseslərə tez qoşula bilər.

Dondurma nümunəsi proqramları:

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

Proqramı yaradaq və onun dondurulduğuna əmin olun:

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

İndi ona qoşulmağa çalışaq:

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

Proqram zənglə bloklanıb pause. Onun siqnallara necə reaksiya verdiyinə baxaq:

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

Dondurulmuş proqramı işə saldıq və istifadə edərək ona qoşulduq strace. İki şey aydın oldu: fasilə sistemi çağırışı işləyicilər olmadan siqnallara məhəl qoymur və daha maraqlısı odur ki, strace təkcə sistem zənglərini deyil, həm də daxil olan siqnalları izləyir.

Nümunə: Uşaq Proseslərinin İzlənməsi

Zəng vasitəsilə proseslərlə işləmək fork - bütün Unix-lərin əsasıdır. Sadə bir “yetişdirmə” nümunəsindən istifadə edərək stracenin proses ağacı ilə necə işlədiyini görək. proqramları:

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

Burada orijinal proses uşaq prosesi yaradır, hər ikisi standart çıxışa yazır:

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

Varsayılan olaraq, biz yalnız ana prosesdən sistem zənglərini görəcəyik:

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

Bayraq bütün proses ağacını izləməyə kömək edir -filə strace uşaq proseslərdə sistem çağırışlarına nəzarət edir. Bu, hər bir çıxış xəttinə əlavə edir pid sistem çıxışı edən proses:

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

Bu kontekstdə sistem zəngləri qrupu üzrə filtrləmə faydalı ola bilər:

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

Yeri gəlmişkən, yeni bir proses yaratmaq üçün hansı sistem çağırışından istifadə olunur?

Misal: tutacaqlar yerinə fayl yolları

Fayl deskriptorlarını bilmək əlbəttə faydalıdır, lakin proqramın daxil olduğu xüsusi faylların adları da faydalı ola bilər.

Növbəti proqram xətti müvəqqəti fayla yazır:

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

Normal zəng zamanı strace sistem çağırışına ötürülən deskriptor nömrəsinin dəyərini göstərəcək:

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

Bayraqla -y Faydalı proqram deskriptorun uyğun olduğu faylın yolunu göstərir:

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

Misal: Fayl Girişinin İzlənməsi

Başqa bir faydalı xüsusiyyət: yalnız müəyyən bir fayl ilə əlaqəli sistem zənglərini göstərin. Sonrakı proqram arqument kimi ötürülən ixtiyari fayla sətir əlavə edir:

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

Qiyabi strace çoxlu lazımsız məlumatları göstərir. Bayraq -P arqumentlə strace yalnız göstərilən fayla zəngləri çap etməyə səbəb olur:

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

Misal: Çoxillik proqramlar

Kommunal strace çox yivli ilə işləyərkən də kömək edə bilər proqramı. Aşağıdakı proqram iki axından standart çıxışa yazır:

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

Təbii ki, o, bağlayıcıya - pthread bayrağına xüsusi bir təbriklə tərtib edilməlidir:

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

Bayraq -f, müntəzəm proseslərdə olduğu kimi, hər bir sətirin əvvəlinə prosesin pidi əlavə edəcək.

Təbii ki, söhbət POSIX Threads standartının tətbiqi mənasında ip identifikatorundan deyil, Linux-da tapşırıq planlaşdırıcısının istifadə etdiyi nömrədən gedir. Sonuncunun nöqteyi-nəzərindən heç bir proses və ya ip yoxdur - maşının mövcud nüvələri arasında bölüşdürülməli olan vəzifələr var.

Bir neçə başlıqda işləyərkən sistem zəngləri həddən artıq çox olur:

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

Özünüzü yalnız idarəetmə və sistem zəngləri ilə məhdudlaşdırmaq məntiqlidir 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 +++

Yeri gəlmişkən, suallar. Yeni başlıq yaratmaq üçün hansı sistem çağırışından istifadə olunur? Bu mövzu çağırışı proseslər üçün çağırışdan nə ilə fərqlənir?

Master-klass: sistem çağırışı zamanı proses yığını

Bu yaxınlarda ortaya çıxan biri strace imkanlar - sistem çağırışı zamanı funksiya çağırışları yığınının göstərilməsi. Sadə misal:

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

Təbii ki, proqram çıxışı çox həcmli olur və bayraqdan əlavə -k (zəng yığını ekranı), sistem zənglərini adla süzgəcdən keçirməyin mənası var:

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

Master-klass: səhv inyeksiya

Və daha bir yeni və çox faydalı xüsusiyyət: səhv inyeksiya. Burada proqram, çıxış axınına iki sətir yazın:

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

Gəlin hər iki yazılı zəngi izləyək:

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

İndi ifadədən istifadə edirik injectxəta daxil etmək üçün EBADF bütün yazı zənglərində:

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

Maraqlıdır ki, hansı səhvlər qaytarılır bütün problemlər write, o cümlədən səhvin arxasında gizlənmiş zəng. Yalnız birinci zəng üçün xətanı qaytarmağın mənası var:

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

Və ya ikincisi:

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

Səhv növünü təyin etmək lazım deyil:

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

Digər bayraqlarla birlikdə, müəyyən bir fayla girişi "sındıra" bilərsiniz. Misal:

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

Səhv inyeksiyadan başqa, olar zənglər edərkən və ya siqnalları qəbul edərkən gecikmələr təqdim edin.

Sözündən sonra

Kommunal strace - sadə və etibarlı alət. Ancaq sistem çağırışlarına əlavə olaraq, proqramların və əməliyyat sisteminin işinin digər aspektləri də düzəldilə bilər. Məsələn, o, dinamik əlaqəli kitabxanalara edilən zəngləri izləyə bilər. ltrace, əməliyyat sisteminin işinə baxa bilərlər System Tap и izləmək, və proqram performansını dərindən araşdırmağa imkan verir perf. Buna baxmayaraq, belədir strace - mənim və başqalarının proqramlarında problem yarandıqda ilk müdafiə xəttidir və həftədə ən azı bir neçə dəfə istifadə edirəm.

Bir sözlə, Unix-i sevirsinizsə, oxuyun man 1 strace və proqramlarınıza nəzər salmaqdan çekinmeyin!

Mənbə: www.habr.com

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