staging.inyokaproject.org

Tipps für Server debugging erwünscht

Status: Gelöst | Ubuntu-Version: Ubuntu MATE 24.04 (Noble Numbat)
Antworten |

Dakuan

Avatar von Dakuan

Anmeldungsdatum:
2. November 2004

Beiträge: 6532

Ich bastele seit einiger Zeit an einem einfachen Server, mit dem ein Benutzer bestimmte Dateien im momentanen Arbeitsverzeichnis freigeben kann (a.k.a. Instant Server). Grundsätzlich funktioniert das auch.

Ich versuche jetzt, dem Server auch "Keep-Alive" beizubringen. Was nicht funktionieren will. Aber deswegen hatte ich jetzt mal genauer hingeschaut und sehe da im Systemprotokollbetrachter folgendes:

...
ERROR: apport (pid 8668) Sat Aug 23 19:34:19 2025: called for pid 8666, signal 11, core limit 0, dump mode 1
ERROR: apport (pid 8668) Sat Aug 23 19:34:19 2025: executable: /home/manfred/prog/net/fserv/ifserver (command line "/home/manfred/prog/net/fserv/ifserver -- -v1 -d1")
ERROR: apport (pid 8668) Sat Aug 23 19:34:19 2025: executable does not belong to a package, ignoring

Wenn ich das richtig sehe, ist das eine Speicherschutz Verletzung, die aber in einem Kind Prozess stattfindet. Wie komme ich da ran? In /var/crash/ ist natürlich nichts. Früher hatte ich in ähnlichen Fällen daher apport ausgetrickst, indem ich folgendes Script gestartet hatte:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
#!/bin/bash
#   Freischalten eines coredumps und als Speicherort "/tmp/" festlegen.
if [ $UID -eq 0 ]; then
    echo "/tmp/core.%e.%p" > /proc/sys/kernel/core_pattern
    echo "1" > /proc/sys/kernel/core_uses_pid
    service procps start
    ulimit -c unlimited
else
    echo "Du bist nicht root!"
fi

und danach noch ein:

ulimit -c unlimited

in dem Terminal, in dem der Server gestartet wurde (offenbar gilt das nur für das Startterminal). Im Systemprotokollbetrachter taucht danach nichts mehr auf. Allerdings finde ich auch nichts in /tmp/core... . Früher hat das aber immer funktioniert.

Kann es daran liegen, dass die Kind Prozesse des Servers keine Konsole haben?

Was kann ich da noch machen? Bisher ist mir das nicht aufgefallen, da der Server macht was er soll und der Absturz des Kind Prozesses wohl erst nach getaner Arbeit auftritt. Hängt wohl auch damit zusammen, dass ich "Keep-Alive" implementieren wollte. Wenn die geladene Datei ca. 100 Vorschaudateien enthält, könnte "Keep-Alive" Sinn machen.

Dakuan

(Themenstarter)
Avatar von Dakuan

Anmeldungsdatum:
2. November 2004

Beiträge: 6532

Mir ist da heute Morgen "Valgrind" eingefallen. Das Programm hat mir erst mal "unmögliche" Fehler aufgezeigt, die ich nicht nachvollziehen kann und die in völlig anderen Programmteilen liegen. Wahrscheinlich sind das Folgefehler. Daraufhin habe ich die letzte Erweiterung rückgängig gemacht.

==8226== Process terminating with default action of signal 11 (SIGSEGV)
==8226==  Access not within mapped region at address 0x0
==8226==    at 0x51FEB30: __strcmp_sse2_unaligned (strcmp-sse2-unaligned.S:24)
==8226==    by 0x10B80D: handle_request (handle_request.c:612)
==8226==    by 0x10AABA: main (fserv.c:198)
==8226==  If you believe this happened as a result of a stack
==8226==  overflow in your program's main thread (unlikely but
==8226==  possible), you can try to increase the size of the
==8226==  main thread stack using the --main-stacksize= flag.
==8226==  The main thread stack size used in this run was 8388608.

Das sagt mir nicht viel, hört sich aber gefährlich an.

Ich habe jetzt erst mal einige Speicherfehler der Kategorie "es ist immer einer mehr oder weniger als man denkt" beseitigt.

seahawk1986

Anmeldungsdatum:
27. Oktober 2006

Beiträge: 11278

Ich würde darauf tippen, dass du da irgendwo einen Nullpointer verarbeitest.

Vielleicht mal das betroffene Programm mit Debug-Symbolen bauen und Backtraces sammeln (man kann apport rauswerfen und sich stattdessen systemd-coredump installieren, das speichert auf Wunsch die Backtraces mit im Journal) - wenn es gekracht hat, kannst du dir mit coredumpctl die Crashes listen lassen und dir einzelne Backtraces mit gdb ansehen - dann wird klar, wo die einzelnen Threads gerade waren und welche Werte bestimmte Variablen hatten.

