Wie kann ich überwachen, was in den Standardausgabepuffer gesteckt wird und brechen, wenn eine bestimmte Zeichenfolge in der Pipe abgelegt wird?

Lesezeit: 12 Minuten

Benutzer-Avatar
Ross Rogers

Wie können Sie unter Linux mit C/C++-Code und gdb einen gdb-Haltepunkt hinzufügen, um die eingehenden Zeichenfolgen zu scannen, um eine bestimmte Zeichenfolge zu unterbrechen?

Ich habe keinen Zugriff auf den Code einer bestimmten Bibliothek, aber ich möchte brechen, sobald diese Bibliothek eine bestimmte Zeichenfolge an die Standardausgabe sendet, damit ich den Stapel zurückgehen und den Teil meines Codes untersuchen kann, der die Bibliothek aufruft. Natürlich möchte ich nicht warten, bis ein Buffer Flush erfolgt. Ist das möglich? Vielleicht eine Routine in libstdc++ ?

Benutzer-Avatar
Antonius

Diese Frage könnte ein guter Ausgangspunkt sein: Wie kann ich in gdb einen Haltepunkt auf “etwas wird an das Terminal gedruckt” setzen?

Sie könnten also zumindest brechen, wenn etwas nach stdout geschrieben wird. Die Methode beinhaltet im Wesentlichen das Setzen eines Haltepunkts auf der write Systemaufruf mit einer Bedingung, die das erste Argument ist 1 (dh STDOUT). In den Kommentaren findet sich auch ein Hinweis, wie man den String-Parameter der inspizieren könnte write auch anrufen.

x86 32-Bit-Modus

Ich habe mir Folgendes ausgedacht und es mit gdb 7.0.1-debian getestet. Es scheint ganz gut zu funktionieren. $esp + 8 enthält einen Zeiger auf den Speicherort der übergebenen Zeichenkette writealso wandeln Sie es zuerst in ein Integral um, dann in einen Zeiger auf char. $esp + 4 enthält den Dateideskriptor, in den geschrieben werden soll (1 für STDOUT).

$ gdb break write if 1 == *(int*)($esp + 4) && strcmp((char*)*(int*)($esp + 8), "your string") == 0

x86 64-Bit-Modus

Wenn Ihr Prozess im x86-64-Modus ausgeführt wird, werden die Parameter durch Scratch-Register geleitet %rdi und %rsi

$ gdb break write if 1 == $rdi && strcmp((char*)($rsi), "your string") == 0

Beachten Sie, dass eine Indirektionsebene entfernt wird, da wir Scratch-Register anstelle von Variablen auf dem Stapel verwenden.

Varianten

Funktionen außer strcmp kann in den obigen Snippets verwendet werden:

  • strncmp ist nützlich, wenn Sie mit dem ersten übereinstimmen möchten n Anzahl der Zeichen des zu schreibenden Strings
  • strstr kann verwendet werden, um Übereinstimmungen innerhalb einer Zeichenfolge zu finden, da Sie nicht immer sicher sein können, dass sich die gesuchte Zeichenfolge an der Stelle befindet Anfang der Zeichenfolge, die durch die geschrieben wird write Funktion.

Bearbeiten: Ich habe diese Frage genossen und die nachfolgende Antwort gefunden. Ich entschied mich für eine Blogeintrag darüber.

  • Diese Antwort hat mich heute wieder gerettet. Ich werde ein Kopfgeld aufstellen, nur damit ich Ihnen mehr Glaubwürdigkeit für diese Antwort geben kann.

    – Ross Rogers

    19. Januar 2012 um 21:55 Uhr

  • Hach, das ist toll. Keine Sorge Kumpel!

    – Antonius

    19. Januar 2012 um 23:21 Uhr

  • Dadurch geht das Programm für mich in den Hintergrund und danach fg es sagt Error in testing breakpoint condition: 'strcmp' has unknown return type; cast the call to its declared return type

    – Riemenfisch

    28. Oktober 2021 um 13:51 Uhr

Benutzer-Avatar
Ciro Santilli Путлер Капут 六四事

catch + strstr Zustand

Das Coole an dieser Methode ist, dass sie nicht von glibc abhängt write verwendet wird: Es verfolgt den tatsächlichen Systemaufruf.

Außerdem ist es widerstandsfähiger gegen printf() Puffern, da es sogar Zeichenfolgen abfangen kann, die über mehrere gedruckt werden printf() Anrufe.

x86_64-Version:

define stdout
    catch syscall write
    commands
        printf "rsi = %s\n", $rsi
        bt
    end
    condition $bpnum $rdi == 1 && strstr((char *)$rsi, "$arg0") != NULL
end
stdout qwer

