Strace في Linux: التاريخ والتصميم والاستخدام

Strace في Linux: التاريخ والتصميم والاستخدام

في أنظمة التشغيل الشبيهة بيونكس، يتصل البرنامج بالعالم الخارجي ونظام التشغيل من خلال مجموعة صغيرة من الوظائف - استدعاءات النظام. لذلك، لأغراض تصحيح الأخطاء، من المفيد التجسس على العمليات الجارية باستخدام استدعاءات النظام.

تساعدك إحدى الأدوات المساعدة على مراقبة "الحياة الحميمة" للبرامج على نظام Linux straceالذي خصصت له هذه المقالة. تم إرفاق تاريخ موجز لأمثلة استخدام معدات "التجسس". strace ووصف لتصميم مثل هذه البرامج.

محتوى

أصل الأنواع

الواجهة الرئيسية بين البرامج و OC kernel في Unix هي مكالمات النظام. مكالمات النظام, مكالمات النظام)، يتم تفاعل البرامج مع العالم الخارجي حصريًا من خلالها.

ولكن في الإصدار العام الأول من Unix (الإصدار 6 يونيكس، 1975) لم تكن هناك طرق ملائمة لتتبع سلوك عمليات المستخدم. لحل هذه المشكلة، ستقوم Bell Labs بالتحديث إلى الإصدار التالي (الإصدار 7 يونيكس، 1979) اقترح استدعاء نظام جديد - ptrace.

تم تطوير ptrace في المقام الأول لمصححات الأخطاء التفاعلية، ولكن بحلول نهاية الثمانينيات (في عصر الإعلانات التجارية بالفعل الإصدار الرابع من النظام الخامس) على هذا الأساس، ظهرت مصححات الأخطاء ذات التركيز الضيق - أدوات تتبع مكالمات النظام - وأصبحت مستخدمة على نطاق واسع.

الأول تم نشر نفس الإصدار من strace بواسطة Paul Cronenburg في القائمة البريدية comp.sources.sun في عام 1992 كبديل للأداة المساعدة الخاصة trace من الشمس. كان كل من النسخة الأصلية والنسخة مخصصة لنظام التشغيل SunOS، ولكن بحلول عام 1994 strace تم نقله إلى System V وSolaris ونظام التشغيل Linux الذي يحظى بشعبية متزايدة.

اليوم، يدعم strace نظام التشغيل Linux فقط ويعتمد عليه ptrace، متضخمة مع العديد من الامتدادات.

مشرف حديث (ونشط للغاية). strace - دميتري ليفين. وبفضله، اكتسبت الأداة ميزات متقدمة مثل إدخال الأخطاء في مكالمات النظام، ودعم مجموعة واسعة من البنيات، والأهم من ذلك، تعويذة. تزعم مصادر غير رسمية أن الاختيار وقع على النعامة بسبب تطابق الكلمة الروسية "النعامة" مع الكلمة الإنجليزية "strace".

ومن الجدير بالذكر أن استدعاء نظام ptrace وأدوات التتبع لم يتم تضمينها مطلقًا في POSIX، على الرغم من تاريخها الطويل وتنفيذها في Linux وFreeBSD وOpenBSD وUnixes التقليدية.

جهاز التتبع باختصار: Piglet Trace

"ليس من المتوقع منك أن تفهم هذا" (دينيس ريتشي، تعليق في الإصدار 6 من كود مصدر Unix)

منذ الطفولة المبكرة، أكره الصناديق السوداء: لم ألعب بالألعاب، لكنني حاولت اكتشاف أجهزتها (استخدم الكبار كلمة "مكسور"، لكن لا تصدق الألسنة الشريرة). ولعل هذا هو السبب وراء قربي الشديد من الثقافة غير الرسمية لنظام يونكس الأول وحركة المصادر المفتوحة الحديثة.

في إطار هذه المقالة، ليس من المعقول تفكيك الكود المصدري لـstrace، الذي أصبح خصبًا على مدى عقود. لكن أسرار القراء لا ينبغي أن تبقى. لذلك، لإظهار مبدأ تشغيل برامج التتبع هذه، سأقدم رمز التتبع المصغر - أثر الخنزير الصغير (بتر). لا يعرف كيفية القيام بأي شيء خاص، ولكن الشيء الرئيسي - استدعاءات النظام للبرنامج - يعرض:

