Strace in Linux: geschiedenis, ontwerp en gebruik

Strace in Linux: geschiedenis, ontwerp en gebruik

In Unix-achtige besturingssystemen vindt de communicatie van een programma met de buitenwereld en het besturingssysteem plaats via een kleine reeks functies: systeemaanroepen. Dit betekent dat het voor foutopsporingsdoeleinden nuttig kan zijn om systeemaanroepen te bespioneren die door processen worden uitgevoerd.

Een hulpprogramma helpt je het ‘intieme leven’ van programma’s op Linux te volgen strace, dat het onderwerp is van dit artikel. Voorbeelden van het gebruik van spionageapparatuur gaan vergezeld van een korte geschiedenis strace en een beschrijving van het ontwerp van dergelijke programma's.

Inhoud

Oorsprong der soorten

De belangrijkste interface tussen programma's en de OS-kernel in Unix zijn systeemaanroepen. systeemoproepen, systeemoproepen), vindt de interactie van programma's met de buitenwereld uitsluitend via hen plaats.

Maar in de eerste publieke versie van Unix (Versie 6 Unix, 1975) waren er geen handige manieren om het gedrag van gebruikersprocessen te volgen. Om dit probleem op te lossen, zal Bell Labs updaten naar de volgende versie (Versie 7 Unix, 1979) stelde een nieuwe systeemoproep voor - ptrace.

ptrace werd in de eerste plaats ontwikkeld voor interactieve debuggers, maar tegen het einde van de jaren 80 (in het tijdperk van commerciële Systeem V versie 4Op deze basis verschenen er specifiek gerichte debuggers (systeemoproeptracers) die op grote schaal werden gebruikt.

eerste dezelfde versie van strace werd in 1992 door Paul Cronenburg op de comp.sources.sun mailinglijst gepubliceerd als alternatief voor het gesloten hulpprogramma trace van zon. Zowel de kloon als het origineel waren bedoeld voor SunOS, maar in 1994 strace werd geport naar System V, Solaris en het steeds populairder wordende Linux.

Tegenwoordig ondersteunt strace alleen Linux en vertrouwt op hetzelfde ptrace, begroeid met veel uitbreidingen.

Moderne (en zeer actieve) beheerder strace - Dmitri Levin. Dankzij hem kreeg het hulpprogramma geavanceerde functies zoals foutinjectie in systeemaanroepen, ondersteuning voor een breed scala aan architecturen en, belangrijker nog, mascotte. Onofficiële bronnen beweren dat de keuze op de struisvogel viel vanwege de klank tussen het Russische woord “struisvogel” en het Engelse woord “strace”.

Het is ook belangrijk dat de ptrace-systeemaanroep en tracers nooit in POSIX zijn opgenomen, ondanks een lange geschiedenis en implementatie in Linux, FreeBSD, OpenBSD en traditionele Unix.

Strace-apparaat in een notendop: Piglet Trace

"Er wordt niet van je verwacht dat je dit begrijpt" (Dennis Ritchie, commentaar in versie 6 Unix-broncode)

Sinds mijn kindertijd kan ik niet tegen zwarte dozen: ik speelde niet met speelgoed, maar probeerde de structuur ervan te begrijpen (volwassenen gebruikten het woord 'kapot', maar geloofden de kwade tongen niet). Misschien is dit de reden waarom de informele cultuur van de eerste Unix en de moderne open source-beweging zo dichtbij mij staan.

Voor de doeleinden van dit artikel is het onredelijk om de broncode van strace, die in de afgelopen decennia is gegroeid, te demonteren. Maar er mogen geen geheimen meer zijn voor de lezers. Om het werkingsprincipe van dergelijke strace-programma's te laten zien, zal ik daarom de code voor een miniatuurtracer leveren - Biggenspoor (ptr). Het weet niet hoe het iets speciaals moet doen, maar het belangrijkste zijn de systeemaanroepen van het programma - het geeft het volgende weer:

$ 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 herkent ongeveer honderden Linux-systeemaanroepen (zie. de tafel) en werkt alleen op x86-64-architectuur. Voor educatieve doeleinden is dit voldoende.

Laten we eens kijken naar het werk van onze kloon. In het geval van Linux gebruiken debuggers en tracers, zoals hierboven vermeld, de ptrace-systeemaanroep. Het werkt door in het eerste argument de opdracht-ID's door te geven, die we alleen nodig hebben PTRACE_TRACEME, PTRACE_SYSCALL и PTRACE_GETREGS.

De tracer start in de gebruikelijke Unix-stijl: fork(2) lanceert een onderliggend proces, dat op zijn beurt gebruikt exec(3) lanceert het programma dat wordt bestudeerd. De enige subtiliteit hier is de uitdaging ptrace(PTRACE_TRACEME) voor exec: Het onderliggende proces verwacht dat het bovenliggende proces het controleert:

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

Het bovenliggende proces zou nu moeten bellen wait(2) in het onderliggende proces, dat wil zeggen, zorg ervoor dat er is overgeschakeld naar de traceermodus:

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

Op dit punt zijn de voorbereidingen voltooid en kunt u direct doorgaan met het volgen van systeemoproepen in een eindeloze lus.

telefoontje ptrace(PTRACE_SYSCALL) garandeert dat daarna wait parent wordt voltooid voordat de systeemaanroep wordt uitgevoerd of onmiddellijk nadat deze is voltooid. Tussen twee oproepen kunt u elke actie uitvoeren: vervang de oproep door een alternatieve oproep, verander de argumenten of de retourwaarde.

We hoeven het commando slechts twee keer uit te voeren ptrace(PTRACE_GETREGS)om de registerstatus te verkrijgen rax vóór de oproep (systeemoproepnummer) en onmiddellijk erna (retourwaarde).

Eigenlijk is de cyclus:

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

Dat is de hele tracer. Nu weet u waar u met de volgende portering moet beginnen DTrace op Linux.

Basisprincipes: een programma uitvoeren met strace

Als eerste gebruiksscenario strace, misschien is het de moeite waard om de eenvoudigste methode te noemen: het starten van een actieve applicatie strace.

Om ons niet te verdiepen in de eindeloze lijst met oproepen van een typisch programma, schrijven we minimaal programma rond 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;
}

