Strace ใน Linux: ประวัติ การออกแบบ และการใช้งาน

Strace ใน Linux: ประวัติ การออกแบบ และการใช้งาน

ในระบบปฏิบัติการที่มีลักษณะคล้าย Unix การสื่อสารของโปรแกรมกับโลกภายนอกและระบบปฏิบัติการเกิดขึ้นผ่านชุดฟังก์ชันเล็กๆ - การเรียกของระบบ ซึ่งหมายความว่าเพื่อวัตถุประสงค์ในการแก้ไขจุดบกพร่อง จะมีประโยชน์ในการสอดแนมการเรียกของระบบที่ดำเนินการโดยกระบวนการ

ยูทิลิตี้ช่วยให้คุณตรวจสอบ "ชีวิตที่ใกล้ชิด" ของโปรแกรมบน Linux straceซึ่งเป็นหัวข้อของบทความนี้ ตัวอย่างการใช้อุปกรณ์สอดแนมมีประวัติโดยย่อแนบมาด้วย strace และคำอธิบายการออกแบบโปรแกรมดังกล่าว

Содержание

ต้นกำเนิดของสายพันธุ์

อินเทอร์เฟซหลักระหว่างโปรแกรมและเคอร์เนลระบบปฏิบัติการใน Unix คือการเรียกของระบบ ระบบเรียก, ซิสคอล) ปฏิสัมพันธ์ของโปรแกรมกับโลกภายนอกเกิดขึ้นผ่านโปรแกรมเหล่านั้นเท่านั้น

แต่ใน Unix เวอร์ชันสาธารณะรุ่นแรก (เวอร์ชัน 6 ยูนิกซ์, 1975) ไม่มีวิธีที่สะดวกในการติดตามพฤติกรรมของกระบวนการผู้ใช้ เพื่อแก้ไขปัญหานี้ Bell Labs จะอัปเดตเป็นเวอร์ชันถัดไป (เวอร์ชัน 7 ยูนิกซ์, 1979) เสนอการเรียกระบบใหม่ - ptrace.

ptrace ได้รับการพัฒนาสำหรับดีบักเกอร์เชิงโต้ตอบเป็นหลัก แต่ในช่วงปลายยุค 80 (ในยุคของการค้า ระบบ V รีลีส 4) บนพื้นฐานนี้ ดีบักเกอร์ที่มุ่งเน้นอย่างแคบ—ตัวติดตามการเรียกของระบบ—ปรากฏขึ้นและมีการใช้กันอย่างแพร่หลาย

เป็นครั้งแรก 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 และ Unix แบบดั้งเดิม

อุปกรณ์ Strace โดยสรุป: Piglet Trace

"คุณไม่คาดหวังที่จะเข้าใจสิ่งนี้" (Dennis Ritchie แสดงความคิดเห็นในซอร์สโค้ด Unix เวอร์ชัน 6)

ตั้งแต่วัยเด็ก ฉันไม่สามารถยืนกล่องดำได้ ฉันไม่ได้เล่นของเล่น แต่พยายามเข้าใจโครงสร้างของพวกเขา (ผู้ใหญ่ใช้คำว่า "พัง" แต่ไม่เชื่อลิ้นที่ชั่วร้าย) บางทีนี่อาจเป็นเหตุผลว่าทำไมวัฒนธรรมที่ไม่เป็นทางการของ Unix แรกและการเคลื่อนไหวโอเพ่นซอร์สสมัยใหม่จึงอยู่ใกล้ฉันมาก

สำหรับวัตถุประสงค์ของบทความนี้ ไม่มีเหตุผลที่จะแยกซอร์สโค้ดของ strace ซึ่งมีการเติบโตมานานหลายทศวรรษออกอย่างไม่สมเหตุสมผล แต่ไม่ควรจะมีความลับเหลือไว้สำหรับผู้อ่าน ดังนั้นเพื่อแสดงหลักการทำงานของโปรแกรม 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 parent จะดำเนินการให้เสร็จสิ้นก่อนดำเนินการเรียกระบบหรือทันทีหลังจากเสร็จสิ้น ระหว่างการโทรสองครั้ง คุณสามารถดำเนินการใดๆ ได้: แทนที่การโทรด้วยการโทรอื่น เปลี่ยนอาร์กิวเมนต์หรือค่าที่ส่งคืน