$ 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 على مئات من مكالمات نظام Linux (انظر. طاولة) ويعمل فقط على بنية x86-64. وهذا يكفي للأغراض التعليمية.

دعونا نلقي نظرة على كيفية عمل استنساخنا. في حالة Linux، تستخدم مصححات الأخطاء والمتتبعات، كما ذكرنا أعلاه، استدعاء نظام ptrace. إنه يعمل عن طريق تمرير معرفات الأوامر في الوسيطة الأولى، والتي نحتاج إليها فقط PTRACE_TRACEME, PTRACE_SYSCALL и PTRACE_GETREGS.

يبدأ التتبع بأسلوب Unix العادي: 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 ./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 ووفرة من مكالمات النظام التي لا تهمنا.

يمكنك فصل دفق الإخراج القياسي للبرنامج وإخراج خطأ التتبع باستخدام رمز التبديل -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 +++

لاحظ علامة التعجب التي تم تجاوزها في قائمة الاستدعاءات المستبعدة: هذا مطلوب بواسطة Shell. قذيفة).

في نسختي من glibc، ينهي استدعاء النظام تنفيذ العملية exit_groupبدلا من التقليدية _exit. هذه هي صعوبة العمل مع مكالمات النظام: الواجهة التي يعمل بها المبرمج لا ترتبط مباشرة باستدعاءات النظام. علاوة على ذلك، فإنه يتغير بانتظام اعتمادًا على التنفيذ والمنصة.

الأساسيات: الانضمام إلى العملية بسرعة

في البداية، يقوم نظام ptrace باستدعاء ملف strace، لا يمكن استخدامه إلا عند تشغيل البرنامج في الوضع الخاص. قد يبدو مثل هذا التقييد معقولًا في أيام الإصدار 6 Unix. في الوقت الحاضر، لم يعد هذا كافيا: في بعض الأحيان تحتاج إلى التحقيق في مشاكل برنامج العمل. والمثال النموذجي هو عملية مقفلة على مقبض أو في وضع السكون. ولذلك الحديثة 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. تم الكشف عن أمرين: يتجاهل استدعاء نظام الإيقاف المؤقت الإشارات دون معالجات، والأمر الأكثر إثارة للاهتمام هو أن strace لا يراقب مكالمات النظام فحسب، بل يراقب أيضًا الإشارات الواردة.

مثال: تتبع العمليات الفرعية

العمل مع العمليات من خلال المكالمة fork هو أساس جميع يونكس. دعونا نرى كيف يعمل strace مع شجرة العملية باستخدام مثال "تربية" بسيط برنامج:

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، كما هو الحال في العمليات العادية، سيسبق كل سطر مع معرف العملية.

بطبيعة الحال، نحن لا نتحدث عن معرف مؤشر ترابط بمعنى تنفيذ معيار POSIX Threads، ولكن عن الرقم الذي يستخدمه برنامج جدولة مهام Linux. من وجهة نظر الأخير، لا توجد عمليات وخيوط - هناك مهام يجب توزيعها بين النوى المتاحة للجهاز.

عند العمل في سلاسل رسائل متعددة، تصبح مكالمات النظام أكثر من اللازم:

$ 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 - أداة بسيطة وموثوقة. ولكن بالإضافة إلى مكالمات النظام، يتم أيضًا تصحيح أخطاء جوانب أخرى من تشغيل البرامج ونظام التشغيل. على سبيل المثال، يمكنه تتبع المكالمات إلى المكتبات المرتبطة ديناميكيًا. ltrace، يمكن النظر في تشغيل نظام التشغيل اضغط على النظام и فتريس، واستكشاف عميق لأداء البرامج يسمح الأداء الإقتصادي الأداء. ومع ذلك فإنه strace - خط الدفاع الأول في حالة حدوث مشاكل في برامجي وبرامج الآخرين، وأستخدمه مرتين على الأقل في الأسبوع.

باختصار، إذا كنت تحب يونكس، اقرأ man 1 strace ولا تتردد في التجسس على برامجك!

المصدر: www.habr.com

إضافة تعليق