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
volatileconstchar*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
voidWait()
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 ;)
-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" :-)
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 ;)
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).
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 :-)
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 :)
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.
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.