Strace di Linux: sejarah, desain dan penggunaan

Strace di Linux: sejarah, desain dan penggunaan

Dalam sistem operasi mirip Unix, komunikasi program dengan dunia luar dan sistem operasi terjadi melalui serangkaian fungsi kecil - panggilan sistem. Ini berarti bahwa untuk tujuan debugging akan berguna untuk memata-matai panggilan sistem yang dijalankan oleh proses.

Sebuah utilitas membantu Anda memantau “kehidupan intim” program di Linux strace, yang merupakan subjek artikel ini. Contoh penggunaan peralatan mata-mata disertai dengan sejarah singkatnya strace dan deskripsi desain program tersebut.

kadar

Asal spesies

Antarmuka utama antara program dan kernel OS di Unix adalah panggilan sistem. panggilan sistem, panggilan sistem), interaksi program dengan dunia luar terjadi secara eksklusif melalui program tersebut.

Namun di Unix versi publik pertama (Versi 6 Unix, 1975) tidak ada cara mudah untuk melacak perilaku proses pengguna. Untuk mengatasi masalah ini, Bell Labs akan memperbarui ke versi berikutnya (Versi 7 Unix, 1979) mengusulkan panggilan sistem baru - ptrace.

ptrace dikembangkan terutama untuk debugger interaktif, tetapi pada akhir tahun 80an (di era komersial Rilis Sistem V 4) atas dasar ini, debugger dengan fokus sempit—pelacak panggilan sistem—muncul dan digunakan secara luas.

Pertama versi strace yang sama diterbitkan oleh Paul Cronenburg di milis comp.sources.sun pada tahun 1992 sebagai alternatif dari utilitas tertutup trace dari Matahari. Baik kloning maupun aslinya ditujukan untuk SunOS, tetapi pada tahun 1994 strace telah di-porting ke System V, Solaris dan Linux yang semakin populer.

Saat ini strace hanya mendukung Linux dan mengandalkan hal yang sama ptrace, ditumbuhi banyak ekstensi.

Pengelola modern (dan sangat aktif). strace - Dmitry Levin. Berkat dia, utilitas tersebut memperoleh fitur-fitur canggih seperti injeksi kesalahan ke dalam panggilan sistem, dukungan untuk berbagai arsitektur dan, yang paling penting, maskot. Sumber tidak resmi menyatakan bahwa pilihan jatuh pada burung unta karena kesesuaian antara kata Rusia “ostrich” dan kata Inggris “strace”.

Penting juga bahwa panggilan sistem ptrace dan pelacak tidak pernah disertakan dalam POSIX, meskipun sejarah dan implementasinya panjang di Linux, FreeBSD, OpenBSD, dan Unix tradisional.

Singkatnya, perangkat Strace: Piglet Trace

"Anda tidak diharapkan untuk memahami hal ini" (Dennis Ritchie, komentar di kode sumber Unix Versi 6)

Sejak masa kanak-kanak, saya tidak tahan dengan kotak hitam: Saya tidak bermain dengan mainan, tetapi mencoba memahami strukturnya (orang dewasa menggunakan kata "rusak", tetapi tidak percaya pada bahasa jahat). Mungkin inilah sebabnya budaya informal Unix pertama dan gerakan sumber terbuka modern begitu dekat dengan saya.

Untuk keperluan artikel ini, tidak masuk akal untuk membongkar kode sumber strace, yang telah berkembang selama beberapa dekade. Namun tidak boleh ada rahasia yang tersisa untuk pembaca. Oleh karena itu, untuk menunjukkan prinsip pengoperasian program strace tersebut, saya akan memberikan kode untuk pelacak mini - Jejak Anak Babi (ptr). Ia tidak tahu bagaimana melakukan sesuatu yang istimewa, tetapi yang utama adalah panggilan sistem dari program - ia menghasilkan:

$ 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 mengenali sekitar ratusan panggilan sistem Linux (lihat. meja) dan hanya berfungsi pada arsitektur x86-64. Ini cukup untuk tujuan pendidikan.

Mari kita lihat hasil kerja klon kita. Dalam kasus Linux, debugger dan pelacak menggunakan, seperti disebutkan di atas, panggilan sistem ptrace. Ia bekerja dengan meneruskan pengidentifikasi perintah ke argumen pertama, yang hanya kita perlukan PTRACE_TRACEME, PTRACE_SYSCALL и PTRACE_GETREGS.

Pelacak dimulai dengan gaya Unix biasa: fork(2) meluncurkan proses anak, yang pada gilirannya menggunakan exec(3) meluncurkan program yang sedang dipelajari. Satu-satunya kehalusan di sini adalah tantangannya ptrace(PTRACE_TRACEME) sebelum exec: Proses anak mengharapkan proses induk untuk memantaunya:

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

