Линукс дээрх Strace: түүх, дизайн, хэрэглээ

Линукс дээрх Strace: түүх, дизайн, хэрэглээ

Unix-тэй төстэй үйлдлийн системүүдэд программ нь гадаад ертөнц болон үйлдлийн системтэй харилцах нь жижиг функцууд буюу системийн дуудлагаар дамжин явагддаг. Энэ нь дибаг хийх зорилгоор процессоор гүйцэтгэж буй системийн дуудлагуудыг тагнах нь ашигтай гэсэн үг юм.

Энэхүү хэрэгсэл нь Линукс дээрх програмуудын "дотно амьдралыг" хянахад тусална strace, энэ нийтлэлийн сэдэв юм. Тагнуулын төхөөрөмжийг ашигласан жишээг товч түүх дагалддаг strace мөн ийм хөтөлбөрүүдийн дизайны тодорхойлолт.

Агуулга

Төрөл зүйлийн гарал үүсэл

Unix дэх програмууд болон үйлдлийн системийн цөмийн хоорондох гол интерфейс нь системийн дуудлага юм. системийн дуудлага, тасалдал), хөтөлбөрүүдийн гадаад ертөнцтэй харилцах нь зөвхөн тэднээр дамжин явагддаг.

Гэхдээ Unix-ийн анхны нийтийн хувилбарт (Unix хувилбар 6, 1975) хэрэглэгчийн үйл явцын зан төлөвийг хянах тохиромжтой арга байхгүй байсан. Энэ асуудлыг шийдэхийн тулд Bell Labs дараагийн хувилбар руу шинэчлэх болно (Unix хувилбар 7, 1979) шинэ системийн дуудлагыг санал болгосон - ptrace.

ptrace нь ихэвчлэн интерактив дибаглагчдад зориулагдсан байсан боловч 80-аад оны сүүлчээр (арилжааны эрин үед) Системийн V хувилбар 4) үүний үндсэн дээр нарийн төвлөрсөн дибаггер-системийн дуудлагыг мөрдөгч-үүд гарч ирэн өргөн хэрэглэгдэх болсон.

Эхнийх нь strace-ийн ижил хувилбарыг Пол Кроненбург 1992 онд comp.sources.sun захидлын жагсаалтад хаалттай хэрэглүүрийн өөр хувилбар болгон нийтлэв. trace Нарнаас. Клон болон эх хувилбар хоёулаа SunOS-д зориулагдсан байсан боловч 1994 он гэхэд strace Систем V, Solaris болон улам бүр түгээмэл болж буй Linux-д шилжүүлсэн.

Өнөөдөр strace нь зөвхөн Линуксыг дэмждэг бөгөөд үүн дээр тулгуурладаг ptrace, олон өргөтгөлөөр ургасан.

Орчин үеийн (мөн маш идэвхтэй) засварчин strace - Дмитрий Левин. Түүний ачаар уг хэрэгсэл нь системийн дуудлагад алдаа гаргах, олон төрлийн архитектурыг дэмжих зэрэг дэвшилтэт боломжуудыг олж авсан бөгөөд хамгийн чухал нь сахиус. Албан бус эх сурвалжууд Оросын "тэмээн хяруул" болон англи хэлний "strace" гэсэн үг хоорондоо зохицож байсан тул тэмээн хяруул дээр сонголт хийсэн гэж мэдэгджээ.

Линукс, FreeBSD, OpenBSD болон уламжлалт Unix дээр олон жилийн түүхтэй, хэрэгжүүлсэн хэдий ч ptrace системийн дуудлагууд болон трекерүүд POSIX-д хэзээ ч ороогүй байх нь чухал юм.

Товчхондоо Strace төхөөрөмж: Piglet Trace

"Та үүнийг ойлгохгүй байна" (Деннис Ричи, Unix-ийн эх кодын 6-р хувилбарын тайлбар)

Бага наснаасаа би хар хайрцгийг тэвчиж чаддаггүй: би тоглоомоор тоглодоггүй, гэхдээ тэдний бүтцийг ойлгохыг хичээдэг байсан (насанд хүрэгчид "эвдэрсэн" гэдэг үгийг ашигладаг байсан, гэхдээ муу хэллэгт итгэдэггүй). Тийм ч учраас анхны Юниксийн албан бус соёл, орчин үеийн нээлттэй эхийн хөдөлгөөн надад маш ойр байдаг.

