Forum: Compiler & IDEs Verständnisfrage zu sprintf und der Ausgabe negativer Zahlen


von Kevin Maurer (Gast)


Lesenswert?

Hallo Spezialisten,

ich habe gerade etwas Verständnisprobleme mit dem Einsatz von sprintf() 
unter Verwendung des AVR-GCC.

Bei positiven Zahlen funktioniert immer alles wie gewünscht und ich kann 
beliebige Variablen in strings wandeln. Bei negativen Zahlen klappt es 
aber nur manchmal korrekt. Dort wird dann beispielsweise aus einer "-1" 
eine "255". Das Zweierkomplement kenne ich natürlich, aber ich weiß 
nicht wann/woran die Funktion sprintf erkennen kann, ob ein Wert negativ 
ist oder nicht. Bisher dachte ich, dass das an dem Typ der Variablen 
erkannt wird. Seltsamerweise scheint das aber nicht zu stimmen, denn ich 
habe hier Codebeispiele bei denen auch das Gegenteil der Fall ist...

Ich verstehe das nicht. Woran liegt das genau?

Ein seltsamerweise funktionierendes Beispiel aus meinem Code:
1
uint16_t a = 5;
2
volatile uint8_t data[260]; 
3
a-=6;
4
sprintf(data,"%d",a);  //jetzt steht "-1" im string

Und hier ein Beispiel aus dem gleichen Programmcode, bei dem es nicht 
funktioniert bei negativen Werten:
1
volatile uint8_t temperature_offset;  
2
3
temperature_offset = eeprom_read_byte(&eep_temperature_offset); 
4
sprintf(data,"%d",temperature_offset);  //hier wird "255" ausgegeben wenn 0xFF im EEPROM steht und nicht "-1"

Mir ist schon klar, dass die obigen Datentypen "unsigned" sind und 
eigentlich nicht für negative Zahlen gedacht sind. Aber auch wenn ich 
sie ändere in "char" oder "int" hilft das nichts und das Ergebnis bleibt 
immer gleich.

Wo genau ist mein Verständnisfehler? Was genau muss ich machen/beachten, 
dass sprintf auch bei negativen Zahlen immer funktioniert bei GCC?

Vielen Dank für Hilfe!!

PS: Dieses Problem habe ich nur mit dem GCC, sonst nutze ich meist den 
CodeVision Compiler. Dort funktioniert immer alles reibungslos.... wo 
ist der Unterschied?

von Johann L. (gjlayde) Benutzerseite


Lesenswert?

Scherzkeks.  uint8 inst unsigned --> hat Werte von 0...255.

"%d" erwartet einen int; da alle Argumente von varargs mindestens zu int 
promotet werden, wird also 255 (als signed 16 Bit) ausgegeben.

Wenn du -1 haben willst dann mach die Variable signed, z.B. int8_t.

von Mark B. (markbrandis)


Lesenswert?

Wobei man einen String wohl am ehesten als

1
char data[260];

deklarieren würde. Faustregel:
-wenn es 8 Bit und kein Vorzeichen haben soll, nimm uint8_t
-wenn es 8 Bit haben und vorzeichenbehaftet sein soll, nimm int8_t
-wenn es ein Zeichen bzw. eine Zeichenkette sein soll, nimm char.

von Markus F. (mfro)


Lesenswert?

Kevin Maurer schrieb:
> Ich verstehe das nicht. Woran liegt das genau?

An einem kleinen, unscheinbaren Tierchen namens "integer promotion".

sprintf() (und printf()) sind Funktionen mit variabler Argumentliste.

Dort lebt dieses Tierchen und sorgt dafür, daß alles, was diesen 
Funktionen als Parameter übergeben wird und von der Bitgröße her kleiner 
ist als das, was auf der entsprechenden Plattform einem int entspräche, 
automatisch vorzeichenrichtig auf die "int-Größe" erweitert wird.

Aus einem uint8_t 0xff wird also 0x00ff und aus einem int8_t 0xff -> 
0xffff.

Man kann also an sprintf() und seine Kollegen gar keine 8-Bit Typen 
übergeben bzw. sie kommen dort nicht als 8-Bit Typen an.