Dakuan

(Themenstarter)
Avatar von Dakuan

Anmeldungsdatum:
2. November 2004

Beiträge: 6532

Das mit dem Nullpointer halte ich für unwahrscheinlich, kann ich aber auch nicht ausschließen. Ich hatte mir die Sache wohle etwas zu einfach gemacht, indem ich den Programmteil, der die Anfragen bearbeitet, einfach in einer Schleife laufen lasse. Ich hatte vergessen dass einige Variablen innerhalb der Schleife neu initialisiert werden müssen. Mindestens ein Zeiger muss wieder auf den Pufferanfang gestellt werden. Aber da ist noch mehr:

==8197== Process terminating with default action of signal 13 (SIGPIPE)
==8197==    at 0x526A1CA: sendfile (syscall-template.S:78)
==8197==    by 0x10C082: send_icon (handle_request.c:781)
==8197==    by 0x10BDAC: handle_request (handle_request.c:727)
==8197==    by 0x10AABA: main (fserv.c:198)
==8197== 

Auf SIGPIPE muss ich auch noch reagieren. Und ich habe keine Ahnung, was da noch alles schief laufen kann.

Eigentlich habe ich zwei Dinge auf dem Zettel. Einmal den Server von Prozessen auf Threads umstellen und dann eben Keep-Alive. Ich dachte das letzteres einfacher ist und schneller geht.

Beispiele für Server mit Threads hatte ich mir schon angesehen, aber da tauchten dann so exotische Sachen wie atomic_store() und atomic_load() auf, das hatte mich abgeschreckt.

Dakuan

(Themenstarter)
Avatar von Dakuan

Anmeldungsdatum:
2. November 2004

Beiträge: 6532

Ich habe deinen Vorschlag umgesetzt, und die Schleife wieder eingebaut um den Fehler zu provozieren. Um drei Ecken herum scheint es natürlich doch ein Nullpointer zu sein.

