Strace Linux-da: tarixi, quruluşu və istifadəsi

Strace Linux-da: tarixi, quruluşu və istifadəsi

Unix-a bənzər əməliyyat sistemlərində proqramların xarici dünya və əməliyyat sistemi ilə əlaqəsi bir kiçik funksiya dəstindən - sistem çağırışlarından keçir. Bu, test məqsədilə icra olunan proseslərin sistem çağırışları tərəfindən izlənməsinin faydalı olacağını göstərir.

Linux-da proqramların "intim həyatını" izləmməyə kömək edən bir utilitdir strace, bu məqalənin mövzusu olan. "Casus" avadanlığının istifadə nümunələrinə qısa bir tarixçə strace və belə proqramların cihazı haqqında təsvir daxildir.

Məzmun

Növlərin mənşəyi

Unix-də proqramlar ilə əməliyyat sisteminin mərkəzi arasındakı əsas interfeys sistem çağırışlarıdır (ing. system calls, syscalls), proqramların xarici dünyayla əlaqəsi yalnız onların vasitəsilə baş verir.

Amma Unix-in ilk ictimai versiyası (Version 6 Unix, 1975-ci il) istifadəçi proseslərinin davranışını izləmək üçün rahat yollar təqdim etmirdi. Bu problemi həll etmək üçün Bell Labs növbəti versiyada (Version 7 Unix, 1979-cu il) yeni bir sistem çağırışını - ptrace.

yaratmışdı ki, bu da əsasən interaktiv debuggers üçün hazırlanmışdı, amma 80-ci illərin sonlarına (kommersiya dövründə System V Release 4) bu baza əsasında spesifik debuggers - sistem çağırışlarının izləndiyi tətbiqetmələr yaranmağa başladı.

İlk strace versiyası Paul Kronenburg tərəfindən 1992-ci ildə sunun bağlı utilitlərinə alternativ olaraq comp.sources.sun siyahısında dərc edildi. trace Sun üçün nəzərdə tutulmuşdu. Həm klon, həm də orijinal SunOS üçün hazırlanmışdı, lakin 1994-cü ildə strace System V, Solaris və populyarlığı artan Linux-a köçürüldü.

Bu gün strace yalnız Linux ilə uyğundur və hələ də eyni ptrace, bir çox genişləndirmələrlə zənginləşmişdir.

Müasir (və olduqca aktiv) saxlayıcısı straceDmitriy Levin. Onun sayəsində utilita sistem çağırışlarında səhv inyeksiya, geniş arxitektura dəstəyi və ən əsası, maskot. Qeyri-rəsmi mənbələrə görə, seçimin "strace" İngilis sözünə uyğun olan Rusiya sözləri "straus" ilə bağlı olduğu bildirilir.

Eyni zamanda sistem çağırışı ptrace və izləyicilərin POSIX-ə daxil edilmədiyinin önəmi var, baxmayaraq ki, uzun tarixə və Linux, FreeBSD, OpenBSD və ənənəvi Unix-də tətbiqinə malikdir.

strace cihazı iki sözlə: Piglet Trace

"Siz bunun nə olduğunu anlamanız gözlənilmir" (Denis Rici, Version 6 Unix-in mənbə kodundakı şərh)

Uşaq yaşlarından etibarən qara qutuları sevmirdim: oyuncaqla oynamırdım, onların quruluşunu anlamağa çalışırdım (böyüklər "qırmaq" sözündən istifadə edirdilər, amma pis dillərə inanmayın). Ola bilsin ki, buna görə ilk Unix-in qeyri-formal mədəniyəti və müasir açıq mənbə hərəkatı mənə o qədər də yaxın gəldi.

Bu məqalə çərçivəsində illər ərzində yumuşalıb dəyişən strace mənbə kodunu təhlil etmək başa düşülən deyil. Ancaq oxuculara heç bir sirr qalmalıdır. Buna görə də, strace kimi proqramların işləmə prinsiplərini göstərmək üçün kiçik bir izləyici kodunu təqdim edəcəyəm — Piglet Trace (ptr). Xüsusi bir işi yoxdur, amma əsas olanı — proqramın sistem çağırışlarını — çıxarır:

$ 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
CLOSE(3) -> 0
FSTAT(5) -> 0
IOCTL(16) -> 18446744073709551591
WRITE(1) -> 5
CLOSE(3) -> 0
CLOSE(3) -> 0
unknown(231)
Tracee terminated

Piglet Trace təxminən yüzdək Linux sistem çağırışlarını tanıyır (bax cədvəl) və yalnız x86-64 arxitekturasında işləyir. Tədris məqsədləri üçün bu yetərlidir.