Laten we het programma bouwen en ervoor zorgen dat het werkt:

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

En tot slot, laten we het onder strikte controle uitvoeren:

$ 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)                           = ?

Zeer “langdradig” en niet erg leerzaam. Er zijn hier twee problemen: de programma-uitvoer wordt gemengd met de uitvoer strace en een overvloed aan systeemoproepen die ons niet interesseren.

U kunt de standaarduitvoerstroom van het programma en de uitvoer van strace-fouten scheiden met behulp van de schakeloptie -o, die de lijst met systeemaanroepen omleidt naar een argumentbestand.

Rest ons nog het probleem van de “extra” oproepen aan te pakken. Laten we aannemen dat we alleen geïnteresseerd zijn in oproepen write. Sleutel -e Hiermee kunt u expressies opgeven waarop systeemaanroepen worden gefilterd. De meest populaire voorwaardeoptie is uiteraard trace=*, waarmee u alleen de oproepen kunt achterlaten die ons interesseren.

Bij gelijktijdig gebruik -o и -e we zullen krijgen:

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

Dus je ziet, het is veel gemakkelijker om te lezen.

U kunt ook systeemaanroepen verwijderen, bijvoorbeeld die gerelateerd aan geheugentoewijzing en -vrijgave:

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

Let op het ontsnapte uitroepteken in de lijst met uitgesloten oproepen: dit is vereist door de opdrachtshell. schelp).

In mijn versie van glibc beëindigt een systeemaanroep het proces exit_group, niet traditioneel _exit. Dit is de moeilijkheid van het werken met systeemaanroepen: de interface waarmee de programmeur werkt, houdt niet direct verband met systeemaanroepen. Bovendien verandert het regelmatig, afhankelijk van de implementatie en het platform.

Basisprincipes: direct deelnemen aan het proces

Aanvankelijk riep het ptrace-systeem op waarop het was gebouwd strace, kan alleen worden gebruikt als het programma in een speciale modus wordt uitgevoerd. Deze beperking klonk misschien redelijk in de tijd van Versie 6 Unix. Tegenwoordig is dit niet meer voldoende: soms moet je de problemen van een werkprogramma onderzoeken. Een typisch voorbeeld is een proces dat geblokkeerd is op een handvat of slaapt. Daarom modern strace kan direct aan processen deelnemen.

Bevriezend voorbeeld programma's:

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

Laten we het programma bouwen en ervoor zorgen dat het bevroren is:

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

Laten we nu proberen mee te doen:

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

Programma geblokkeerd door oproep pause. Laten we eens kijken hoe ze op de signalen reageert:

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

We lanceerden het bevroren programma en sloten ons eraan aan via strace. Twee dingen werden duidelijk: de pauzesysteemoproep negeert signalen zonder handlers en, interessanter, strace monitort niet alleen systeemoproepen, maar ook inkomende signalen.

Voorbeeld: onderliggende processen volgen

Werken met processen via een call fork - de basis van alle Unixen. Laten we eens kijken hoe strace werkt met een procesboom aan de hand van het voorbeeld van een eenvoudige “veredeling” programma's:

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

Hier creëert het originele proces een onderliggend proces, beide schrijven naar standaarduitvoer:

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

