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.
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:
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, ®isters) == -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, ®isters) == -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:
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.
$ ./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:
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:
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:
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:
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!