Strace i Linux: historia, design och användning

Strace i Linux: historia, design och användning

I Unix-liknande operativsystem sker ett programs kommunikation med omvärlden och operativsystemet genom en liten uppsättning funktioner – systemanrop. Detta innebär att det för felsökningsändamål kan vara användbart att spionera på systemanrop som exekveras av processer.

Ett verktyg hjälper dig att övervaka det "intima livet" för program på Linux strace, som är ämnet för den här artikeln. Exempel på användning av spionutrustning åtföljs av en kort historik strace och en beskrivning av utformningen av sådana program.

Innehåll

Artens ursprung

Huvudgränssnittet mellan program och OS-kärnan i Unix är systemanrop. systemsamtal, syscalls), samspelet mellan program och omvärlden sker uteslutande genom dem.

Men i den första offentliga versionen av Unix (Version 6 Unix, 1975) fanns det inga bekväma sätt att spåra beteendet hos användarprocesser. För att lösa det här problemet kommer Bell Labs att uppdatera till nästa version (Version 7 Unix, 1979) föreslog ett nytt systemanrop - ptrace.

ptrace utvecklades främst för interaktiva debuggers, men i slutet av 80-talet (i den kommersiella eran System V version 4) på grundval av detta dök snävt fokuserade debuggers – systemanropsspårare – upp och blev allmänt använda.

första samma version av strace publicerades av Paul Cronenburg på e-postlistan comp.sources.sun 1992 som ett alternativ till ett slutet verktyg trace från Sun. Både klonen och originalet var avsedda för SunOS, men 1994 strace portades till System V, Solaris och det allt populärare Linux.

Idag stöder strace bara Linux och förlitar sig på detsamma ptrace, bevuxen med många förlängningar.

Modern (och mycket aktiv) underhållare strace - Dmitry Levin. Tack vare honom fick verktyget avancerade funktioner som felinjektion i systemanrop, stöd för ett brett utbud av arkitekturer och, viktigast av allt, maskot. Inofficiella källor hävdar att valet föll på strutsen på grund av konsonansen mellan det ryska ordet "struts" och det engelska ordet "strace".

Det är också viktigt att ptrace-systemanropet och spårare aldrig inkluderades i POSIX, trots en lång historia och implementering i Linux, FreeBSD, OpenBSD och traditionella Unix.

Strace-enhet i ett nötskal: Piglet Trace

"Du förväntas inte förstå detta" (Dennis Ritchie, kommentar i version 6 Unix källkod)

Sedan tidig barndom tål jag inte svarta lådor: jag lekte inte med leksaker utan försökte förstå deras struktur (vuxna använde ordet "bröt", men tror inte på de onda tungorna). Kanske är det därför som den informella kulturen i den första Unix och den moderna öppen källkodsrörelsen står mig så nära.

För den här artikelns syften är det orimligt att ta isär källkoden för strace, som har vuxit under decennier. Men det ska inte finnas några hemligheter kvar för läsarna. Därför, för att visa funktionsprincipen för sådana strace-program, kommer jag att tillhandahålla koden för en miniatyrspårare - Spår av smågrisar (ptr). Det vet inte hur man gör något speciellt, men det viktigaste är programmets systemanrop - det matar ut:

$ 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 terminated

Piglet Trace känner igen ungefär hundratals Linux-systemanrop (se. bord) och fungerar bara på x86-64-arkitektur. Detta är tillräckligt för utbildningsändamål.

Låt oss titta på vår klons arbete. När det gäller Linux använder debuggers och spårare, som nämnts ovan, systemanropet ptrace. Det fungerar genom att i det första argumentet skicka kommandoidentifierarna, som vi bara behöver PTRACE_TRACEME, PTRACE_SYSCALL и PTRACE_GETREGS.

Spåraren startar i vanlig Unix-stil: fork(2) lanserar en barnprocess, som i sin tur använder exec(3) lanserar programmet som studeras. Den enda subtiliteten här är utmaningen ptrace(PTRACE_TRACEME) innan exec: Den underordnade processen förväntar sig att den överordnade processen övervakar den:

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");
}

Föräldraprocessen bör nu ringa wait(2) i den underordnade processen, det vill säga se till att byte till spårningsläge har skett:

/* 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");

Vid det här laget är förberedelserna klara och du kan fortsätta direkt till att spåra systemsamtal i en oändlig slinga.

samtal ptrace(PTRACE_SYSCALL) garanterar att efterföljande wait förälder kommer att slutföras antingen innan systemanropet exekveras eller omedelbart efter att det slutförts. Mellan två anrop kan du utföra vilka åtgärder som helst: ersätta anropet med ett alternativt, ändra argumenten eller returvärdet.

Vi behöver bara ringa kommandot två gånger ptrace(PTRACE_GETREGS)för att få registerstatus rax före samtalet (systemanropsnummer) och omedelbart efter (returvärde).

Egentligen, cykeln:

/* 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, &registers) == -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, &registers) == -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);
}

Det är hela spårämnet. Nu vet du var du ska börja nästa portering DTrace på Linux.

Grunder: köra ett program som kör strace

Som ett första användningsfall strace, kanske det är värt att nämna den enklaste metoden - att starta en applikation som körs strace.

För att inte fördjupa oss i den ändlösa listan med samtal i ett typiskt program, skriver vi lägsta program runt 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;
}

Låt oss bygga programmet och se till att det fungerar:

$ gcc examples/write-simple.c -o write-simple
$ ./write-simple
write me to stdout

Och slutligen, låt oss köra det under strace-kontroll:

$ 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, "177ELF21133>1260342"..., 832) = 832
fstat(3, {st_mode=S_IFREG|0755, st_size=2030544, ...}) = 0
mmap(NULL, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f7a2a631000
mmap(NULL, 4131552, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_DENYWRITE, 3, 0) = 0x7f7a2a04c000
mprotect(0x7f7a2a233000, 2097152, PROT_NONE) = 0
mmap(0x7f7a2a433000, 24576, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x1e7000) = 0x7f7a2a433000
mmap(0x7f7a2a439000, 15072, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0) = 0x7f7a2a439000
close(3)                                = 0
arch_prctl(ARCH_SET_FS, 0x7f7a2a6324c0) = 0
mprotect(0x7f7a2a433000, 16384, PROT_READ) = 0
mprotect(0x55ff52b52000, 4096, PROT_READ) = 0
mprotect(0x7f7a2a664000, 4096, PROT_READ) = 0
munmap(0x7f7a2a633000, 197410)          = 0
write(1, "write me to stdoutn", 20write me to stdout
)  = 20
exit_group(0)                           = ?

Väldigt "ordrik" och inte särskilt pedagogisk. Det finns två problem här: programutgången blandas med utgången strace och ett överflöd av systemsamtal som inte intresserar oss.

Du kan separera programmets standardutgångsström och strace error output med -o-växeln, som omdirigerar listan med systemanrop till en argumentfil.

Det återstår att ta itu med problemet med "extra" samtal. Låt oss anta att vi bara är intresserade av samtal write. Nyckel -e låter dig ange uttryck som systemanrop ska filtreras efter. Det mest populära skickalternativet är naturligtvis trace=*, med vilken du bara kan lämna de samtal som intresserar oss.

När den används samtidigt -o и -e vi kommer få:

$ 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 +++

Så du förstår, det är mycket lättare att läsa.

Du kan också ta bort systemanrop, till exempel de som är relaterade till minnesallokering och frigöring:

$ 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 +++

Notera det escaped utropstecknet i listan över uteslutna samtal: detta krävs av kommandoskalet. skal).

I min version av glibc avslutar ett systemanrop processen exit_group, inte traditionellt _exit. Detta är svårigheten att arbeta med systemanrop: gränssnittet som programmeraren arbetar med är inte direkt relaterat till systemanrop. Dessutom ändras det regelbundet beroende på implementering och plattform.

Grunderna: gå med i processen i farten

Till en början anropade ptrace-systemet som det byggdes på strace, kunde endast användas när programmet körs i ett specialläge. Denna begränsning kan ha låtit rimlig i dagarna av version 6 Unix. Nuförtiden är detta inte längre tillräckligt: ​​ibland behöver du undersöka problemen med ett fungerande program. Ett typiskt exempel är en process som blockeras på ett handtag eller sover. Därför modern strace kan ansluta sig till processer i farten.

Frysande exempel program:

int main(int argc, char *argv[])
{
    (void) argc; (void) argv;

    char str[] = "write men";

    write(STDOUT_FILENO, str, sizeof(str));

    /* Sleep indefinitely or until a signal arrives */
    pause();

    write(STDOUT_FILENO, str, sizeof(str));

    return EXIT_SUCCESS;
}

Låt oss bygga programmet och se till att det är fruset:

$ gcc examples/write-sleep.c -o write-sleep
$ ./write-sleep
./write-sleep
write me
^C
$

Låt oss nu försöka gå med:

$ ./write-sleep &
[1] 15329
write me
$ strace -p 15329
strace: Process 15329 attached
pause(
^Cstrace: Process 15329 detached
 <detached ...>

Programmet blockeras av ett samtal pause. Låt oss se hur hon reagerar på signalerna:

$ 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 +++

Vi lanserade det frusna programmet och gick med i det med hjälp av strace. Två saker blev tydliga: paussystemanropet ignorerar signaler utan hanterare och, mer intressant, strace övervakar inte bara systemanrop utan även inkommande signaler.

Exempel: Spårning av underordnade processer

Arbeta med processer genom ett samtal fork - grunden för alla Unixar. Låt oss se hur strace fungerar med ett processträd med exemplet på en enkel "uppfödning" program:

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);
}

Här skapar den ursprungliga processen en underordnad process, båda skriver till standardutdata:

$ gcc examples/fork-write.c -o fork-write
$ ./fork-write
parent (self=11274, child=11275)
child (self=11275)

Som standard ser vi bara systemanrop från den överordnade processen:

$ strace -e trace=write -ofork-write.log ./fork-write
child (self=22049)
parent (self=22048, child=22049)
$ cat fork-write.log
write(1, "parent (self=22048, child=22049)"..., 33) = 33
--- SIGCHLD {si_signo=SIGCHLD, si_code=CLD_EXITED, si_pid=22049, si_uid=1001, si_status=0, si_utime=0, si_stime=0} ---
+++ exited with 0 +++

Flaggan hjälper dig att spåra hela processträdet -f, som strace övervakar systemanrop i underordnade processer. Detta läggs till varje utdatarad pid process som gör en systemutgång:

$ strace -f -e trace=write -ofork-write.log ./fork-write
parent (self=22710, child=22711)
child (self=22711)
$ cat fork-write.log
22710 write(1, "parent (self=22710, child=22711)"..., 33) = 33
22711 write(1, "child (self=22711)n", 19) = 19
22711 +++ exited with 0 +++
22710 --- SIGCHLD {si_signo=SIGCHLD, si_code=CLD_EXITED, si_pid=22711, si_uid=1001, si_status=0, si_utime=0, si_stime=0} ---
22710 +++ exited with 0 +++

I detta sammanhang kan filtrering efter grupp av systemanrop vara användbart:

$ strace -f -e trace=%process -ofork-write.log ./fork-write
parent (self=23610, child=23611)
child (self=23611)
$ cat fork-write.log
23610 execve("./fork-write", ["./fork-write"], 0x7fff696ff720 /* 63 vars */) = 0
23610 arch_prctl(ARCH_SET_FS, 0x7f3d03ba44c0) = 0
23610 clone(child_stack=NULL, flags=CLONE_CHILD_CLEARTID|CLONE_CHILD_SETTID|SIGCHLD, child_tidptr=0x7f3d03ba4790) = 23611
23610 wait4(-1,  <unfinished ...>
23611 exit_group(0)                     = ?
23611 +++ exited with 0 +++
23610 <... wait4 resumed> NULL, 0, NULL) = 23611
23610 --- SIGCHLD {si_signo=SIGCHLD, si_code=CLD_EXITED, si_pid=23611, si_uid=1001, si_status=0, si_utime=0, si_stime=0} ---
23610 exit_group(0)                     = ?
23610 +++ exited with 0 +++

Förresten, vilket systemanrop används för att skapa en ny process?

Exempel: filsökvägar istället för handtag

Att känna till filbeskrivningar är verkligen användbart, men namnen på de specifika filerna som ett program kommer åt kan också vara praktiskt.

nästa programmet skriver raden till den temporära filen:

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;
}

Under ett vanligt samtal strace kommer att visa värdet på beskrivningsnumret som skickas till systemanropet:

$ 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 +++

Med en flagga -y Verktyget visar sökvägen till filen som beskrivningen motsvarar:

$ 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 +++

Exempel: Filåtkomstspårning

En annan användbar funktion: visa endast systemanrop associerade med en specifik fil. Nästa programmet lägger till en rad till en godtycklig fil som skickas som ett argument:

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;
}

Som standard strace visar mycket onödig information. Flagga -P med ett argument gör att strace endast skriver ut anrop till den angivna filen:

$ 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 +++

Exempel: Flertrådade program

Verktyg strace kan också hjälpa när du arbetar med flertrådiga programmet. Följande program skriver till standardutdata från två strömmar:

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);
}

Naturligtvis måste den sammanställas med en speciell hälsning till länken - flaggan -pthread:

$ 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
$

flag -f, som i fallet med vanliga processer, kommer att lägga till pid för processen i början av varje rad.

Naturligtvis talar vi inte om en trådidentifierare i betydelsen implementeringen av POSIX Threads-standarden, utan om numret som används av uppgiftsschemaläggaren i Linux. Ur den senares synvinkel finns det inga processer eller trådar - det finns uppgifter som måste fördelas mellan de tillgängliga kärnorna i maskinen.

När du arbetar i flera trådar blir systemanrop för många:

