
Unix-alapú operációs rendszerekben a programok és a külső világ, valamint az operációs rendszer közötti kommunikáció egy kis funkcióhalmazon — rendszerhívásokon keresztül történik. Ezért hibakeresési célokra hasznos lehet figyelni a végrehajtott folyamatokat a rendszerhívásokon.
A Linux programok "intim életének" nyomon követésére a következő segédprogram segít strace, aminek ez a cikk szenteltetett. Az „kémes” berendezések használatának példáihoz egy rövid történet is járul. strace és az ilyen programok eszközeinek leírása.
Tartalom
A fajok eredete
A programok és az Unix operációs rendszer magja közötti fő interfész a rendszerhívások (angolul system calls, syscalls), a programok külső világgal való kölcsönhatása kizárólag ezen keresztül valósul meg.
Azonban az első nyilvános Unix verzióban (, 1975) kényelmes módok nem álltak rendelkezésre a felhasználói folyamatok viselkedésének nyomon követésére. E problémára a Bell Labs a következő verzióra (, 1979) új rendszerhívást javasolt — ptrace.
A ptrace-t elsősorban interaktív hibakeresők számára fejlesztették ki, de a 80-as évek végére (a már kereskedelmi ) ezen alapul új, speciális hibakeresők — a rendszerhívások nyomozói — jelentek meg, és széles körben elterjedtek.
A strace ezen verzióját Paul Kronenburg publikálta a comp.sources.sun hírlevélben 1992-ben, mint alternatíváját a Sun zárt segédprogramjának. trace Mind a klón, mind az eredeti a SunOS-hoz készült, de 1994-re strace portálták a System V-re, Solarisra, és a népszerű Linuxra.
Ma már a strace csak Linuxot támogat és az összes ptrace, amely számos kiterjesztéssel bővült.
A modern (és meglehetősen aktív) karbantartó strace — . Kizárólag ennek köszönhetően a segédprogram fejlettebb funkciókkal gazdagodott, mint a rendszerhívások hibáinak injektálása, széles architektúrák támogatása és, ami a legfontosabb, Nem hivatalos források szerint a választás a struccra esett, mivel a "strucc" orosz szó és az angol "strace" hasonlósága miatt.
Fontos megemlíteni, hogy a ptrace rendszerhívás és a nyomozók nem kerültek be a POSIX szabványba, annak ellenére, hogy hosszú múltra tekintenek vissza, és megvalósításuk elérhető Linuxban, FreeBSD-ben, OpenBSD-ben és a hagyományos Unix rendszerekben.
A strace röviden: Piglet Trace
"Nem várják el, hogy megértsd ezt" (Dennis Ritchie, megjegyzés a Version 6 Unix forráskódjában)
Gyermekkorom óta ki nem állhatom a fekete dobozokat: a játékokkal nem játszottam, hanem próbáltam megérteni, hogyan működnek (a felnőttek azt mondták, hogy "törtem őket", de ne higgy a gonosz nyelveknek). Talán ezért érzem magam közel a korai Unix informális kultúrájához és a modern nyílt forráskódú mozgalomhoz.
Ennek a cikknek a keretein belül nem érdemes a strace évtizedek alatt kifejlesztett forráskódját elemezni. De titkok sem maradhatnak az olvasók előtt. Ezért, hogy bemutassam az ilyen strace programok működési elvét, elővezetem egy apró nyomozó kódját — (ptr). Nem csinál különösebb dolgokat, de a lényeg, hogy a program rendszerszintű hívásait kiírja:
$ 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 terminatedA Piglet Trace nagyjából száz Linux rendszerhívást ismer fel (lásd a ). Ez kizárólag x86-64 architektúrán működik. Tanulmányi célokra ez elegendő.
Nézzük meg a klónunk működését. A Linux esetében a hibakeresők és nyomozók a fent említett ptrace rendszerhívást használják. Ez az első argumentumban parancsazonosítók átadásával működik, amelyekből nekünk csak a következők szükségesek: PTRACE_TRACEME, PTRACE_SYSCALL és PTRACE_GETREGS.
A nyomozó munkája szokásos Unix-stílusban kezdődik: fork(2) elindít egy gyermek folyamatot, ez pedig a exec(3) hívással elindítja a vizsgált programot. Az egyetlen kis finomság itt a ptrace(PTRACE_TRACEME) előhívása: a gyermek folyamat várja, hogy a szülő folyamat nyomon kövesse őt: 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");
}A szülő folyamatnak most meg kell hívnia wait(2) a gyermek folyamatban, azaz biztosítania kell, hogy a nyomozási módra való váltás megtörtént:
/* 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");Ezzel a készülődés befejeződött, és most már közvetlenül elkezdhetjük a rendszerhívások nyomozását végtelen ciklusban.
Hívás ptrace(PTRACE_SYSCALL) biztosítja, hogy a következő várakozás szülő befejeződik vagy a rendszerhívás végrehajtása előtt, vagy közvetlenül utána. Két hívás között bármilyen műveletet végezhetünk: helyettesíthetjük a hívást egy alternatívával, megváltoztathatjuk az argumentumokat vagy a visszatérési értéket.
Nekünk elegendő kétszer meghívni a parancsot ptrace(PTRACE_GETREGS), hogy megkapjuk a regiszter állapotát rax a hívás előtt (a rendszerhívás száma) és közvetlenül utána (a 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);
}Íme az egész nyomozó. Most már tudja, honnan kezdje a következő portálást Linuxon.
Alapok: program indítása strace kezelésével
Az első példájaként feltehetően a legegyszerűbb módszert érdemes bemutatni — egy alkalmazás indítása a straceFelesleges tovább kutatni egy tipikus program hívásainak végtelen listájában, írjunk strace.
egy minimális programot 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; } write:
Fordítsuk le a programot, és győződjünk meg róla, hogy működik:
$ gcc examples/write-simple.c -o write-simple $ ./write-simple write me to stdout
És végül indítsuk el a strace alá:$ 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 >
Unix-szerű rendszerekben.