In der Konsequenz bedeutet das z.B., daß wenn man ein Feld aus int8_t's 
hexadezimal ausgeben möchte, immer eine Feldlänge mit angeben muß, weil 
negative Zahlen sonst vierstellig werden.

Es lohnt sich, mal über integer promotion nachzulesen, weil sie auch 
noch an ein paar anderen Stellen eine Rolle spielt (arithmetische 
Operationen).

Dort bewirkt sie beispielsweise, daß man eine 8 Bit breite Variable ohne 
Warnung um mehr als sieben Bit shiften kann. Die Warnung kommt erst, 
wenn man weiter schiebt, als ein int breit ist.

Der Compiler darf integer promotion wegoptimieren, muß aber dafür 
sorgen, daß das Ergebnis dasselbe bleibt.

von Kevin Maurer (Gast)


Lesenswert?

Guten Morgen,

erst einmal ganz gerzlichen Dank für die tollen Antworten und Hinweise. 
Das Thema "integer promotion" habe ich mir bisher noch nicht so genau 
angesehen. Ist aber offensichtlich genau mein Problem/Wissenslücke.

Wenn ich überall mit int_8 arbeite statt uint_8, dann funktioniert es 
auch korrekt mit den negativen Zahlen (wie erwartet).

Was ich dann aber immer noch nicht so ganz verstehe, ist mein Beispiel 
oben:
1
uint16_t a = 5;
2
volatile uint8_t data[260]; 
3
a-=6;
4
sprintf(data,"%d",a);  //jetzt steht "-1" im string

Hier bekomme ich bei uint16_t auch "-1" heraus und nicht "255", obwohl 
der Datentyp "unsigned ist". Das liegt dann an dem "%d" bei sprintf(), 
oder?

Und müssten es dann nicht 4 Stellen sein wie im Post oben beschrieben?

Nochmals vielen Dank!

Kevin

von Markus F. (mfro)


Lesenswert?

Kevin Maurer schrieb:
> Was ich dann aber immer noch nicht so ganz verstehe,

Das "Problem" hat jetzt mit integer promotion nichts mehr zu tun.

Das ist streng genommen erst mal ein Programmierfehler. Mit der 
Deklaration als uint versicherst Du dem Compiler, daß die Zahl nie 
negativ werden kann/soll. Wenn Du sie dann (absichtlich oder 
unabsichtlich) negativ machst, ist das wohl ein Fehler, bzw. es wird 
nicht das rauskommen, was Du erwartest.

-1 als unsigned 16-Bit Zahl ist keine -1 (und auch nicht 255), sondern 
halt 65535. Und das würde auch ausgegeben, wenn Du die Zahl im 
Formatstring - wie's richtig wäre - als unsigned (Format-Specifier = 
"%u") behandeln würdest. Dort behauptest Du aber plötzlich wieder, es 
handle sich um ein signed int (Format-Specifier = "%d"), also wird eben 
-1 ausgegeben.

von Kevin Maurer (Gast)


Lesenswert?

Hallo Markus,

vielen Dank für die ausführliche Info, jetzt habe ich es verstanden. 
Habe auch viel nachgelesen, jetzt wird die Sache "rund"!

Mein Programm läuft übrigens jetzt an allen Stellen einwandfrei mit den 
Tips.

vielen Dank noch einmal !

Kevin

von Bronco (Gast)


Lesenswert?

Ein Hinweis noch zum Verständnis von variadischen Funktionen:

Im Gegensatz zu normalen Funktionen ist bei variadischen Funktion nicht 
festgelegt, welche Datentypen übergeben werden.
Du kannst ja beliebige Datentypen in beliebiger Reihenfolge übergeben:
1
uint8_t x_u8;
2
uint32_t y_u32;
3
printf("%i", x_u8);
4
printf("%i", y_u32);
5
printf("%i %i", x_u8, y_u32);
6
printf("%i %i", y_u32, x_u32);
7
printf("%s", "Hello world!");

