Strace trong Linux: lịch sử, thiết kế và sử dụng

Strace trong Linux: lịch sử, thiết kế và sử dụng

Trong các hệ điều hành giống Unix, giao tiếp của chương trình với thế giới bên ngoài và hệ điều hành diễn ra thông qua một tập hợp nhỏ các chức năng - lệnh gọi hệ thống. Điều này có nghĩa là với mục đích gỡ lỗi, việc theo dõi các cuộc gọi hệ thống đang được thực hiện bởi các quy trình có thể hữu ích.

Tiện ích giúp bạn theo dõi “cuộc sống thân mật” của các chương trình trên Linux strace, đó là chủ đề của bài viết này. Ví dụ về việc sử dụng thiết bị gián điệp được kèm theo một lịch sử ngắn gọn strace và mô tả về thiết kế của các chương trình đó.

nội dung

Nguồn gốc của loài

Giao diện chính giữa các chương trình và nhân hệ điều hành trong Unix là các cuộc gọi hệ thống. cuộc gọi hệ thống, tòa nhà chọc trời), sự tương tác của các chương trình với thế giới bên ngoài chỉ diễn ra thông qua chúng.

Nhưng trong phiên bản công khai đầu tiên của Unix (Phiên bản 6 Unix, 1975) không có cách nào thuận tiện để theo dõi hành vi của quy trình người dùng. Để giải quyết vấn đề này, Bell Labs sẽ cập nhật lên phiên bản tiếp theo (Phiên bản 7 Unix, 1979) đề xuất một cuộc gọi hệ thống mới - ptrace.

ptrace được phát triển chủ yếu cho các trình gỡ lỗi tương tác, nhưng đến cuối những năm 80 (trong kỷ nguyên thương mại Phiên bản hệ thống V 4) trên cơ sở này, các trình gỡ lỗi tập trung ở phạm vi hẹp—các trình theo dõi cuộc gọi hệ thống—đã xuất hiện và được sử dụng rộng rãi.

Đầu tiên phiên bản tương tự của strace đã được Paul Cronenburg xuất bản trên danh sách gửi thư comp.sources.sun vào năm 1992 như một giải pháp thay thế cho một tiện ích đóng trace từ Mặt Trời. Cả bản sao và bản gốc đều dành cho SunOS, nhưng đến năm 1994 strace đã được chuyển sang System V, Solaris và Linux ngày càng phổ biến.

Ngày nay strace chỉ hỗ trợ Linux và dựa trên cùng một nền tảng ptrace, phát triển quá mức với nhiều phần mở rộng.

Người bảo trì hiện đại (và rất năng động) strace - Dmitry Levin. Nhờ anh ấy, tiện ích này có được các tính năng nâng cao như chèn lỗi vào các lệnh gọi hệ thống, hỗ trợ nhiều loại kiến ​​trúc và quan trọng nhất là linh vật. Các nguồn tin không chính thức cho rằng sự lựa chọn thuộc về đà điểu vì sự đồng âm giữa từ “ostrich” trong tiếng Nga và từ “strace” trong tiếng Anh.

Điều quan trọng nữa là lệnh gọi hệ thống ptrace và trình theo dõi không bao giờ được đưa vào POSIX, mặc dù có lịch sử và triển khai lâu dài trong Linux, FreeBSD, OpenBSD và Unix truyền thống.

Tóm tắt về thiết bị theo dõi: Piglet Trace

"Bạn không cần phải hiểu điều này" (Dennis Ritchie, nhận xét trong mã nguồn Unix phiên bản 6)

Từ nhỏ, tôi đã không chịu nổi những chiếc hộp đen: Tôi không chơi đồ chơi mà cố gắng tìm hiểu cấu trúc của chúng (người lớn dùng từ “vỡ”, nhưng không tin những lời ác độc). Có lẽ đây là lý do tại sao văn hóa không chính thức của Unix đầu tiên và phong trào nguồn mở hiện đại lại gần gũi với tôi đến vậy.

