Strace Linux-ում. պատմություն, դիզայն և օգտագործում

Strace Linux-ում. պատմություն, դիզայն և օգտագործում

Unix-ի նման օպերացիոն համակարգերում ծրագրի հաղորդակցությունն արտաքին աշխարհի և օպերացիոն համակարգի հետ տեղի է ունենում մի փոքր ֆունկցիաների՝ համակարգային զանգերի միջոցով: Սա նշանակում է, որ վրիպազերծման նպատակով կարող է օգտակար լինել լրտեսել համակարգային զանգերը, որոնք իրականացվում են գործընթացների միջոցով:

Կոմունալ ծրագիրը օգնում է ձեզ վերահսկել Linux-ի ծրագրերի «ինտիմ կյանքը»: strace, որը սույն հոդվածի թեման է։ Լրտեսական սարքավորումների օգտագործման օրինակները ուղեկցվում են համառոտ պատմությամբ strace և նման ծրագրերի նախագծման նկարագրությունը:

Պարունակություն

Տեսակների ծագումը

Unix-ում ծրագրերի և ՕՀ միջուկի միջև հիմնական ինտերֆեյսը համակարգային զանգերն են: համակարգային զանգեր, syscals), ծրագրերի փոխազդեցությունն արտաքին աշխարհի հետ տեղի է ունենում բացառապես դրանց միջոցով։

Բայց Unix-ի առաջին հրապարակային տարբերակում (Unix 6 տարբերակ, 1975) օգտագործողի գործընթացների վարքագծին հետևելու հարմար եղանակներ չկային: Այս խնդիրը լուծելու համար Bell Labs-ը կթարմացվի հաջորդ տարբերակին (Unix 7 տարբերակ, 1979) առաջարկեց նոր համակարգային կոչ. ptrace.

ptrace-ը մշակվել է հիմնականում ինտերակտիվ կարգաբերիչների համար, բայց մինչև 80-ականների վերջը (առևտրի դարաշրջանում Համակարգ V թողարկում 4) այս հիման վրա հայտնվեցին և լայնորեն կիրառվեցին նեղ կենտրոնացված վրիպազերծիչներ՝ համակարգային կանչերի հետագծեր:

Առաջին strace-ի նույն տարբերակը հրապարակվել է Փոլ Քրոնենբուրգի կողմից comp.sources.sun փոստային ցուցակում 1992 թվականին՝ որպես փակ կոմունալ ծառայության այլընտրանք: trace արևից։ Ե՛վ կլոնը, և՛ բնօրինակը նախատեսված էին SunOS-ի համար, սակայն 1994թ strace տեղափոխվեց System V, Solaris և գնալով ավելի տարածված Linux:

Այսօր strace-ն աջակցում է միայն Linux-ին և հենվում է նույնի վրա ptrace, գերաճած բազմաթիվ ընդարձակումներով։

Ժամանակակից (և շատ ակտիվ) սպասարկող strace - Դմիտրի Լևին. Նրա շնորհիվ կոմունալը ձեռք բերեց առաջադեմ առանձնահատկություններ, ինչպիսիք են սխալի ներարկումը համակարգային զանգերի մեջ, աջակցություն ճարտարապետությունների լայն շրջանակին և, ամենակարևորը, թալիսման. Ոչ պաշտոնական աղբյուրները պնդում են, որ ընտրությունն ընկել է ջայլամի վրա՝ ռուսերեն «ջայլամ» և անգլերեն «strace» բառի համահունչ լինելու պատճառով։

Կարևոր է նաև, որ ptrace համակարգի զանգը և հետագծերը երբեք չեն ներառվել POSIX-ում, չնայած երկար պատմությանը և ներդրմանը Linux-ում, FreeBSD-ում, OpenBSD-ում և ավանդական Unix-ում:

Strace սարքը մի խոսքով. Piglet Trace

«Դուք չեք ակնկալում, որ դա հասկանաք» (Դենիս Ռիչի, մեկնաբանություն Unix-ի 6-րդ տարբերակում)

Վաղ մանկությունից ես տանել չեմ կարող սև արկղերին. ես չէի խաղում խաղալիքների հետ, այլ փորձում էի հասկանալ դրանց կառուցվածքը (մեծահասակները օգտագործում էին «կոտրվել» բառը, բայց չար լեզուներին չեն հավատում): Թերևս դա է պատճառը, որ առաջին Unix-ի և ժամանակակից բաց կոդով շարժման ոչ պաշտոնական մշակույթն այդքան մոտ է ինձ:

Այս հոդվածի նպատակների համար անհիմն է ապամոնտաժել 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.

Որպեսզի չխորանանք տիպիկ ծրագրի զանգերի անվերջ ցանկի մեջ, գրում ենք նվազագույն ծրագիր շուրջը 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 ./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 և համակարգային զանգերի առատություն, որոնք մեզ չեն հետաքրքրում:

Դուք կարող եք առանձնացնել ծրագրի ստանդարտ ելքային հոսքը և strace error ելքը՝ օգտագործելով -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. Երկու բան պարզ դարձավ. pause system call-ը անտեսում է ազդանշաններն առանց կարգավորիչների և, որ ավելի հետաքրքիր է, 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);
}

Այստեղ սկզբնական գործընթացը ստեղծում է երեխա գործընթաց, երկուսն էլ գրելով ստանդարտ ելքի վրա.

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

By default 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 ստանդարտի ներդրման իմաստով թեմայի նույնացուցիչի մասին չէ, այլ Linux-ում առաջադրանքների ժամանակացույցի կողմից օգտագործվող թվի մասին։ Վերջինիս տեսանկյունից չկան գործընթացներ կամ թելեր. կան առաջադրանքներ, որոնք պետք է բաշխվեն մեքենայի առկա միջուկների միջև:

Մի քանի թելերով աշխատելիս համակարգային զանգերը դառնում են չափազանց շատ.

$ 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 - պարզ և հուսալի գործիք: Բայց բացի համակարգային զանգերից, կարող են վրիպազերծվել ծրագրերի և օպերացիոն համակարգի գործունեության այլ ասպեկտներ: Օրինակ, այն կարող է հետևել դինամիկ կապակցված գրադարանների զանգերին: հետք, նրանք կարող են ուսումնասիրել օպերացիոն համակարգի աշխատանքը SystemTap и ֆետրասև թույլ է տալիս խորապես ուսումնասիրել ծրագրի կատարումը պերֆ. Այնուամենայնիվ, այդպես է strace -Պաշտպանության առաջին գիծը սեփական և այլոց ծրագրերի հետ կապված խնդիրների դեպքում, և ես շաբաթական առնվազն մի երկու անգամ օգտագործում եմ այն։

Մի խոսքով, եթե սիրում եք Յունիքս, կարդացեք man 1 strace և ազատ զգալ նայելու ձեր ծրագրերին:

Source: www.habr.com

Добавить комментарий