Linux жүйесіндегі Strace: тарихы, дизайны және қолданылуы

Linux жүйесіндегі Strace: тарихы, дизайны және қолданылуы

Unix тәрізді операциялық жүйелерде бағдарламаның сыртқы әлеммен және операциялық жүйемен байланысы функциялардың шағын жиынтығы – жүйелік шақырулар арқылы жүзеге асады. Бұл отладтау мақсаттары үшін процестермен орындалатын жүйелік қоңырауларға тыңшылық жасау пайдалы болуы мүмкін дегенді білдіреді.

Утилита Linux жүйесіндегі бағдарламалардың «интимдік өмірін» бақылауға көмектеседі 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 System V, Solaris және барған сайын танымал Linux жүйесіне ауыстырылды.

Бүгінгі strace тек Linux-ті қолдайды және оған сүйенеді ptrace, көптеген ұзартулармен толып кеткен.

Заманауи (және өте белсенді) жөндеуші strace - Дмитрий Левин. Оның арқасында утилита жүйелік қоңырауларға қате енгізу, архитектураның кең ауқымын қолдау және ең бастысы, тұмар. Бейресми дереккөздердің айтуынша, таңдау орыстың «түйеқұс» сөзі мен ағылшын тіліндегі «страце» сөзі арасындағы үндестікке байланысты түйеқұсқа түсті.

Linux, FreeBSD, OpenBSD және дәстүрлі Unix жүйелерінде ұзақ тарихы мен іске асырылуына қарамастан, ptrace жүйесінің шақыруы мен тресерлері ешқашан POSIX-ке қосылмағаны маңызды.

Қысқаша айтқанда Strace құрылғысы: Piglet Trace

«Сіз мұны түсінбейсіз» (Деннис Ричи, Unix бастапқы кодының 6 нұсқасындағы түсініктеме)

Мен кішкентай кезімнен қара жәшіктерге шыдай алмаймын: мен ойыншықтармен ойнамадым, бірақ олардың құрылымын түсінуге тырыстым (ересек адамдар «сынды» деген сөзді қолданды, бірақ жаман тілдерге сенбейді). Бірінші Unix-тің бейресми мәдениеті мен заманауи ашық коды қозғалысының маған соншалықты жақын екендігі сондықтан болуы мүмкін.

Осы мақаланың мақсаттары үшін ондаған жылдар бойы өскен 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 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 жүйесінде.

Негіздер: run 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 ./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 +++

Шығарылған қоңыраулар тізімінде қашып шыққан леп белгісіне назар аударыңыз: бұл пәрмен қабығы үшін қажет. Shell).

Менің 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
$

Ту -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 - қарапайым және сенімді құрал. Бірақ жүйелік қоңыраулардан басқа, бағдарламалар мен операциялық жүйенің жұмысының басқа аспектілерін жөндеуге болады. Мысалы, ол динамикалық байланысқан кітапханаларға қоңырауларды бақылай алады. ltrace, олар операциялық жүйенің жұмысын қарай алады Жүйені түрту и ftrace, және бағдарлама өнімділігін терең зерттеуге мүмкіндік береді Perf. Соған қарамастан, солай strace - өзімнің және басқа адамдардың бағдарламаларымен проблемалар туындаған жағдайда бірінші қорғаныс желісі және мен оны аптасына кемінде екі рет қолданамын.

Қысқасы, егер сіз Unixті жақсы көрсеңіз, оқыңыз man 1 strace және бағдарламаларыңызды қарауға болады!

Ақпарат көзі: www.habr.com

пікір қалдыру