در سیستم عامل های شبه یونیکس، ارتباط یک برنامه با دنیای خارج و سیستم عامل از طریق مجموعه کوچکی از توابع - فراخوانی های سیستمی اتفاق می افتد. این بدان معناست که برای اهداف اشکالزدایی، جاسوسی از فراخوانهای سیستمی که توسط فرآیندها اجرا میشوند میتواند مفید باشد.
یک ابزار به شما کمک می کند تا "زندگی صمیمی" برنامه ها را در لینوکس نظارت کنید strace، که موضوع این مقاله می باشد. نمونه هایی از استفاده از تجهیزات جاسوسی با تاریخچه مختصری همراه است strace و شرح طراحی این گونه برنامه ها.
رابط اصلی بین برنامه ها و هسته سیستم عامل در یونیکس تماس های سیستمی است. تماس های سیستمی, syscals) تعامل برنامه ها با دنیای خارج منحصراً از طریق آنها اتفاق می افتد.
اما در اولین نسخه عمومی یونیکس (نسخه 6 یونیکس، 1975) هیچ راه مناسبی برای ردیابی رفتار فرآیندهای کاربر وجود نداشت. برای حل این مشکل، آزمایشگاه بل به نسخه بعدی (نسخه 7 یونیکس، 1979) یک فراخوان سیستم جدید را پیشنهاد کرد - ptrace.
ptrace در درجه اول برای دیباگرهای تعاملی توسعه داده شد، اما در پایان دهه 80 (در عصر تجاری System V نسخه 4) بر این اساس، اشکال زدایی با تمرکز محدود - ردیاب های فراخوانی سیستم - ظاهر شدند و به طور گسترده مورد استفاده قرار گرفتند.
ابتدا همان نسخه strace توسط Paul Cronenburg در لیست پستی comp.sources.sun در سال 1992 به عنوان جایگزینی برای یک ابزار بسته منتشر شد. trace از سان هم کلون و هم نسخه اصلی برای SunOS در نظر گرفته شده بودند، اما تا سال 1994 strace به System V، Solaris و لینوکس به طور فزاینده محبوب منتقل شد.
امروزه strace فقط از لینوکس پشتیبانی میکند و به همان متکی است ptrace، رشد بیش از حد با پسوندهای فراوان.
نگهدارنده مدرن (و بسیار فعال). strace - دیمیتری لوین. به لطف او، این ابزار دارای ویژگی های پیشرفته ای مانند تزریق خطا در تماس های سیستم، پشتیبانی از طیف گسترده ای از معماری ها و مهمتر از همه، طلسم. منابع غیر رسمی ادعا می کنند که انتخاب شترمرغ به دلیل همخوانی بین کلمه روسی "شتر مرغ" و کلمه انگلیسی "strace" بود.
همچنین مهم است که فراخوانی سیستم ptrace و ردیابها با وجود سابقه طولانی و پیادهسازی در لینوکس، FreeBSD، OpenBSD و یونیکس سنتی، هرگز در POSIX گنجانده نشدند.
دستگاه Strace به طور خلاصه: Piglet Trace
"از شما انتظار نمی رود این را بفهمید" (دنیس ریچی، نظر در کد منبع یونیکس نسخه 6)
از اوایل کودکی، من نمی توانم جعبه های سیاه را تحمل کنم: من با اسباب بازی ها بازی نکردم، اما سعی کردم ساختار آنها را درک کنم (بزرگسالان از کلمه "شکستن" استفاده می کردند، اما زبان های شیطانی را باور نمی کنند). شاید به همین دلیل است که فرهنگ غیررسمی اولین یونیکس و جنبش متن باز مدرن بسیار به من نزدیک است.
برای اهداف این مقاله، جدا کردن کد منبع strace که در طول چندین دهه رشد کرده است، غیرمنطقی است. اما هیچ رازی نباید برای خوانندگان باقی بماند. بنابراین، برای نشان دادن اصل عملکرد چنین برنامه های strace، کد یک ردیاب مینیاتوری را ارائه می کنم - رد خوکچه (ptr). نمی داند چگونه کار خاصی انجام دهد، اما نکته اصلی تماس های سیستمی برنامه است - خروجی آن:
Piglet Trace حدود صدها تماس سیستمی لینوکس را تشخیص می دهد (نگاه کنید به. جدول) و فقط روی معماری x86-64 کار می کند. این برای اهداف آموزشی کافی است.
بیایید به کار کلون خود نگاه کنیم. در مورد لینوکس، دیباگرها و ردیاب ها همانطور که در بالا ذکر شد از فراخوانی سیستم ptrace استفاده می کنند. با ارسال شناسههای فرمان در آرگومان اول کار میکند، که فقط به آنها نیاز داریم PTRACE_TRACEME, PTRACE_SYSCALL и PTRACE_GETREGS.
ردیاب به سبک معمول یونیکس شروع می شود: 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 والد یا قبل از اجرای فراخوانی سیستم یا بلافاصله پس از تکمیل آن تکمیل می شود. بین دو تماس میتوانید هر عملی را انجام دهید: تماس را با یک جایگزین جایگزین کنید، آرگومانها یا مقدار بازگشتی را تغییر دهید.
فقط باید دوبار دستور را فراخوانی کنیم 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, ®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);
}
این کل ردیاب است. اکنون می دانید که انتقال بعدی را از کجا شروع کنید DTrace در لینوکس
مبانی: اجرای یک برنامه در حال اجرا strace
به عنوان اولین مورد استفاده strace، شاید ارزش ذکر ساده ترین روش را داشته باشد - راه اندازی یک برنامه در حال اجرا strace.
برای اینکه وارد لیست بی پایان تماس های یک برنامه معمولی نشویم، می نویسیم حداقل برنامه در اطراف write:
int main(int argc, char *argv[])
{
char str[] = "write me to stdoutn";
/* write(2) is a simple wrapper around a syscall so it should be easy to
* find in the syscall trace. */
if (sizeof(str) != write(STDOUT_FILENO, str, sizeof(str))){
perror("write");
return EXIT_FAILURE;
}
return EXIT_SUCCESS;
}
بیایید برنامه را بسازیم و مطمئن شویم که کار می کند:
$ gcc examples/write-simple.c -o write-simple
$ ./write-simple
write me to stdout
و در نهایت، اجازه دهید آن را تحت کنترل strace اجرا کنیم:
بسیار "کلامی" و نه چندان آموزشی. در اینجا دو مشکل وجود دارد: خروجی برنامه با خروجی مخلوط شده است strace و تعداد زیادی تماس سیستمی که برای ما جالب نیست.
شما می توانید جریان خروجی استاندارد برنامه و خروجی خطای strace را با استفاده از سوئیچ -o جدا کنید، که لیست فراخوانی های سیستم را به یک فایل آرگومان هدایت می کند.
باقی مانده است که با مشکل تماس های "اضافی" مقابله کنیم. بیایید فرض کنیم که ما فقط به تماس علاقه مند هستیم write... کلید -e به شما امکان می دهد عباراتی را مشخص کنید که تماس های سیستمی با آن فیلتر می شوند. محبوب ترین گزینه شرایط طبیعی است، trace=*، که با آن فقط می توانید تماس های مورد علاقه ما را بگذارید.
در صورت استفاده همزمان -o и -e دریافت خواهیم کرد:
$ strace -e trace=write -owrite-simple.log ./write-simple
write me to stdout
$ cat write-simple.log
write(1, "write me to stdoutn", 20
) = 20
+++ exited with 0 +++
بنابراین، می بینید، خواندن آن بسیار ساده تر است.
همچنین می توانید تماس های سیستمی را حذف کنید، به عنوان مثال تماس های مربوط به تخصیص و آزادسازی حافظه:
$ strace -e trace=!brk,mmap,mprotect,munmap -owrite-simple.log ./write-simple
write me to stdout
$ cat write-simple.log
execve("./write-simple", ["./write-simple"], 0x7ffe9972a498 /* 69 vars */) = 0
access("/etc/ld.so.nohwcap", F_OK) = -1 ENOENT (No such file or directory)
access("/etc/ld.so.preload", R_OK) = -1 ENOENT (No such file or directory)
openat(AT_FDCWD, "/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3
fstat(3, {st_mode=S_IFREG|0644, st_size=124066, ...}) = 0
close(3) = 0
access("/etc/ld.so.nohwcap", F_OK) = -1 ENOENT (No such file or directory)
openat(AT_FDCWD, "/lib/x86_64-linux-gnu/libc.so.6", O_RDONLY|O_CLOEXEC) = 3
read(3, "177ELF21133>1260342"..., 832) = 832
fstat(3, {st_mode=S_IFREG|0755, st_size=2030544, ...}) = 0
close(3) = 0
arch_prctl(ARCH_SET_FS, 0x7f00f0be74c0) = 0
write(1, "write me to stdoutn", 20) = 20
exit_group(0) = ?
+++ exited with 0 +++
به علامت تعجب فراری در لیست تماس های حذف شده توجه کنید: این مورد توسط پوسته فرمان لازم است. صدف).
در نسخه من از glibc، یک تماس سیستمی فرآیند را خاتمه می دهد exit_group، سنتی نیست _exit. این مشکل کار با تماس های سیستمی است: رابطی که برنامه نویس با آن کار می کند مستقیماً با تماس های سیستمی مرتبط نیست. علاوه بر این، بسته به اجرا و پلتفرم به طور مرتب تغییر می کند.
مبانی: پیوستن به فرآیند در پرواز
در ابتدا سیستم ptrace که روی آن ساخته شده بود فراخوانی می کرد strace، فقط هنگام اجرای برنامه در حالت خاص قابل استفاده است. این محدودیت ممکن است در روزهای نسخه 6 یونیکس منطقی به نظر می رسید. امروزه این دیگر کافی نیست: گاهی اوقات باید مشکلات یک برنامه کاری را بررسی کنید. یک مثال معمولی فرآیندی است که روی یک دسته یا خواب مسدود شده است. بنابراین مدرن strace می تواند به فرآیندها در پرواز بپیوندد.
$ ./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. دو چیز مشخص شد: تماس سیستم مکث سیگنالها را بدون کنترلر نادیده میگیرد و جالبتر اینکه استریس نه تنها تماسهای سیستم، بلکه سیگنالهای دریافتی را نیز زیر نظر دارد.
مثال: ردیابی فرآیندهای کودک
کار با فرآیندها از طریق تماس fork - اساس همه یونیکس ها. بیایید ببینیم که چگونه استریس با یک درخت فرآیند با استفاده از مثال یک "پرورش" ساده کار می کند. برنامه:
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);
}
در اینجا فرآیند اصلی یک فرآیند فرزند ایجاد می کند که هر دو در خروجی استاندارد نوشته می شوند:
پرچم به شما کمک می کند کل درخت فرآیند را ردیابی کنید -f، که strace تماس های سیستمی را در فرآیندهای فرزند نظارت می کند. این به هر خط خروجی اضافه می کند pid فرآیندی که خروجی سیستم را می سازد:
void do_write(int out_fd)
{
char str[] = "write me to a filen";
if (sizeof(str) != write(out_fd, str, sizeof(str))){
perror("write");
exit(EXIT_FAILURE);
}
}
int main(int argc, char *argv[])
{
char tmp_filename_template[] = "/tmp/output_fileXXXXXX";
int out_fd = mkstemp(tmp_filename_template);
if (out_fd == -1) {
perror("mkstemp");
exit(EXIT_FAILURE);
}
do_write(out_fd);
return EXIT_SUCCESS;
}
در طول یک تماس معمولی strace مقدار عدد توصیفگر ارسال شده به فراخوانی سیستم را نشان می دهد:
$ strace -e trace=write -o write-tmp-file.log ./write-tmp-file
$ cat write-tmp-file.log
write(3, "write me to a filen", 20) = 20
+++ exited with 0 +++
با پرچم -y ابزار مسیر فایلی را که توصیفگر با آن مطابقت دارد نشان می دهد:
$ strace -y -e trace=write -o write-tmp-file.log ./write-tmp-file
$ cat write-tmp-file.log
write(3</tmp/output_fileCf5MyW>, "write me to a filen", 20) = 20
+++ exited with 0 +++
مثال: ردیابی دسترسی به فایل
یکی دیگر از ویژگی های مفید: نمایش فقط تماس های سیستمی مرتبط با یک فایل خاص. بعد برنامه یک خط به فایل دلخواه ارسال شده به عنوان آرگومان اضافه می کند:
void do_write(int out_fd)
{
char str[] = "write me to a filen";
if (sizeof(str) != write(out_fd, str, sizeof(str))){
perror("write");
exit(EXIT_FAILURE);
}
}
int main(int argc, char *argv[])
{
/*
* Path will be provided by the first program argument.
* */
const char *path = argv[1];
/*
* Open an existing file for writing in append mode.
* */
int out_fd = open(path, O_APPEND | O_WRONLY);
if (out_fd == -1) {
perror("open");
exit(EXIT_FAILURE);
}
do_write(out_fd);
return EXIT_SUCCESS;
}
به طور پیش فرض strace اطلاعات غیر ضروری زیادی را نمایش می دهد. پرچم -P با یک آرگومان باعث می شود که strace فقط فراخوانی های فایل مشخص شده را چاپ کند:
$ strace -y -P/tmp/test_file.log -o write-file.log ./write-file /tmp/test_file.log
$ cat write-file.log
openat(AT_FDCWD, "/tmp/test_file.log", O_WRONLY|O_APPEND) = 3</tmp/test_file.log>
write(3</tmp/test_file.log>, "write me to a filen", 20) = 20
+++ exited with 0 +++
مثال: برنامه های چند رشته ای
سودمندی strace همچنین می تواند هنگام کار با چند رشته ای کمک کند برنامه. برنامه زیر در خروجی استاندارد از دو جریان می نویسد:
void *thread(void *arg)
{
(void) arg;
printf("Secondary thread: workingn");
sleep(1);
printf("Secondary thread: donen");
return NULL;
}
int main(int argc, char *argv[])
{
printf("Initial thread: launching a threadn");
pthread_t thr;
if (0 != pthread_create(&thr, NULL, thread, NULL)) {
fprintf(stderr, "Initial thread: failed to create a thread");
exit(EXIT_FAILURE);
}
printf("Initial thread: joining a threadn");
if (0 != pthread_join(thr, NULL)) {
fprintf(stderr, "Initial thread: failed to join a thread");
exit(EXIT_FAILURE);
};
printf("Initial thread: done");
exit(EXIT_SUCCESS);
}
طبیعتاً باید با یک تبریک ویژه به پیوند دهنده - پرچم -pthread کامپایل شود:
$ gcc examples/thread-write.c -pthread -o thread-write
$ ./thread-write
/thread-write
Initial thread: launching a thread
Initial thread: joining a thread
Secondary thread: working
Secondary thread: done
Initial thread: done
$
فلگ -fمانند فرآیندهای معمولی، pid فرآیند را به ابتدای هر خط اضافه می کند.
طبیعتاً، ما در مورد یک شناسه رشته به معنای اجرای استاندارد POSIX Threads صحبت نمی کنیم، بلکه در مورد تعداد مورد استفاده توسط زمانبندی کار در لینوکس صحبت می کنیم. از نقطه نظر دومی، هیچ فرآیند یا رشته ای وجود ندارد - وظایفی وجود دارد که باید بین هسته های موجود دستگاه توزیع شوند.
هنگام کار در چندین رشته، فراخوانی های سیستم بسیار زیاد می شود:
جالب است چه خطاهایی برگردانده می شود همه چالش ها write، از جمله تماس پنهان شده در پشت خطا. بازگشت خطا فقط برای اولین تماس منطقی است:
$ strace -e trace=write -e inject=write:error=EBADF:when=1 -owrite-twice.log ./write-twice
write: Bad file descriptor
$ cat write-twice.log
write(1, "write me 1n", 12) = -1 EBADF (Bad file descriptor) (INJECTED)
write(3, "write: Bad file descriptorn", 27) = 27
+++ exited with 1 +++
یا دومی:
$ strace -e trace=write -e inject=write:error=EBADF:when=2 -owrite-twice.log ./write-twice
write me 1
write: Bad file descriptor
$ cat write-twice.log
write(1, "write me 1n", 12) = 12
write(1, "write me 2n", 12) = -1 EBADF (Bad file descriptor) (INJECTED)
write(3, "write: Bad file descriptorn", 27) = 27
+++ exited with 1 +++
لازم نیست نوع خطا را مشخص کنید:
$ strace -e trace=write -e fault=write:when=1 -owrite-twice.log ./write-twice
$ cat write-twice.log
write(1, "write me 1n", 12) = -1 ENOSYS (Function not implemented) (INJECTED)
write(3, "write: Function not implementedn", 32) = 32
+++ exited with 1 +++
در ترکیب با پرچمهای دیگر، میتوانید دسترسی به یک فایل خاص را «شکست» کنید. مثال:
$ strace -y -P/tmp/test_file.log -e inject=file:error=ENOENT -o write-file.log ./write-file /tmp/test_file.log
open: No such file or directory
$ cat write-file.log
openat(AT_FDCWD, "/tmp/test_file.log", O_WRONLY|O_APPEND) = -1 ENOENT (No such file or directory) (INJECTED)
+++ exited with 1 +++
علاوه بر تزریق خطا، می توان ایجاد تأخیر در هنگام برقراری تماس یا دریافت سیگنال.
پس از کلمه
سودمندی strace - یک ابزار ساده و قابل اعتماد. اما علاوه بر فراخوانی سیستم، سایر جنبه های عملکرد برنامه ها و سیستم عامل را می توان اشکال زدایی کرد. به عنوان مثال، میتواند تماسهای کتابخانههای متصل به صورت پویا را ردیابی کند. ردیابی، آنها می توانند عملکرد سیستم عامل را بررسی کنند SystemTap и ftraceو به شما امکان می دهد عملکرد برنامه را عمیقا بررسی کنید پرفیوم. با این وجود، این است strace - خط اول دفاع در صورت بروز مشکل در برنامه های خودم و دیگران و حداقل هفته ای یکی دو بار از آن استفاده می کنم.
به طور خلاصه، اگر عاشق یونیکس هستید، بخوانید man 1 strace و با خیال راحت به برنامه های خود نگاه کنید!