Unix antzeko sistema eragileetan, programa batek kanpoko munduarekin eta sistema eragilearekin komunikazioa funtzio multzo txiki baten bidez gertatzen da - sistema-deiak. Horrek esan nahi du arazketa helburuetarako baliagarria izan daitekeela prozesuek exekutatzen ari diren sistema-deiak espiatzea.
Utilitate batek Linux-en programen "bizitza intimoa" kontrolatzen laguntzen dizu strace, artikulu honen gaia dena. Espioitza ekipamenduen erabileraren adibideek historia labur batekin batera doaz strace eta programa horien diseinuaren deskribapena.
Unix-en programen eta OS kernelaren arteko interfaze nagusia sistema deiak dira. sistema deiak, syscall-ak), programek kanpoko munduarekin duten elkarrekintza haien bitartez soilik gertatzen da.
Baina Unix-en lehen bertsio publikoan (6. bertsioa Unix, 1975) ez zegoen erabiltzaile-prozesuen portaeraren jarraipena egiteko modu erosoak. Arazo hau konpontzeko, Bell Labs hurrengo bertsiora eguneratuko da (7. bertsioa Unix, 1979) sistema deialdi berri bat proposatu zuen - ptrace.
ptrace arazgailu interaktiboetarako garatu zen batez ere, baina 80ko hamarkadaren amaieran (komertzialaren garaian). System V bertsioa 4) oinarri horretan, fokatutako araztaileak βsistema deien trazatzaileakβ agertu ziren eta oso erabiliak izan ziren.
Lehen strace-ren bertsio bera Paul Cronenburg-ek argitaratu zuen comp.sources.sun posta-zerrendan 1992an, utilitate itxi baten alternatiba gisa. trace Eguzkitik. Klona eta jatorrizkoa SunOSerako pentsatuta zeuden, baina 1994rako strace System V, Solaris eta gero eta ezagunagoa den Linuxera eraman zen.
Gaur egun, strace-k Linux bakarrik onartzen du eta horretan oinarritzen da ptrace, luzapen askorekin hazita.
Mantentzaile modernoa (eta oso aktiboa). strace - Dmitri Levin. Berari esker, utilitateak ezaugarri aurreratuak eskuratu zituen, hala nola sistema deietan akatsen injekzioa, arkitektura sorta zabalerako laguntza eta, batez ere, maskota. Iturri ez ofizialek diotenez, hautua ostrukaren gainean erori zen, errusiar "ostruka" hitzaren eta ingelesezko "strace" hitzaren arteko kontsonantziagatik.
Garrantzitsua da, halaber, ptrace sistema-deia eta trazatzaileak POSIX-en inoiz sartu ez izana, Linux, FreeBSD, OpenBSD eta Unix tradizionaletan historia eta inplementazio luzea izan arren.
Txikitatik, ezin ditut kutxa beltzak jasan: ez nintzen jostailuekin jolasten, haien egitura ulertzen saiatu nintzen (helduek "hautsi" hitza erabiltzen zuten, baina ez sinetsi hizkuntza gaiztoak). Beharbada horregatik dago niregandik hain hurbil lehen Unix-en eta kode irekiko mugimendu modernoaren kultura informala.
Artikulu honen ondorioetarako, zentzugabea da strace-ren iturburu kodea desmuntatzea, hamarkadetan zehar hazi dena. Baina ez luke sekreturik geratu behar irakurleentzat. Hori dela eta, horrelako programen funtzionamendu-printzipioa erakusteko, miniaturazko trazatzaile baten kodea emango dut - Txerrikumeen arrastoa (ptr). Ez daki ezer berezirik nola egin, baina gauza nagusia programaren sistema-deiak dira - ateratzen ditu:
Piglet Trace-k Linux sistemaren ehunka dei ezagutzen ditu (ikus. taula) eta x86-64 arkitekturan bakarrik funtzionatzen du. Hau nahikoa da hezkuntza helburuetarako.
Ikus dezagun gure klonaren lana. Linux-en kasuan, arazteek eta trazatzaileek goian esan bezala, ptrace sistema deia erabiltzen dute. Lehen argumentuan komandoen identifikatzaileak pasatuz funtzionatzen du, horietatik soilik behar ditugunak PTRACE_TRACEME, PTRACE_SYSCALL ΠΈ PTRACE_GETREGS.
Trazatzailea ohiko Unix estiloan hasten da: fork(2) ume-prozesu bat abiarazten du, eta horrek, aldi berean, erabiltzen du exec(3) aztergai den programa martxan jartzen du. Hemen sotiltasun bakarra erronka da ptrace(PTRACE_TRACEME) aurretik exec: Haurraren prozesuak gurasoen prozesuak kontrolatzea espero du:
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");
}
Guraso-prozesuak deitu beharko luke orain wait(2) haurraren prozesuan, hau da, ziurtatu traza modura aldatu dela:
/* 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");
Une honetan, prestaketak amaitu dira eta zuzenean jarraitu dezakezu sistema-deien jarraipena amaigabeko begizta batean.
deiaren ptrace(PTRACE_SYSCALL) ondorengoa bermatzen du wait gurasoak sistemaren deia exekutatu aurretik edo amaitu eta berehala osatuko du. Bi deien artean edozein ekintza egin dezakezu: ordezkatu deia ordezko batekin, aldatu argumentuak edo itzultzeko balioa.
Komandoari bi aldiz deitzea besterik ez dugu behar ptrace(PTRACE_GETREGS)erregistro-egoera lortzeko rax deiaren aurretik (sistemaren dei-zenbakia) eta berehala (itzultzeko balioa).
Egia esan, zikloa:
/* 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);
}
Hori da arrasto osoa. Orain badakizu non hasi hurrengo porturatzea DTrace Linux-en.
Oinarrizkoak: Strace exekutatzen duen programa bat martxan jartzea
Lehen erabilera kasu gisa strace, beharbada merezi du metodorik errazena aipatzea: exekutatzen ari den aplikazio bat abiaraztea strace.
Programa tipiko baten deien zerrenda amaigabean ez sakontzeko, idazten dugu gutxieneko programa inguruan 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;
}
Eraiki dezagun programa eta ziurtatu funtzionatzen duela:
$ gcc examples/write-simple.c -o write-simple
$ ./write-simple
write me to stdout
Eta azkenik, exekutatu dezagun strace kontrolpean:
Oso βhitzakβ eta ez oso hezitzailea. Hemen bi arazo daude: programaren irteera irteerarekin nahasten da strace eta interesatzen ez zaizkigun sistema-dei ugari.
Programaren irteera-korronte estandarra eta estrace errorearen irteera bereiz ditzakezu -o etengailua erabiliz, sistema-deien zerrenda argumentu-fitxategi batera birbideratzen duena.
Dei βgehigarrienβ arazoari aurre egiteko geratzen da. Demagun deiak bakarrik interesatzen zaizkigula write. Gakoa -e sistema-deiak iragaziko diren esamoldeak zehazteko aukera ematen du. Baldintza aukerarik ezagunena da, jakina, trace=*, zeinekin interesatzen zaizkigun deiak bakarrik utzi ditzakezu.
Aldi berean erabiltzen denean -o ΠΈ -e lortuko dugu:
$ 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 +++
Beraz, ikusten duzu, askoz errazagoa da irakurtzen.
Sistema-deiak ere ken ditzakezu, adibidez, memoria esleitzeari eta askatzeari dagozkionak:
$ 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 +++
Kontuan izan baztertutako deien zerrendan ihes egindako harridura-ikurra: hori komando shell-ak eskatzen du. shell).
Glibc-en nire bertsioan, sistema dei batek prozesua amaitzen du exit_group, ez tradizionala _exit. Hau da sistema-deiekin lan egiteko zailtasuna: programatzaileak lan egiten duen interfazeak ez du zerikusi zuzena sistema-deiekin. Gainera, aldizka aldatzen da ezarpenaren eta plataformaren arabera.
Oinarriak: prozesuarekin bat egitea
Hasieran, eraiki zen ptrace sistema deia strace, programa modu berezi batean exekutatzen denean bakarrik erabil daiteke. Baliteke muga honek zentzuzkoa iruditu 6 bertsioaren Unix-en garaian. Gaur egun, hori ez da nahikoa: batzuetan lan-programa baten arazoak ikertu behar dira. Adibide tipiko bat helduleku batean edo lotan blokeatutako prozesu bat da. Beraz, modernoa strace prozesuetan bat egin dezake hegalean.
$ ./write-sleep &
[1] 15329
write me
$ strace -p 15329
strace: Process 15329 attached
pause(
^Cstrace: Process 15329 detached
<detached ...>
Deiak blokeatu du programa pause. Ikus dezagun nola erreakzionatzen duen seinaleen aurrean:
$ 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 +++
Frozen programa abiarazi genuen eta erabiliz sartu ginen strace. Bi gauza argi geratu ziren: pausa sistemako deiak kudeatzailerik gabeko seinaleei jaramonik egiten ez die eta, interesgarriagoa dena, strace-k sistema deiak ez ezik, sarrerako seinaleak ere kontrolatzen ditu.
Adibidea: Haurren Prozesuen jarraipena
Dei baten bidez prozesuekin lan egitea fork - Unix guztien oinarria. Ikus dezagun nola funtzionatzen duen strace-k prozesu-zuhaitz batekin "hazkuntza" sinple baten adibidea erabiliz programak:
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);
}
Hemen jatorrizko prozesuak haur prozesu bat sortzen du, biak irteera estandarrera idatziz:
Banderak prozesu-zuhaitzaren jarraipena egiten laguntzen dizu -f, zein strace sistema-deiak kontrolatzen ditu haur-prozesuetan. Honek irteera-lerro bakoitzari gehitzen dio pid Sistemaren irteera egiten duen prozesua:
Bide batez, zein sistema-dei erabiltzen da prozesu berri bat sortzeko?
Adibidea: fitxategien bideak heldulekuen ordez
Fitxategien deskribatzaileak ezagutzea erabilgarria da, zalantzarik gabe, baina programa batek sartzen dituen fitxategi zehatzen izenak ere erabilgarriak izan daitezke.
Hurrengoa programa lerroa aldi baterako fitxategi batean idazten du:
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;
}
Dei arrunt batean strace sistema-deiari emandako deskribatzaile-zenbakiaren balioa erakutsiko du:
$ 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 +++
Bandera batekin -y Utilitateak deskribatzailea dagokion fitxategirako bidea erakusten du:
$ 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 +++
Adibidez: Fitxategien Sarbideen Jarraipena
Beste ezaugarri erabilgarria: fitxategi zehatz bati lotutako sistema-deiak soilik bistaratu. Hurrengoa programa argumentu gisa pasatako fitxategi arbitrario bati lerro bat eransten dio:
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;
}
Lehenespenez strace beharrezkoa ez den informazio asko erakusten du. Bandera -P argumentu batekin strace-k zehaztutako fitxategirako deiak soilik inprimatzea eragiten du:
$ 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 +++
Adibidea: Hari anitzeko programak
Erabilgarritasuna strace hari anitzeko lanarekin ere lagun dezake programa. Programa honek irteera estandarrean idazten du bi korronteetatik:
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);
}
Jakina, lokailuari agur berezi batekin bildu behar da -pthread bandera:
$ 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
$
bandera -f, prozesu arrunten kasuan bezala, prozesuaren pid-a gehituko du lerro bakoitzaren hasieran.
Berez, ez gara hari-identifikatzaile bati buruz ari POSIX Threads estandarraren ezarpenaren zentzuan, baizik eta Linux-en ataza-planifikatzaileak erabiltzen duen zenbakiaz. Azken honen ikuspuntutik, ez dago prozesu edo haririk - makinaren nukleoen artean banatu behar diren zereginak daude.
Hainbat haritan lan egiten duzunean, sistema-deiak gehiegi bihurtzen dira:
Bide batez, galderak. Zein sistema-dei erabiltzen da hari berri bat sortzeko? Zertan desberdintzen da hari-dei hau prozesu-deiarekin?
Klase magistrala: prozesu pila sistema-dei baten unean
Duela gutxi agertu den bat strace gaitasunak - sistema-deiaren unean funtzio-deien pila bistaratzea. Sinplea Adibidez:
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;
}
Jakina, programaren irteera oso bolumentsua bihurtzen da, eta, banderaz gain -k (deien pila bistaratzea), zentzuzkoa da sistemaren deiak izenaren arabera iragaztea:
$ 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 +++
Klase magistrala: akatsen injekzioa
Eta beste ezaugarri berri eta oso erabilgarria: akatsen injekzioa. Hemen programa, irteerako korrontean bi lerro idatziz:
Interesgarria da zein errore itzultzen diren guztiak erronkak write, perroren atzean ezkutatzen den deia barne. Zentzuzkoa da lehen deietarako errore bat itzultzea:
$ 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 +++
Edo bigarrena:
$ 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 +++
Ez da beharrezkoa errore mota zehaztea:
$ 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 +++
Beste bandera batzuekin batera, fitxategi zehatz baterako sarbidea "apurtu" dezakezu. Adibidea:
$ 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 +++
Akatsen injekzioaz gain, ko ahal deiak egitean edo seinaleak jasotzean atzerapenak sartu.
afterword
Erabilgarritasuna strace - Tresna sinple eta fidagarria. Baina sistema-deiez gain, programen eta sistema eragilearen funtzionamenduaren beste alderdi batzuk arazketa daitezke. Adibidez, dinamikoki lotuta dauden liburutegietarako deien jarraipena egin dezake. ltraza, sistema eragilearen funtzionamendua azter dezakete SistemaTap ΠΈ aztarna, eta programaren errendimendua sakon ikertzeko aukera ematen du perfektua. Hala ere, hala da strace - Lehen defentsa-lerroa nire eta besteen programekin arazoen kasuan, eta astean gutxienez pare bat aldiz erabiltzen dut.
Laburbilduz, Unix maite baduzu, irakurri man 1 strace eta anima zaitez zure programetara begiratzera!