Forum: Compiler & IDEs avr-gcc Compilerbug (Endlosschleife ab -O1)


von Daniel G. (Gast)


Lesenswert?

Hallo zusammen,

seit tagen pople ich an einem winzigen Stück Debugcode herum. Eigentlich 
wollte ich nur mal schnell über den USART eines ATmega32 ein wenig 
Debugtext ausgeben. Dabei bin ich anscheinend auf einen Bug im GCC 
gestoßen.

In einer Schleife warte ich auf das Ende der Übertragung des letzten 
Zeichens im String. Mein Sendepufferzeiger wird daraufhin auf NULL 
gesetzt. In Optimierungsstufe -O0 klappt das auch einwandfrei. Ab -O1 
geht es in die Hoste und Wait() wartet für immer.

Ich habe ein Testprogramm welches das gleiche Verhalten zeigt einmal 
angehägt.

Hier nur die Highlights:
1
volatile const char* g_pszCurrentString;
2
3
ISR(USART_TXC_vect)
4
{    
5
  //-- Den Puffer auf das nächste Zeichen weiterrücken
6
  g_pszCurrentString++;
7
8
  //-- Wenn noch Daten im Puffer sind, das nächte Byte senden - sonst den Puffer auf NULL setzen damit Wait ausgelöst wird
9
  if (*g_pszCurrentString)
10
    UDR=*g_pszCurrentString;
11
  else
12
    g_pszCurrentString=NULL;
13
}
14
15
[...]
16
17
void Wait()
18
{    
19
  while (g_pszCurrentString!=NULL);
20
}
21
22
[...]

Der Compiler macht aus der Wait-Funktion ab Optimierungsstufe -O1:
1
  while (g_pszCurrentString!=NULL);
2
 108:  80 91 68 00   lds  r24, 0x0068
3
 10c:  90 91 69 00   lds  r25, 0x0069
4
 110:  89 2b         or  r24, r25
5
 112:  09 f0         breq  .+2        ; 0x116 <_Z4Waitv+0xe>
6
 114:  ff cf         rjmp  .-2        ; 0x114 <_Z4Waitv+0xc>
7
 116:  08 95         ret

Was natürlich Quatsch mit Soße ist, da der rjmp um -2 ja in einer 
Endlosschleife endet. D.h. der Compiler ignoriert nicht nur das volatile 
völlig, sondern er optimiert so gut, dass er auch die Register nicht 
noch einmal lesen möchte :)

Ich verwende aktuell winavr (20090313) mit avr-gcc 4.3.2.

Kann jemand das Verhalten bestätigen? Ist es wirklich ein Bug oder bin 
ich nur zu dämlich?

Wenn jemandem ein einigermaßen eleganter Workaround einfällt, nur her 
damit. Wenn nicht, vielleicht hilft's jemandem mit einem ähnlichen 
Problem, nicht auch zwei Tage zu versenken ;)

von Johann L. (gjlayde) Benutzerseite


Lesenswert?

-1-

g_pszCurrentString muß volatile sein:
1
volatile const char * volatile g_pszCurrentString;

-2-

Beim Anfragen von g_pszCurrentString hast du eine Race-Condition. Du 
musst atomar auf g_pszCurrentString zugreifen!

Siehe Compilerfehler: "Die häufigsten Nicht-Fehler" :-)

von Daniel G. (Gast)


Lesenswert?

Hallo Johann L.,

zuerst mal danke für den Hinweis. Klar, die Konstante volatile zu 
deklarieren macht natürlich keinen Sinn. Der Zeiger ist volatile und 
dann kompiliert er es auch richtig. Tja, also doch ein Entwickler-Bug 
und kein Compiler-Bug. Nach zwei Tagen suchen, schiebt man es dann doch 
mal gerne auf den armen Compiler.

Zur Race-Kondition: Die kann ich wirklich nicht sehen. Da ein Interrupt 
kein Thread ist, wird die Schleife im schlimmsten Fall (Halber Wert im 
Register, der andere noch nicht) in der Mitte unterbrochen. Der Wert 
wird auf NULL gesetzt und dann wird aus dem Interrupt zurückgesprungen. 
Nun weiß die Schleife noch nichts von Ihrem Glück und läuft einen 
Durchlauf zu lange. Ich denke das ist - verglichen mit dem Aufwand für 
einen automaren Zugriff - kein echter Performanceverlust. - Oder habe 
ich hier etwas übersehen?
Natürlich gibt es noch die Zuweisung an g_pszCurrentString. Doch auch 
bei dieser kann nichts passieren, da zu diesem Zeitpunkt das 
Senderegister ja leer ist und daher der besagte Interrupt nicht 
zuschlagen kann.

DANKE für's Wald in den Bäumen zeigen ;)

von Jörg W. (dl8dtl) (Moderator) Benutzerseite


Lesenswert?

Daniel G. schrieb:

> Zur Race-Kondition: Die kann ich wirklich nicht sehen. Da ein Interrupt
> kein Thread ist, wird die Schleife im schlimmsten Fall (Halber Wert im
> Register, der andere noch nicht) in der Mitte unterbrochen.