Standaard zien we alleen systeemaanroepen van het bovenliggende proces:

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

Met de vlag kunt u de gehele procesboom volgen -f, welke strace bewaakt systeemaanroepen in onderliggende processen. Dit draagt ​​bij aan elke uitvoerregel pid proces dat een systeemuitvoer maakt:

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

In deze context kan filteren op groep systeemaanroepen nuttig zijn:

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

Trouwens, welke systeemaanroep wordt gebruikt om een ​​nieuw proces te creëren?

Voorbeeld: bestandspaden in plaats van handvatten

Het kennen van bestandsdescriptors is zeker nuttig, maar de namen van de specifieke bestanden waartoe een programma toegang heeft, kunnen ook van pas komen.

de volgende programma schrijft de regel naar het tijdelijke bestand:

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

Tijdens een normaal gesprek strace toont de waarde van het descriptornummer dat is doorgegeven aan de systeemaanroep:

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

Met een vlag -y Het hulpprogramma toont het pad naar het bestand waarmee de descriptor overeenkomt:

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

Voorbeeld: bijhouden van bestandstoegang

Nog een handige functie: geef alleen systeemoproepen weer die aan een specifiek bestand zijn gekoppeld. Volgende programma voegt een regel toe aan een willekeurig bestand dat als argument wordt doorgegeven:

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

Bij verstek strace geeft veel onnodige informatie weer. Vlag -P met een argument zorgt ervoor dat strace alleen oproepen naar het opgegeven bestand afdrukt:

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

Voorbeeld: Multithreaded-programma's

Nut strace kan ook helpen bij het werken met multi-threaded het programma. Het volgende programma schrijft naar standaarduitvoer vanuit twee streams:

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

Uiteraard moet het worden gecompileerd met een speciale groet aan de linker: de vlag -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
$

vlag -f, zoals in het geval van reguliere processen, zal de pid van het proces aan het begin van elke regel toevoegen.

Uiteraard hebben we het niet over een thread-ID in de zin van de implementatie van de POSIX Threads-standaard, maar over het nummer dat door de taakplanner in Linux wordt gebruikt. Vanuit het oogpunt van laatstgenoemde zijn er geen processen of threads - er zijn taken die moeten worden verdeeld over de beschikbare kernen van de machine.

Wanneer u in meerdere threads werkt, worden systeemaanroepen te veel:

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

Het is zinvol om u te beperken tot alleen procesbeheer en systeemaanroepen 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 +++

Trouwens, vragen. Welke systeemaanroep wordt gebruikt om een ​​nieuwe thread te maken? Hoe verschilt deze oproep tot threads van de oproep tot processen?

Masterclass: processtapel op het moment van een systeemaanroep

Een van de onlangs verschenen strace mogelijkheden - weergave van de stapel functieaanroepen op het moment van de systeemaanroep. Eenvoudig voorbeeld:

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

Uiteraard wordt de programma-uitvoer erg omvangrijk, en naast de vlag -k (call stack-weergave), is het zinvol om systeemoproepen op naam te filteren:

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

Masterclass: foutinjectie

En nog een nieuwe en zeer nuttige functie: foutinjectie. Hier programma, waarbij twee regels naar de uitvoerstroom worden geschreven:

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

Laten we beide schrijfaanroepen traceren:

$ 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 gebruiken we de uitdrukking injectom een ​​fout in te voegen EBADF in alle schrijfoproepen:

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

Het is interessant welke fouten worden geretourneerd alle uitdagingen write, inclusief de oproep verborgen achter perror. Het heeft alleen zin om een ​​fout te retourneren voor de eerste van de oproepen:

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

Of de tweede:

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

Het is niet nodig om het fouttype op te geven:

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

In combinatie met andere vlaggen kunt u de toegang tot een specifiek bestand “verbreken”. Voorbeeld:

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

Naast foutinjectie, men kan vertragingen introduceren bij het bellen of ontvangen van signalen.

nawoord

Nut strace - een eenvoudig en betrouwbaar hulpmiddel. Maar naast systeemaanroepen kunnen ook andere aspecten van de werking van programma's en het besturingssysteem worden opgespoord. Het kan bijvoorbeeld oproepen naar dynamisch gekoppelde bibliotheken volgen. ltrace, kunnen ze de werking van het besturingssysteem onderzoeken SysteemTik и ftras, en stelt u in staat de programmaprestaties diepgaand te onderzoeken perf. Niettemin is dat zo strace - de eerste verdedigingslinie bij problemen met mijn eigen programma's en die van anderen, en ik gebruik het minstens een paar keer per week.

Kortom, als je van Unix houdt, lees dan man 1 strace en kijk gerust eens naar je programma's!

Bron: www.habr.com

Voeg een reactie