
В Unix-подобни операционни системи комуникацията между програми и операционната система се осъществява чрез малък набор функции — системни извиквания. Това означава, че за отстраняване на грешки е полезно да се наблюдават извикванията на системните функции от изпълняваните процеси.
Следенето на „интимния живот“ на програмите в Linux улеснява утилитата strace, на която е посветена тази статия. Към примерите за използване на „шпионското“ оборудване са приложени кратка история strace и описание на устройството на такива програми.
Съдържание
Произход на видовете
Главният интерфейс между програмите и ядрото на ОС в Unix — системните извиквания (на англ. system calls, syscalls.), взаимодействието на програмите с външния свят става изключително и само чрез тях.
Но в първата публична версия на Unix (, 1975 година) нямаше удобни начини за проследяване на поведението на потребителските процеси. За решаването на този проблем Bell Labs за следващата версия (, 1979 година) предложиха ново системно извикване — ptrace.
Разработката на ptrace беше предимно за интерактивни отладчици, но към края на 80-те (в ерата на търговския вече ) на тази основа се появиха и получиха широко разпространение узконасочени отладчици — трасировчици на системни извиквания.
Същата версия на strace беше публикувана от Пол Кроненбург в mailing лист comp.sources.sun през 1992 година като алтернатива на затворената утилита trace от Sun. И клонингът, и оригиналът бяха предназначени за SunOS, но до 1994 година strace беше портната на System V, Solaris и набиращия популярност Linux.
Днес strace поддържа само Linux и се основава на същото ptrace, обросло с множество разширения.
Съвременният (и доста активен) мейнтейнер strace — . Благодарение на него, утилитата придоби напреднали възможности, като инжекции на грешки в системните извиквания, поддръжка на широк спектър архитектури и, най-важното, . Неофициални източници твърдят, че изборът е паднал на щраус заради съвпадението между руската дума «страус» и английската "strace".
Не по-малко важно е, че системният вик ptrace и трасировките не бяха включени в POSIX, въпреки дългата им история и наличието на реализация в Linux, FreeBSD, OpenBSD и традиционните Unix.
Устройството strace в две думи: Piglet Trace
"You are not expected to understand this" (Денис Ричи, коментар в изходния код на Version 6 Unix)
От ранно детство ненавиждам черните кутии: не играех с играчки, а се опитвах да разбера как са направени (възрастните употребяваха термина „чупех“, но не вярвайте на злите езици). Може би затова ми е толкова близка неформалната култура на първите Unix и съвременното open-source движение.
В рамките на тази статия не е разумно да разглеждаме изходния код на strace, която е узрявала през десетилетия. Но не трябва да оставяме и тайни за читателите. Затова, за да покажем принципа на работа на подобни strace програми, ще представя кода на миниатюрен трасировчик — (ptr). Нищо особено не умее, но основното — системните извиквания на програмата — извежда:
$ 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 terminatedPiglet 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 родителят ще завърши или преди изпълнението на системно повикване, или веднага след неговото завършване. Между двете повиквания могат да се извършат действия: да се замени повикването с алтернативно, да се променят аргументите или върнатата стойност.
Нам ни е достатъчно да извикаме командата 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);
}Ето я и цялата проследяваща програма. Сега знаете от къде да започнете поредното портирование на Linux.
Основи: стартиране на програма под управлението на strace
Като първи пример за използване strace, вероятно, е уместно да приведем най-простия начин — стартиране на приложение под управление на strace.
За да не се ровим в безкраен списък от повиквания на типична програма, нека напишем около write:
int main(int argc, char *argv[])
{
char str[] = "write me to stdoutn";
/* write(2) е прост обвивка около системно повикване, така че трябва да е лесно да
* се намери в трасето на системното повикване. */
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, "177ELF2113 3 >