kostenloser Webspace werbefrei: lima-city


gcc-gdb-Problem (C++): Ungewollte Optimierung?

lima-cityForumProgrammiersprachenC/C++ und D

  1. Autor dieses Themas

    presencia

    presencia hat kostenlosen Webspace.

    Hallo allerseits,

    ich habe hier ein etwas spezielles Problem beim Debuggen mit dem gdb. Eigentlich nutze ich Eclipse-CDT als Frontend, jedoch liegt das Problem offensichtlich an dem gcc-Compiler oder am gdb selbst. Daher habe ich es einmal ganz ohne Eclipse nachvollzogen.

    Mein Beispielprogramm
    Um das Problem einzugrenzen und zu erklären, habe ich folgendes "Minimalprogramm" geschrieben und als Datei "main.cpp" abgespeichert.
    #include<vector>
    #include <iostream>
    
    struct I{
    	std::vector<unsigned> p;
    	I() : p(std::vector<unsigned>(1,0)){
    		std::cout << "p has length " << p.size() << " and its first value is " << p[0] << std::endl;
    	}
    };
    
    struct T{
    	T (I i){
    		std::cout << "i.p has length " << i.p.size() << " and its first value is " << i.p[0] << std::endl;
    	}
    };
    
    int main(){
    	I i;
    	T t(i);
    }


    Dieses Programm habe ich wie folgt kompiliert (ich benutze Ubuntu 10.04, gcc-Version 4.4.3):
    g++ -O0 -g -o test main.cpp

    Es tut genau das Erwartete, wenn man es laufen lässt. Die Ausgabe ist

    p has length 1 and its first value is 0
    i.p has length 1 and its first value is 0


    Seltsames Verhalten beim Debugging
    Mein Problem ist das Debugging. Es geht um die Frage, ob der Konstruktor T::T(I) in Zeile 12 sein Argument i als Wert (call-by-value) oder als Referenz (call-by-reference) übergeben bekommt. Ersteres sollte imho der Fall sein. Auf der Kommandozeile ergibt sich folgende Debugging-Session (gestartet durch Eingabe von "gdb test"):

    GNU gdb (GDB) 7.1-ubuntu
    Copyright (C) 2010 Free Software Foundation, Inc.
    License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
    This is free software: you are free to change and redistribute it.
    There is NO WARRANTY, to the extent permitted by law.  Type "show copying"
    and "show warranty" for details.
    This GDB was configured as "i486-linux-gnu".
    For bug reporting instructions, please see:
    <http://www.gnu.org/software/gdb/bugs/>...
    Reading symbols from /home/jonathan/eclipse-workspace/test/test...done.
    (gdb) break main.cpp:19
    Breakpoint 1 at 0x80488ab: file main.cpp, line 19.
    (gdb) break main.cpp:13
    Breakpoint 2 at 0x8048a82: file main.cpp, line 13.
    (gdb) run
    Starting program: /home/jonathan/eclipse-workspace/test/test 
    p has length 1 and its first value is 0
    
    Breakpoint 1, main () at main.cpp:19
    19	T t(i);
    (gdb) print i
    $1 = {p = {<std::_Vector_base<unsigned int, std::allocator<unsigned int> >> = {
          _M_impl = {<std::allocator<unsigned int>> = {<__gnu_cxx::new_allocator<unsigned int>> = {<No data fields>}, <No data fields>}, _M_start = 0x804c008, _M_finish = 0x804c00c, 
            _M_end_of_storage = 0x804c00c}}, <No data fields>}}
    (gdb) print {unsigned int}0x804c008
    $2 = 0
    (gdb) c
    Continuing.
    
    Breakpoint 2, T (this=0xbffff41f, i=...) at main.cpp:13
    13			std::cout << "i.p has length " << i.p.size() << " and its first value is " << i.p[0] << std::endl;
    (gdb) print i
    $3 = {p = {<std::_Vector_base<unsigned int, std::allocator<unsigned int> >> = {
          _M_impl = {<std::allocator<unsigned int>> = {<__gnu_cxx::new_allocator<unsigned int>> = {<No data fields>}, <No data fields>}, _M_start = 0xbffff404, _M_finish = 0xbffff408, 
            _M_end_of_storage = 0x80486d4}}, <No data fields>}}
    (gdb) print {unsigned int}0xbffff404
    $4 = 134529048
    (gdb) print {I}0xbffff404
    $5 = {p = {<std::_Vector_base<unsigned int, std::allocator<unsigned int> >> = {
          _M_impl = {<std::allocator<unsigned int>> = {<__gnu_cxx::new_allocator<unsigned int>> = {<No data fields>}, <No data fields>}, _M_start = 0x804c018, _M_finish = 0x804c01c, 
            _M_end_of_storage = 0x804c01c}}, <No data fields>}}
    (gdb) print {unsigned int}0x804c018
    $6 = 0
    (gdb) c
    Continuing.
    i.p has length 1 and its first value is 0
    
    Program exited normally.
    (gdb) quit


    Ich erkläre die Schritte beim Debugging:
    Zunächst erstelle ich zwei Breakpoints, nämlich in Zeile 18 und 13. Dann starte ich das Programm im gdb, es wird bis vor Zeile 19 ausgeführt. Nun lasse ich mir das in Zeile 18 erstellte i ausgeben (also den Wert der Variablen i). Variable i ist vom Typ struct I und enthält daher nur ein std::vector<unsigned int>-Objekt. Dieses speichert die drei Pointer _M_start, _M_finish und _M_end_of_storage. Will ich auf den Wert des ersten Eintrags des Vektors zugreifen, so muss ich nur den Pointer _M_start dereferenzieren. Ich lasse mir also den Speicher an Stelle 0x804c008, geparsed als unsigned int ausgeben. Der Wert ist - wie erwartet und völlig zu Recht - 0. Nun lasse ich das Programm bis zum nächsten Breakpoint in Zeile 13 weiter laufen. Dort lasse ich mir wieder die Variable i ausgeben und dereferenziere den _M_start-Pointer. Der Wert ist 134529048, also nicht 0, wie er eigentlich sein sollte. Nun derefenziere ich den angeblichen _M_start-Pointer mal als Pointer auf ein Objekt der Klasse I, und siehe da, ich finde ein Ordentliches I-Objekt vor. Der Vektor i.p hat Länge 1 und sein erster Eintrag ist 0, wie man sieht. Ich lasse das Programm weiter laufen, es gibt mir seinerseits Informationen über i (über std::cout). Dann ist das Programm beendet und ich verlasse den gdb.

    Mein Fazit:
    Es scheint ganz so, als werde dem Konstruktor T::T(I) in Zeile 12 sein Argument i nicht als Wert übergeben, also als Kopie des Wertes i aus Zeile 18, wie man eigentlich vermuten sollte. Vielmehr scheint das i aus Zeile 18 zwar kopiert zu werden, aber dann wird dem Konstruktor nur eine Referenz (technisch: ein Pointer) auf diese Kopie übergeben. Streng genommen ist das weder call-by-value noch call-by-reference. Doch damit scheint der Konstruktor umzugehen können, wie die Ausgabe des Programms zeigt. Einzig der Debugger hält sich ganz an den Code (call-by-value) und interpretiert die Referenz nun als Wert der Variablen i, was zu Verwirrungen führt. Denn ich sehe beim Debuggen ein anderes i (print i) als mein Programm.

    Mein Problem damit:
    Ein solches Verhalten deutet eigentlich auf eine Art ungewollte Optimierung durch den Compiler hin. Doch ich habe ausdrücklich alle Optimierung verboten (Option -O0 beim Kompilieren). Ist das ein Compiler-Bug? Oder habe ich vielleicht etwas nicht richtig verstanden? Eigentlich ist es mir egal, wie die Übergabe der Parameter an meine Funktionen erfolgen, so lange sie tatsächlich kopiert werden (was ja auch hier geklappt hat) und mein Programm das tut, was es soll (auch das ist hier der Fall). Jedoch sind die Funktionen und Klassen bei meinen Programmier-Projekten deutlich komplizierter, und leider begegnet mir dieser Fehler an vielen Stellen beim Debuggen. Das erschwert mir die Arbeit sehr und macht eine Beobachtung der Werte meiner Variablen manchmal sogar unmöglich. Ich wüsste also auch gern, ob ich etwas dagegen tun kann.

    Sollte also irgend jemand eine Idee haben, woran es liegt oder was hier zu tun wäre, schreibt mir bitte.
    Gruß
    ~<><~~~~~~ presencia
  2. Diskutiere mit und stelle Fragen: Jetzt kostenlos anmelden!

    lima-city: Gratis werbefreier Webspace für deine eigene Homepage

  3. Hallo presencia,

    dieses Verhalten ist ganz normal und hat nichts mit Optimierung zu tun.
    Wenn ein großes Objekt By-Value übergeben wird, dann wird eine Kopie im Stack-Bereich der aufrufenden Funktion erzeugt und als Argument nur der Zeiger auf diesen Speicherbereich übergeben.
    Eine genauere Begründung findest Du hier in den Abschnitten The copy-constructor -> Passing & returning large objects bis einschließlich The copy-constructor -> Re-entrancy.
  4. Autor dieses Themas

    presencia

    presencia hat kostenlosen Webspace.

    Hallo darkpandemic,

    danke für die superschnelle Antwort. Ich habe inzwischen den Abschnitt gelesen, und ein bisschen drum herum. Nettes online-Buch, danke für den Tipp. Allerdings geht es dort nur um das Problem, große Objekte zurückzugeben und nicht an die Funktion zu übergeben. Jedoch wenn du sagst, dass die Kopie im Stack-Bereich der aufrufenden Funktion ist, glaube ich es gern.

    Mein Problem mit dem gcc bleibt jedoch im Grunde bestehen. Selbst wenn es sich nicht um ungewollte Optimierung handelt, sind die Debugging-Informationen, die der gdb liest, nicht korrekt. Und das ist imho ein Fehler des Compilers und nicht des Debuggers. Entweder der Debugger sollte wissen, dass es sich bei i um eine Referenz und nicht um das Objekt selbst handelt, oder er sollte gleich an die referenzierte Adresse verwiesen werden, da findet man ja auch das korrekte I-Objekt.

    Ich habe mir heute mal die gcc-Versionen 4.5 und 4.1 installiert (bisher hatte ich nur 4.4). Wenn ich das Programm mit Version 4.5 kompiliere, ändert das nichts an dem Problem. Version 4.1 jedoch ändert scheinbar die Debugging-Informationen: Der Debugger erkennt nun i (in Zeile 13) als eine Referenz auf ein I-Objekt und gibt nun auch die korrekten Infos über i.p aus. Warum hat man das wohl geändert? Sind jetzt also die neueren gcc-Versionen fehlerhaft? (Leider konnte ich Version 4.6 bei mir nicht installieren).

    Gruß
    ~<><~~~~~ presencia
  5. Hallo presencia,

    bei der Debug-Info hast Du wohl recht, das macht er tatsächlich falsch. Allerdings weis ich auch nicht, ob er das jemals richtig gemacht hat, da ich eigentlich nie Objekte by-value übergebe. Meistens hat man ja entweder Zeiger oder const Reference (strings jetzt mal ausgenommen).
    Das das mit dem Stack auch beim Aufruf so ist, kann man direkt am Assembler-Listing ablesen:
    push   ebp
    mov    ebp,esp
    and    esp,0xfffffff0
    push   ebx
    sub    esp,0x3c                        /* <- 60 Bytes Stack für main() */
    lea    eax,[esp+0x18]                  /* <- Adresse von Stack[24] nach eax */
    mov    DWORD PTR [esp],eax             /* <- Adresse von Stack[24] nach Stack[0] */
                                           /* insgesamt: Stack[0-3] = &Stack[24] */
                                           
    call   0x8048980 <I::I()>              /* <- Konstruktor für i aufrufen */
                                           /* Stack[0-3] enthält Startadresse des zu */ 
                                           /* initialisierenden Speicherbereiches. */
                                           
    lea    eax,[esp+0x18]                  /* <- Adresse von i nach eax */
    mov    DWORD PTR [esp+0x4],eax         /* <- Adresse von i nach Stack[4-7] */
    lea    eax,[esp+0x24]                  /* <- Adresse von Stack[36] nach eax */
    mov    DWORD PTR [esp],eax             /* <- Adresse von Stack[36] nach Stack[0-3] */
    call   0x8048af8 <I::I(I const&)>      /* <- Aufruf des Copy-Konstruktors */
                                           /* Stack[0-3] enthält Startadresse des zu */
                                           /* initialisierenden Speicherbereiches. Stack[4-7] */
                                           /* enthält die Adresse des zu kopierenden Objektes */
                                           
    lea    eax,[esp+0x24]                  /* <- Adresse der Kopie von i nach eax */
    mov    DWORD PTR [esp+0x4],eax         /* <- Adresse der Kopie von i nach Stack[4-7] */
    lea    eax,[esp+0x17]                  /* <- Adresse von Stack[23] nach eax */
    mov    DWORD PTR [esp],eax             /* <- Adresse von Stack[23] nach Stack[0-3] */
    call   0x8048a66 <T::T(I)>             /* <- Aufruf des Konstruktors für t */
    
    lea    eax,[esp+0x24]                  /* <- Adresse der Kopie von i nach eax */
    mov    DWORD PTR [esp],eax             /* <- Adresse der Kopie von i nach Stack[0-3] */
    call   0x8048ae4 <I::~I()>             /* <- Kopie von i destruieren */
    
    ...
    Das ist jetzt der relevante Teil Deiner main(). Man muss bedenken, dass der Stack nach unten wächst, d.h. die Adressen werden immer kleiner. Die Double-Words Stack[0-3] und Stack[4-7] sind immer die Argumente für die Funktionen.
  6. Autor dieses Themas

    presencia

    presencia hat kostenlosen Webspace.

    Hallo darkpanedmic,

    ich bin noch nicht so weit, den Assembler-Code zu verstehen. Danke trotzdem für dein engagiertes Erklären desselben.

    Nach der Lektüre des ticpp-e-books habe ich mich auch entschlossen, meine Objekte nicht mehr by-value zu übergeben. Super: Ich habe deutlich dazu lernen können. Und somit kann ich dann auch wieder die neueren gcc-Versionen verwenden.

    Wahrscheinlich sollte ich trotzdem einen Bug-Report erstellen, damit der gcc ein bisschen besser werden kann. Ich habe auf http://gcc.gnu.org/bugzilla/ schon nach einem Report gesucht, der mein Problem beschreibt, habe einen solchen jedoch nicht gefunden. Ich finde die Suchfunktionen schwer zu bedienen und die Bug-Reports schwer verständlich.

    Danke nochmal für die schnellen Antworten. Ich sollte öfter hier posten, wenn ich Fragen habe.

    Gruß
    ~<><~~~~~~~ presencia

  7. Diskutiere mit und stelle Fragen: Jetzt kostenlos anmelden!

    lima-city: Gratis werbefreier Webspace für deine eigene Homepage

Dir gefällt dieses Thema?

Über lima-city

Login zum Webhosting ohne Werbung!