Strace در لینوکس: تاریخچه، طراحی و استفاده

Strace در لینوکس: تاریخچه، طراحی و استفاده

در سیستم عامل های شبه یونیکس، ارتباط یک برنامه با دنیای خارج و سیستم عامل از طریق مجموعه کوچکی از توابع - فراخوانی های سیستمی اتفاق می افتد. این بدان معناست که برای اهداف اشکال‌زدایی، جاسوسی از فراخوان‌های سیستمی که توسط فرآیندها اجرا می‌شوند می‌تواند مفید باشد.

یک ابزار به شما کمک می کند تا "زندگی صمیمی" برنامه ها را در لینوکس نظارت کنید strace، که موضوع این مقاله می باشد. نمونه هایی از استفاده از تجهیزات جاسوسی با تاریخچه مختصری همراه است strace و شرح طراحی این گونه برنامه ها.

مقدار

منشاء گونه ها

رابط اصلی بین برنامه ها و هسته سیستم عامل در یونیکس تماس های سیستمی است. تماس های سیستمی, syscals) تعامل برنامه ها با دنیای خارج منحصراً از طریق آنها اتفاق می افتد.

اما در اولین نسخه عمومی یونیکس (نسخه 6 یونیکس، 1975) هیچ راه مناسبی برای ردیابی رفتار فرآیندهای کاربر وجود نداشت. برای حل این مشکل، آزمایشگاه بل به نسخه بعدی (نسخه 7 یونیکس، 1979) یک فراخوان سیستم جدید را پیشنهاد کرد - ptrace.

ptrace در درجه اول برای دیباگرهای تعاملی توسعه داده شد، اما در پایان دهه 80 (در عصر تجاری System V نسخه 4) بر این اساس، اشکال زدایی با تمرکز محدود - ردیاب های فراخوانی سیستم - ظاهر شدند و به طور گسترده مورد استفاده قرار گرفتند.

ابتدا همان نسخه strace توسط Paul Cronenburg در لیست پستی comp.sources.sun در سال 1992 به عنوان جایگزینی برای یک ابزار بسته منتشر شد. trace از سان هم کلون و هم نسخه اصلی برای SunOS در نظر گرفته شده بودند، اما تا سال 1994 strace به System V، Solaris و لینوکس به طور فزاینده محبوب منتقل شد.

امروزه strace فقط از لینوکس پشتیبانی می‌کند و به همان متکی است ptrace، رشد بیش از حد با پسوندهای فراوان.

نگهدارنده مدرن (و بسیار فعال). strace - دیمیتری لوین. به لطف او، این ابزار دارای ویژگی های پیشرفته ای مانند تزریق خطا در تماس های سیستم، پشتیبانی از طیف گسترده ای از معماری ها و مهمتر از همه، طلسم. منابع غیر رسمی ادعا می کنند که انتخاب شترمرغ به دلیل همخوانی بین کلمه روسی "شتر مرغ" و کلمه انگلیسی "strace" بود.

همچنین مهم است که فراخوانی سیستم ptrace و ردیاب‌ها با وجود سابقه طولانی و پیاده‌سازی در لینوکس، FreeBSD، OpenBSD و یونیکس سنتی، هرگز در POSIX گنجانده نشدند.

دستگاه Strace به طور خلاصه: Piglet Trace

"از شما انتظار نمی رود این را بفهمید" (دنیس ریچی، نظر در کد منبع یونیکس نسخه 6)

از اوایل کودکی، من نمی توانم جعبه های سیاه را تحمل کنم: من با اسباب بازی ها بازی نکردم، اما سعی کردم ساختار آنها را درک کنم (بزرگسالان از کلمه "شکستن" استفاده می کردند، اما زبان های شیطانی را باور نمی کنند). شاید به همین دلیل است که فرهنگ غیررسمی اولین یونیکس و جنبش متن باز مدرن بسیار به من نزدیک است.

