Forum: Mikrocontroller und Digitale Elektronik C++: Objekt mit Adresse einer Variablen initialisieren klappt nicht


von Manuel W. (multisync)


Lesenswert?

Hallo zusammen,

ich bringe mir gerade selbst C++ bei, und bin auf ein Problem bei der 
Initialisierung von Objekten gestoßen.
1
struct fifo_char_t Empfangskanal;
2
static Shell prompt(&Empfangskanal);

Ich erzeuge eine globale Variable vom Typ 'struct fifo_char_t'.
Anschließend erzeuge ich ein 'prompt'-Objekt und übergebe die Adresse 
der erwähnten Variable dem Initializer. Diese soll dann als data member 
im Objekt abgelegt werden.

Der Initializer sieht folendermaßen aus:
1
class Shell {
2
  public:
3
          Shell(struct fifo_char_t *);
4
    void UpdateBuffer();
5
    bool IsFullLineAvailable();
6
    bool IsBufferEmpty();
7
    char ReadBuffer();
8
  private:
9
    struct fifo_char_t * input_buffer_;
10
    struct fifo_char_t buffer_;
11
    bool line_received_;
12
};
1
Shell::Shell(struct fifo_char_t * input_buffer)
2
{
3
        input_buffer_ = input_buffer;
4
}

Im Debugger sehe ich, dass 'Empfangskanal' auch korrekt in dem RAM 
meines Mikrocontrollers gelinkt wurde. Das Befüllen dieser Variable 
klappt reibungslos.

Allerdings: Wenn ich mir zur Laufzeit die data member meines Objektes 
'prompt' ansehe, sehe ich, dass 'input_buffer_' auf 0x0 zeigt.

Wie gesagt bin ich noch recht neu was C++ anbelangt. Hat das 
irgendwelche offensichtlichen Gründe?

Danke
Manuel

: Bearbeitet durch User
von Rufus Τ. F. (rufus) Benutzerseite


Lesenswert?

Manuel W. schrieb:
> Allerdings: Wenn ich mir zur Laufzeit die data member meines Objektes
> 'prompt' ansehe,

Wie und womit?

von Sven B. (scummos)


Lesenswert?

Soweit ich weiß findet die Initialisierung aller statics statt, bevor 
deine globale Variable initialisiert wird. Bin aber nicht sicher.

von Manuel W. (multisync)


Lesenswert?

Rufus Τ. F. schrieb:
> Manuel W. schrieb:
>> Allerdings: Wenn ich mir zur Laufzeit die data member meines Objektes
>> 'prompt' ansehe,
>
> Wie und womit?

Mit dem Debugger. Ich benutze Em::Blocks. Das Ding hat ein GCC-Backend 
und verwendet zum debuggen soweit ich weiß GDB.

: Bearbeitet durch User
von Manuel W. (multisync)


Lesenswert?

Sven B. schrieb:
> Soweit ich weiß findet die Initialisierung aller statics statt,
> bevor
> deine globale Variable initialisiert wird. Bin aber nicht sicher.

Also ich habe jetzt mal testweise das 'static' weggenommen, leider 
besteht das Problem jedoch weiterhin.

von Mikro 7. (mikro77)


Lesenswert?

Manuel W. schrieb:
> Hat das irgendwelche offensichtlichen Gründe?

Wenn es welche gibt, dann sehe ich sie auch nicht.

Da du dem constructor eine Adresse übergibst, kann der Wert niemals NULL 
sein.

Es gibt keinen (impliziten) default constructor, der auf NULL 
initialisieren könnte.

Der implizite copy constructor kopiert die Adresse (kann also auch in 
der Kopie nicht NULL sein).

Da brauchen wir wohl mehr Infos.

von Sven B. (scummos)


Lesenswert?

Mikro 7. schrieb:
> Manuel W. schrieb:
>> Hat das irgendwelche offensichtlichen Gründe?
> Da du dem constructor eine Adresse übergibst, kann der Wert niemals NULL
> sein.

??

Foo(nullptr);

von Rufus Τ. F. (rufus) Benutzerseite


Lesenswert?

Sven B. schrieb:
> ??

Du hast die Formulierung nicht verstanden.

