
Í stýrikerfum sem líkjast Unix á forrit samskipti við umheiminn og stýrikerfið í gegnum lítið safn af föllum sem kallast kerfisköll. Þess vegna getur verið gagnlegt, til að njósna um kerfisköll sem ferlar framkvæma, til að framkvæma villuleit.
Gagnsemi hjálpar til við að fylgjast með „nánu lífi“ forrita á Linux strace, sem er efni þessarar greinar. Dæmi um notkun „njósnabúnaðar“ fylgja stutt saga strace og lýsingu á uppbyggingu slíkra áætlana.
efni
Uppruni tegunda
Helsta viðmótið milli forrita og kjarna stýrikerfisins í Unix eru kerfisköll (eng. kerfisköll, syscals), samskipti forrita við umheiminn eiga sér eingöngu stað í gegnum þau.
En í fyrstu opinberu útgáfunni af Unix (, 1975) voru engar þægilegar leiðir til að fylgjast með hegðun notendaferla. Til að leysa þetta vandamál bætti Bell Labs við eftirfarandi útgáfu (, 1979) lagði til nýtt kerfiskall - ptrace.
Ptrace var fyrst og fremst þróað fyrir gagnvirka villuleitarforrit, en í lok níunda áratugarins (á tímum viðskipta ) á þessum grundvelli komu fram þröngt einbeittar villuleitarforrit – kerfiskallrakningar – og urðu mikið notuð.
Sama útgáfa af strace var birt af Paul Kronenburg á póstlistanum comp.sources.sun árið 1992 sem valkostur við einkaleyfisverndaða gagnsemi. trace frá Sun. Bæði klónið og upprunalega útgáfan voru ætluð fyrir SunOS, en árið 1994 strace var flutt fyrir System V, Solaris og sífellt vinsælla Linux.
Í dag styður strace aðeins Linux og byggir á því sama. ptrace, gróinn mörgum viðbyggingum.
Nútímalegur (og mjög virkur) viðhaldsmaður strace - Þökk sé honum öðlaðist veitan háþróaða getu eins og villuleit í kerfisköllum, stuðning við fjölbreytt úrval arkitektúra og, síðast en ekki síst, Óopinberar heimildir herma að strúturinn hafi verið valinn vegna líktar hljóðs milli rússneska orðsins „ostrich“ og enska orðsins „strace“.
Það er einnig þýðingarmikið að kerfiskallið ptrace og rekjaforrit voru aldrei innifalin í POSIX, þrátt fyrir langa sögu þeirra og innleiðingu í Linux, FreeBSD, OpenBSD og hefðbundnu Unix.
Strace-tækið í hnotskurn: Piglet Trace
„Það er ekki búist við að þú skiljir þetta“ (Dennis Ritchie, athugasemd í frumkóða útgáfu 6 fyrir Unix)
Frá unga aldri hef ég hatað svarta kassa: Ég lék mér ekki með leikföng heldur reyndi að átta mig á því hvernig þau virkuðu (fullorðnir notuðu orðið „tölvuþrjótar“ en trúið ekki þessu leiðinlega slúðri). Kannski er það þess vegna sem ég er svo hrifinn af óformlegri menningu fyrstu Unix-stýrikerfanna og nútíma hreyfingu opins hugbúnaðar.
Það er ekki raunhæft að greina frumkóðann fyrir strace, sem hefur orðið traustari með áratugunum, innan ramma þessarar greinar. En það ættu ekki að vera nein leyndarmál fyrir lesendur. Þess vegna, til að sýna fram á virkni slíkra strace forrita, mun ég birta kóðann fyrir smækkaðan rekjaforrit: (ptr). Það getur ekki gert neitt sérstakt, en aðalatriðið — kerfisköll forritsins — birtist:
$ 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 þekkir um hundrað Linux kerfisköll (sjá ) og virkar aðeins á x86-64 arkitektúrnum. Þetta nægir í fræðsluskyni.
Við skulum skoða hvernig klóninn okkar virkar. Í Linux nota villuleitarforrit og rakningarforrit kerfiskallið ptrace, eins og áður hefur komið fram. Það virkar með því að senda skipunarauðkenni sem fyrsta færibreytu, sem við þurfum aðeins PTRACE_TRACEME, PTRACE_SYSCALL и PTRACE_GETREGS.
Rekjaforritið byrjar í venjulegum Unix stíl: fork(2) ræsir undirferli, sem aftur notar exec(3) ræsir forritið sem verið er að rannsaka. Eina fínleikinn hér er kallið ptrace(PTRACE_TRACEME) áður exec: undirferlið býst við að foreldraferlið fylgist með því:
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");
}Yfirferlið verður nú að kalla wait(2) í undirferlinu, þ.e. til að tryggja að skipt hafi verið yfir í rakningarstillingu:
/* 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");Á þessum tímapunkti er undirbúningnum lokið og við getum byrjað að rekja kerfisköll í endalausri lykkju.
Hringdu ptrace(PTRACE_SYSCALL) tryggir að síðari wait Foreldrið mun annað hvort ljúka áður en kerfiskallinu er framkvæmt eða strax eftir að því lýkur. Á milli kallanna tveggja er hægt að framkvæma hvaða aðgerð sem er: skipta kallinu út fyrir valkost, breyta færibreytum eða skilagildi.
Allt sem við þurfum að gera er að kalla skipunina tvisvar. ptrace(PTRACE_GETREGS)til að fá skráningarstöðuna rax fyrir símtalið (kerfiskallsnúmer) og strax á eftir (skilagildi).
Reyndar hringrásin:
/* 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);
}Þetta er allur rekjanleiki. Nú veistu hvar þú átt að byrja næstu höfn. á Linux.
Grunnatriði: Að keyra forrit undir spennu
Sem fyrsta dæmi um notkun strace, kannski er það þess virði að gefa einföldustu leiðina - að keyra forritið undir stjórn strace.
Til að forðast að þurfa að grafa í gegnum endalausan lista af köllum í dæmigerðu forriti, munum við skrifa í kring 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;
}
Við skulum þýða forritið og ganga úr skugga um að það virki:
$ gcc examples/write-simple.c -o write-simple
$ ./write-simple
write me to stdoutOg að lokum, keyrum þetta undir ströngu:
$ 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, "177ELF2113 3 >