برای اهداف این مقاله، جدا کردن کد منبع strace که در طول چندین دهه رشد کرده است، غیرمنطقی است. اما هیچ رازی نباید برای خوانندگان باقی بماند. بنابراین، برای نشان دادن اصل عملکرد چنین برنامه های strace، کد یک ردیاب مینیاتوری را ارائه می کنم - رد خوکچه (ptr). نمی داند چگونه کار خاصی انجام دهد، اما نکته اصلی تماس های سیستمی برنامه است - خروجی آن:

$ 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 حدود صدها تماس سیستمی لینوکس را تشخیص می دهد (نگاه کنید به. جدول) و فقط روی معماری x86-64 کار می کند. این برای اهداف آموزشی کافی است.

بیایید به کار کلون خود نگاه کنیم. در مورد لینوکس، دیباگرها و ردیاب ها همانطور که در بالا ذکر شد از فراخوانی سیستم ptrace استفاده می کنند. با ارسال شناسه‌های فرمان در آرگومان اول کار می‌کند، که فقط به آنها نیاز داریم PTRACE_TRACEME, PTRACE_SYSCALL и PTRACE_GETREGS.

ردیاب به سبک معمول یونیکس شروع می شود: fork(2) فرآیند فرزند را راه اندازی می کند که به نوبه خود از آن استفاده می کند exec(3) برنامه در دست مطالعه را راه اندازی می کند. تنها نکته ظریف اینجا چالش است ptrace(PTRACE_TRACEME) قبل از exec: فرآیند فرزند انتظار دارد که فرآیند والدین بر آن نظارت داشته باشد:

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

فرآیند والد اکنون باید فراخوانی شود wait(2) در فرآیند فرزند، یعنی مطمئن شوید که تغییر به حالت ردیابی رخ داده است:

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

در این مرحله، آماده سازی کامل شده است و می توانید مستقیماً به ردیابی تماس های سیستمی در یک حلقه بی پایان ادامه دهید.

تماس بگیرید ptrace(PTRACE_SYSCALL) تضمین می کند که بعدی wait والد یا قبل از اجرای فراخوانی سیستم یا بلافاصله پس از تکمیل آن تکمیل می شود. بین دو تماس می‌توانید هر عملی را انجام دهید: تماس را با یک جایگزین جایگزین کنید، آرگومان‌ها یا مقدار بازگشتی را تغییر دهید.

فقط باید دوبار دستور را فراخوانی کنیم ptrace(PTRACE_GETREGS)برای دریافت وضعیت ثبت نام rax قبل از تماس (شماره تماس سیستمی) و بلافاصله پس از آن (مقدار برگشتی).

در واقع چرخه:

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

این کل ردیاب است. اکنون می دانید که انتقال بعدی را از کجا شروع کنید DTrace در لینوکس

مبانی: اجرای یک برنامه در حال اجرا strace

به عنوان اولین مورد استفاده strace، شاید ارزش ذکر ساده ترین روش را داشته باشد - راه اندازی یک برنامه در حال اجرا strace.

برای اینکه وارد لیست بی پایان تماس های یک برنامه معمولی نشویم، می نویسیم حداقل برنامه در اطراف 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;
}

بیایید برنامه را بسازیم و مطمئن شویم که کار می کند:

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

و در نهایت، اجازه دهید آن را تحت کنترل 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)                           = ?

بسیار "کلامی" و نه چندان آموزشی. در اینجا دو مشکل وجود دارد: خروجی برنامه با خروجی مخلوط شده است strace و تعداد زیادی تماس سیستمی که برای ما جالب نیست.

شما می توانید جریان خروجی استاندارد برنامه و خروجی خطای strace را با استفاده از سوئیچ -o جدا کنید، که لیست فراخوانی های سیستم را به یک فایل آرگومان هدایت می کند.

باقی مانده است که با مشکل تماس های "اضافی" مقابله کنیم. بیایید فرض کنیم که ما فقط به تماس علاقه مند هستیم write... کلید -e به شما امکان می دهد عباراتی را مشخص کنید که تماس های سیستمی با آن فیلتر می شوند. محبوب ترین گزینه شرایط طبیعی است، trace=*، که با آن فقط می توانید تماس های مورد علاقه ما را بگذارید.