Natürlich kann man einen Nullpointer übergeben. Das aber geschieht hier 
nicht, hier wird die Adresse eines existierenden Objektes übergeben. Und 
die kann nicht NULL sein.

Manuel W. schrieb:
> Mit dem Debugger.

Und was bedeutet in diesem Kontext Deine Formulierung "zur Laufzeit"?

von Sebastian E. (Gast)


Lesenswert?

Hast du mal einen Breakpoint (oder LED/printf-Debug) in den Konstruktor 
gesetzt, um zu schauen, ob dieser überhaupt aufgerufen wird? Wenn er 
nicht aufgerufen wird, stimmt wahrscheinlich mit deinem Startup-Code 
etwas nicht.

Wird dein Objekt korrekt initialisiert, wenn du es innerhalb einer 
Funktion (z.B. main) auf dem Stack anlegst (ohne new)?

von Manuel W. (multisync)


Lesenswert?

Rufus Τ. F. schrieb:
> Manuel W. schrieb:
>> Mit dem Debugger.
>
> Und was bedeutet in diesem Kontext Deine Formulierung "zur Laufzeit"?

Damit meine ich, dass ich einen Breakpoint auf den Beginn der 
main()-Funktion gelegt habe, und mir Inhalt des Objektes anzeigen hab 
lassen.

von Manuel W. (multisync)


Lesenswert?

Sebastian E. schrieb:
> Hast du mal einen Breakpoint (oder LED/printf-Debug) in den Konstruktor
> gesetzt, um zu schauen, ob dieser überhaupt aufgerufen wird? Wenn er
> nicht aufgerufen wird, stimmt wahrscheinlich mit deinem Startup-Code
> etwas nicht.

Nein, habe ich nicht. Mein Verständnis war, dass Objekte die global 
angelegt werden (also außerhalb einer Funktion) bereits zur 
Kompilierzeit instanziert werden. (Dh. die dahinterliegenden Strukturen 
werden so wie .data oder .bss section vom Startup Code initialisiert.) 
Ich hatte nicht angenommen, dass tatsächlich ein Funktionseinsprung in 
den Initializer erfolgt, da zu diesem Zeitpunkt die Vorraussetzungen zum 
Ausführen einer C-Funktion ja evtl. noch nicht gegeben sind. 
(Initialisierter Stackpointer usw.)

Sebastian E. schrieb:
> Wird dein Objekt korrekt initialisiert, wenn du es innerhalb einer
> Funktion (z.B. main) auf dem Stack anlegst (ohne new)?

Ja, dann funktioniert es es reibungslos!

von Sven B. (scummos)


Lesenswert?

Manuel W. schrieb:
> Ich hatte nicht angenommen, dass tatsächlich ein Funktionseinsprung in
> den Initializer erfolgt, da zu diesem Zeitpunkt die Vorraussetzungen zum
> Ausführen einer C-Funktion ja evtl. noch nicht gegeben sind.
> (Initialisierter Stackpointer usw.)

Deshalb passiert das ja auch erst später. Aber wie soll der Konstruktor 
zur Compilezeit ausgeführt werden? Das passiert nur, wenn er als 
constexpr markiert ist.

von Mikro 7. (mikro77)


Lesenswert?

1
struct Foo { char *p ; Foo(char *p) : p(p) {} } ;
2
3
static char text[] = "Is it you, Kurt?" ;
4
5
static Foo foo(text) ;
6
7
int main()
8
{
9
  return 0 ;
10
}
1
prompt> g++ -g3 -Wall -o foo foo.cc
2
prompt> gdb foo
3
...
4
(gdb) break main
5
Breakpoint 1 at 0x4004f1: file foo.cc, line 9.
6
(gdb) run
7
Starting program: /var/export/Data/Projects/rpi/lib/foo 
8
9
Breakpoint 1, main () at foo.cc:9
10
9    return 0 ;
11
(gdb) print foo
12
$1 = {p = 0x601040 <text> "Is it you, Kurt?"}

von Daniel A. (daniel-a)


Lesenswert?