Gəlin, bizim klonun işini araşdıraq. Linux üçün dəbugerlər və izləyicilər üçün, yuxarıda qeyd edildiyi kimi, sistem çağırışı ptrace istifadə olunur. O, yalnız ilk arqumentdə komandaların identifikatorlarını ötürməklə işləyir, bizə yalnız PTRACE_TRACEME, . İdarə olunan proses hər hansı bir sistem çağırışı işə saldıqda, idarəetməPTRACE_GETREGS.

İzləyici normal Unix tərzinə başlayır: fork(2) uşaq prosesi yaradır, sonra o, exec(3) sayəsində araşdırılan proqramı işə salır. Burada yeganə incəlik odur ki, çağırış ptrace(PTRACE_TRACEME) yerinə yetirilir: uşaq prosesi ana prosesin onu izləməsini gözləyir: execpid_t child_pid = fork(); switch (child_pid) { case -1: err(EXIT_FAILURE, "fork"); case 0: /* Uşaq burada */ /* İzlənən rejimin aktivləşdirilməsi lazımdır. Ana proses wait(2) etməli olacaq * * bunun baş verəcəyini gözləyir. */ ptrace(PTRACE_TRACEME, 0, NULL, NULL); /* Özünü çalıştıracağı proqramla əvəz edir. */ execvp(argv[1], argv + 1); err(EXIT_FAILURE, "exec"); }

Ana proses indi

wait(2) uşaq prosesini çağırmalıdır, yəni izləmə rejiminin aktivləşdirildiyinə əmin olmalıdır: Bütün bunlardan sonra hazırlıqlar tamamdır və sistem çağırışlarını sonsuz dövrədə izləməyə başlamaq olar.

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

Çağırış

ptrace(PTRACE_SYSCALL) gələcəkdəki wait ana prosesi ya sistem çağırışı icra olunmadan, ya da dərhal ardından başa çatmasını təmin edir. İki çağırış arasında hər hansı bir fəaliyyət göstərmək mümkündür: çağırışı alternativ ilə əvəz etmək, arqumentləri və ya qaytarılan dəyəri dəyişdirmək. Bizə isə iki dəfə

ptrace(PTRACE_GETREGS) çağırmaq kifayətdir. ptrace(PTRACE_GETREGS), qeydiyyat vəziyyətini əldə etmək üçün rax sistem çağırışının çağırılması (sistem çağırışı nömrəsi) və dərhal sonra (qaytarılan dəyər).

Əslində, dövr:

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

Budur, bütün izleyici. İndi növbəti portlaşdırmaya nədən başlayacağınızı bilirsiniz DTrace Linux-da.

Baza: strace altında proqramın başladılması

İlk nümunə olaraq strace, bəlkə də, ən sadə yolu — tətbiqi başladmaqdır strace.

Tipik proqramın sonsuz çağırışlar siyahısına girməmək üçün yazacağıq minimal proqram ətrafında write:

int main(int argc, char *argv[])
{
    char str[] = "stdout-a yazın";
    
    /* write(2) sadə bir sistem çağırışının ətrafında olan bir örtükdür, buna görə
     * sistem çağırış izində asanlıqla tapıla bilər. */
    if (sizeof(str) != write(STDOUT_FILENO, str, sizeof(str))){
        perror("write");
        return EXIT_FAILURE;
    }
    return EXIT_SUCCESS;
}

Proqramı toplayaq və işlədiyinə əmin olaq:

$ gcc examples/write-simple.c -o write-simple
$ ./$write-simple
stdout-a yazın

Və nəhayət, onu strace altında işə salaq:

$ strace ./$write-simple
pexecve("./write", ["./write"], 0x7ffebd6145b0 /* 71 dəyişən */) = 0
brk(NULL) = 0x55ff5489e000
access("/etc/ld.so.nohwcap", F_OK) = -1 ENOENT (Belə bir fayl və ya direktor yoxdur)
access("/etc/ld.so.preload", R_OK) = -1 ENOENT (Belə bir fayl və ya direktor yoxdur)
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 (Belə bir fayl və ya direktor yoxdur)
openat(AT_FDCWD, "/lib/x86_64-linux-gnu/libc.so.6", O_RDONLY|O_CLOEXEC) = 3
read(3, "177ELF2113        3 >
DDoS qoruması olan saytlara etibarlı hosting satın alın, VPS VDS serverlər 🔥 DDoS qoruması olan saytlara etibarlı hosting satın alın, VPS VDS serverlər | ProHoster