Linux'ta Strace: tarih, tasarım ve kullanım

Linux'ta Strace: tarih, tasarım ve kullanım

Unix benzeri işletim sistemlerinde, bir programın dış dünyayla ve işletim sistemiyle iletişimi, küçük bir dizi işlev (sistem çağrıları) aracılığıyla gerçekleşir. Bu, hata ayıklama amacıyla işlemler tarafından yürütülen sistem çağrılarını gözetlemenin yararlı olabileceği anlamına gelir.

Bir yardımcı program, Linux'taki programların "özel yaşamını" izlemenize yardımcı olur straceBu makalenin konusu da budur. Casus ekipmanı kullanımına ilişkin örneklere kısa bir tarihçe eşlik ediyor strace ve bu tür programların tasarımının bir açıklaması.

Içerik

Türlerin Kökeni

Unix'teki programlar ile işletim sistemi çekirdeği arasındaki ana arayüz sistem çağrılarıdır. sistem çağrıları, sistem çağrıları), programların dış dünyayla etkileşimi yalnızca onlar aracılığıyla gerçekleşir.

Ancak Unix'in ilk genel sürümünde (Sürüm 6 Unix, 1975) kullanıcı işlemlerinin davranışını izlemenin uygun bir yolu yoktu. Bu sorunu çözmek için Bell Labs bir sonraki sürüme güncelleyecektir (Sürüm 7 Unix, 1979) yeni bir sistem çağrısı önerdi - ptrace.

ptrace öncelikle etkileşimli hata ayıklayıcılar için geliştirildi, ancak 80'lerin sonunda (ticari çağda) Sistem V Sürüm 4) bu temelde, dar odaklı hata ayıklayıcılar (sistem çağrısı izleyicileri) ortaya çıktı ve yaygın olarak kullanıldı.

ilk strace'nin aynı sürümü, kapalı bir yardımcı programa alternatif olarak 1992 yılında Paul Cronenburg tarafından comp.sources.sun e-posta listesinde yayınlandı. trace Sun'dan. Hem klon hem de orijinal SunOS için tasarlandı, ancak 1994'te strace System V, Solaris ve giderek daha popüler hale gelen Linux'a taşındı.

Bugün strace yalnızca Linux'u destekliyor ve aynı temele dayanıyor ptrace, birçok uzantıyla büyümüş.

Modern (ve çok aktif) bakımcı strace - Dimitri Levin. Onun sayesinde yardımcı program, sistem çağrılarına hata ekleme, çok çeşitli mimarilere destek ve en önemlisi, maskot. Resmi olmayan kaynaklar, Rusça "devekuşu" kelimesi ile İngilizce "strace" kelimesi arasındaki uyum nedeniyle seçimin devekuşuna düştüğünü iddia ediyor.

Linux, FreeBSD, OpenBSD ve geleneksel Unix'teki uzun bir geçmişe ve uygulamaya rağmen ptrace sistem çağrısının ve izleyicilerinin hiçbir zaman POSIX'e dahil edilmemiş olması da önemlidir.

Kısaca Strace cihazı: Piglet Trace

"Bunu anlamanız beklenmiyor" (Dennis Ritchie, Sürüm 6 Unix kaynak kodundaki yorum)

Erken çocukluğumdan beri kara kutulara dayanamıyorum: Oyuncaklarla oynamadım ama yapılarını anlamaya çalıştım (yetişkinler "kırık" kelimesini kullandılar ama kötü dillere inanmıyorum). Belki de ilk Unix'in resmi olmayan kültürünün ve modern açık kaynak hareketinin bana bu kadar yakın olmasının nedeni budur.

Bu makalenin amaçları doğrultusunda, onlarca yıldır büyüyen strace'nin kaynak kodunu parçalara ayırmak mantıksızdır. Ancak okuyucular için hiçbir sır kalmamalı. Bu nedenle, bu tür strace programlarının çalışma prensibini göstermek için minyatür bir izleyicinin kodunu vereceğim - Domuz Yavrusu İzi (ptr). Özel bir şeyin nasıl yapılacağını bilmiyor, ancak asıl önemli olan programın sistem çağrılarıdır - çıktı verir:

$ 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üzlerce Linux sistem çağrısını tanır (bkz. tablo) ve yalnızca x86-64 mimarisinde çalışır. Bu eğitim amaçlı yeterlidir.

Klonumuzun çalışmasına bakalım. Linux durumunda, hata ayıklayıcılar ve izleyiciler yukarıda belirtildiği gibi ptrace sistem çağrısını kullanır. Yalnızca ihtiyacımız olan komut tanımlayıcılarını ilk argümana ileterek çalışır. PTRACE_TRACEME, PTRACE_SYSCALL и PTRACE_GETREGS.