Kann dein Debugger einen watchpoint auf input_buffer_ setzen, so dass du 
siehst wenn dieser gesetzt wird? Dann könnte man die Addresse von this 
ausgeben und ein backtrace machen. Wenn die Variable wieder auf 0 
gesetzt wird könnte man das wiederholen, und wenn man später die 
Addresse von prompt ausgibt wenn input_buffer_ 0 ist kann man schauen, 
ob es das gleiche Objekt wie bei bei der Ausgabe von this damals war, 
indem man die Addresse mit der von damals vergleicht.

von Manuel W. (multisync)


Lesenswert?

Sven B. schrieb:
> Manuel W. schrieb:
>> Ich hatte nicht angenommen, dass tatsächlich ein Funktionseinsprung in
>> den Initializer erfolgt, da zu diesem Zeitpunkt die Vorraussetzungen zum
>> Ausführen einer C-Funktion ja evtl. noch nicht gegeben sind.
>> (Initialisierter Stackpointer usw.)
>
> Deshalb passiert das ja auch erst später. Aber wie soll der Konstruktor
> zur Compilezeit ausgeführt werden? Das passiert nur, wenn er als
> constexpr markiert ist.

Ich dachte, dass der Konstruktor dann nicht im eigentlichen Sinne 
"ausgeführt" wird, sondern sich der Compiler bereits zur Kompilierzeit 
herleiten würde, was dann an den entsprechenden Speicherstellen zu 
stehen hat. In meinem Fall wäre das einfach: Da das Object als data 
member die Adresse der anderen globalen Struktur 'Empfangskanal' hat, 
müsste der Compiler wohl nur das symbol dieser anderen Struktur 
referenzieren. Das Eintragen der konkreten Adresse macht dann der 
Linker.

Von constexpr habe ich schonmal gehört. Das kam mit C++11, oder? Derzeit 
bin ich in meinem C++-Verständnis noch meilenweit davon entfernt... 
Leider.

: Bearbeitet durch User
von Rolf M. (rmagnus)


Lesenswert?

Was ist denn nun eigentlich das Problem? Kommt beim Zugriff Mist raus? 
Wird überhaupt zugegriffen? Oder hast du ein im Programm komplett 
ungenutztes Objekt per Debugger angeschaut?

Manuel W. schrieb:
> Ich dachte, dass der Konstruktor dann nicht im eigentlichen Sinne
> "ausgeführt" wird, sondern sich der Compiler bereits zur Kompilierzeit
> herleiten würde, was dann an den entsprechenden Speicherstellen zu
> stehen hat.

Meine Vermutung ist, dass er noch einen Schritt weiter geht: Er erkennt, 
dass die Variable nirgends benutzt wird und der Konstruktor keine 
Nebeneffekte hat und optimiert deshalb die Initialisierung komplett weg. 
Was nicht gebraucht wird, muss ja nicht extra befüllt werden.

von Manuel W. (multisync)


Lesenswert?

Ich benutze das Objekt tatsächlich. Der Mikrocontroller dereferenziert 
den Pointer und schreibt in's Nirvana.

Ich danke euch allen vielmals für die Unterstützung! Leider muss ich 
mich jetzt auf den Weg machen zu einer 30er-Feier. Sofern es mein 
Zustand zulässt melde ich mich morgen wieder!

Danke!!

von Sven B. (scummos)


Lesenswert?

Manuel W. schrieb:
> Ich dachte, dass der Konstruktor dann nicht im eigentlichen Sinne
> "ausgeführt" wird, sondern sich der Compiler bereits zur Kompilierzeit
> herleiten würde, was dann an den entsprechenden Speicherstellen zu
> stehen hat. In meinem Fall wäre das einfach: Da das Object als data
> member die Adresse der anderen globalen Struktur 'Empfangskanal' hat,
> müsste der Compiler wohl nur das symbol dieser anderen Struktur
> referenzieren. Das Eintragen der konkreten Adresse macht dann der
> Linker.
Nein, i.A. geht das nicht. Der Konstruktur kann ja die Member auch mit 
Sachen initialisieren die er aus einer Datei liest, oder vom Netzwerk, 
oder sonstwoher. Was du beschreibst geht nur in eng eingegrenzten 
Spezialfällen, nämlich wenn der Konstruktur constexpr ist.

