Strace i Linux: historie, design og bruk

Strace i Linux: historie, design og bruk

I Unix-lignende operativsystemer skjer et programs kommunikasjon med omverdenen og operativsystemet gjennom et lite sett med funksjoner – systemanrop. Dette betyr at for feilsøkingsformål kan det være nyttig å spionere på systemanrop som utføres av prosesser.

Et verktøy hjelper deg med å overvåke det "intime livet" til programmer på Linux strace, som er emnet for denne artikkelen. Eksempler på bruk av spionutstyr er ledsaget av en kort historikk strace og en beskrivelse av utformingen av slike programmer.

Innhold

Artenes opprinnelse

Hovedgrensesnittet mellom programmer og OS-kjernen i Unix er systemanrop. systemanrop, syscaller), samspillet mellom programmer og omverdenen skjer utelukkende gjennom dem.

Men i den første offentlige versjonen av Unix (Versjon 6 Unix, 1975) var det ingen praktiske måter å spore oppførselen til brukerprosesser. For å løse dette problemet vil Bell Labs oppdatere til neste versjon (Versjon 7 Unix, 1979) foreslo et nytt systemoppkall - ptrace.

ptrace ble først og fremst utviklet for interaktive debuggere, men på slutten av 80-tallet (i den kommersielle tiden System V utgivelse 4) på dette grunnlaget dukket snevert fokuserte feilsøkere – systemanropsporere – opp og ble mye brukt.

første den samme versjonen av strace ble publisert av Paul Cronenburg på comp.sources.sun-postlisten i 1992 som et alternativ til et lukket verktøy trace fra Sun. Både klonen og originalen var ment for SunOS, men innen 1994 strace ble portert til System V, Solaris og den stadig mer populære Linux.

I dag støtter strace bare Linux og er avhengig av det samme ptrace, overgrodd med mange utvidelser.

Moderne (og veldig aktiv) vedlikeholder strace - Dmitry Levin. Takket være ham fikk verktøyet avanserte funksjoner som feilinjeksjon i systemanrop, støtte for et bredt spekter av arkitekturer og, viktigst av alt, maskot. Uoffisielle kilder hevder at valget falt på strutsen på grunn av konsonansen mellom det russiske ordet "struts" og det engelske ordet "strace".

Det er også viktig at ptrace-systemkallet og tracere aldri ble inkludert i POSIX, til tross for en lang historie og implementering i Linux, FreeBSD, OpenBSD og tradisjonell Unix.

Strace-enhet i et nøtteskall: Piglet Trace

"Du forventes ikke å forstå dette" (Dennis Ritchie, kommentar i versjon 6 Unix-kildekoden)

Siden tidlig barndom tåler jeg ikke svarte bokser: Jeg lekte ikke med leker, men prøvde å forstå strukturen deres (voksne brukte ordet "brøt", men tror ikke på de onde tungene). Kanskje dette er grunnen til at den uformelle kulturen til den første Unix og den moderne åpen kildekode-bevegelsen er så nær meg.

For formålet med denne artikkelen er det urimelig å demontere kildekoden til strace, som har vokst over flere tiår. Men det skal ikke være noen hemmeligheter igjen for leserne. Derfor, for å vise prinsippet om drift av slike strace-programmer, vil jeg gi koden for en miniatyrsporer - Grisingspor (ptr). Det vet ikke hvordan man gjør noe spesielt, men det viktigste er systemanropene til programmet - det gir 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 gjenkjenner omtrent hundrevis av Linux-systemanrop (se. bord) og fungerer bare på x86-64-arkitektur. Dette er tilstrekkelig for utdanningsformål.

La oss se på arbeidet til klonen vår. Når det gjelder Linux, bruker debuggere og sporere, som nevnt ovenfor, ptrace-systemkallet. Det fungerer ved å sende inn kommandoidentifikatorene i det første argumentet, som vi bare trenger PTRACE_TRACEME, PTRACE_SYSCALL и PTRACE_GETREGS.

Traceren starter i vanlig Unix-stil: fork(2) lanserer en barneprosess, som igjen bruker exec(3) lanserer programmet som studeres. Den eneste subtiliteten her er utfordringen ptrace(PTRACE_TRACEME) før exec: Den underordnede prosessen forventer at den overordnede prosessen overvåker 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");
}