Testprogramm:

#define _XOPEN_SOURCE 700
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

int main() {
    write(STDOUT_FILENO, "asdf1", 5);
    write(STDOUT_FILENO, "qwer1", 5);
    write(STDOUT_FILENO, "zxcv1", 5);
    write(STDOUT_FILENO, "qwer2", 5);
    printf("as");
    printf("df");
    printf("qw");
    printf("er");
    printf("zx");
    printf("cv");
    fflush(stdout);
    return EXIT_SUCCESS;
}

Ergebnis: Pausen bei:

  • qwer1
  • qwer2
  • fflush. Der Vorherige printf druckte eigentlich nichts, sie wurden gepuffert! Das write syacall passierte nur auf dem fflush.

Anmerkungen:

  • $bpnum danke an Tromey unter: https://sourceware.org/bugzilla/show_bug.cgi?id=18727
  • rdi: Register, das die Nummer des Linux-Systemaufrufs in x86_64 enthält, 1 ist für write
  • rsi: erstes Argument des Systemaufrufs, z write es zeigt auf den Puffer
  • strstr: Standard-C-Funktionsaufruf, sucht nach Unterübereinstimmungen, gibt NULL zurück, wenn nicht gefunden

Getestet in Ubuntu 17.10, gdb 8.0.1.

spur

Eine weitere Option, wenn Sie sich interaktiv fühlen:

setarch "$(uname -m)" -R strace -i ./stdout.out |& grep '\] write'

Beispielausgabe:

[00007ffff7b00870] write(1, "a\nb\n", 4a

Kopieren Sie nun diese Adresse und fügen Sie sie ein in:

setarch "$(uname -m)" -R strace -i ./stdout.out |& grep -E '\] write\(1, "a'

Der Vorteil dieser Methode besteht darin, dass Sie die üblichen UNIX-Tools zur Manipulation verwenden können strace Ausgabe, und es erfordert kein tiefes GDB-fu.

Erläuterung:

  • -i macht strace Ausgabe RIP
  • setarch -R deaktiviert ASLR für einen Prozess mit a personality Systemaufruf: Wie man mit strace -i debuggt, wenn die Adresse jedes Mal anders ist GDB macht das bereits standardmäßig, also muss es nicht noch einmal gemacht werden.

  • Das war absolut augenöffnend! Lief wie am Schnürchen. Besser als andere Lösungen basierend auf strstr, strcmp und so, weil diese “Python”-Funktionen nicht bereits in Ihrem Programm vorhanden sein müssen.

    – Dunkles Auge

    22. April 2018 um 0:30 Uhr


  • Für Anfänger wie mich. Schreiben Sie das Skript hinein script.gdb und ausführen $gdb /path/yourprogramTyp (gdb) source script.gdb und führen Sie Ihr Programm mit aus (gdb) run.

    – Dunkles Auge

    22. April 2018 um 0:32 Uhr

  • Tut mir leid, ich habe gerade vergessen, dass dieser Beitrag diese Funktionen nicht verwendet, aber ich kann den richtigen Beitrag nicht finden, also schaue für andere einfach hier: sourceware.org/gdb/current/onlinedocs/gdb/…

    – Dunkles Auge

    22. April 2018 um 0:36 Uhr

Benutzer-Avatar
feihu

Anthonys Antwort ist großartig. Nach seiner Antwort probierte ich eine andere Lösung aus Fenster(x86-64-Bit-Windows). Ich weiß, diese Frage hier ist für GDB Unter Linux denke ich jedoch, dass diese Lösung eine Ergänzung für diese Art von Frage ist. Es könnte für andere hilfreich sein.

Lösung unter Windows

Unter Linux ein Aufruf an printf würde zu einem Aufruf der API führen write. Und da Linux ein Open-Source-Betriebssystem ist, konnten wir innerhalb der API debuggen. Die API ist jedoch unter Windows anders, sie stellt eine eigene API bereit WriteFile. Da Windows ein kommerzielles Nicht-Open-Source-Betriebssystem ist, konnten Haltepunkte in den APIs nicht hinzugefügt werden.

Aber ein Teil des Quellcodes von VC wird zusammen mit Visual Studio veröffentlicht, sodass wir im Quellcode herausfinden konnten, wo der schließlich aufgerufen wurde WriteFile API und setzen Sie dort einen Haltepunkt. Nach dem Debuggen des Beispielcodes fand ich die printf -Methode könnte zu einem Aufruf von führen _write_nolock in welchem WriteFile wird genannt. Die Funktion befindet sich in:

your_VS_folder\VC\crt\src\write.c

Der Prototyp ist:

/* now define version that doesn't lock/unlock, validate fh */
int __cdecl _write_nolock (
        int fh,
        const void *buf,
        unsigned cnt
        )

Im Vergleich zu den write API unter Linux:

#include <unistd.h>

ssize_t write(int fd, const void *buf, size_t count); 

Sie haben völlig die gleichen Parameter. Wir könnten also einfach a setzen condition breakpoint in _write_nolock beziehen Sie sich einfach auf die obigen Lösungen, mit nur einigen Unterschieden im Detail.

Portable Lösung für Win32 und x64

Es ist ein großes Glück, dass wir verwenden konnten den Namen von Parametern direkt in Visual Studio beim Festlegen einer Bedingung für Haltepunkte sowohl auf Win32 als auch auf x64. So wird es sehr einfach, die Bedingung zu schreiben:

  1. Fügen Sie einen Haltepunkt hinzu _write_nolock

    NOTIZ: Es gibt kaum Unterschiede zwischen Win32 und x64. Wir könnten einfach den Funktionsnamen verwenden, um die Position von Haltepunkten auf Win32 festzulegen. Es funktioniert jedoch nicht auf x64, da am Eingang der Funktion die Parameter nicht initialisiert werden. Daher konnten wir den Parameternamen nicht verwenden, um die Bedingung von Haltepunkten festzulegen.

    Aber glücklicherweise haben wir eine Lösung: Verwenden Sie die Position in der Funktion und nicht den Funktionsnamen, um die Breakpoints zu setzen, z. B. die 1. Zeile der Funktion. Dort sind die Parameter bereits initialisiert. (Ich meine, benutze die filename+line number um die Haltepunkte zu setzen, oder direkt die Datei öffnen und in der Funktion einen Haltepunkt setzen, nicht den Eingang sondern die erste Zeile. )

  2. Einschränkung der Bedingung:

    fh == 1 && strstr((char *)buf, "Hello World") != 0
    

NOTIZ: Hier gibt es immer noch ein Problem, ich habe zwei verschiedene Möglichkeiten getestet, etwas in stdout zu schreiben: printf und std::cout. printf würde alle Zeichenfolgen in die schreiben _write_nolock Funktion auf einmal. Jedoch std::cout würde nur zeichenweise an übergeben _write_nolockwas bedeutet, dass die API aufgerufen wird strlen("your string") mal. In diesem Fall konnte die Bedingung nicht für immer aktiviert werden.

Win32-Lösung

Natürlich könnten wir die gleichen Methoden wie verwenden Anthony bereitgestellt: Setzen Sie die Bedingung von Haltepunkten durch Register.

Für ein Win32-Programm ist die Lösung fast identisch mit GDB auf Linux. Sie werden vielleicht bemerken, dass es eine Dekoration gibt __cdecl im Prototyp von _write_nolock. Diese Aufrufkonvention bedeutet:

  • Die Reihenfolge der Argumentübergabe ist von rechts nach links.
  • Die aufrufende Funktion holt die Argumente aus dem Stack.
  • Namensverzierungskonvention: Namen wird ein Unterstrich (_) vorangestellt.
  • Keine Fallübersetzung durchgeführt.

Es gibt eine Beschreibung hier. Und es gibt eine Beispiel die verwendet wird, um die Register und Stacks auf der Microsoft-Website anzuzeigen. Das Ergebnis konnte gefunden werden hier.

Dann ist es sehr einfach, die Bedingung von Breakpoints zu setzen:

  1. Legen Sie einen Haltepunkt fest _write_nolock.
  2. Einschränkung der Bedingung:

    *(int *)($esp + 4) == 1 && strstr(*(char **)($esp + 8), "Hello") != 0
    

Es ist die gleiche Methode wie unter Linux. Die erste Bedingung besteht darin, sicherzustellen, dass in die Zeichenfolge geschrieben wird stdout. Die zweite ist die Übereinstimmung mit der angegebenen Zeichenfolge.

x64-Lösung

Zwei wichtige Änderung von x86 bis x64 sind die 64-Bit-Adressierungsfunktion und ein flacher Satz von 16 64-Bit-Registern für den allgemeinen Gebrauch. Als Erhöhung der Register wird nur x64 verwendet __fastcall als Aufrufkonvention. Die ersten vier Integer-Argumente werden in Registern übergeben. Argumente ab fünf werden auf dem Stack übergeben.

Sie könnten auf die verweisen Parameterübergabe Seite auf der Microsoft-Website. Die vier Register (in der Reihenfolge von links nach rechts) sind RCX, RDX, R8 und R9. Es ist also sehr einfach, die Bedingung einzuschränken:

  1. Legen Sie einen Haltepunkt fest _write_nolock.

    NOTIZ: Es unterscheidet sich von der obigen portablen Lösung, wir könnten einfach die Position des Haltepunkts auf die Funktion und nicht auf die 1. Zeile der Funktion setzen. Der Grund ist, dass alle Register bereits am Eingang initialisiert sind.

  2. Einschränkungsbedingung:

    $rcx == 1 && strstr((char *)$rdx, "Hello") != 0
    

Der Grund, warum wir casten und dereferenzieren müssen esp ist dass $esp greift auf die zu ESP registrieren, und für alle Absichten und Zwecke ist a void*. Während die Register hier direkt die Werte von Parametern speichern. Eine weitere Indirektionsebene wird also nicht mehr benötigt.

Post

Mir macht diese Frage auch sehr viel Spaß, also habe ich übersetzt Anthonys Beitrag ins Chinesische und füge meine Antwort als Ergänzung hinzu. Der Beitrag konnte gefunden werden hier. Danke für die Erlaubnis von @anthony-arnold .

Benutzer-Avatar
JeanP

Anthonys Antwort ist sehr interessant und liefert definitiv einige Ergebnisse. Ich denke jedoch, dass die Pufferung von printf fehlen könnte. In der Tat können Sie unter Unterschied zwischen write () und printf () Folgendes lesen: “printf ruft nicht unbedingt jedes Mal schreiben auf. Vielmehr puffert printf seine Ausgabe.”

STDIO-WRAPPER-LÖSUNG

Daher kam ich mit einer anderen Lösung, die darin besteht, eine Hilfsbibliothek zu erstellen, die Sie vorab laden können, um die printf-ähnlichen Funktionen zu verpacken. Sie können dann einige Breakpoints auf dieser Bibliotheksquelle setzen und zurückverfolgen, um die Informationen über das Programm zu erhalten, das Sie debuggen.

Es funktioniert unter Linux und zielt auf die libc ab, ich weiß es nicht für c++ IOSTREAM, auch wenn das Programm direkt schreiben verwendet, wird es es vermissen.

Hier ist der Wrapper zum Entführen von printf (io_helper.c).

#include<string.h>
#include<stdio.h>
#include<stdarg.h>

#define MAX_SIZE 0xFFFF

int printf(const char *format, ...){
    char target_str[MAX_SIZE];
    int i=0;

    va_list args1, args2;

    /* RESOLVE THE STRING FORMATING */
    va_start(args1, format);
    vsprintf(target_str,format, args1);
    va_end(args1);

    if (strstr(target_str, "Hello World")){ /* SEARCH FOR YOUR STRING */
       i++; /* BREAK HERE */
    }   

    /* OUTPUT THE STRING AS THE PROGRAM INTENTED TO */
    va_start(args2, format);
    vprintf(format, args2);
    va_end(args2);
    return 0;
}

int puts(const char *s) 
{   
   return printf("%s\n",s);
}

Ich habe Puts hinzugefügt, weil gcc dazu neigt, printf durch Puts zu ersetzen, wenn es möglich ist. Also zwinge ich es zurück zu printf.

Als nächstes kompilieren Sie es einfach in eine gemeinsam genutzte Bibliothek.

gcc -shared -fPIC io_helper.c -o libio_helper.so -g

Und Sie laden es, bevor Sie gdb ausführen.

LD_PRELOAD=$PWD/libio_helper.so; gdb test

Wobei test das Programm ist, das Sie debuggen.

Dann kannst du mit brechen break io_helper.c:19 weil Sie die Bibliothek mit -g kompiliert haben.

ERKLÄRUNGEN

Unser Glück hier ist, dass printf und andere fprintf, sprintf … nur hier sind, um die variadischen Argumente aufzulösen und ihr ‘v’-Äquivalent zu nennen. (vprintf in unserem Fall). Diese Aufgabe zu erledigen ist einfach, also können wir es tun und die eigentliche Arbeit mit der ‘v’-Funktion der libc überlassen. Um die variablen Argumente von printf zu erhalten, müssen wir nur va_start und va_end verwenden.

Der Hauptvorteil dieser Methode besteht darin, dass Sie sicher sind, dass Sie sich beim Abbrechen in dem Teil des Programms befinden, der Ihre Zielzeichenfolge ausgibt, und dass dies kein Überbleibsel in einem Puffer ist. Auch machen Sie keine Annahmen über die Hardware. Der Nachteil ist, dass Sie davon ausgehen, dass das Programm die libc stdio-Funktion verwendet, um Dinge auszugeben.

1012100cookie-checkWie kann ich überwachen, was in den Standardausgabepuffer gesteckt wird und brechen, wenn eine bestimmte Zeichenfolge in der Pipe abgelegt wird?

This website is using cookies to improve the user-friendliness. You agree by using the website further.

Privacy policy