เราเพียงแค่ต้องเรียกคำสั่งสองครั้ง 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 +++

สังเกตเครื่องหมายอัศเจรีย์ที่ใช้ Escape ในรายการการเรียกที่แยกออก: สิ่งนี้จำเป็นสำหรับเชลล์คำสั่ง เปลือก).

ใน glibc เวอร์ชันของฉัน การเรียกของระบบจะยุติกระบวนการ exit_groupไม่ใช่แบบดั้งเดิม _exit. นี่คือความยากลำบากในการทำงานกับการเรียกของระบบ: อินเทอร์เฟซที่โปรแกรมเมอร์ทำงานไม่เกี่ยวข้องโดยตรงกับการเรียกของระบบ นอกจากนี้ยังเปลี่ยนแปลงเป็นประจำขึ้นอยู่กับการใช้งานและแพลตฟอร์ม

พื้นฐาน: เข้าร่วมกระบวนการได้ทันที

ในขั้นต้น การเรียกระบบ ptrace ที่ถูกสร้าง straceสามารถใช้ได้เฉพาะเมื่อรันโปรแกรมในโหมดพิเศษเท่านั้น ข้อจำกัดนี้อาจฟังดูสมเหตุสมผลในสมัยของ Unix เวอร์ชัน 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. มีสองสิ่งที่ชัดเจน: การหยุดการเรียกของระบบจะเพิกเฉยต่อสัญญาณที่ไม่มีตัวจัดการ และที่น่าสนใจกว่านั้นคือ strace monitor ไม่เพียงแต่การเรียกของระบบเท่านั้น แต่ยังรวมไปถึงสัญญาณขาเข้าด้วย

ตัวอย่าง: การติดตามกระบวนการย่อย

การทำงานกับกระบวนการผ่านการโทร fork - พื้นฐานของ Unix ทั้งหมด เรามาดูกันว่า 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 +++

ว่าแต่การเรียกของระบบใดที่ใช้ในการสร้างกระบวนการใหม่?

ตัวอย่าง: เส้นทางไฟล์แทนการจัดการ

การรู้จัก file descriptor นั้นมีประโยชน์อย่างแน่นอน แต่ชื่อของไฟล์เฉพาะที่โปรแกรมเข้าถึงก็มีประโยชน์เช่นกัน

ถัดไป โครงการ เขียนบรรทัดไปยังไฟล์ชั่วคราว:

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 ไปใช้ แต่เกี่ยวกับหมายเลขที่ใช้โดยตัวกำหนดเวลางานใน 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 (การแสดง call stack) ควรกรองการโทรของระบบตามชื่อ:

$ 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 - เครื่องมือที่ง่ายและเชื่อถือได้ แต่นอกเหนือจากการเรียกของระบบแล้ว ยังสามารถดีบั๊กด้านอื่น ๆ ของการทำงานของโปรแกรมและระบบปฏิบัติการได้อีกด้วย ตัวอย่างเช่น สามารถติดตามการเรียกไปยังไลบรารีที่เชื่อมโยงแบบไดนามิก ลิเทรซก็สามารถเข้าไปดูการทำงานของระบบปฏิบัติการได้ ระบบแตะ и รอยเท้าและช่วยให้คุณตรวจสอบประสิทธิภาพของโปรแกรมได้อย่างลึกซึ้ง perf. อย่างไรก็ตามมันเป็น strace - แนวป้องกันแรกในกรณีที่เกิดปัญหากับโปรแกรมของฉันและของผู้อื่น และฉันใช้อย่างน้อยสองครั้งต่อสัปดาห์

สรุปง่ายๆ ถ้าคุณรัก Unix ให้อ่าน man 1 strace และอย่าลังเลที่จะดูโปรแกรมของคุณ!

ที่มา: will.com

เพิ่มความคิดเห็น