Linux ичинде Strace: тарыхы, дизайн жана колдонуу

Linux ичинде Strace: тарыхы, дизайн жана колдонуу

Unix окшош операциондук системаларда программанын тышкы дүйнө жана операциялык система менен байланышы функциялардын кичинекей топтому – системалык чакыруулар аркылуу ишке ашат. Бул мүчүлүштүктөрдү оңдоо максатында процесстер тарабынан аткарылып жаткан системалык чалууларды чалгындоо пайдалуу болушу мүмкүн дегенди билдирет.

Утилита Linux'та программалардын "интимдик жашоосун" көзөмөлдөөгө жардам берет strace, бул макаланын темасы болуп саналат. Шпиондук жабдууларды колдонуунун мисалдары кыскача тарых менен коштолот strace жана мындай программалардын дизайнын баяндоо.

ыраазы

Түрлөрдүн келип чыгышы

Unixтеги программалар менен ОС ядросунун ортосундагы негизги интерфейс бул системалык чалуулар. системалык чалуулар, системалар), программалардын тышкы дүйнө менен өз ара аракеттенүүсү алар аркылуу гана ишке ашат.

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

ptrace биринчи кезекте интерактивдүү мүчүлүштүктөрдү оңдоочулар үчүн иштелип чыккан, бирок 80-жылдардын аягында (коммерциялык доордо) Системанын V релизи 4) ошонун негизинде тар чөйрөдөгү мүчүлүштүктөрдү оңдоочулар — системалык чалууларды издөөчүлөр пайда болуп, кеңири колдонула баштады.

биринчи Ошол эле strace версиясы Пол Кроненбург тарабынан 1992-жылы comp.sources.sun почта тизмесинде жабык утилитага альтернатива катары жарыяланган. trace Күндөн. Клон да, оригинал да SunOS үчүн арналган, бирок 1994-жылга чейин strace System V, Solaris жана барган сайын популярдуу болгон Linux'ка көчүрүлгөн.

Бүгүнкү күндө strace Linuxту гана колдойт жана ошого таянат ptrace, көптөгөн кеңейтүүлөр менен өскөн.

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

Linux, FreeBSD, OpenBSD жана салттуу Unixте узак тарыхка жана ишке ашырууга карабастан, ptrace тутумунун чалуулары жана трекерлери эч качан POSIXке кошулбаганы маанилүү.

Кыскача айтканда 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.

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 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 көзөмөлү астында иштетели:

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

Мисал: File Access Tracking

Дагы бир пайдалуу өзгөчөлүк: белгилүү бир файл менен байланышкан тутум чалууларын гана көрсөтүү. Кийинки программа аргумент катары берилген ыктыярдуу файлга сапты кошот:

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, кадимки процесстердегидей эле, процесстин пидин ар бир саптын башына кошот.

Албетте, биз 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, алар операциялык системанын иштешин карай алышат SystemTap и ftrace, жана программанын аткарылышын терең изилдөөгө мүмкүндүк берет перф. Ошого карабастан, ошондой strace - өзүмдүн жана башка адамдардын программалары менен көйгөйлөр болгон учурда биринчи коргонуу линиясы, мен аны жумасына бир-эки жолудан кем эмес колдоном.

Кыскасы, эгер сиз Unixти сүйсөңүз, анда окуңуз man 1 strace жана программаларыңызга көз чаптырыңыз!

Source: www.habr.com

Комментарий кошуу