> Von constexpr habe ich schonmal gehört. Das kam mit C++11, oder? Derzeit
> bin ich in meinem C++-Verständnis noch meilenweit davon entfernt...
Ja, in 11 kann man es aber wirklich nur auf sehr wenige Dinge anwenden. 
In C++14 ist es etwas flexibler.

von Rolf M. (rmagnus)


Lesenswert?

Sven B. schrieb:
> Nein, i.A. geht das nicht. Der Konstruktur kann ja die Member auch mit
> Sachen initialisieren die er aus einer Datei liest, oder vom Netzwerk,
> oder sonstwoher. Was du beschreibst geht nur in eng eingegrenzten
> Spezialfällen, nämlich wenn der Konstruktur constexpr ist.

Er kann es als Optimierung auch ohne constexpr machen, wenn er den 
Inhalt des Konstruktors kennt und da eben die oben genannten Aktionen 
nicht gemacht werden.

Manuel W. schrieb:
> Ich benutze das Objekt tatsächlich. Der Mikrocontroller dereferenziert
> den Pointer und schreibt in's Nirvana.

Hmm, dann würde mir nur noch einfallen, dass im Startup-Code deiner 
nicht näher genannten Laufzeitumgebung die Konstruktoren von statischen 
Objekten nicht aufruft. Manche handhaben C++-Code da nicht korrekt.

von Manuel W. (multisync)


Angehängte Dateien:

Lesenswert?

Rolf M. schrieb:
> Manuel W. schrieb:
>> Ich benutze das Objekt tatsächlich. Der Mikrocontroller dereferenziert
>> den Pointer und schreibt in's Nirvana.
>
> Hmm, dann würde mir nur noch einfallen, dass im Startup-Code deiner
> nicht näher genannten Laufzeitumgebung die Konstruktoren von statischen
> Objekten nicht aufruft. Manche handhaben C++-Code da nicht korrekt.

Ich benutze Em::Blocks mit dem mitgelieferten GCC.
1
C:\Program Files (x86)\EmBlocks\2.30\share\em_armgcc\arm-none-eabi\bin>gcc.exe --version
2
gcc.exe (EmBlocks ARM Embedded Processors GNU tools) 4.7.3

Der Startup-Code ist anbei. Bei der Durchsicht habe ich tatsächlich 
nichts gefunden was einen Aufruf von Konstruktoren andeutet... 
Interessant.

Dieser Startup Code war bei Em::Blocks dabei. Ich habe mir jetzt 
allerdings auch den angesehen, der bei CMSIS von ARM und bei der 
Standard-Peripheral-Library von ST dabei ist, im wesentlichen dürfte 
sich der aber nicht all zu groß unterscheiden.

Auch das im Map-File 'prompt' (das ist der Name des Objekts dessen 
Konstruktor wohl nicht aufgerufenn wird) nur an einer Stelle vorkommt 
wundert mich.


P.S.: Bei dem Mikrocontroller handelt es sich um einen STM32F103. 
(Cortex-M3)

: Bearbeitet durch User
von Markus F. (mfro)


Lesenswert?

Welchen Compiler verwendest Du?

Wenn Du nicht mit dem g++ (C++) sondern dem gcc (C) Frontend 
compilierst, werden die static initializer nicht aufgerufen.
Wenn Du's tust, wirst Du wahrscheinlich feststellen, daß dein 
Startupcode nicht ausreichend ist.

von Marc (Gast)


Lesenswert?

gcc ist der Frontend-Treiber. Für C, C++, Java, Ada, Fortran und wer 
weiß, was noch.

von Manuel W. (multisync)


Lesenswert?

Markus F. schrieb:
> Welchen Compiler verwendest Du?
>
> Wenn Du nicht mit dem g++ (C++) sondern dem gcc (C) Frontend
> compilierst, werden die static initializer nicht aufgerufen.
> Wenn Du's tust, wirst Du wahrscheinlich feststellen, daß dein
> Startupcode nicht ausreichend ist.

Um ehrlich zu sein: Ich weiß es nicht genau.

