Linux 中的 Strace:歷史、設計與使用

Linux 中的 Strace:歷史、設計與使用

在類別 Unix 作業系統中,程式與外界和作業系統的通訊是透過一小組函數(系統呼叫)進行的。 這意味著出於調試目的,監視進程正在執行的系統呼叫可能很有用。

一個實用程式可幫助您監控 Linux 上程式的“親密生活” strace,這就是本文的主題。 間諜設備的使用範例附有簡要歷史 strace 以及此類程序設計的描述。

Содержание

物種起源

Unix中程式與OS核心之間的主要介面是系統呼叫。 系統調用, 系統調用),程式與外界的互動完全透過它們發生。

但在 Unix 的第一個公共版本(版本 6 Unix,1975)沒有方便的方法來追蹤使用者進程的行為。 為了解決這個問題,貝爾實驗室將更新到下一個版本(版本 7 Unix,1979)提出了一個新的系統呼叫 - ptrace.

ptrace 主要是為了互動式調試器而開發的,但到了 80 年代末(商業化時代) 系統 V 版本 4)在此基礎上,出現了狹隘的調試器——系統調用追蹤器——並且廣泛使用。

第一 Paul Cronenburg 於 1992 年在 comp.sources.sun 郵件列表上發布了 strace 的相同版本,作為封閉實用程式的替代品 trace 來自太陽。 克隆版和原版都是針對 SunOS 的,但到了 1994 年 strace 移植到 System V、Solaris 和日益普及的 Linux。

目前strace僅支援Linux並且依賴相同的 ptrace,長滿了許多擴展。

現代(並且非常活躍)的維護者 strace - 德米特里·萊文。 感謝他,該實用程式獲得了高級功能,例如係統調用中的錯誤注入、對各種體系結構的支持,最重要的是, 吉祥物。 非官方消息稱,這個選擇落在了鴕鳥身上,因為俄語單字「ostrich」和英語單字「strace」之間的諧音。

同樣重要的是,儘管 ptrace 系統呼叫和追蹤器在 Linux、FreeBSD、OpenBSD 和傳統 Unix 中有著悠久的歷史和實現,但從未包含在 POSIX 中。

Strace 設備簡而言之:Piglet Trace

「你不應該理解這一點」(Dennis Ritchie,版本 6 Unix 原始碼中的評論)

從孩提時代起,我就無法忍受黑盒子:我不玩玩具,而是試圖理解它們的結構(大人用「壞」這個詞,但不要相信邪惡的舌頭)。 也許這就是為什麼第一個 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的 在 Linux 上。

基礎知識:運行運行 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 以及大量我們不感興趣的系統呼叫。

您可以使用 -o 開關分隔程式的標準輸出流和 strace 錯誤輸出,該開關將系統呼叫清單重定向到參數檔案。

仍然需要解決“額外”調用的問題。 假設我們只對呼叫感興趣 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,只能在特殊模式下執行程式時使用。 在 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 不僅監視系統調用,還監視傳入訊號。

範例:追蹤子進程

透過調用處理流程 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 +++

順便問一下,建立新進程使用什麼系統呼叫?

範例:檔案路徑而不是句柄

了解文件描述符當然很有用,但程式存取的特定文件的名稱也可以派上用場。

下一個 程序 將該行寫入臨時檔案:

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 執行緒標準實作意義上的執行緒標識符,而是 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,包括隱藏在 perror 後面的呼叫。 只有在第一次呼叫時傳回錯誤才有意義:

$ 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 並隨意查看您的程式!

來源: www.habr.com

添加評論