Proses induk sekarang harus memanggil wait(2) dalam proses anak, yaitu memastikan peralihan ke mode penelusuran telah terjadi:

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

Pada titik ini, persiapan telah selesai dan Anda dapat melanjutkan langsung ke pelacakan panggilan sistem dalam putaran tanpa akhir.

Tantangan ptrace(PTRACE_SYSCALL) menjamin hal berikutnya wait induk akan selesai sebelum panggilan sistem dijalankan atau segera setelah selesai. Di antara dua panggilan, Anda dapat melakukan tindakan apa pun: mengganti panggilan dengan panggilan alternatif, mengubah argumen, atau nilai kembalian.

Kita hanya perlu memanggil perintah itu dua kali ptrace(PTRACE_GETREGS)untuk mendapatkan status register rax sebelum panggilan (nomor panggilan sistem) dan segera setelahnya (nilai pengembalian).

Sebenarnya siklusnya:

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

Itulah keseluruhan pelacaknya. Sekarang Anda tahu di mana harus memulai porting berikutnya DTrace di Linux.

Dasar-dasar: menjalankan program yang menjalankan strace

Sebagai kasus penggunaan pertama strace, mungkin ada baiknya mengutip cara paling sederhana - meluncurkan aplikasi yang sedang berjalan strace.

Agar tidak mempelajari daftar panggilan program biasa yang tak ada habisnya, kami menulis program minimal sekitar 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;
}

Mari buat programnya dan pastikan programnya berfungsi:

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

Dan terakhir, mari kita jalankan di bawah kendali 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)                           = ?

Sangat “bertele-tele” dan tidak terlalu mendidik. Ada dua masalah di sini: keluaran program tercampur dengan keluaran strace dan banyaknya panggilan sistem yang tidak menarik minat kami.

Anda dapat memisahkan aliran keluaran standar program dan keluaran kesalahan strace menggunakan saklar -o, yang mengalihkan daftar panggilan sistem ke file argumen.

Yang masih harus dilakukan adalah mengatasi masalah panggilan “ekstra”. Anggaplah kita hanya tertarik pada panggilan telepon write. Kunci -e memungkinkan Anda menentukan ekspresi yang digunakan untuk memfilter panggilan sistem. Opsi kondisi yang paling populer adalah, tentu saja, trace=*, yang dengannya Anda hanya dapat meninggalkan panggilan yang kami minati.

Bila digunakan secara bersamaan -o и -e kita akan mendapatkan:

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

Jadi, Anda tahu, ini jauh lebih mudah untuk dibaca.

Anda juga dapat menghapus panggilan sistem, misalnya yang terkait dengan alokasi dan pembebasan memori:

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

Perhatikan tanda seru yang lolos dalam daftar panggilan yang dikecualikan: ini diperlukan oleh shell perintah. tempurung).

Di versi glibc saya, panggilan sistem menghentikan proses exit_group, tidak tradisional _exit. Inilah kesulitan bekerja dengan panggilan sistem: antarmuka yang digunakan programmer untuk bekerja tidak berhubungan langsung dengan panggilan sistem. Selain itu, ini berubah secara berkala tergantung pada implementasi dan platformnya.

Dasar-dasar: bergabung dengan proses dengan cepat

Awalnya, panggilan sistem ptrace yang menjadi dasar pembuatannya strace, hanya dapat digunakan saat menjalankan program dalam mode khusus. Batasan ini mungkin terdengar masuk akal pada zaman Unix Versi 6. Saat ini, hal ini tidak lagi cukup: terkadang Anda perlu menyelidiki masalah program yang sedang berjalan. Contoh tipikal adalah proses yang diblokir pada pegangan atau proses tidur. Oleh karena itu modern strace dapat bergabung dengan proses dengan cepat.

Contoh pembekuan program:

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

Mari kita buat programnya dan pastikan programnya dibekukan:

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

Sekarang mari kita coba bergabung:

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

Program diblokir melalui panggilan pause. Mari kita lihat bagaimana dia bereaksi terhadap sinyal tersebut:

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

Kami meluncurkan program yang dibekukan dan bergabung menggunakan strace. Dua hal menjadi jelas: jeda panggilan sistem mengabaikan sinyal tanpa penangan dan, yang lebih menarik, strace memonitor tidak hanya panggilan sistem, tetapi juga sinyal masuk.

Contoh: Melacak Proses Anak

Bekerja dengan proses melalui panggilan fork - dasar dari semua Unix. Mari kita lihat bagaimana strace bekerja dengan pohon proses menggunakan contoh “breeding” sederhana program:

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