Ich denke, ich bin schon langsam so weit, dass mir die Abstraktion die 
mir ein fertiges Buildsystem bietet mehr schadet als nutzt.

Standardmäßig war für alle Dateien in der IDE (= Em::Blocks) als 
"compiler variable" 'CC' angegeben. Für meine C-Files habe ich das so 
gelassen, für die .cpp-Dateien habe ich das jedoch auf 'CPP' geändert.

Über Google habe ich jetzt folgendes gefunden: (Em::Blocks benutzt make 
im Hintergrund.)
https://www.gnu.org/software/make/manual/html_node/Implicit-Variables.html

Anscheinend steht 'CC' für den C-Compiler, während 'CPP' für den 
C-Preprozessor steht. Für meine C++-Dateien hätte ich also wohl 'CXX' 
benutzen müssen.

Gesagt getan -- jetzt bekomme ich im Startup Code jedoch tatsächlich 
eine 'undefined reference to `main''-Fehlermeldung! War das das was du 
meintest? Liegt das daran, dass main() jetzt ge-name-mangled wird? Aber 
warum war das dann bis dato nicht der Fall? C++-Code (der auch wirklich 
C++-Features (Klassen) benutzt) war's ja trotzdem.


(Und wie die GCC im Hintergrund funktioniert ist mir ehrlich gesagt ein 
Rätsel: C, CC, CXX, CPP, c++, g++, usw. usf.)

: Bearbeitet durch User
von Markus F. (mfro)


Lesenswert?

Manuel W. schrieb:
> Gesagt getan -- jetzt bekomme ich im Startup Code jedoch tatsächlich
> eine 'undefined reference to `main''-Fehlermeldung!

main, _main, oder __main?

von Manuel W. (multisync)


Lesenswert?

Markus F. schrieb:
> Manuel W. schrieb:
>> Gesagt getan -- jetzt bekomme ich im Startup Code jedoch tatsächlich
>> eine 'undefined reference to `main''-Fehlermeldung!
>
> main, _main, oder __main?

'main'.

Startup code:
1
_start:
2
3
    /* Zero fill the bss segment. */
4
    ldr   r1, = __bss_start__
5
    ldr   r2, = __bss_end__
6
    movs  r3, #0
7
    b  .fill_zero_bss
8
.loop_zero_bss:
9
    str  r3, [r1], #4
10
11
.fill_zero_bss:
12
    cmp  r1, r2
13
    bcc  .loop_zero_bss
14
15
    /* Jump to our main */
16
    bl main
17
    b .
18
    .size    _start, . - _start
19
20
/*    Macro to define default handlers. Default handler
21
 *    will be weak symbol and just dead loops. They can be
22
 *    overwritten by other handlers */
23
blablabla

C++-Code:
1
int main(void)
2
{
3
     blablbala
4
}

Ein 'extern "C"' (um das name mangling zu deaktivieren) ändert aber auch 
nichts.
1
extern "C" int main(void)
2
{
3
     blablbala
4
}

: Bearbeitet durch User
von Manuel W. (multisync)


Lesenswert?

Ich habe jetzt mal testweise folgenden Code ausgeführt, und mir am 
Haltepunkt den Inhalt des Objects angesehen. Ergebnis: 'run_' war 0, dh. 
der Konstruktor wurde nicht ausgeführt.

Sehr seltsam das Ganze.

1
class A {
2
public:
3
        int run_;
4
        A(int num) {run_ = num;};
5
};
6
7
A test(1);
8
9
int main(void)
10
{
11
    A.run_ = 5; // break point here
12
}

von Markus F. (mfro)


Lesenswert?

Mit meinem Hinweis auf g++ als Frontend habe ich wahrscheinlich in die 
falsche Richtung geschickt. Genauso wie mit dem Hinweis auf _main(). Ich 
habe einen älteren (non-elf) Compiler, bei dem das nur so geht, das ist 
aber nicht (mehr) Standard. Sorry dafür.