Vì mục đích của bài viết này, việc phân tách mã nguồn của strace, vốn đã phát triển qua nhiều thập kỷ, là không hợp lý. Nhưng không nên để lại bí mật nào cho độc giả. Vì vậy, để trình bày nguyên lý hoạt động của các chương trình strace như vậy, tôi sẽ cung cấp mã cho một máy theo dõi thu nhỏ - Dấu vết heo con (ptr). Nó không biết làm bất cứ điều gì đặc biệt, nhưng điều chính là các lệnh gọi hệ thống của chương trình - nó đưa ra:

$ 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 nhận ra khoảng hàng trăm lệnh gọi hệ thống Linux (xem. cái bàn) và chỉ hoạt động trên kiến ​​trúc x86-64. Điều này là đủ cho mục đích giáo dục.

Hãy nhìn vào công việc của bản sao của chúng tôi. Trong trường hợp của Linux, trình gỡ lỗi và trình theo dõi sử dụng lệnh gọi hệ thống ptrace, như đã đề cập ở trên. Nó hoạt động bằng cách chuyển vào đối số đầu tiên các mã định danh lệnh mà chúng ta chỉ cần PTRACE_TRACEME, PTRACE_SYSCALL и PTRACE_GETREGS.

Trình theo dõi bắt đầu theo kiểu Unix thông thường: fork(2) khởi chạy một tiến trình con, sau đó nó sử dụng exec(3) khởi động chương trình đang nghiên cứu. Sự tinh tế duy nhất ở đây là thử thách ptrace(PTRACE_TRACEME) trước exec: Tiến trình con mong muốn tiến trình cha giám sát nó:

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

Quá trình cha mẹ bây giờ sẽ gọi wait(2) trong tiến trình con, nghĩa là đảm bảo rằng việc chuyển sang chế độ theo dõi đã diễn ra:

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

Tại thời điểm này, quá trình chuẩn bị đã hoàn tất và bạn có thể tiến hành trực tiếp việc theo dõi các cuộc gọi hệ thống theo một vòng lặp vô tận.

Gọi ptrace(PTRACE_SYSCALL) đảm bảo rằng sau này wait parent sẽ hoàn thành trước khi lệnh gọi hệ thống được thực thi hoặc ngay sau khi nó hoàn thành. Giữa hai cuộc gọi, bạn có thể thực hiện bất kỳ hành động nào: thay thế cuộc gọi bằng một cuộc gọi khác, thay đổi đối số hoặc giá trị trả về.

Chúng ta chỉ cần gọi lệnh hai lần ptrace(PTRACE_GETREGS)để có được trạng thái đăng ký rax trước cuộc gọi (số cuộc gọi hệ thống) và ngay sau (giá trị trả về).

Trên thực tế, chu kỳ:

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

Đó là toàn bộ dấu vết. Bây giờ bạn biết bắt đầu chuyển tiếp tiếp theo từ đâu DTrace trên Linux.

Khái niệm cơ bản: chạy chương trình chạy strace

Là trường hợp sử dụng đầu tiên strace, có lẽ nên trích dẫn phương pháp đơn giản nhất - khởi chạy một ứng dụng đang chạy strace.

Để không đi sâu vào danh sách vô tận các cuộc gọi của một chương trình điển hình, chúng tôi viết chương trình tối thiểu xung quanh 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;
}

Hãy xây dựng chương trình và đảm bảo nó hoạt động:

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

Và cuối cùng, hãy chạy nó dưới sự kiểm soát 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)                           = ?

Rất “dài dòng” và không có tính giáo dục cao. Có hai vấn đề ở đây: đầu ra của chương trình bị trộn lẫn với đầu ra strace và vô số cuộc gọi hệ thống mà chúng tôi không quan tâm.

Bạn có thể tách luồng đầu ra tiêu chuẩn của chương trình và đầu ra lỗi strace bằng cách sử dụng khóa chuyển -o, chuyển hướng danh sách các lệnh gọi hệ thống đến một tệp đối số.

Vẫn còn phải giải quyết vấn đề về các cuộc gọi "thêm". Giả sử rằng chúng ta chỉ quan tâm đến các cuộc gọi write. Chìa khóa -e cho phép bạn chỉ định các biểu thức mà cuộc gọi hệ thống sẽ được lọc. Tất nhiên, tùy chọn điều kiện phổ biến nhất là trace=*, mà bạn chỉ có thể để lại những cuộc gọi mà chúng tôi quan tâm.