Энэ нийтлэлийн зорилгын үүднээс олон арван жилийн турш өсөн нэмэгдэж буй 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 нь хэдэн зуун Линукс системийн дуудлагыг таньдаг (харна уу. хүснэгт) бөгөөд зөвхөн x86-64 архитектур дээр ажилладаг. Энэ нь боловсролын зорилгоор хангалттай юм.

Клоныхоо ажлыг харцгаая. Линуксийн хувьд дибаг хийгчид болон тракерууд дээр дурдсанчлан ptrace системийн дуудлагыг ашигладаг. Энэ нь зөвхөн бидэнд хэрэгтэй командын танигчийг эхний аргумент дотор дамжуулснаар ажилладаг PTRACE_TRACEME, PTRACE_SYSCALL и PTRACE_GETREGS.

Tracer нь ердийн 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 Линукс дээр.

Үндсэн ойлголт: 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 ./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, "177ELF21133>1260342"..., 832) = 832
fstat(3, {st_mode=S_IFREG|0755, st_size=2030544, ...}) = 0
mmap(NULL, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f7a2a631000
mmap(NULL, 4131552, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_DENYWRITE, 3, 0) = 0x7f7a2a04c000
mprotect(0x7f7a2a233000, 2097152, PROT_NONE) = 0
mmap(0x7f7a2a433000, 24576, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x1e7000) = 0x7f7a2a433000
mmap(0x7f7a2a439000, 15072, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0) = 0x7f7a2a439000
close(3)                                = 0
arch_prctl(ARCH_SET_FS, 0x7f7a2a6324c0) = 0
mprotect(0x7f7a2a433000, 16384, PROT_READ) = 0
mprotect(0x55ff52b52000, 4096, PROT_READ) = 0
mprotect(0x7f7a2a664000, 4096, PROT_READ) = 0
munmap(0x7f7a2a633000, 197410)          = 0
write(1, "write me to stdoutn", 20write me to stdout
)  = 20
exit_group(0)                           = ?

Маш "үгтэй" бөгөөд тийм ч сургамжтай биш. Энд хоёр асуудал байна: програмын гаралт нь гаралттай холилдсон 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, програмыг тусгай горимд ажиллуулах үед л ашиглах боломжтой. Энэ хязгаарлалт нь Unix-ийн 6-р хувилбарын өдрүүдэд боломжийн сонсогдож байсан байж магадгүй юм. Өнөө үед энэ нь хангалттай байхаа больсон: заримдаа та ажлын програмын асуудлыг судлах хэрэгтэй болдог. Ердийн жишээ бол бариул дээр хаагдсан эсвэл унтаж байгаа процесс юм. Тиймээс орчин үеийн strace процессуудад шууд нэгдэх боломжтой.

Хөлдөлтийн жишээ хөтөлбөр:

int main(int argc, char *argv[])
{
    (void) argc; (void) argv;

    char str[] = "write men";

    write(STDOUT_FILENO, str, sizeof(str));

    /* Sleep indefinitely or until a signal arrives */
    pause();

    write(STDOUT_FILENO, str, sizeof(str));

    return EXIT_SUCCESS;
}

Хөтөлбөрийг бүтээж, царцсан эсэхийг шалгацгаая:

$ gcc examples/write-sleep.c -o write-sleep
$ ./write-sleep
./write-sleep
write me
^C
$

Одоо үүнд нэгдэхийг хичээцгээе:

$ ./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. Хоёр зүйл тодорхой болсон: түр зогсоох системийн дуудлага нь зохицуулагчгүйгээр дохиог үл тоомсорлодог бөгөөд хамгийн сонирхолтой нь strace нь зөвхөн системийн дуудлага төдийгүй ирж ​​буй дохиог хянадаг.

Жишээ нь: Хүүхдийн үйл явцыг хянах

Дуудлагаар дамжуулан процессуудтай ажиллах fork - бүх Unix-ийн үндэс суурь. Энгийн "үржүүлгийн" жишээг ашиглан процессын модтой strace хэрхэн ажилладагийг харцгаая. хөтөлбөр:

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

Энд анхны процесс нь стандарт гаралт руу бичдэг хүүхэд процессыг үүсгэдэг:

$ gcc examples/fork-write.c -o fork-write
$ ./fork-write
parent (self=11274, child=11275)
child (self=11275)

Анхдагч байдлаар, бид зөвхөн эх процессоос системийн дуудлагуудыг харах болно:

$ strace -e trace=write -ofork-write.log ./fork-write
child (self=22049)
parent (self=22048, child=22049)
$ cat fork-write.log
write(1, "parent (self=22048, child=22049)"..., 33) = 33
--- SIGCHLD {si_signo=SIGCHLD, si_code=CLD_EXITED, si_pid=22049, si_uid=1001, si_status=0, si_utime=0, si_stime=0} ---
+++ exited with 0 +++

Туг нь танд процессын модыг бүхэлд нь хянахад тусална -f, аль strace хүүхдийн процесс дахь системийн дуудлагыг хянадаг. Энэ нь гаралтын мөр бүрт нэмэгддэг pid Системийн гаралтыг хийх процесс:

$ strace -f -e trace=write -ofork-write.log ./fork-write
parent (self=22710, child=22711)
child (self=22711)
$ cat fork-write.log
22710 write(1, "parent (self=22710, child=22711)"..., 33) = 33
22711 write(1, "child (self=22711)n", 19) = 19
22711 +++ exited with 0 +++
22710 --- SIGCHLD {si_signo=SIGCHLD, si_code=CLD_EXITED, si_pid=22711, si_uid=1001, si_status=0, si_utime=0, si_stime=0} ---
22710 +++ exited with 0 +++

Энэ нөхцөлд системийн дуудлагын бүлгээр шүүх нь ашигтай байж болно:

$ strace -f -e trace=%process -ofork-write.log ./fork-write
parent (self=23610, child=23611)
child (self=23611)
$ cat fork-write.log
23610 execve("./fork-write", ["./fork-write"], 0x7fff696ff720 /* 63 vars */) = 0
23610 arch_prctl(ARCH_SET_FS, 0x7f3d03ba44c0) = 0
23610 clone(child_stack=NULL, flags=CLONE_CHILD_CLEARTID|CLONE_CHILD_SETTID|SIGCHLD, child_tidptr=0x7f3d03ba4790) = 23611
23610 wait4(-1,  <unfinished ...>
23611 exit_group(0)                     = ?
23611 +++ exited with 0 +++
23610 <... wait4 resumed> NULL, 0, NULL) = 23611
23610 --- SIGCHLD {si_signo=SIGCHLD, si_code=CLD_EXITED, si_pid=23611, si_uid=1001, si_status=0, si_utime=0, si_stime=0} ---
23610 exit_group(0)                     = ?
23610 +++ 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[])
{
    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
$

Flag -f, ердийн процессуудын нэгэн адил мөр бүрийн эхэнд процессын pid-г нэмнэ.

Мэдээжийн хэрэг, бид POSIX Threads стандартыг хэрэгжүүлэх утгаараа thread танигчийн тухай биш, харин Линукс дээр даалгавар төлөвлөгчийн ашигладаг дугаарын тухай ярьж байна. Сүүлчийн үүднээс авч үзвэл ямар ч процесс, утас байхгүй - машины боломжтой цөмүүдэд хуваарилах шаардлагатай ажлууд байдаг.

Олон урсгалтай ажиллах үед системийн дуудлага хэт олширдог:

$ strace -f -othread-write.log ./thread-write
$ wc -l thread-write.log
60 thread-write.log

Зөвхөн менежмент болон системийн дуудлагыг боловсруулахад өөрийгөө хязгаарлах нь утга учиртай write:

$ strace -f -e trace="%process,write" -othread-write.log ./thread-write
$ cat thread-write.log
18211 execve("./thread-write", ["./thread-write"], 0x7ffc6b8d58f0 /* 64 vars */) = 0
18211 arch_prctl(ARCH_SET_FS, 0x7f38ea3b7740) = 0
18211 write(1, "Initial thread: launching a thre"..., 35) = 35
18211 clone(child_stack=0x7f38e9ba2fb0, flags=CLONE_VM|CLONE_FS|CLONE_FILES|CLONE_SIGHAND|CLONE_THREAD|CLONE_SYSVSEM|CLONE_SETTLS|CLONE_PARENT_SETTID|CLONE_CHILD_CLEARTID, parent_tidptr=0x7f38e9ba39d0, tls=0x7f38e9ba3700, child_tidptr=0x7f38e9ba39d0) = 18212
18211 write(1, "Initial thread: joining a thread"..., 33) = 33
18212 write(1, "Secondary thread: workingn", 26) = 26
18212 write(1, "Secondary thread: donen", 23) = 23
18212 exit(0)                           = ?
18212 +++ exited with 0 +++
18211 write(1, "Initial thread: done", 20) = 20
18211 exit_group(0)                     = ?
18211 +++ exited with 0 +++

Дашрамд хэлэхэд асуултууд. Шинэ хэлхээ үүсгэхийн тулд ямар системийн дуудлагыг ашигладаг вэ? Энэ хэлхээний дуудлага нь процессуудын дуудлагаас юугаараа ялгаатай вэ?

Мастер анги: системийн дуудлага хийх үеийн процессын стек

Саяхан гарч ирсэн нэг нь strace чадварууд - системийн дуудлагын үед функцын дуудлагын стекийг харуулах. Энгийн жишээ нь:

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;
}