Foreldreprosessen skal nå ringe wait(2) i den underordnede prosessen, det vil si at du har byttet til sporingsmodus:

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

På dette tidspunktet er forberedelsene fullført, og du kan fortsette direkte til å spore systemanrop i en endeløs loop.

samtale ptrace(PTRACE_SYSCALL) garanterer at senere wait overordnet vil fullføre enten før systemanropet utføres eller umiddelbart etter at det er fullført. Mellom to anrop kan du utføre alle handlinger: erstatte anropet med et alternativt, endre argumentene eller returverdien.

Vi trenger bare å ringe kommandoen to ganger ptrace(PTRACE_GETREGS)for å få registerstatus rax før samtalen (systemanropsnummer) og umiddelbart etter (returverdi).

Faktisk, syklusen:

/* 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 er hele sporstoffet. Nå vet du hvor du skal starte neste portering DTrace på Linux.

Grunnleggende: kjøre et program som kjører strace

Som en første brukssak strace, kanskje det er verdt å sitere den enkleste metoden - å starte en applikasjon som kjører strace.

For ikke å fordype oss i den endeløse listen over samtaler til et typisk program, skriver vi minimumsprogram rundt 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;
}

La oss bygge programmet og sørge for at det fungerer:

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

Og til slutt, la oss kjøre den 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)                           = ?

Veldig "ordholdig" og ikke veldig lærerikt. Det er to problemer her: programutgangen blandes med utgangen strace og en overflod av systemanrop som ikke interesserer oss.

Du kan skille programmets standard utdatastrøm og strace error output ved å bruke -o-bryteren, som omdirigerer listen over systemkall til en argumentfil.

Det gjenstår å håndtere problemet med "ekstra" samtaler. La oss anta at vi kun er interessert i samtaler write. Nøkkel -e lar deg spesifisere uttrykk som systemanrop skal filtreres etter. Det mest populære tilstandsalternativet er naturligvis trace=*, som du bare kan legge igjen samtalene som interesserer oss.

Når den brukes samtidig -o и -e vi vil 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 skjønner, det er mye lettere å lese.

Du kan også fjerne systemanrop, for eksempel de som er relatert til minnetildeling og frigjø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 +++

Legg merke til det escaped utropstegnet i listen over ekskluderte anrop: dette kreves av kommandoskallet. shell).

I min versjon av glibc avslutter et systemanrop prosessen exit_group, ikke tradisjonelle _exit. Dette er vanskeligheten med å jobbe med systemanrop: grensesnittet som programmereren jobber med er ikke direkte relatert til systemanrop. Dessuten endres det regelmessig avhengig av implementering og plattform.

Grunnleggende: bli med i prosessen på farten

Opprinnelig anropet ptrace-systemet som det ble bygget på strace, kunne bare brukes når programmet kjøres i en spesiell modus. Denne begrensningen kan ha hørtes rimelig ut i tiden med versjon 6 Unix. I dag er dette ikke lenger nok: noen ganger må du undersøke problemene med et fungerende program. Et typisk eksempel er en prosess blokkert på et håndtak eller sover. Derfor moderne strace kan bli med i prosesser på farten.

Eksempel på fryser programmer:

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

La oss bygge programmet og sørge for at det er frosset:

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

La oss nå prøve å bli med:

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

Program blokkert av anrop pause. La oss se hvordan hun reagerer på signalene:

$ 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 lanserte det frosne programmet og ble med i det ved hjelp av strace. To ting ble klart: pausesystemanropet ignorerer signaler uten behandlere, og mer interessant, strace overvåker ikke bare systemanrop, men også innkommende signaler.

Eksempel: Sporing av underordnede prosesser

Arbeide med prosesser gjennom en samtale fork - grunnlaget for alle Unixer. La oss se hvordan strace fungerer med et prosesstre ved å bruke eksempelet på en enkel "avl" programmer:

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

Her oppretter den opprinnelige prosessen en underordnet prosess, som begge skriver til standardutdata:

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

Som standard vil vi bare se systemanrop fra foreldreprosessen:

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

Flagget hjelper deg med å spore hele prosesstreet -f, hvilken strace overvåker systemanrop i underordnede prosesser. Dette legger til hver linje med utdata pid prosess som lager en systemutgang:

$ 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 denne sammenhengen kan filtrering etter gruppe av systemanrop være nyttig:

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

Forresten, hvilket systemkall brukes til å lage en ny prosess?

Eksempel: filstier i stedet for håndtak

Å kjenne til filbeskrivelser er absolutt nyttig, men navnene på de spesifikke filene som et program får tilgang til kan også komme godt med.

den neste program skriver linjen til den midlertidige 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 en vanlig samtale strace vil vise verdien av beskrivelsesnummeret som ble sendt til 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 flagg -y Verktøyet viser banen til filen som beskrivelsen tilsvarer:

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

Eksempel: Filtilgangssporing

En annen nyttig funksjon: vis kun systemanrop knyttet til en bestemt fil. Neste program legger til en linje til en vilkårlig fil sendt som et 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 viser mye unødvendig informasjon. Flagg -P med et argument får strace til å skrive ut kun anrop til den angitte 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 +++

Eksempel: flertrådede programmer

Nytte strace kan også hjelpe når du arbeider med multi-threaded programmet. Følgende program skriver til standard utdata fra to strømmer:

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

Naturligvis må den kompileres med en spesiell hilsen til linkeren - -pthread-flagget:

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

flagg -f, som i tilfellet med vanlige prosesser, vil legge til pid av prosessen til begynnelsen av hver linje.

Naturligvis snakker vi ikke om en trådidentifikator i betydningen implementeringen av POSIX Threads-standarden, men om nummeret som brukes av oppgaveplanleggeren i Linux. Fra sistnevntes synspunkt er det ingen prosesser eller tråder - det er oppgaver som må fordeles mellom de tilgjengelige kjernene i maskinen.

Når du jobber i flere tråder, blir systemanrop for mange:

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

Det er fornuftig å begrense deg til kun prosessadministrasjon og 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 +++

Forresten, spørsmål. Hvilket systemkall brukes til å opprette en ny tråd? Hvordan skiller denne oppfordringen for tråder seg fra oppfordringen til prosesser?

Master class: prosessstabel på tidspunktet for et systemanrop

En av de nylig dukket opp strace funksjoner - viser stabelen med funksjonsanrop på tidspunktet for systemanropet. Enkel eksempel:

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

Naturligvis blir programutgangen veldig omfangsrik, og i tillegg til flagget -k (visning av anropsstabel), er det fornuftig å filtrere systemanrop etter navn:

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

Mesterklasse: feilinjeksjon

Og enda en ny og veldig nyttig funksjon: feilinjeksjon. Her program, skriver to linjer til 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;
}

La oss spore begge skriveanropene:

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

Nå bruker vi uttrykket injectfor å sette inn en feil EBADF i alle skrivesamtaler:

$ 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 er interessant hvilke feil som returneres alle utfordringer write, inkludert samtalen skjult bak perror. Det er bare fornuftig å returnere en feil for den første av anropene:

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

$ 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 er ikke nødvendig å spesifisere feiltypen:

$ 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 kombinasjon med andre flagg kan du "bryte" tilgangen til en bestemt fil. Eksempel:

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

I tillegg til feilinjeksjon, man kan introdusere forsinkelser når du ringer eller mottar signaler.

etterord

Nytte strace - et enkelt og pålitelig verktøy. Men i tillegg til systemanrop kan andre aspekter ved driften av programmer og operativsystemet feilsøkes. For eksempel kan den spore anrop til dynamisk koblede biblioteker. spore, kan de se på driften av operativsystemet SystemTap и ftrace, og lar deg undersøke programmets ytelse grundig perf. Likevel er det det strace - den første forsvarslinjen ved problemer med egne og andres programmer, og jeg bruker den minst et par ganger i uken.

Kort sagt, hvis du elsker Unix, les man 1 strace og ta gjerne en titt på programmene dine!

Kilde: www.habr.com

Legg til en kommentar