Strace Linuxban: történelem, tervezés és használat
A Unix-szerű operációs rendszerekben a program kommunikációja a külvilággal és az operációs rendszerrel egy kis függvénykészleten – rendszerhívásokon – keresztül történik. Ez azt jelenti, hogy hibakeresési célokra hasznos lehet a folyamatok által végrehajtott rendszerhívások után kémkedni.
Egy segédprogram segít nyomon követni a programok „intim életét” Linuxon strace, amely ennek a cikknek a témája. A kémfelszerelések használatának példáit rövid történet kíséri strace és az ilyen programok tervezésének leírása.
A programok és az operációs rendszer kernelje közötti fő interfész a Unixban a rendszerhívások. rendszerhívások, syscalls), a programok interakciója a külvilággal kizárólag rajtuk keresztül történik.
De a Unix első nyilvános verziójában (6-es Unix verzió, 1975) nem voltak kényelmes módszerek a felhasználói folyamatok viselkedésének nyomon követésére. A probléma megoldása érdekében a Bell Labs frissít a következő verzióra (7-es Unix verzió, 1979) új rendszerhívást javasolt - ptrace.
A ptrace-t elsősorban interaktív hibakeresőkhöz fejlesztették ki, de a 80-as évek végére (a kereskedelmi korszakban) System V 4. kiadás) ezen az alapon megjelentek és széles körben elterjedtek a szűk fókuszú hibakeresők – rendszerhívás-nyomkövetők.
Első A strace ugyanazt a verzióját Paul Cronenburg tette közzé a comp.sources.sun levelezőlistán 1992-ben egy zárt segédprogram alternatívájaként trace a Suntól. Mind a klónt, mind az eredetit SunOS-re szánták, de 1994-re strace a System V-re, a Solarisra és az egyre népszerűbb Linuxra portolták.
Ma a strace csak a Linuxot támogatja, és ugyanerre támaszkodik ptrace, sok toldással benőtt.
Modern (és nagyon aktív) karbantartó strace - Dmitrij Levin. Neki köszönhetően a segédprogram olyan fejlett funkciókat szerzett meg, mint a hibabefecskendezés a rendszerhívásokba, az architektúrák széles skálájának támogatása, és ami a legfontosabb, kabala. Nem hivatalos források azt állítják, hogy a választás az orosz „strucc” szó és az angol „strace” szó közötti összhang miatt esett a struccra.
Az is fontos, hogy a ptrace rendszerhívás és a nyomkövetők soha nem kerültek bele a POSIX-be, annak ellenére, hogy a Linux, FreeBSD, OpenBSD és hagyományos Unix rendszerekben hosszú története és megvalósítása van.
Strace eszköz dióhéjban: Piglet Trace
"Ezt nem várható el, hogy megértse" (Dennis Ritchie, megjegyzés a 6-os Unix-forráskódban)
Gyerekkorom óta nem bírom a fekete dobozokat: nem játszottam a játékokkal, hanem próbáltam megérteni a szerkezetüket (a felnőttek a „törött” szót használták, de ne higgyenek a gonosz nyelveknek). Talán ezért is áll olyan közel hozzám az első Unix informális kultúrája és a modern nyílt forráskódú mozgalom.
E cikk szempontjából ésszerűtlen a strace forráskódjának szétszedése, amely évtizedek óta nőtt. De nem szabad titkot hagyni az olvasók számára. Ezért az ilyen strace programok működési elvének bemutatásához megadom egy miniatűr nyomkövető kódját - Malac nyoma (ptr). Nem tudja, hogyan kell semmi különöset tenni, de a legfontosabb a program rendszerhívásai - a kimenet:
A Piglet Trace körülbelül több száz Linux rendszerhívást ismer fel (lásd. táblázat) és csak x86-64 architektúrán működik. Ez oktatási célokra elegendő.
Nézzük meg klónunk munkáját. Linux esetén a hibakeresők és nyomkövetők, mint fentebb említettük, a ptrace rendszerhívást használják. Úgy működik, hogy az első argumentumban átadja a parancsazonosítókat, amelyekre csak szükségünk van PTRACE_TRACEME, PTRACE_SYSCALL и PTRACE_GETREGS.
A nyomkövető a szokásos Unix stílusban indul: fork(2) elindít egy gyermekfolyamatot, amely viszont használja exec(3) elindítja a vizsgált programot. Az egyetlen finomság itt a kihívás ptrace(PTRACE_TRACEME) előtt exec: A gyermekfolyamat elvárja, hogy a szülő folyamat figyelje:
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");
}
A szülő folyamatnak most meg kell hívnia wait(2) a gyermekfolyamatban, azaz győződjön meg arról, hogy megtörtént a nyomkövetési módra váltás:
/* 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");
Ezen a ponton az előkészületek befejeződtek, és közvetlenül folytathatja a rendszerhívások követését egy végtelen ciklusban.
hívás ptrace(PTRACE_SYSCALL) garantálja, hogy a későbbi wait szülő befejezi vagy a rendszerhívás végrehajtása előtt, vagy közvetlenül a befejezése után. Két hívás között bármilyen műveletet végrehajthat: helyettesítheti a hívást egy másikkal, módosíthatja az argumentumokat vagy a visszatérési értéket.
Csak kétszer kell meghívnunk a parancsot ptrace(PTRACE_GETREGS)hogy megkapja a regiszter állapotát rax a hívás előtt (rendszerhívószám) és közvetlenül utána (visszatérési érték).
Valójában a ciklus:
/* 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);
}
Ez az egész nyomkövető. Most már tudja, hol kezdje a következő portolást DTrace Linuxon.
Alapok: strace futtató program futtatása
Első használati esetként strace, talán érdemes a legegyszerűbb módszert említeni - egy futó alkalmazás elindítását strace.
Annak érdekében, hogy ne mélyedjünk el egy tipikus program hívásainak végtelen listájában, írunk minimális program körül 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;
}
Építsük fel a programot, és győződjünk meg a működéséről:
$ gcc examples/write-simple.c -o write-simple
$ ./write-simple
write me to stdout
Nagyon „szavas” és nem túl tanulságos. Itt két probléma van: a program kimenete keveredik a kimenettel strace és rengeteg olyan rendszerhívás, amelyek nem érdekelnek minket.
A program szabványos kimeneti adatfolyamát és a strace error kimenetét szétválaszthatja a -o kapcsolóval, amely átirányítja a rendszerhívások listáját egy argumentumfájlba.
Marad az „extra” hívások problémája. Tegyük fel, hogy minket csak a hívások érdekelnek write. Kulcs -e lehetővé teszi olyan kifejezések megadását, amelyek alapján a rendszerhívások szűrésre kerülnek. A legnépszerűbb feltételes lehetőség természetesen a trace=*, amellyel csak a minket érdeklő hívásokat hagyhatja el.
Egyidejű használat esetén -o и -e kapunk:
$ 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 +++
Szóval, látod, sokkal könnyebben olvasható.
Eltávolíthatja a rendszerhívásokat is, például a memóriafoglalással és -felszabadítással kapcsolatosakat:
$ 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 +++
Jegyezze meg a kihagyott felkiáltójelet a kizárt hívások listájában: erre a parancshéjnak szüksége van. héj).
Az én glibc verziómban egy rendszerhívás leállítja a folyamatot exit_group, nem hagyományos _exit. Ez a rendszerhívásokkal való munka nehézsége: az interfész, amellyel a programozó dolgozik, nem kapcsolódik közvetlenül a rendszerhívásokhoz. Ezenkívül rendszeresen változik a megvalósítástól és a platformtól függően.
Alapok: menet közbeni csatlakozás a folyamathoz
Kezdetben a ptrace rendszerhívás, amelyre épült strace, csak a program speciális módban történő futtatásakor használható. Ez a korlátozás ésszerűnek hangozhatott a Unix 6-os verziójának idejében. Manapság ez már nem elég: időnként ki kell vizsgálni egy működő program problémáit. Tipikus példa egy fogantyún blokkolt folyamat vagy alvás. Ezért modern strace menet közben csatlakozhat a folyamatokhoz.
$ ./write-sleep &
[1] 15329
write me
$ strace -p 15329
strace: Process 15329 attached
pause(
^Cstrace: Process 15329 detached
<detached ...>
A programot hívás blokkolta pause. Lássuk, hogyan reagál a jelzésekre:
$ 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 +++
Elindítottuk a fagyasztott programot és segítségével csatlakoztunk hozzá strace. Két dolog vált világossá: a pause rendszerhívás figyelmen kívül hagyja a kezelők nélküli jeleket, és ami még érdekesebb, a strace nem csak a rendszerhívásokat, hanem a bejövő jeleket is figyeli.
Példa: Gyermekfolyamatok nyomon követése
Folyamatokkal való munka híváson keresztül fork - az összes Unix alapja. Nézzük meg, hogyan működik a strace egy folyamatfával egy egyszerű „tenyésztés” példáján. programok:
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);
}
Itt az eredeti folyamat utódfolyamatot hoz létre, mindkettő szabványos kimenetre ír:
A zászló segít a teljes folyamatfa nyomon követésében -f, melyik strace figyeli a rendszerhívásokat a gyermekfolyamatokban. Ez hozzáad minden kimeneti sort pid folyamat, amely a rendszer kimenetét hozza létre:
Egyébként milyen rendszerhívással hozunk létre új folyamatot?
Példa: leírók helyett fájl útvonalak
A fájlleírók ismerete mindenképpen hasznos, de a program által elért konkrét fájlok neve is jól jöhet.
a következő program beírja a sort az ideiglenes fájlba:
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;
}
Normál hívás közben strace megmutatja a rendszerhívásnak átadott leíró szám értékét:
$ 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 +++
Egy zászlóval -y A segédprogram megmutatja annak a fájlnak az elérési útját, amelyhez a leíró tartozik:
$ 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 +++
Példa: Fájlhozzáférés nyomon követése
Egy másik hasznos funkció: csak az adott fájlhoz tartozó rendszerhívások megjelenítése. Következő program egy sort hozzáfűz egy argumentumként átadott tetszőleges fájlhoz:
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;
}
Alapértelmezésben strace sok felesleges információt jelenít meg. Zászló -P egy argumentummal a strace csak a megadott fájl hívásait nyomtatja ki:
$ 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 +++
Példa: Többszálú programok
Hasznosság strace többszálas munkánál is segíthet a program. A következő program szabványos kimenetre ír két adatfolyamból:
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);
}
Természetesen a linker külön üdvözlésével kell összeállítani - a -pthread jelzővel:
$ 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
$
zászló -f, mint a reguláris folyamatok esetében, minden sor elejéhez hozzáadja a folyamat pid-jét.
Természetesen nem a POSIX Threads szabvány megvalósításának értelmében vett szálazonosítóról beszélünk, hanem a Linuxban a feladatütemező által használt számról. Ez utóbbi szempontjából nincsenek folyamatok vagy szálak - vannak feladatok, amelyeket el kell osztani a gép elérhető magjai között.
Ha több szálban dolgozik, a rendszerhívás túl sok lesz:
Apropó, kérdések. Milyen rendszerhívással hozunk létre új szálat? Miben különbözik ez a szálak felhívása a folyamatok felhívásától?
Mesterosztály: folyamatverem rendszerhíváskor
Az egyik nemrég jelent meg strace képességek – a függvényhívások halmazának megjelenítése a rendszerhívás idején. Egyszerű példa:
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;
}
Természetesen a program kimenete nagyon terjedelmes lesz, és a zászló mellett -k (hívás verem megjelenítése), célszerű a rendszerhívásokat név szerint szűrni:
$ 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 +++
Mesterosztály: hibabefecskendezés
És még egy új és nagyon hasznos funkció: hibabefecskendezés. Itt program, két sort ír a kimeneti adatfolyamba:
Érdekes, hogy milyen hibákat adnak vissza minden kihívások write, beleértve a perror mögé rejtett hívást is. Csak az első hívásnál van értelme hibát visszaadni:
$ 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 +++
Vagy a második:
$ 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 +++
Nem szükséges megadni a hiba típusát:
$ 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 +++
Más jelzőkkel kombinálva „megszakíthatja” a hozzáférést egy adott fájlhoz. Példa:
$ 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 +++
A hibabefecskendezés mellett tud késleltetést okoz hívások kezdeményezése vagy jelek fogadása során.
utószó
Hasznosság strace - egyszerű és megbízható eszköz. De a rendszerhívásokon kívül a programok és az operációs rendszer működésének egyéb aspektusai is hibakereshetők. Például nyomon tudja követni a dinamikusan összekapcsolt könyvtárak hívását. ltnyom, utánanézhetnek az operációs rendszer működésének SystemTap и ftrace, és lehetővé teszi a program teljesítményének alapos vizsgálatát perf. Ennek ellenére az strace - az első védelmi vonal saját és mások programjaival kapcsolatos problémák esetén, és hetente legalább pár alkalommal használom.
Röviden: ha szereted a Unixot, olvass man 1 strace és bátran kukkants be a programjaidba!