در صورت استفاده همزمان -o и -e دریافت خواهیم کرد:

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

بنابراین، می بینید، خواندن آن بسیار ساده تر است.

همچنین می توانید تماس های سیستمی را حذف کنید، به عنوان مثال تماس های مربوط به تخصیص و آزادسازی حافظه:

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

به علامت تعجب فراری در لیست تماس های حذف شده توجه کنید: این مورد توسط پوسته فرمان لازم است. صدف).

در نسخه من از glibc، یک تماس سیستمی فرآیند را خاتمه می دهد exit_group، سنتی نیست _exit. این مشکل کار با تماس های سیستمی است: رابطی که برنامه نویس با آن کار می کند مستقیماً با تماس های سیستمی مرتبط نیست. علاوه بر این، بسته به اجرا و پلتفرم به طور مرتب تغییر می کند.

مبانی: پیوستن به فرآیند در پرواز

در ابتدا سیستم ptrace که روی آن ساخته شده بود فراخوانی می کرد strace، فقط هنگام اجرای برنامه در حالت خاص قابل استفاده است. این محدودیت ممکن است در روزهای نسخه 6 یونیکس منطقی به نظر می رسید. امروزه این دیگر کافی نیست: گاهی اوقات باید مشکلات یک برنامه کاری را بررسی کنید. یک مثال معمولی فرآیندی است که روی یک دسته یا خواب مسدود شده است. بنابراین مدرن strace می تواند به فرآیندها در پرواز بپیوندد.

مثال انجماد برنامه:

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

بیایید برنامه را بسازیم و مطمئن شویم که ثابت است:

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

حالا بیایید سعی کنیم به آن بپیوندیم:

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

برنامه با تماس مسدود شد pause. بیایید ببینیم که او چگونه به سیگنال ها واکنش نشان می دهد:

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

ما برنامه یخ زده را راه اندازی کردیم و با استفاده از آن به آن ملحق شدیم strace. دو چیز مشخص شد: تماس سیستم مکث سیگنال‌ها را بدون کنترلر نادیده می‌گیرد و جالب‌تر اینکه استریس نه تنها تماس‌های سیستم، بلکه سیگنال‌های دریافتی را نیز زیر نظر دارد.

مثال: ردیابی فرآیندهای کودک

کار با فرآیندها از طریق تماس fork - اساس همه یونیکس ها. بیایید ببینیم که چگونه استریس با یک درخت فرآیند با استفاده از مثال یک "پرورش" ساده کار می کند. برنامه:

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

در اینجا فرآیند اصلی یک فرآیند فرزند ایجاد می کند که هر دو در خروجی استاندارد نوشته می شوند:

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

به طور پیش فرض، ما فقط تماس های سیستمی را از فرآیند والد مشاهده خواهیم کرد:

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

پرچم به شما کمک می کند کل درخت فرآیند را ردیابی کنید -f، که strace تماس های سیستمی را در فرآیندهای فرزند نظارت می کند. این به هر خط خروجی اضافه می کند pid فرآیندی که خروجی سیستم را می سازد:

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

در این زمینه، فیلتر کردن بر اساس گروهی از تماس های سیستمی می تواند مفید باشد:

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

به هر حال، چه فراخوانی سیستمی برای ایجاد یک فرآیند جدید استفاده می شود؟

مثال: مسیرهای فایل به جای دسته

دانستن توصیفگرهای فایل مطمئناً مفید است، اما نام فایل‌های خاصی که برنامه به آنها دسترسی دارد نیز می‌تواند مفید باشد.

بعد برنامه خط را در فایل موقت می نویسد:

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

در طول یک تماس معمولی strace مقدار عدد توصیفگر ارسال شده به فراخوانی سیستم را نشان می دهد:

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

با پرچم -y ابزار مسیر فایلی را که توصیفگر با آن مطابقت دارد نشان می دهد:

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

مثال: ردیابی دسترسی به فایل