Das Auslesen des 16-bit-Zeigers wird im schlimmsten Fall mitten
zwischen beiden Bytes unterbrochen.  Es müsste in deinem Falle
aber harmlos sein, jedoch will diese Aussage ziemlich genau durchdacht
sein.  Wenn man beispielsweise eine 16-bit-Zählvariable auf 0 testet,
die im Interrupt herunter gezählt wird, dann entsteht eine typische
race condition in dem Moment, wo von 0x100 auf 0x0FF gezählt wird:
der Compiler liest zuerst das low-byte (das gerade 0 ist), danach
kommt der Interrupt und dekrementiert den Zähler, danach liest der
Compiler das high-byte -- und das ist jetzt gerade auch 0.  Aha,
wir haben das Schleifenende erreicht... (aber der Zähler steht auf
0xff).

von P. S. (Gast)


Lesenswert?

Daniel G. schrieb:

> Zur Race-Kondition: Die kann ich wirklich nicht sehen. Da ein Interrupt
> kein Thread ist, wird die Schleife im schlimmsten Fall (Halber Wert im
> Register, der andere noch nicht) in der Mitte unterbrochen.

Auch mit Threads waere es so richtig. So kann man einige 
Locking-Szenarien gut beschleunigen - natuerlich muss man immer wissen, 
was man tut :-)

von Daniel G. (Gast)


Lesenswert?

Hallo Peter,

bei einem Thread (und präemptivem Multitasking) wäre das Szenario nicht 
ganz wasserdicht:

Nehmen wir an, das letzte Zeichen wurde gesendet und die Warteschleife 
brummt. Der Zeiger steht auf 0x00FE.

Nun läd die Schleife das High-Byte zur Prüfung in das Register.

{Kontextwechsel]

Das letzte Zeichen wurde gesendet. Der Zeiger wird auf 0 gesetzt. Der 
Compiler schreibt das Low-Byte zuerst (warum auch immer) und setzt es 
auf 0.

[Kontextwechsel]
Die Schleife läd das Low-Byte (welches jetzt 0 ist) in das Register und 
führt den vergleich aus. Dieser trifft zu und die Schleife wird 
verlassen. Der neue String steht an Adresse 0x0123 und diese Adresse 
wird nun von der Sendefunktion als neuer Wert für g_pszCurrentString 
gesetzt.

[Kontextwechsel]
Jetzt sind wir wieder im Sendethread. Dieser ist noch nicht ganz mit dem 
"auf 0 setzen" fertig und setzt nun das High-Byte auf 0.

Boom, jetzt hat der Thread unsere Adresse in g_pszCurrentString versaut. 
Die ist jetzt nämlich 0x0023 und nicht 0x0123.

Wenn man davon ausgeht, das der Compiler immer in der gleichen 
Reihenfolge auf die Variablen zugreift, so sollte hier nichts passieren. 
Die Zugriffsreihenfolge ist jedoch nicht immer genau definiert. Ich 
würde bei einem Thread auf jeden Fall den Zugriff auf g_pszCurrentString 
synchronisieren oder (besser) ein eigenes Sperrobjekt (Mutex) zum warten 
verwenden. Gerade weil dann der Thread schlafen kann, solange er warten 
muss.

Ich hoffe der Unterschied zwischen Interrupt und Thread ist ausreichend 
dargestellt. Der Interrupt kehrt erst zurück, wenn er fertig ist und das 
macht einiges einfacher.

@Alle: Danke für die erhellenden Antworten :)

von P. S. (Gast)


Lesenswert?

Daniel G. schrieb:

> Ich hoffe der Unterschied zwischen Interrupt und Thread ist ausreichend
> dargestellt. Der Interrupt kehrt erst zurück, wenn er fertig ist und das
> macht einiges einfacher.

Du gehst hier anscheinend davon aus, dass es nur einen Interrupt gibt 
oder diese sich nicht gegenseitig unterbrechen koennen. Nur dann 
unterscheiden sich die Szenarien.

Allerdings muss ich hier meine Aussage relativieren, ich habe 
tatsaechlich uebersehen, dass es um eine 16Bit-Adresse auf einer 
8Bit-Maschine geht. Da ist zaehlen in einem Kontext und pruefen im 
Anderen in der Tat ein Problem.

von Daniel G. (Gast)


Lesenswert?

Hallo Peter,

das ist natürlich richtig. Sobald man Interrupts sich gegenseitig 
unterbrechen lässt, wird die Sache wieder erheblich komplizierter. 
Deshalb versuche ich das nach Möglichkeit zu vermeiden...

Ps: Normalerweise schreibe ich Software für PCs. Da ist es schon 
spannend, wenn man mal nicht 2 GiByte RAM, endlose Rechenpower, 
Gleitkommaeinheit, MMX und einen Haufen Bibliotheken (z.B. zur 
Threadsynchronisation ;) ) zur Verfügung hat.

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.