Khi sử dụng đồng thời -o и -e chúng ta sẽ lấy:

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

Vì vậy, bạn thấy đấy, nó dễ đọc hơn nhiều.

Bạn cũng có thể xóa các lệnh gọi hệ thống, ví dụ như các lệnh gọi liên quan đến cấp phát và giải phóng bộ nhớ:

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

Lưu ý dấu chấm than thoát trong danh sách các cuộc gọi bị loại trừ: điều này được yêu cầu bởi trình bao lệnh. shell).

Trong phiên bản glibc của tôi, lệnh gọi hệ thống sẽ kết thúc quá trình exit_group, không truyền thống _exit. Đây là khó khăn khi làm việc với các lệnh gọi hệ thống: giao diện mà người lập trình làm việc không liên quan trực tiếp đến các lệnh gọi hệ thống. Hơn nữa, nó thay đổi thường xuyên tùy thuộc vào việc triển khai và nền tảng.

Thông tin cơ bản: tham gia quá trình một cách nhanh chóng

Ban đầu, lệnh gọi hệ thống ptrace mà nó được xây dựng strace, chỉ có thể được sử dụng khi chạy chương trình ở chế độ đặc biệt. Hạn chế này nghe có vẻ hợp lý trong thời kỳ Unix phiên bản 6. Ngày nay, điều này không còn đủ nữa: đôi khi bạn cần điều tra các vấn đề của một chương trình đang hoạt động. Một ví dụ điển hình là tiến trình bị chặn trên tay cầm hoặc đang ngủ. Vì thế hiện đại strace có thể tham gia các quy trình một cách nhanh chóng.

Ví dụ đóng băng chương trình:

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

Hãy xây dựng chương trình và đảm bảo rằng nó đã được cố định:

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

Bây giờ chúng ta hãy thử tham gia nó:

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

Chương trình bị chặn bởi cuộc gọi pause. Hãy xem cô ấy phản ứng thế nào với các tín hiệu:

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

Chúng tôi đã khởi chạy chương trình đông lạnh và tham gia chương trình đó bằng cách sử dụng strace. Hai điều đã trở nên rõ ràng: lệnh gọi hệ thống tạm dừng bỏ qua các tín hiệu không có bộ xử lý và thú vị hơn là strace giám sát không chỉ các lệnh gọi hệ thống mà còn cả các tín hiệu đến.

Ví dụ: Theo dõi tiến trình con

Làm việc với các quy trình thông qua cuộc gọi fork - nền tảng của tất cả các Unix. Hãy xem cách strace hoạt động với cây quy trình bằng cách sử dụng ví dụ về “nhân giống” đơn giản chương trình:

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

Ở đây, quy trình ban đầu tạo ra một quy trình con, cả hai đều ghi vào đầu ra tiêu chuẩn:

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

Theo mặc định, chúng ta sẽ chỉ thấy các lệnh gọi hệ thống từ tiến trình gốc:

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

Cờ giúp bạn theo dõi toàn bộ cây quy trình -f, cái mà strace giám sát các cuộc gọi hệ thống trong các tiến trình con. Điều này thêm vào mỗi dòng đầu ra pid quá trình tạo ra đầu ra của hệ thống:

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

Trong ngữ cảnh này, việc lọc theo nhóm lệnh gọi hệ thống có thể hữu ích:

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

Nhân tiện, lệnh gọi hệ thống nào được sử dụng để tạo quy trình mới?

Ví dụ: đường dẫn tệp thay vì thẻ điều khiển

Biết các bộ mô tả tệp chắc chắn là hữu ích, nhưng tên của các tệp cụ thể mà chương trình truy cập cũng có thể hữu ích.

tiếp theo chương trình ghi dòng vào tập tin tạm thời:

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

Trong cuộc gọi bình thường strace sẽ hiển thị giá trị của số mô tả được truyền cho lệnh gọi hệ thống:

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

Với một lá cờ -y Tiện ích hiển thị đường dẫn đến tệp mà bộ mô tả tương ứng:

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

Ví dụ: Theo dõi truy cập tệp