Di sini proses asli menciptakan proses anak, keduanya menulis ke keluaran standar:

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

Secara default, kita hanya akan melihat panggilan sistem dari proses induk:

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

Bendera membantu Anda melacak seluruh pohon proses -f, yang strace memonitor panggilan sistem dalam proses anak. Ini menambah setiap baris output pid proses yang menghasilkan keluaran sistem:

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

Dalam konteks ini, pemfilteran berdasarkan grup panggilan sistem dapat bermanfaat:

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

Ngomong-ngomong, system call apa yang digunakan untuk membuat proses baru?

Contoh: jalur file, bukan pegangan

Mengetahui deskriptor file tentu berguna, tetapi nama file spesifik yang diakses suatu program juga berguna.

berikutnya program menulis baris ke file sementara:

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

Selama panggilan normal strace akan menampilkan nilai nomor deskriptor yang diteruskan ke panggilan sistem:

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

Dengan bendera -y Utilitas menunjukkan jalur ke file yang sesuai dengan deskriptornya:

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

Contoh: Pelacakan Akses File

Fitur berguna lainnya: hanya menampilkan panggilan sistem yang terkait dengan file tertentu. Berikutnya program menambahkan baris ke file arbitrer yang diteruskan sebagai argumen:

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 menampilkan banyak informasi yang tidak perlu. Bendera -P dengan argumen menyebabkan strace hanya mencetak panggilan ke file yang ditentukan:

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

Contoh: Program Multithread

а strace juga dapat membantu saat bekerja dengan multi-threaded programnya. Program berikut menulis ke keluaran standar dari dua aliran:

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

Tentu saja, itu harus dikompilasi dengan salam khusus untuk linker - flag -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
$

Bendera -f, seperti halnya proses biasa, akan menambahkan pid proses ke awal setiap baris.

Tentu saja, kita tidak berbicara tentang pengidentifikasi thread dalam arti implementasi standar POSIX Threads, tetapi tentang nomor yang digunakan oleh penjadwal tugas di Linux. Dari sudut pandang yang terakhir, tidak ada proses atau thread - ada tugas yang perlu didistribusikan di antara inti mesin yang tersedia.

Saat bekerja di banyak thread, panggilan sistem menjadi terlalu banyak:

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

Masuk akal untuk membatasi diri Anda pada manajemen proses dan panggilan sistem saja 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 +++

Ngomong-ngomong, pertanyaan. Panggilan sistem apa yang digunakan untuk membuat thread baru? Apa perbedaan panggilan untuk thread ini dengan panggilan untuk proses?

Kelas master: tumpukan proses pada saat panggilan sistem

Salah satunya baru-baru ini muncul strace kemampuan - menampilkan tumpukan panggilan fungsi pada saat panggilan sistem. Sederhana contoh:

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

Secara alami, keluaran program menjadi sangat banyak, dan selain bendera -k (tampilan tumpukan panggilan), masuk akal untuk memfilter panggilan sistem berdasarkan nama:

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

Kelas master: injeksi kesalahan

Dan satu lagi fitur baru dan sangat berguna: injeksi kesalahan. Di Sini program, menulis dua baris ke aliran keluaran:

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

Mari kita telusuri kedua panggilan tulis:

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

Sekarang kita menggunakan ekspresi tersebut injectuntuk memasukkan kesalahan EBADF di semua panggilan tulis:

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

Sangat menarik kesalahan apa yang dikembalikan semua tantangan write, termasuk panggilan yang tersembunyi di balik perror. Masuk akal untuk mengembalikan kesalahan pada panggilan pertama:

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

Atau yang kedua:

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

Jenis kesalahan tidak perlu ditentukan:

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

Jika dikombinasikan dengan flag lain, Anda dapat “memutus” akses ke file tertentu. Contoh:

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

Selain injeksi kesalahan, satu bisa menimbulkan penundaan saat melakukan panggilan atau menerima sinyal.

penutup

а strace - alat yang sederhana dan andal. Namun selain panggilan sistem, aspek lain dari pengoperasian program dan sistem operasi dapat di-debug. Misalnya, ia dapat melacak panggilan ke perpustakaan yang tertaut secara dinamis. jejak, mereka dapat melihat pengoperasian sistem operasi Ketuk Sistem и ftrace, dan memungkinkan Anda menyelidiki kinerja program secara mendalam Perf. Namun demikian, memang demikian strace - garis pertahanan pertama jika ada masalah dengan program saya sendiri dan orang lain, dan saya menggunakannya setidaknya beberapa kali seminggu.

Singkatnya, jika Anda menyukai Unix, bacalah man 1 strace dan jangan ragu untuk mengintip program Anda!

Sumber: www.habr.com

Tambah komentar