Woher soll jetzt der Compiler beim compilieren wissen, welche Datentypen 
da übergeben werden? Er kann es nicht wissen, daher packet er jeden 
Parameter in ein oder mehrere "int".
Variable Argumente werden als Liste von ints auf dem Stack abgelegt.
Wenn Du ein uint8_t übergibst, wird dieses in einem int auf dem Stack 
abgelegt. Wenn Du einen String übergibst, wird dessen Adresse als int 
auf dem Stack abgelegt. Wenn Du einen größeren Datentyp als int 
übergibts, wird dieser in Form von mehreren ints hintereinander auf dem 
Stack abgelegt (wobei man wieder das Endian beachten muß).

von Karl H. (kbuchegg)


Lesenswert?

Bronco schrieb:

> Variable Argumente werden als Liste von ints auf dem Stack abgelegt.

Mit dieser Aussage bin ich nicht wirklich glücklich. In C ist genau 
festgelegt, wie in diesem Fall vorgegangen wird.

> Wenn Du ein uint8_t übergibst, wird dieses in einem int auf dem Stack
> abgelegt. Wenn Du einen String übergibst, wird dessen Adresse als int
> auf dem Stack abgelegt.

Und mit dem letzten Teilsatz ... no. Das kann man so nicht sagen. Das 
ist grob sinnentstellend. Nirgends in C ist festgelegt, das sizeog(int) 
== sizeof(void*) sein muss. Das hier zu einem "wird als int" übergeben, 
ist einfach nur missverständlich.

> Wenn Du einen größeren Datentyp als int
> übergibts, wird dieser in Form von mehreren ints hintereinander auf dem
> Stack abgelegt (wobei man wieder das Endian beachten muß).

Auch das kann man so nicht sagen.


Alles in allem: Verlasse diese Schiene des 'alles wird auf int 
runtergebrochen' wieder. Das ist keine zielführende Sichtweise und 
bestenfalls grob vereinfacht. Es erklärt zb nicht, wie ein float über 
eine variadische Schnittstelle gebracht wird und warum bei einem printf 
das Formatierzeichen "%lf" überflüssig ist.

von Bronco (Gast)


Lesenswert?

Karl Heinz schrieb:
> In C ist genau
> festgelegt, wie in diesem Fall vorgegangen wird.

Okay, ich vertraue Deiner Expertise. Aber wie wird denn dann genau 
vorgegangen?

von Markus F. (mfro)


Lesenswert?

Bronco schrieb:
> Woher soll jetzt der Compiler beim compilieren wissen, welche Datentypen
> da übergeben werden? Er kann es nicht wissen, daher packet er jeden
> Parameter in ein oder mehrere "int".

Falsch.