Một tính năng hữu ích khác: chỉ hiển thị các cuộc gọi hệ thống được liên kết với một tệp cụ thể. Kế tiếp chương trình nối thêm một dòng vào một tệp tùy ý được truyền dưới dạng đối số:

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

Theo mặc định strace hiển thị nhiều thông tin không cần thiết. Lá cờ -P với một đối số khiến strace chỉ in các lệnh gọi đến tệp đã chỉ định:

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

Ví dụ: Chương trình đa luồng

Tính thiết thực strace cũng có thể trợ giúp khi làm việc với đa luồng chương trình. Chương trình sau ghi vào đầu ra tiêu chuẩn từ hai luồng:

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

Đương nhiên, nó phải được biên dịch kèm theo lời chào đặc biệt tới trình liên kết - cờ -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, như trong trường hợp các quy trình thông thường, sẽ thêm pid của quy trình vào đầu mỗi dòng.

Đương nhiên, chúng ta không nói về mã định danh luồng theo nghĩa triển khai tiêu chuẩn Chuỗi POSIX, mà là về số được sử dụng bởi bộ lập lịch tác vụ trong Linux. Theo quan điểm sau này, không có tiến trình hoặc luồng nào cả - có những nhiệm vụ cần được phân bổ giữa các lõi có sẵn của máy.

Khi làm việc trong nhiều luồng, lệnh gọi hệ thống trở nên quá nhiều:

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

Sẽ hợp lý nếu bạn chỉ giới hạn bản thân trong việc quản lý quy trình và các cuộc gọi hệ thống 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 +++

Nhân tiện, câu hỏi. Cuộc gọi hệ thống nào được sử dụng để tạo một chủ đề mới? Lệnh gọi luồng này khác với lệnh gọi tiến trình như thế nào?

Lớp chính: ngăn xếp quy trình tại thời điểm gọi hệ thống

Một trong những thứ xuất hiện gần đây strace khả năng - hiển thị chồng lệnh gọi hàm tại thời điểm gọi hệ thống. Đơn giản Ví dụ:

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

Đương nhiên, đầu ra của chương trình trở nên rất đồ sộ và ngoài cờ -k (hiển thị ngăn xếp cuộc gọi), việc lọc các cuộc gọi hệ thống theo tên là hợp lý:

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

Lớp thạc sĩ: tiêm lỗi

Và một tính năng mới và rất hữu ích nữa: chèn lỗi. Đây chương trình, viết hai dòng vào luồng đầu ra:

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

Hãy theo dõi cả hai cuộc gọi viết:

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

Bây giờ chúng ta sử dụng biểu thức injectđể chèn một lỗi EBADF trong tất cả các cuộc gọi viết:

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

Thật thú vị khi có lỗi được trả về tất cả thách thức write, bao gồm cả cuộc gọi ẩn đằng sau perror. Việc trả lại lỗi cho lần gọi đầu tiên chỉ có ý nghĩa:

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

Hoặc cái thứ hai:

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

Không cần thiết phải chỉ định loại lỗi:

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

Kết hợp với các cờ khác, bạn có thể "phá" quyền truy cập vào một tệp cụ thể. Ví dụ:

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

Bên cạnh việc tiêm lỗi, ai có thể gây ra sự chậm trễ khi thực hiện cuộc gọi hoặc nhận tín hiệu.

bạt

Tính thiết thực strace - một công cụ đơn giản và đáng tin cậy. Nhưng ngoài các cuộc gọi hệ thống, các khía cạnh khác trong hoạt động của chương trình và hệ điều hành có thể được gỡ lỗi. Ví dụ: nó có thể theo dõi các cuộc gọi đến các thư viện được liên kết động. dấu vết, họ có thể xem xét hoạt động của hệ điều hành Hệ thốngTap и ftracevà cho phép bạn điều tra sâu hiệu suất chương trình perf. Tuy nhiên, nó là strace - tuyến phòng thủ đầu tiên trong trường hợp có vấn đề với chương trình của tôi và của người khác, và tôi sử dụng nó ít nhất vài lần một tuần.

Tóm lại, nếu bạn yêu thích Unix, hãy đọc man 1 strace và thoải mái xem qua các chương trình của bạn!

Nguồn: www.habr.com

Thêm một lời nhận xét