Verständnisfrage zu sprintf und der Ausgabe negativer Zahlen

Gast #3877056
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?
#3877129
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.
Gast #3877143
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
#3877169
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.
Gast #3877238
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
Gast #3892268
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ß).
#3892307
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.
#3892364
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...
Moderator Persönliche Seite #3892409
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.
#3892439
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.
Moderator Persönliche Seite #3892460
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.
Persönliche Seite #3892567
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
...

Antwort schreiben

Bitte melde dich an, um einen Beitrag zu schreiben.

oder

Mit Google-Account einloggen

Die Registrierung ist kostenlos und dauert nur eine Minute.

Jetzt registrieren