Core was generated by `./ifserver -- -v1 -d1'.
Program terminated with signal SIGSEGV, Segmentation fault.
#0  0x000055ece33efb21 in handle_request (soc_fd=4) at handle_request.c:607
607	            if( *cp1 == '/' )
(gdb) bt
#0  0x000055ece33efb21 in handle_request (soc_fd=4) at handle_request.c:607
#1  0x000055ece33eecc8 in main (argc=4, argv=0x7ffffa1722f8) at fserv.c:198
(gdb) 

Der zugehörige Quelltext:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
...
    while( 1 ) {
        n = receive_line( soc_fd, rcv_buf, BUFFER_SIZE );
        printf( "rcvl: %d\n", n );                        // eingefügte Testausgabe
        cptr = rcv_buf;
        if( *cptr == '\r' && *(cptr+1) == '\n') {
            break;  // end of request header (empty line)
        }
        print_msg( 1, "%s", cptr );
        if( first ) {   // must be method
            cp1 = strtok( rcv_buf, " \t\r\n" );
            snprintf( method, sizeof(method), "%s", cp1 );
            cp1 = strtok( NULL, " \t\r\n" );
            if( *cp1 == '/' )                             // <- hier passiert der SIGSEGV - strtok() findet nichts
                cp1++;
            snprintf( uri, sizeof(uri), "%s", cp1 );
            first = 0;
            continue;
        }
...

Ich bin irrtümlicherweise davon aus gegangen, dass dies nicht passiert, da ich vorher dachte mit:

fcntl( soc_fd, F_GETFL )

testen zu können, ob die Verbindung noch besteht. Wahr wohl etwas zu kurz gegriffen.

Humbi

Anmeldungsdatum:
14. Juni 2014

Beiträge: Zähle...

Was mir auffaellt:

1
cp1 = strtok( NULL, " \t\r\n" );

Du suchst ja in NULL, damit wird das Ergebnis immer NULL sein.

Was ist first? Die Initialisierung fehlt.

Durch strtok sehe ich keinen Grund warum du das erste Vorkommen anderst behandeln solltest. Ich denke das if sollte ein loop sein.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
do {
    printf( "rcvl: %d\n", n );                        // eingefügte Testausgabe
    cptr = rcv_buf;
    if( cptr && *cptr == '\r' && *(cptr+1) == '\n') {
        break;  // end of request header (empty line)
    }
    print_msg( 1, "%s", cptr );
    //if( first ) {   // must be method
    cp1 = strtok( rcv_buf, " \t\r\n" );
    do {
        snprintf( method, sizeof(method), "%s", cp1 );
        if( *cp1 == '/' )                             // <- hier passiert der SIGSEGV - strtok() findet nichts
            cp1++;
            snprintf( uri, sizeof(uri), "%s", cp1 );
            first = 0;
        }
        cp1 = strtok( rcv_buf, " \t\r\n" );
    } while ( cp1 != NULL );
    if ( cp1 == NULL )
        break;
} while (receive_line( soc_fd, rcv_buf, BUFFER_SIZE ) > 0);

n wurde nicht verwendet, brauchst du das?

Ungetesteter Code Snippet, wie ich mir die Verarbeitung des Inputs vorstellen koennte.

EDIT: Ich weiss nicht was rein kommt, evtl kann man auf eine Schleife verzichten.

seahawk1986

Anmeldungsdatum:
27. Oktober 2006

Beiträge: 11278

Humbi schrieb:

Was mir auffaellt:

1
cp1 = strtok( NULL, " \t\r\n" );

Du suchst ja in NULL, damit wird das Ergebnis immer NULL sein.

Man kann das auch zum Weitersuchen in einem bereits angeknabberten c-String verwenden, vgl. https://en.cppreference.com/w/cpp/string/byte/strtok - ohne Kontext sind Schnipsel leider wenig hilfreich, aber nachdem strtok() so oder so einen nullptr zurückgeben kann, muss man darauf prüfen, bevor man den Dereferenziert.

Dakuan

(Themenstarter)
Avatar von Dakuan

Anmeldungsdatum:
2. November 2004

Beiträge: 6532

@ Humbi Der (unvollständige) Code Ausschnitt sollte nur den Zusammenhang mit dem Core-Dump/Backtrace zeigen. Die Variable 'n' hatte ich zu debugging Zwecken eingefügt, weil ich wissen wollte welchen wert receive_line() zurückgibt. Normalerweise ist das die Anzahl der empfangenen Zeichen, die hier erstmal nicht benötigt wird. In hier beobachteten Fehlerfall wird 0 (end of file) zurückgegeben. Es hätte auch -1 sein können. Eigentlich sollte die Schleife aber vorher beendet sein.

Der gezeigte Schleifenanfang ist der Teil, wo einzelnen Zeilen der GET Anfrage analysiert wird. Aber ich gebe zu, wenn ich wirklich "Keep-Alive" realisieren will, muss ich mehrere Abbruchkriterien berücksichtigen.

Und natürlich sollte strtok() durch strtok_r() ersetzt werden. Man sollt eben vorsichtig sein, wenn man alten Code recycled.

Humbi

Anmeldungsdatum:
14. Juni 2014

Beiträge: Zähle...

Man kann das auch zum Weitersuchen in einem bereits angeknabberten c-String verwenden,

Gut zu wissen, habe nur die ersten paar Absaetze der Manpage durch gelesen.

sollte strtok() durch strtok_r() ersetzt werden.

Was natuerlich wesentlich mehr Sinn macht, wenn man nach Zeilenenden sucht.

Ich bin mir ziemlich sicher, dass dein Problem an der angezeigten Stelle durch dereferenzieren von NULL entstanden ist.

Ich rate auch dazu die Rueckgabewerte von Syscalls wie receive, select, read,.. zu checken um Errors abfangen zu koennen. Und unser aller code kann nicht soo wichtig sein, dass es auf einen check eines pointers ankommt. Deswegen sollte man immer schreiben ( p && *p ) wenn man Werte aus Pointern will. Dann spart man sich schon viel Zeit beim debuggen.

(Ich muss auch mal wieder C schreiben)

Dakuan

(Themenstarter)
Avatar von Dakuan

Anmeldungsdatum:
2. November 2004

Beiträge: 6532

Was natuerlich wesentlich mehr Sinn macht, wenn man nach Zeilenenden sucht.

Nein, das 'r' steht für reentrant. Das bedeutet, das die Funktion von mehreren Prozessen gleichzeitig verwendet werden kann. Das funktioniert nicht, wenn die Funktion statischen Speicher verwendet. Bei strtok() wird der Zeiger für die Fortsetzung statisch gespeichert. Bei strtok_r() muss der Anwender diesen Speicherplatz bereitstellen.

Ich habe jetzt als als zusätzliches Abbruchkriterium den Empfang von EOF (n==0) aufgenommen.

1
2
3
4
5
6
7
8
...
    while( 1 ) {
        n = receive_line( soc_fd, rcv_buf, BUFFER_SIZE );
        if( n <= 0 ) {
            ret_val = -1;
            break;
        }
...

Damit läuft das erstmal durch. Das passiert, wenn Firefox die Verbindung nach der angegebenen Wartezeit beendet. Trotzdem wird eine bestehende Verbindung maximal einmal wiederverwendet. Danach kommen immer neue Verbindungen (mehrere gleichzeitig).

Wirklich sauber ist diese Lösung noch nicht, aber es ist ein Anfang.

Nachtrag: Wenn Firefox 'Keep-Alive' akzeptiert, startet er 5 weitere Verbindungen und verwendet diese dann auch weiter. Wenn das erzeugte HTML Dokument aber nur 4 oder 5 Vorschaudateien enthält, erkennt man das nicht. Bei 20+ aber schon ...

Antworten |