Bei moderneren gcc-xxx-xxx-elf-Compilern ist es offensichtlich wurscht, 
welches Frontend man benutzt. Trotzdem müssen die static initializers 
vor dem Start von main() (also noch im Startupcode) aufgerufen werden.
Wie das geht, ist - so meine ich - immer noch vom Compiler abhängig.
Der arm-none-eabi-g++ packt die in die .init_array section (die 
natürlich gelinkt werden und deshalb im Linker Skript extra 
berücksichtigt werden muß).

Wenn Du normalerweise mit -nostdlib linkst, mußt Du zusätzlich die 
Funktionszeiger in dieser Section im Startupcode "von Hand" aufrufen 
(sonst würde das die C/C++-Library erledigen):
1
extern "C"
2
{
3
    void static_init(void)
4
    {
5
        typedef void (InitFunc)(void);
6
        extern InitFunc __init_array_start;
7
        extern InitFunc __init_array_end;
8
9
        InitFunc pFunc = &__init_array_start;
10
11
        for ( ; pFunc < &__init_array_end; ++pFunc )
12
        {
13
            (*pFunc)();
14
        }
15
    }
16
}

Ich hab's nicht komplett selbst durchgespielt, denke aber, daß Du damit 
weiterkommen müsstest. Laß' hören, ob's funktioniert.

von Jim M. (turboj)


Lesenswert?

Manuel W. schrieb:
> 'main'.
>
> Startup code:

Das ist ein reiner "C" Startup Code. Für die C++ Static Konstruktoren 
muss das etwa so aussehen:
1
/* Call static constructors */
2
  bl __libc_init_array
3
/* Call the application's entry point.*/
4
  bl main
5
6
  b .
Anderenfalls wundert man sich wieso da was nicht tut.

Viele ARM (Cortex M) Startups sehen so aus,
weil vorher noch Low Level Initialisierung nötig ist - wie Clocks oder 
FPU:
1
/* Call the clock system intitialization function.*/
2
  bl  SystemInit
3
4
/* Call static constructors */
5
  bl __libc_init_array
6
7
/* Call the application's entry point.*/
8
  bl main
9
10
  b .

von Manuel W. (multisync)


Lesenswert?

Markus F. schrieb:
> Mit meinem Hinweis auf g++ als Frontend habe ich wahrscheinlich in die
> falsche Richtung geschickt. Genauso wie mit dem Hinweis auf _main(). Ich
> habe einen älteren (non-elf) Compiler, bei dem das nur so geht, das ist
> aber nicht (mehr) Standard. Sorry dafür.

Kein Problem! War gut gemeint, und zumindest hatte ich somit Anlass mich 
in die Unterschiede zwischen das gcc- und g++-Frontend einzuarbeiten. 
Das wird sicher nicht umsonst gewesen sein.

~~~

Der Tipp von dir und Jim Meba war goldrichtig!

Ich musste einfach nur ein
1
bl __libc_init_array
unmittelbar vor den Aufruf meiner main() in meinen Startup-Code 
einfügen, und schon war ich annähernd am Ziel.

Der Linker beschwerte sich dann nur noch wegen eines 'undefined 
reference to `init''. Da das jedoch irgendwas mit mit der Schnittstelle 
zum Host-Betriebssystem zu tun hat, und ich jedoch bare-metal 
programmiere, reichte es aus ihn mit einer Dummy-Definition dieses 
Symbols ruhig zu stellen.
1
extern "C" { void _init(void){}; }

Es scheint jetzt wirklich alles zu funktionieren! Die Konstruktoren 
werden aufgerufen, und den data membern werden die erwarteten Werte 
zugewiesen.


Vielen lieben Dank (an alle!) für die tatkräftige Unterstützung!


Ich sehe schon, wenn ich von C++ wirklich Ahnung haben will werde mich 
bei nächster Gelegenheit auf die Interna von C++ fokussieren müssen. Für 
mich als Neuling ist es ein Wahnsinn, was da im Hintergrund durch das 
Zusammenspiel der diversen Libraries, Compiler-/Linkerflags usw. 
geschieht...

Bitte melde dich an um einen Beitrag zu schreiben. Anmeldung ist kostenlos und dauert nur eine Minute.
Bestehender Account
Schon ein Account bei Google/GoogleMail? Keine Anmeldung erforderlich!
Mit Google-Account einloggen
Noch kein Account? Hier anmelden.