Мэдээжийн хэрэг, програмын гаралт нь тугнаас гадна маш том хэмжээтэй болдог -k (дуудлагын стек дэлгэц), системийн дуудлагыг нэрээр нь шүүх нь утга учиртай.

$ 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 +++

Мастер анги: алдааны тарилга

Бас нэг шинэ бөгөөд маш хэрэгтэй функц: алдааны тарилга. Энд хөтөлбөр, гаралтын урсгал руу хоёр мөр бичих:

#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>

void do_write(const char *str, ssize_t len)
{
    if (len != write(STDOUT_FILENO, str, (size_t)len)){
        perror("write");
        exit(EXIT_FAILURE);
    }
}

int main(int argc, char *argv[])
{
    (void) argc; (void) argv;

    char str1[] = "write me 1n";
    do_write(str1, sizeof(str1));

    char str2[] = "write me 2n";
    do_write(str2, sizeof(str2));

    return EXIT_SUCCESS;
}

Бичсэн дуудлагыг хоёуланг нь мөрдье:

$ gcc examples/write-twice.c -o write-twice
$ ./write-twice
write me 1
write me 2
$ strace -e trace=write -owrite-twice.log ./write-twice
write me 1
write me 2
$ cat write-twice.log
write(1, "write me 1n", 12)          = 12
write(1, "write me 2n", 12)          = 12
+++ exited with 0 +++