یکی دیگر از ویژگی های مفید: نمایش فقط تماس های سیستمی مرتبط با یک فایل خاص. بعد برنامه یک خط به فایل دلخواه ارسال شده به عنوان آرگومان اضافه می کند:

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

به طور پیش فرض strace اطلاعات غیر ضروری زیادی را نمایش می دهد. پرچم -P با یک آرگومان باعث می شود که strace فقط فراخوانی های فایل مشخص شده را چاپ کند:

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

مثال: برنامه های چند رشته ای

سودمندی strace همچنین می تواند هنگام کار با چند رشته ای کمک کند برنامه. برنامه زیر در خروجی استاندارد از دو جریان می نویسد:

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

طبیعتاً باید با یک تبریک ویژه به پیوند دهنده - پرچم -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
$

فلگ -fمانند فرآیندهای معمولی، pid فرآیند را به ابتدای هر خط اضافه می کند.

طبیعتاً، ما در مورد یک شناسه رشته به معنای اجرای استاندارد POSIX Threads صحبت نمی کنیم، بلکه در مورد تعداد مورد استفاده توسط زمانبندی کار در لینوکس صحبت می کنیم. از نقطه نظر دومی، هیچ فرآیند یا رشته ای وجود ندارد - وظایفی وجود دارد که باید بین هسته های موجود دستگاه توزیع شوند.

هنگام کار در چندین رشته، فراخوانی های سیستم بسیار زیاد می شود:

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

منطقی است که خود را فقط به مدیریت پردازش و تماس های سیستمی محدود کنید 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 +++

اتفاقاً سؤالات. از چه فراخوانی سیستمی برای ایجاد یک موضوع جدید استفاده می شود؟ این فراخوانی موضوعات چه تفاوتی با فراخوانی فرآیندها دارد؟

کلاس اصلی: پشته پردازش در زمان تماس سیستمی

یکی از اخیرا ظاهر شد strace قابلیت ها - نمایش پشته فراخوانی های تابع در زمان تماس سیستمی. ساده مثال:

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

طبیعتاً خروجی برنامه بسیار حجیم می شود و علاوه بر پرچم -k (نمایش پشته تماس)، منطقی است که تماس های سیستم را با نام فیلتر کنید:

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

کلاس کارشناسی ارشد: تزریق خطا

و یک ویژگی جدید و بسیار مفید دیگر: تزریق خطا. اینجا برنامه، نوشتن دو خط در جریان خروجی:

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

بیایید هر دو تماس نوشتن را ردیابی کنیم:

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

حالا از عبارت استفاده می کنیم injectبرای درج خطا EBADF در همه تماس های نوشتن:

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

جالب است چه خطاهایی برگردانده می شود همه چالش ها write، از جمله تماس پنهان شده در پشت خطا. بازگشت خطا فقط برای اولین تماس منطقی است:

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

یا دومی:

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

لازم نیست نوع خطا را مشخص کنید:

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

در ترکیب با پرچم‌های دیگر، می‌توانید دسترسی به یک فایل خاص را «شکست» کنید. مثال:

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

علاوه بر تزریق خطا، می توان ایجاد تأخیر در هنگام برقراری تماس یا دریافت سیگنال.

پس از کلمه

سودمندی strace - یک ابزار ساده و قابل اعتماد. اما علاوه بر فراخوانی سیستم، سایر جنبه های عملکرد برنامه ها و سیستم عامل را می توان اشکال زدایی کرد. به عنوان مثال، می‌تواند تماس‌های کتابخانه‌های متصل به صورت پویا را ردیابی کند. ردیابی، آنها می توانند عملکرد سیستم عامل را بررسی کنند SystemTap и ftraceو به شما امکان می دهد عملکرد برنامه را عمیقا بررسی کنید پرفیوم. با این وجود، این است strace - خط اول دفاع در صورت بروز مشکل در برنامه های خودم و دیگران و حداقل هفته ای یکی دو بار از آن استفاده می کنم.

به طور خلاصه، اگر عاشق یونیکس هستید، بخوانید man 1 strace و با خیال راحت به برنامه های خود نگاه کنید!

منبع: www.habr.com

اضافه کردن نظر