Strace в Linux: история, устройство и използване

Strace в Linux: история, устройство и използване

В Unix-подобни операционни системи комуникацията между програми и операционната система се осъществява чрез малък набор функции — системни извиквания. Това означава, че за отстраняване на грешки е полезно да се наблюдават извикванията на системните функции от изпълняваните процеси.

Следенето на „интимния живот“ на програмите в Linux улеснява утилитата strace, на която е посветена тази статия. Към примерите за използване на „шпионското“ оборудване са приложени кратка история strace и описание на устройството на такива програми.

Съдържание

Произход на видовете

Главният интерфейс между програмите и ядрото на ОС в Unix — системните извиквания (на англ. system calls, syscalls.), взаимодействието на програмите с външния свят става изключително и само чрез тях.

Но в първата публична версия на Unix (Version 6 Unix, 1975 година) нямаше удобни начини за проследяване на поведението на потребителските процеси. За решаването на този проблем Bell Labs за следващата версия (Version 7 Unix, 1979 година) предложиха ново системно извикване — ptrace.

Разработката на ptrace беше предимно за интерактивни отладчици, но към края на 80-те (в ерата на търговския вече System V Release 4) на тази основа се появиха и получиха широко разпространение узконасочени отладчици — трасировчици на системни извиквания.

Първа Същата версия на 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 програми, ще представя кода на миниатюрен трасировчик — Piglet Trace (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 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 родителят ще завърши или преди изпълнението на системно повикване, или веднага след неговото завършване. Между двете повиквания могат да се извършат действия: да се замени повикването с алтернативно, да се променят аргументите или върнатата стойност.

Нам ни е достатъчно да извикаме командата 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) е прост обвивка около системно повикване, така че трябва да е лесно да
     * се намери в трасето на системното повикване. */
    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 >
Купете надежден хостинг за сайтове със защита от DDoS, VPS и VDS сървъри 🔥 Купете надежден хостинг за сайтове със защита от DDoS, VPS и VDS сървъри | ProHoster