İzleyici her zamanki Unix stilinde başlar: fork(2) bir alt süreç başlatır ve bu da sırayla kullanır exec(3) incelenmekte olan programı başlatır. Buradaki tek incelik, meydan okumadır ptrace(PTRACE_TRACEME) önce exec: Alt süreç, ana sürecin onu izlemesini bekler:

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 süreç şimdi aramalıdır wait(2) alt süreçte, yani izleme moduna geçişin gerçekleştiğinden emin 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 noktada hazırlıklar tamamlanmıştır ve sistem çağrılarını sonsuz bir döngü içerisinde doğrudan takip etmeye başlayabilirsiniz.

Вызов ptrace(PTRACE_SYSCALL) daha sonra garanti eder wait ebeveyn, sistem çağrısı yürütülmeden önce veya tamamlandıktan hemen sonra tamamlayacaktır. İki çağrı arasında istediğiniz eylemi gerçekleştirebilirsiniz: çağrıyı alternatif bir çağrıyla değiştirin, argümanları veya dönüş değerini değiştirin.

Sadece komutu iki kez çağırmamız gerekiyor ptrace(PTRACE_GETREGS)kayıt durumunu almak için rax çağrıdan önce (sistem çağrı numarası) ve hemen sonra (dönüş değeri).

Aslında döngü:

/* 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 izleyici bu. Artık bir sonraki taşıma işlemine nereden başlayacağınızı biliyorsunuz DTrace Linux'ta.

Temel bilgiler: strace çalıştıran bir programın çalıştırılması

İlk kullanım durumu olarak stracebelki de en basit yöntemden bahsetmeye değer - çalışan bir uygulamayı başlatmak strace.

Tipik bir programın sonsuz çağrı listesine girmemek için yazıyoruz asgari program вокруг 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;
}

Programı derleyelim ve çalıştığından emin olalım:

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

Ve son olarak strace kontrolü altında çalıştıralım:

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

Çok "sözlü" ve pek eğitici değil. Burada iki sorun var: program çıktısı çıktıyla karıştırılıyor strace ve bizi ilgilendirmeyen çok sayıda sistem çağrısı.

Sistem çağrılarının listesini bir bağımsız değişken dosyasına yönlendiren -o anahtarını kullanarak programın standart çıktı akışını ve strace hata çıktısını ayırabilirsiniz.

Geriye "ekstra" çağrı sorunuyla uğraşmak kalıyor. Yalnızca aramalarla ilgilendiğimizi varsayalım write. Anahtar -e sistem çağrılarının filtreleneceği ifadeleri belirtmenize olanak tanır. En popüler durum seçeneği doğal olarak trace=*, yalnızca bizi ilgilendiren aramaları bırakabilirsiniz.

Aynı anda kullanıldığında -o и -e alacağız:

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

Yani, görüyorsunuz, okumak çok daha kolay.

Ayrıca, örneğin bellek ayırma ve boşaltmayla ilgili sistem çağrılarını da kaldırabilirsiniz:

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

Hariç tutulan çağrılar listesindeki çıkışlı ünlem işaretine dikkat edin: bu, komut kabuğu için gereklidir. kabuk).

Benim glibc versiyonumda bir sistem çağrısı süreci sonlandırıyor exit_group, geleneksel değil _exit. Sistem çağrılarıyla çalışmanın zorluğu budur: programcının çalıştığı arayüz doğrudan sistem çağrılarıyla ilgili değildir. Üstelik uygulamaya ve platforma bağlı olarak düzenli olarak değişmektedir.

Temel bilgiler: Sürece anında katılma

Başlangıçta üzerine inşa edildiği ptrace sistem çağrısı strace, yalnızca programı özel bir modda çalıştırırken kullanılabilir. Bu sınırlama, Sürüm 6 Unix'in olduğu günlerde kulağa makul gelebilirdi. Günümüzde bu artık yeterli olmuyor: Bazen çalışan bir programın sorunlarını araştırmanız gerekiyor. Tipik bir örnek, bir tanıtıcıda engellenen veya uykuda olan bir işlemdir. Bu nedenle modern strace anında işlemlere katılabilir.

Donma örneği program:

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

Programı derleyelim ve donmuş olduğundan emin olalım:

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

Şimdi ona katılmayı deneyelim:

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

Program çağrı nedeniyle engellendi pause. Bakalım sinyallere nasıl tepki verecek:

$ 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ş programı başlattık ve kullanarak katıldık strace. İki şey netleşti: duraklatma sistem çağrısı, işleyiciler olmadan sinyalleri yok sayar ve daha ilginci, strace yalnızca sistem çağrılarını değil, aynı zamanda gelen sinyalleri de izler.

Örnek: Alt Süreçlerin Takibi

Bir çağrı aracılığıyla süreçlerle çalışma fork - tüm Unix'lerin temeli. Basit bir "üreme" örneğini kullanarak strace'nin bir süreç ağacıyla nasıl çalıştığını görelim. program:

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 süreç, her ikisi de standart çıktıya yazan bir alt süreç yaratır:

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

Varsayılan olarak yalnızca ana süreçten gelen sistem çağrılarını göreceğiz:

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

Bayrak, süreç ağacının tamamını izlemenize yardımcı olur -fhangisiyle strace Alt süreçlerdeki sistem çağrılarını izler. Bu, her çıktı satırına eklenir pid Bir sistem çıktısı oluşturan süreç:

$ 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 bağlamda sistem çağrıları grubuna göre filtreleme yararlı olabilir:

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

Bu arada, yeni bir süreç oluşturmak için hangi sistem çağrısı kullanılıyor?

Örnek: tanıtıcılar yerine dosya yolları

Dosya tanımlayıcılarını bilmek kesinlikle faydalıdır, ancak bir programın eriştiği belirli dosyaların adları da kullanışlı olabilir.

sonraki program satırı geçici dosyaya yazar:

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 bir görüşme sırasında strace sistem çağrısına iletilen tanımlayıcı numarasının değerini gösterecektir:

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

Bayraklı -y Yardımcı program, tanımlayıcının karşılık geldiği dosyanın yolunu gösterir:

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

Örnek: Dosya Erişim Takibi

Başka bir kullanışlı özellik: yalnızca belirli bir dosyayla ilişkili sistem çağrılarını görüntüleme. Sonraki program argüman olarak iletilen rastgele bir dosyaya bir satır ekler:

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

Varsayılan olarak strace birçok gereksiz bilgi görüntüler. Bayrak -P bir argüman kullanıldığında strace'nin yalnızca belirtilen dosyaya yapılan çağrıları yazdırmasına neden 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 +++

Örnek: Çok İş parçacıklı Programlar

Yarar strace çok iş parçacıklı sistemlerle çalışırken de yardımcı olabilir program. Aşağıdaki program iki akıştan standart çıktıya yazar:

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

Doğal olarak, bağlayıcıya özel bir selamlamayla -pthread bayrağıyla derlenmelidir:

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

Bayrak -f, normal süreçlerde olduğu gibi, sürecin pid'ini her satırın başına ekleyecektir.

Doğal olarak, POSIX Threads standardının uygulanması anlamında bir iş parçacığı tanımlayıcısından değil, Linux'ta görev zamanlayıcı tarafından kullanılan sayıdan bahsediyoruz. İkincisinin bakış açısından, hiçbir süreç veya iş parçacığı yoktur; makinenin mevcut çekirdekleri arasında dağıtılması gereken görevler vardır.

Birden çok iş parçacığında çalışırken sistem çağrıları çok fazla olur:

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

Kendinizi yalnızca süreç yönetimi ve sistem çağrılarıyla sınırlamak mantıklıdır 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 +++

Bu arada sorular. Yeni bir iş parçacığı oluşturmak için hangi sistem çağrısı kullanılır? Bu iş parçacığı çağrısının süreç çağrısından farkı nedir?

Ana sınıf: sistem çağrısı sırasındaki işlem yığını

Yakın zamanda ortaya çıkanlardan biri strace yetenekler - sistem çağrısı sırasında işlev çağrıları yığınını görüntüleme. Basit örnek:

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

Doğal olarak program çıktısı çok hacimli hale gelir ve bayrağa ek olarak -k (çağrı yığını ekranı), sistem çağrılarını ada göre filtrelemek mantıklıdı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 +++

Ana sınıf: hata enjeksiyonu

Ve bir yeni ve çok kullanışlı özellik daha: hata ekleme. Burada program, çıkış akışına iki satır yazıyor:

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

Her iki yazma çağrısını da izleyelim:

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

Şimdi ifadeyi kullanıyoruz injecthata eklemek için EBADF tüm yazma çağrılarında:

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

Hangi hataların döndürüldüğü ilginç tüm zorluklar write, terörün arkasına gizlenmiş çağrı da dahil. Yalnızca ilk çağrı için hata döndürmek mantıklıdır:

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

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

Hata tipini belirtmenize gerek yoktur:

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

Diğer işaretlerle birlikte belirli bir dosyaya erişimi "kesebilirsiniz". Örnek:

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

Hata enjeksiyonunun yanı sıra, kimse yapamaz Arama yaparken veya sinyal alırken gecikmelere neden olun.

Послесловие

Yarar strace - basit ve güvenilir bir araç. Ancak sistem çağrılarına ek olarak, programların ve işletim sisteminin diğer işleyişinde de hata ayıklama yapılabilir. Örneğin, dinamik olarak bağlantılı kütüphanelere yapılan çağrıları izleyebilir. iz, işletim sisteminin işleyişini inceleyebilirler SystemTap'ı и izve program performansını derinlemesine araştırmanıza olanak tanır perf. Yine de öyle strace - kendimin ve başkalarının programlarında sorun olması durumunda ilk savunma hattı ve bunu haftada en az birkaç kez kullanıyorum.

Kısacası Unix'i seviyorsanız okuyun man 1 strace ve programlarınıza göz atmaktan çekinmeyin!

Kaynak: habr.com

Yorum ekle