Одоо бид илэрхийлэлийг ашиглаж байна injectалдаа оруулах EBADF бүх бичих дуудлагад:

$ strace -e trace=write -e inject=write:error=EBADF -owrite-twice.log ./write-twice
$ cat write-twice.log
write(1, "write me 1n", 12)          = -1 EBADF (Bad file descriptor) (INJECTED)
write(3, "write: Bad file descriptorn", 27) = -1 EBADF (Bad file descriptor) (INJECTED)
+++ exited with 1 +++

Ямар алдаа буцаах нь сонирхолтой юм бүх сорилтууд 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 - энгийн бөгөөд найдвартай хэрэгсэл. Гэхдээ системийн дуудлагаас гадна програмууд болон үйлдлийн системийн үйл ажиллагааны бусад талуудыг дибаг хийх боломжтой. Жишээлбэл, энэ нь динамик холбогдсон номын сангуудын дуудлагыг хянах боломжтой. ltrace, тэд үйлдлийн системийн ажиллагааг шалгаж болно Системийн товшилт и ftrace, мөн танд хөтөлбөрийн гүйцэтгэлийг гүнзгий судлах боломжийг олгоно perf. Гэсэн хэдий ч тийм л байна strace - Өөрийнхөө болон бусад хүмүүсийн хөтөлбөрт асуудал гарсан тохиолдолд хамгаалалтын эхний шугам, би үүнийг долоо хоногт дор хаяж хоёр удаа ашигладаг.

Товчхондоо, хэрэв та Unix-д дуртай бол уншаарай man 1 strace мөн хөтөлбөрүүдээ чөлөөтэй үзэх боломжтой!

Эх сурвалж: www.habr.com

сэтгэгдэл нэмэх