$ strace -f -othread-write.log ./thread-write
$ wc -l thread-write.log
60 thread-write.log

Det är vettigt att begränsa dig till processhantering och systemanrop write:

$ strace -f -e trace="%process,write" -othread-write.log ./thread-write
$ cat thread-write.log
18211 execve("./thread-write", ["./thread-write"], 0x7ffc6b8d58f0 /* 64 vars */) = 0
18211 arch_prctl(ARCH_SET_FS, 0x7f38ea3b7740) = 0
18211 write(1, "Initial thread: launching a thre"..., 35) = 35
18211 clone(child_stack=0x7f38e9ba2fb0, flags=CLONE_VM|CLONE_FS|CLONE_FILES|CLONE_SIGHAND|CLONE_THREAD|CLONE_SYSVSEM|CLONE_SETTLS|CLONE_PARENT_SETTID|CLONE_CHILD_CLEARTID, parent_tidptr=0x7f38e9ba39d0, tls=0x7f38e9ba3700, child_tidptr=0x7f38e9ba39d0) = 18212
18211 write(1, "Initial thread: joining a thread"..., 33) = 33
18212 write(1, "Secondary thread: workingn", 26) = 26
18212 write(1, "Secondary thread: donen", 23) = 23
18212 exit(0)                           = ?
18212 +++ exited with 0 +++
18211 write(1, "Initial thread: done", 20) = 20
18211 exit_group(0)                     = ?
18211 +++ exited with 0 +++

Förresten, frågor. Vilket systemanrop används för att skapa en ny tråd? Hur skiljer sig detta anrop till trådar från anropet för processer?

Master class: processstack vid tidpunkten för ett systemanrop

En av de nyligen dök upp strace funktioner - visar stacken av funktionsanrop vid tidpunkten för systemanropet. Enkel exempel:

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;
}

Naturligtvis blir programmets utdata mycket omfattande, och förutom flaggan -k (visning av samtalsstack), är det vettigt att filtrera systemanrop efter namn:

$ 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 +++

Master class: felinjektion

Och ytterligare en ny och mycket användbar funktion: felinjektion. Här programmet, skriver två rader till utdataströmmen:

#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>

void do_write(const char *str, ssize_t len)
{
    if (len != write(STDOUT_FILENO, str, (size_t)len)){
        perror("write");
        exit(EXIT_FAILURE);
    }
}

int main(int argc, char *argv[])
{
    (void) argc; (void) argv;

    char str1[] = "write me 1n";
    do_write(str1, sizeof(str1));

    char str2[] = "write me 2n";
    do_write(str2, sizeof(str2));

    return EXIT_SUCCESS;
}

Låt oss spåra båda skrivsamtal:

$ gcc examples/write-twice.c -o write-twice
$ ./write-twice
write me 1
write me 2
$ strace -e trace=write -owrite-twice.log ./write-twice
write me 1
write me 2
$ cat write-twice.log
write(1, "write me 1n", 12)          = 12
write(1, "write me 2n", 12)          = 12
+++ exited with 0 +++

Nu använder vi uttrycket injectför att infoga ett fel EBADF i alla skrivsamtal:

$ strace -e trace=write -e inject=write:error=EBADF -owrite-twice.log ./write-twice
$ cat write-twice.log
write(1, "write me 1n", 12)          = -1 EBADF (Bad file descriptor) (INJECTED)
write(3, "write: Bad file descriptorn", 27) = -1 EBADF (Bad file descriptor) (INJECTED)
+++ exited with 1 +++

Det är intressant vilka fel som returneras alla utmaningar write, inklusive samtalet gömt bakom perror. Det är bara vettigt att returnera ett fel för det första av samtalen:

$ 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 +++

Eller den andra:

$ 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 +++

Det är inte nödvändigt att ange feltyp:

$ 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 +++

I kombination med andra flaggor kan du "bryta" åtkomst till en specifik fil. Exempel:

$ 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 +++

Förutom felinjektion, kan man införa förseningar när du ringer samtal eller tar emot signaler.

efterordet

Verktyg strace - ett enkelt och pålitligt verktyg. Men förutom systemanrop kan andra aspekter av driften av program och operativsystem felsökas. Till exempel kan den spåra anrop till dynamiskt länkade bibliotek. Spåra, kan de undersöka hur operativsystemet fungerar SystemTap и ftrace, och låter dig undersöka programmets prestanda på djupet perfekt. Ändå är det så strace – första försvarslinjen vid problem med mina egna och andras program, och jag använder den minst ett par gånger i veckan.

Kort sagt, om du älskar Unix, läs man 1 strace och kika gärna på dina program!

Källa: will.com

Lägg en kommentar