Der Compiler weiß auf der Aufruferseite (die bei so gut wie allen ABI's 
auch für's anschließende Aufräumen zuständig ist) sehr genau, welche und 
wieviele Parameter da übergeben wurden, er hat's ja schließlich grade 
eben erst selbst gemacht. Also sollte der Aufrufer auch sehr wohl wissen 
wieviele und welche Parameter er wieder "abräumen" soll - das wird (und 
muss) er sich über den Funktionsaufruf hinweg doch wohl noch merken 
können.

Die aufgerufene Funktion weiß das hingegen nicht und ist darauf 
angewiesen, daß ihr das auf irgendeine Weise (z.B. in einem 
Formatstring) als "nicht variadisches" Argument mitgeteilt wird. Dann 
kann sie über va_arg() die einzelnen Parameter referenzieren. Dabei 
setzt va_start() einen internen Zeiger auf den Anfang der 
Parameterliste, va_arg() holt das nächste Argument und schiebt den 
Zeiger weiter. Wie weit va_arg() den Zeiger jeweils weiterschiebt, ist 
prinzipiell Wurscht, das könnte auch mal (zumindest auf Plattformen, die 
ungerade Stackadressen erlauben) nur 8 Bit weit oder grundsätzlich und 
immer 128 Bit weit sein.

Ein va_end() ist eigentlich nur für die (wenigen) Plattformen notwendig, 
bei denen der Aufgerufene für's Aufräumen zuständig ist.

Die "integer promotion" wäre an dieser Stelle - zumindest bei den 
gängigen ABIs - streng genommen gar nicht notwendig. Ein va_arg(arglist, 
char) z.B. könnte völlig problemlos auch ein char Argument vom Stack 
holen.

Ich nehme an, die C-Erfinder wollten sich Probleme mit unaligned Stacks 
ersparen und sind davon ausgegangen, daß die Parameterübergabe 
unabhängig von der Plattform per "natural type size" (= sizeof(int)) am 
einfachsten und effizientesten zu implementieren ist.

Da ist ja auch was dran...

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


Lesenswert?

Markus F. schrieb:
> Wie weit va_arg() den Zeiger jeweils weiterschiebt, ist prinzipiell
> Wurscht, das könnte auch mal (zumindest auf Plattformen, die ungerade
> Stackadressen erlauben) nur 8 Bit weit oder grundsätzlich und immer 128
> Bit weit sein.

Wie im Nachbarthread schon geschrieben, muss es auch gar nicht auf
dem Stack sein: (Ultra-)SPARC beispielsweise benutzt für die ersten
(auch variadischen) Argumente Register.  Irgendwie muss sich va_arg()
auch das dann merken.

von Markus F. (mfro)


Lesenswert?

Jörg Wunsch schrieb:
> Markus F. schrieb:
> Wie im Nachbarthread schon geschrieben, muss es auch gar nicht auf
> dem Stack sein: (Ultra-)SPARC beispielsweise benutzt für die ersten
> (auch variadischen) Argumente Register.  Irgendwie muss sich va_arg()
> auch das dann merken.

Klar doch. Wenn Du meinen Beitrag genau studiert hast, hast Du sicher 
festgestellt, daß ich das Wort "Stack" bis auf diese eine Stelle 
sorgfältig vermieden habe.

In der wirklichen Welt sind viele (die meisten?) Architekturen aber eben 
nun mal so gestrickt, daß sie Funktionsparameter (zumindest die, die 
eine bestimmte Anzahl überschreiten) auf dem Stack übergeben und bei 
zumindest etlichen davon hat ebender besondere Einschränkungen, was das 
Alignment angeht. Deswegen war's wohl einfacher, parameter promotion für 
Variadische Funktionsparameter zu erzwingen.

Darauf wollte ich hier raus.

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


Lesenswert?

Markus F. schrieb:
> Deswegen war's wohl einfacher, parameter promotion für Variadische
> Funktionsparameter zu erzwingen.

Das Ur-C kannte ja keine Prototypen und musste dadurch immer
die promotion benutzen.  Das hat man bei C89 für variadische
Funktionen übernommen.  Ein "Rationale" dafür habe ich nicht im
Netz gesehen, kann mir aber vorstellen, dass es damals besonders
wichtig erschien, dass beide Varianten (Prototyp und "K&R style")
nebeneinander existieren können und binärkompatibel sind.

von Markus F. (mfro)


Lesenswert?

Hört sich an wie ein vernünftiges Argument ;).

Jörg Wunsch schrieb:
> Das Ur-C kannte ja keine Prototypen und musste dadurch immer
> die promotion benutzen.

Hätte es deswegen nicht unbedingt müssen, hat aber sicher viele 
Programmfehler (bzw. viele hässliche casts) vermieden.

von Johann L. (gjlayde) Benutzerseite


Lesenswert?

Jörg Wunsch schrieb:
> (Ultra-)SPARC beispielsweise benutzt für die ersten
> (auch variadischen) Argumente Register.

Kann die Register indirekt adressieren?  Ansonsten geben Bedinungen 
ziemlich üblen Code weil man abhängig von argp jedes Register abtesten 
muss:
1
va_list argp;
2
...
3
if (bedingung_1)
4
  var1 = va_arg (argp, typ1);
5
if (bedingung_2)
6
  var2 = va_arg (argp, typ2);
7
...

von Markus F. (mfro)


Lesenswert?

das ist bei x86_64 übrigens nicht wesentlich anders. va_list ist dann 
eben kein Zeiger, sondern ein struct, der beschreibt wo und wie die 
Argumente zu finden sind.

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


Lesenswert?

Johann L. schrieb:
> Kann die Register indirekt adressieren?

Ich glaub' schon, müsste mir die Details aber auch wieder anlesen.

Fand das eine interessante CPU-Architektur, hat sich eben nur leider
nicht durchgesetzt.

von Bronco (Gast)


Lesenswert?

Danke, wieder was gelernt!

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.