
Unix-հօխոր համակարգերում ծրագրերի և օպերացիոն համակարգի միջև հաղորդակցությունը կատարվում է մի փոքր ֆունկցիաների խումբի միջոցով՝ համակարգային ծածականների: Այդ հետեւութաբերությամբ, սխալների հետազոտման նպատակով օգտակար է մուտք գործել կատարվող գործընթացների համակարգային ծածականներին:
Linux-ում ծրագրերի «հանդիպումների» հետեւում է օգնական ծրագիր՝ strace, որը նվիրաբերված է այս հոդվածին: «քրտնակության» սարքավորումների օգտագործման օրինակների հետ միասին ներկայացվեցին կարճ պատմություն strace և նման ծրագրերի սարքի նկարագրություն։
Բովանդակություն
Աքաղաղների ծագումը
Unix-ի համար ծրագրերի և օպերացիոն համակարգի միջեւ գլխավոր կապը համակարգային ծածականներն են (անգլ. system calls, syscalls)։ Ծրագրերի և արտաքին աշխարհ մուտք հասնելու միջեւ կապը բացառապես դրանց միջոցով է:
Սակայն Unix-ի առաջին հանրային տարբերակում (, 1975 թվական) հարմար միջոցներ օգտագործողի գործընթացների վարքի հետեւելու համար չկային: Այս խնդիրը լուծելու համար Bell Labs-ն հաջորդ տարբերակում (, 1979 թվական) առաջարկեց նոր համակարգային ծածական՝ ptrace.
Ptrace-ն առաջին հերթին մշակվում էր ինտերակտիվ հեռախոսանոցների համար, սակայն 80-ականների վերջում (կոմերցիոն эпохայում ) այդ հիման վրա ի հայտ եկան և լայնորեն տարածում գտան արտոնված հեռախոսանոցներ՝ համակարգային ծածականների հետեւում:
strace-ի նման տարբերակը Պոլ Քրոնենբուրգի կողմից հրապարակվեց comp.sources.sun հաշվարկացացում 1992 թվականին փակ նյութերի փոխարինման։ Ինչպես clone-ն, այնպես էլ սկզբնական տարբերակը նախատեսված էին SunOS-ի համար, բայց 1994 թվականին կուտակվել էր System V, Solaris և ճանաչող Linux: Այսօր strace-ը աջակցում է միայն Linux-ին և հենվում է հոգնած strace , որը բազմաթիվ ընդլայնումներով զուգորդվել է:
Արդենակա (և բավական ակտիվ) մատաղային ելակետ ptraceԴմիտրի Լևին
, շնորհիվ նրա, այս ծրագիրը ձեռք բերեց առաջընթաց հնարավորություններ, ինչպիսիք են համակարգային ծածականներում սխալների ներմուծումը, տարբեր ճարտարապետությունների տեղադրում և, ամենակարևորը, strace — Չեն պաշտոնական աղբյուրները ասում, որ ընտրությունը հաջորդեց ստրուճին՝ ռուսերեն «ստրուճ» և անգլերեն "strace"-ի համահունչ լինելու պատճառով։ "Դուք ակնկալում չեք, թե պետք է հասկանալ դա" (Դենիս Ռիչի, մեկնաբանություն Version 6 Unix-ի աղբյուրային կոդում)
You are not expected to understand this (Դենիսի Ռիչի, մեկնաբանություն Version 6 Unix-ի աղբյուրային կոդում)
strace ծրագիրը երկու խոսքով՝ Piglet Trace
"Դուք ակնկալում չեք, թե պետք է հասկանալ դա" (Դենիս Ռիչի, մեկնաբանություն Version 6 Unix-ի աղբյուրային կոդում)
Մեր մանկության ժամանակներից ես չսիրեցի մութ տուփերը. որքան էլ ինձ ասում էին, որ խաղալիքների հետ խաղացել եմ, ես մշտապես փորձում էի հասկանալ նրանց կառուցվածքը (մեծահասակները հաճախ օգտագործում էին «կոտրել», սակայն մի հավատացեք չարի լեզուներին). հավանաբար, այդ պատճառով էլ ինձ մոտեցնում է առաջին Unix-ի և ժամանակակից open-source շարժման ոչ ֆորմալ մշակույթը։
Այս հոդվածի շրջանակներում քննարկելը 20 տարիների ընթացքում բարելաված strace-ի स्रोतային կոդը անհոգ են։ Բայց նաև գաղտնիություններ չպետք է մնան ընթերցողների համար։ Այսպիսով, 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 terminatedPiglet Trace-ն ճանաչում է մոտավորապես հարյուր համակարգային կանչեր Linux-ում (տես ) և աշխատում է միայն x86-64 արխիտեկուրայի վրա։ Կրթության նպատակով այսը՝ բավարար է։
Եկեք վերլուծենք մեր կրկնողի աշխատանքը։ Linux-ի դեպքում debug-երի և trace-երի համար օգտագործվում է, ինչպես նշվել է վերևում, համակարգային կանչ ptrace։ Այն աշխատում է ձեռք բերելով առաջին արգումենտով կարգավորման ինդենտիֆիկատորների, որոնցից մեզ անհրաժեշտ են միայն PTRACE_TRACEME, PTRACE_SYSCALL և PTRACE_GETREGS.
Trace-երի աշխատանքը սկսվում է սովորական Unix ոճով։ fork(2) սկսում է երեխանական գործընթացը, իսկ այն իր հերթին ներմուծում է դիտվող ծրագրի միջոցով exec(3) ։ Միակ նրբությունը այստեղ այն է, որ կանչում է ptrace(PTRACE_TRACEME) առաջին հերթին։ պրոցես-երեխան ակնկալում է, որ պրոցես-հայրն իր հետևում կլինի։ execpid_t child_pid = fork(); switch (child_pid) { case -1: err(EXIT_FAILURE, "fork"); case 0: /* Երեխան այստեղ */ /* Թրանսիրկային մոդը պետք է միացվի։ Հայրը պետք է սպասի(2) որ դա տեղի ունենա։ */ ptrace(PTRACE_TRACEME, 0, NULL, NULL); /* Ճողարձակի այն ծրագրի հետ, որը պետք է գործի */ 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, ®isters) == -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, ®isters) == -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);
}Սա է ամբողջ трасեր : Հիմա դուք գիտեք, որտեղից սկսել հաջորդ պորտավորումը Linux-ում:
Արդյունքներ՝ strace-ի ղեկավարման ներքո ծրագրի մեկնարկում
Ինչպես առաջին օրինակ օգտագործման strace, գուցե, արժե բերել ամենահեշտ եղանակը՝ գործարկելով ծրագիրը կառավարելու տակ strace.
Որպեսզի չգրավվենք սովորական ծրագրի անփոփոխ զանգի ցանկում, գիրեք ճակատի շուրջ write:
int main(int argc, char *argv[])
{
char str[] = "write me to stdoutn";
/* write(2)-ը պարզ հարակից է համակարգային զանգին, այնպես որ այն պետք է հեշտ լինի
* գտնել համակարգային զանգի տրասում. */
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 (Այդպիսի ֆայլ կամ մաէմբ չկան)
access("/etc/ld.so.preload", R_OK) = -1 ENOENT (Այդպիսի ֆայլ կամ մաէմբ չկան)
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 (Այդպիսի ֆայլ կամ մաէմբ չկան)
openat(AT_FDCWD, "/lib/x86_64-linux-gnu/libc.so.6", O_RDONLY|O_CLOEXEC) = 3
read(3, "177ELF2113 3 >