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_ta=5;
2
volatileuint8_tdata[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:
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?
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.
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.
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.
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_ta=5;
2
volatileuint8_tdata[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
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.
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
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_tx_u8;
2
uint32_ty_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ß).
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.
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?
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...
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.
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.
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.
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.
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:
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.
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.