0 Daumen
778 Aufrufe

Mein C-Compiler spinnt...

In einem produktiven Code bin ich auf ein Problem gestoßen, das ich wie folgt verkürzt und extrahiert habe:

void myfunc( unsigned short a, unsigned short b) {
unsigned int x;
x= a*b;
if (x>=0x80000000)
printf("%u >= %u\n", x, 0x80000000);
else
printf("%u < %u\n", x, 0x80000000);
}

Ich rufe die Funktion auf mit:

my_func(65535,65535);

und erhalte als Ausgabe:

4294836225 < 2147483648

Ich programmiere seit über 20 Jahren in C und finde keinen Fehler.

Hat irgendjemand eine Idee, was hier schief läuft?


Danke euch vorab und viele Grüße

Bernd

Avatar von

Welchen Compiler und welches Betriebssystem verwendest du?

Ich nutze den C51 C Compiler auf einem RTX51 Real-Time-Kernel.

2 Antworten

0 Daumen
 
Beste Antwort

Aloha :)

Es gibt zwei wichtige Grundprinzipien in "C":

1) Programmierer wissen was sie tun \(\implies\) der Compiler hinterfragt keinen Code

2) Du bezahlst nur das, was du brauchst \(\implies\) der Compiler optimiert alles Unnötige weg

Gerade im Embedded-Bereich, wo man oft nur wenig Speicher hat und auf einer speziellen Hardware "tricksen" muss, sind diese Prinzipien nützlich. Das macht das Programmieren in C aber auch schwierig, weil man wirklich verstehen muss, was im Hintergrund passiert.

Das Entscheidende passiert in der Zeile

x= a*b;

In "C" ist der kleinst-mögliche Datentyp für Ganzzahl-Berechungen der Typ int. Das heißt, die beiden unsigned short Variablen a und b werden vor der Multiplikation in int umgewandelt. Der Compiler interpretiert die Programmzeile also so:

x= (int)a * (int)b;

Bei der Multiplikation der beiden int-Werte kommt es nun mit deinen Übergabeparametern (65535) zu einem Integer-Überlauf. Der führt bereits nach dem C-Standard zu undefiniertem Verhalten (UB = Undefined Behaviour). Wegen Regel (1) ignoriert der Compiler das aber, es könnte ja auf der speziellen Hardware vom Programmierer genau so gewollt sein.

Das Ergebnis der int-Multiplikation ist wieder vom Typ int, sodass der Compiler bei der Zuweisung an \(x\) eine automatische Konvertierung nach unsigned int durchführt. Hier sollte es zumindest auf höchster Warnstufe einen Hinweis vom Compiler geben.

In der nächsten Zeile

if (x>=0x80000000)

kommt nun die Optimierung ins Spiel. Da \(x\) zuvor ein int-Wert zugewiesen wurde, kann die Bedingung niemals erfüllt sein, denn der Maximalwert für int ist ja 0x7FFFFFFF. Der if-Zweig kann also aus Sicht des Compilers nie durchlaufen werden, sodass dieser nach Regel (2) vom Compiler wegoptimiert wird. Im Endeffekt wird daher nur der Code im else-Zweig compiliert.

Zusammengefasst wir das Folgende compiliert:

void myfunc( unsigned short a, unsigned short b) {
  unsigned int x;
  x= (int)a * (int)b;
  printf("%u < %u\n", x, 0x80000000);
}

Das Ergebnis ist das, was du beschrieben hast...

Schau dir auch mal selbst an, was der C-Compiler aus deinem Code macht:

https://godbolt.org/

Du wirst überrascht sein, wie unglaublich effizient C übersetzt wird.


PS: Vor dem C99-Standard wurden auch sämtliche Fließkomma-Operationen mindestens im Datentyp double durchgeführt. Das heißt, alle float-Werte wurden vor irgendeiner Rechnung stets nach double konvertiert. Seit dem C99-Standard muss der Compiler diese Hochskalierung nicht mehr machen. Mit dem vordefinierten Macro FLT_EVAL_METHOD kannst du abfragen, welcher Datentyp als Minimum für die Fließkommaberechnungen verwendet wird: -1 unbekannt, 0 float, 1 double, 2 long double. Bei guten Compilern kann man das wählen.

Für ganzzahlige Rechnungen ist hingegen immer int als minimaler Datentyp gesetzt.

Avatar von

Ich bin baff und meine Kollegen auch!

Wir haben uns vom Compiler den Assembler-Output generieren lassen und nun auch festgestellt, dass tatsächlich nur der else-Zweig übersetzt wurde.

Es ist wirklich unglaublich, wie mächtig und gefährlich zugleich die Sprache C ist. Diese Freiheiten (zu programmieren und Fehler zu machen) bietet einem wirklich keine andere Sprache.

Vielen Dank, das hat wirklich geholfen.

0 Daumen

Beim C51 C Compiler besteht ein unsigned int aus 2 Bytes, kann also nicht größer als 65535 sein (siehe Data Storage Formats im Handbuch des Compilers).

Der Typ des Literals 0x80000000 ist long int, weil das der kleinste Typ ist, der den Wert 2147483648 aufnehmen kann.

Avatar von 5,7 k

Hmm, erstmal Danke für deine Antwort.

Wenn ich sizeof (unsigned int) eingebe, antwortet mir der Compiler mit 4. Vielleicht haben wir eine andere Version, die auf die Hardware optimiert ist. Ich frage da mal nach...

Das heißt aber bei unserer Compiler-Version ist ein unsigned int 4 Bytes groß. Daher kann das nicht die Erklärung sein.

Ein anderes Problem?

Stell deine Frage

Willkommen bei der Stacklounge! Stell deine Frage einfach und kostenlos

x
Made by a lovely community