Hallo zusammen, ich hatte einen Bug in einem AVR-Programm, Ursache ist dtostrf() bzw. meine Fehlinterpretation der Parameter. Ein float-Wert wurde doch größer als erwartet, der Buffer für dtostrf ist übergelaufen. Aus der Doku zur avr-libc: The dtostrf() function converts the double value passed in val into an ASCII representation that will be stored under s. The caller is responsible for providing sufficient storage in s. The minimum field width of the output string (including the '.' and the possible sign for negative values) is given in width [...] Jetzt frag ich mich: Was ist "sufficient storage"? Wie verwendet man die Funktion auf "sichere" Art und Weise? Gibts eine wirklich "maximale" Länge des Strings? Kann ich das sonstwie begrenzen? Oder gibts überhaupt eine wesentlich bessere Funktion als dtostrf()?
:
Verschoben durch User
Eine rein analytische Angabe rein vom Angucken des Codes her ist nicht ganz einfach. So überschlagsmäßig würde ich sagen, dass da um die 100 Zeichen maximal rauskommen können. Die Idee hinter dtostrf() ist, dass du den Wertebereich deiner Argumente kennst. Unter der Ausgabe 23413418934189348192439812439812431243 kann sich dein Nutzer schließlich sowieso nicht viel vorstellen. ;-) Wenn du den Wertebereich nicht genau kennst, dann hast du zwei Möglichkeiten: entweder nimmst du dtostre(), oder snprintf() mit einem Gleitkommaformat. snprintf() gibt die Anzahl der Zeichen zurück, die konvertiert werden würden. Man lässt es also zweimal laufen, einmal mit einem viel zu kleinen Puffer, dann alloziert man einen genügend großen und ruft es nochmal auf. Dann kannst du deinem Nutzer auch stolz obige Zahl präsentieren. :-)
Danke für deine Antwort! snprintf() möchte ich bewusst vermeiden. Den Code hab ich mir auch angesehen, ist wirklich nicht ganz einfach zu verstehen... Irgendwo hab ich einen "maximallänge" von 60 entdeckt. Dürfte mit der Beschränkung des Exponenten von float (AVR hat ja kein double) auf +/- 40 zusammenhängen. und dann ist da noch die (Assembler-) Funktion __ftoa_engine. Die liefert den Exponenten + die signifikanten Digits, und das dürften nicht mehr als 8 sein, jedenfalls gibts da ein char buf[9], wobei buf[0] die Flags beinhaltet... Das mit dem Wertebereich ist auch so eine Sache: Eigentlich kenn ich den schon, wenn aber aufgrund eines Messfehlers ein Divisor plötzlich ziemlich klein wird...
Michael Reinelt schrieb: > snprintf() möchte ich bewusst vermeiden. Ist dein Flash zu knapp? Ansonsten ist das natürlich die elegantere Lösung. Hab' mich früher auch mit dtostrf() rumgeschlagen, würde ich heute vermeiden, wenn's nur irgendwie geht. > Irgendwo hab ich einen "maximallänge" von 60 entdeckt. Ja, da steht aber dann weiter unten noch
1 | ndigs += exp; |
> Das mit dem Wertebereich ist auch so eine Sache: Eigentlich kenn ich den > schon, wenn aber aufgrund eines Messfehlers ein Divisor plötzlich > ziemlich klein wird... Dann mach einen Wertebereichstest zuvor und ruf bei unerwarteten Werten lieber dtostre() auf. Desse Gesamtlänge ist überschaubar.
Michael Reinelt schrieb: > Das mit dem Wertebereich ist auch so eine Sache: Eigentlich kenn ich den > schon, wenn aber aufgrund eines Messfehlers ein Divisor plötzlich > ziemlich klein wird... man könnte ihn ja vor der ausgabe zurechtstutzen.
Michael Reinelt schrieb: Das Problem, das du entdeckt hast, hängt im Prinzip mit dem Exponenten zusammen. Denn im Grunde macht eine Floating Point Ausgabe ab einer gewissen Anzahl x an Vorkommastellen keinen Sinn mehr. Ab dann ist es dann einfacher und auch für den Benutzer besser auf eine Exponentenschreibweise zurückzugreifen, wobei ich persönlich dann auch die wissenschaftliche Konvention bevorzuge, bei der der Exponent in 1000-er Schritten wächst. Leider kann dtostrf das nicht. > Das mit dem Wertebereich ist auch so eine Sache: Eigentlich kenn ich den > schon, wenn aber aufgrund eines Messfehlers ein Divisor plötzlich > ziemlich klein wird... Du musst dich sowieso von der Idee verabschieden, dass du Floating-Point rechnen kannst, ohne dich um so Kleinigkeiten wie Fehlerabfragen zu kümmern. Floating Point Rechnereien sind normalerweise gespickt mit diversen Epsilon Abfragen, um genau derartige Dinge auszuschliessen und frühzeitig abzufangen. Wer naiv an Floating Point rangeht, hat schon verloren. Floating Point korrekt einzusetzen, ist wesentlich schwieriger als das bischen Overflow-Behandlung in der Ganzzahlrechnerei. http://www.validlab.com/goldberg/paper.pdf
Karl Heinz Buchegger schrieb: > Floating Point korrekt einzusetzen, ist wesentlich schwieriger als das > bischen Overflow-Behandlung in der Ganzzahlrechnerei. Auch wenn ich dir nicht gern widerspreche, hier aber schon.
Jörg Wunsch schrieb: > Karl Heinz Buchegger schrieb: >> Floating Point korrekt einzusetzen, ist wesentlich schwieriger als das >> bischen Overflow-Behandlung in der Ganzzahlrechnerei. > > Auch wenn ich dir nicht gern widerspreche, hier aber schon. Ich finde den Abschnitt nicht mehr. Ich bin aber sicher, dass er mal in dem Paper "What every Computer Scientist should know about Floating Point" drinnen war. Es ging um ein Beispiel, welches zeigt, dass Transitiviät nicht hielt. Die Ausgansgzahlen waren alle korrekt und in IEEE Floating Point exakt darstelltbar. Trotzdem kam eine naive Implementierung, ich glaube es war eine Dreiecksungleichung, zu einem falschen Ergebnis. Im verlinkten Paper ist als Beispiel die Dreicksfläche angegeben, die im konkreten Beispiel anstelle eines korrekten Ergebnisses von 2.34 ein Ergebnis von 3.04 rausbringt. Floating Point Arithmetik hat immer damit zu tun, Espilons zu berücksichtigen bzw. Formeln so umzustellen, dass die Fehler klein bleiben. Und das ist oft schwieriger als gedacht. (Und ja. Ich hab mich mit solchen Problemen rumgeschlagen. In der Geometrie bleibt sowas nicht aus. Schleifende Schnitte sind ein Albtraum, leider aber nicht zu vermeiden wenn man boolsche Operationen machen will. Sind im Solid Modelling 2 Flächen, deren Flächengleichungen sich um E-12 unterscheiden als gleich zu behandeln, Ja oder nein. Welche der beiden Flächen ist die 'äussere', welche zb. Materialeigenschaften gelten? Mein erster, naiver Ansatz in der Robotik, endete damit, dass die Robotergeometrie nach ca 200 Matrixmultiplikationen anfing zu explodieren. Das war so ziemlich das erste mal, dass es mir (und dem mich betreuenden Mathematiker) dämmerte, dass Floating Point ein bischen mehr ist, als einfach nur Multiplikationen und Divisionen hinzuschreiben)
Dass FP komplex sein kann, ist mir klar, das ist aber nicht unbedingt mein Problem. Eine ungenaue/falsche Berechnung ist eine Sache, ein buffer overflow eine andere. Ich wäre schon zufrieden, wenn ich erkennen könnte ob meine Angaben width und precision ausreichen um die zahl darzustellen, ansonsten den buffer mit "***.**" zu füllen, damit man sofort sieht dass der Wert nicht darstellbar ist; aber keinen unnötigen Buffer verschwenden. Kurz dachte ich daran, mich selbst mit __ftoa_engine herumzuschlagen, aber das Zeug kommt in den "exportierten" Headern gar nicht vor, ist scheinbar "for internal use only". snprintf vermeide ich, weil ich eigentlich eh mein eigenes "mini-printf" habe, das nur ein subset des großen printf kann, dafür aber ein paar andere Nettigkeiten wie Binärzahlen. natürlich kann ich mit Bereichsprüfungen in Kombination mit width prüfen ob das Ergebnis darstellbar ist, aber schön ist das nicht... @Karl Heinz: Oh mann, ja, schleifende Schnitte... ich hab einige jahre lang eins der ersten 3D-CAD-Systeme betreut (I-deas von SDRC) und da war der "boolean operation failed" der Horror...
Hallo nochmal, ich habe beschlossen, das selbst unter Zuhilfenahme von ftoa_engine() zu implementieren. Eine Frage dazu: Kann mir jemand erklären was der sinn des Flags FTOA_CARRY ist? in der ftoa_engine.h find ich den Kommentar "Carry was to master position." Aus den Funktionen in der avr-libc welche auf ftoa_engine zugreifen, erschließt sich mir der Sinn leider auch nicht wirklich... Danke, Michi
Michael Reinelt schrieb: > Ich wäre schon zufrieden, wenn ich erkennen könnte ob meine Angaben > width und precision ausreichen um die zahl darzustellen, ansonsten den > buffer mit "***.**" zu füllen, damit man sofort sieht dass der Wert > nicht darstellbar ist; aber keinen unnötigen Buffer verschwenden. So kompliziert ist das jetzt aber nicht, einen Wrapper um das originale dtostrf zu schreiben, der die Eingabedaten abprüft, und dann entweder "***.**" oder eben das Ergebnis von dtostrf zurückgibt. Oliver
Oliver schrieb: > So kompliziert ist das jetzt aber nicht, einen Wrapper um das originale > dtostrf zu schreiben, der die Eingabedaten abprüft, und dann entweder > "***.**" oder eben das Ergebnis von dtostrf zurückgibt. ganz ganz trivial ist es leider auch nicht. Aufgrund der Runderei (da kommt wieder das Epsilon ins Spiel) ist es nicht so einfach vorauszusagen, wieviele Stellen dtostrf benötigen wird. Beispiel: keine Nachkommastellen (der Einfachheit halber), und eine Maximale Stellenanzahl von 4 es lässt sich also von 0 bis 9999 alles darstellen (Minus lass ich auch mal weg) bedingung: val < 10000 Input: 9999.999999999999999999999999999999 Ergebnis: 100000 => Buffer overflow
Michael Reinelt schrieb: > bedingung: val < 10000 > > Input: 9999.999999999999999999999999999999 > Ergebnis: 100000 => Buffer overflow Hm. Warum zum einen "val < 10000" wahr sein soll, in folgenden der Wert dann doch zu 10000 aufgerundet wird, erschliesst sich mir jetzt nicht. Was passieren kann, ist, daß 9999.99999999999999999 fälschlicherweise als "***.**" dargestellt wird, mehr aber doch nicht. Oliver
Nachtrag: Im Zweifel sepndiert man dem Buffer halt ein Byte mehr (im Beispiel für val < 10000 also 5 Bytes). Wenn es an dem einen Byte Ramverbrauch scheitern sollte, dann ist das Projekt sowieso zu sehr auf Kante genäht. Oliver
Oliver schrieb: > Michael Reinelt schrieb: >> bedingung: val < 10000 >> >> Input: 9999.999999999999999999999999999999 >> Ergebnis: 100000 => Buffer overflow > > Hm. Warum zum einen "val < 10000" wahr sein soll, in folgenden der Wert > dann doch zu 10000 aufgerundet wird, erschliesst sich mir jetzt nicht. > Was passieren kann, ist, daß 9999.99999999999999999 fälschlicherweise > als "***.**" dargestellt wird, mehr aber doch nicht. > > Oliver Sorry, schlechtes Beispiel Input: 9999.999 ist ziemlich sicher < 10000 durch die Rundung die dtorstrf() durchführen wird, wird 10000 zurückgeliefert Sicher, gehen würde es schon irgendwie, mit vielen if()s und dynamischen epsilons und diesem und jenem. Aber schön ist es nicht.
Michael Reinelt schrieb: > durch die Rundung die dtorstrf() durchführen wird, wird 10000 > zurückgeliefert Wenn dtorstrf das tatsächlich macht (warum sollte die Funktion runden?), ist das 1.) blöd ;) , und 2.) wie ich gerade schrieb, kostet das halt ein Byte im Puffer zusätzlich. Oliver
Michael Reinelt schrieb: > ich habe beschlossen, das selbst unter Zuhilfenahme von ftoa_engine() zu > implementieren. Davon würde ich dir abraten. __ftoa_engine() fängt nicht umsonst mit zwei Unterstrichen an: es gehört zum implementation namespace. Da die implementation (in diesem Falle die avr-libc) diese Funktion nicht dokumentiert, kannst du dich auf das entsprechende API nicht verlassen: es kann von heute auf morgen begründungslos geändert werden, wenn das innerhalb der avr-libc sinnvoll erscheint oder auch komplett entfallen. (In einer früheren Implementierung von dtostrf() gab es diese Funktion ja auch noch nicht.)
Oliver schrieb: > Wenn dtorstrf das tatsächlich macht (warum sollte die Funktion runden?) Weil printf() auch rundet und das im Allgemeinen gewünscht ist? Wenn du "double x = 10000.;" eingibst, dann willst du garantiert nicht, dass da 9999.999 angezeigt werden. Da die interne Binärdarstellung aber nicht jede x-beliebige Dezimalzahl exakt darstellen lässt, bleibt nichts anderes sinnvolles übrig, als bei der Ausgabe zu runden.
Jörg Wunsch schrieb: > Michael Reinelt schrieb: >> ich habe beschlossen, das selbst unter Zuhilfenahme von ftoa_engine() zu >> implementieren. > > Davon würde ich dir abraten. > > __ftoa_engine() fängt nicht umsonst mit zwei Unterstrichen an: es > gehört zum implementation namespace. Da die implementation (in > diesem Falle die avr-libc) diese Funktion nicht dokumentiert, kannst > du dich auf das entsprechende API nicht verlassen: es kann von heute > auf morgen begründungslos geändert werden, wenn das innerhalb der > avr-libc sinnvoll erscheint oder auch komplett entfallen. (In einer > früheren Implementierung von dtostrf() gab es diese Funktion ja auch > noch nicht.) Ja eh, ich weiss. Allerschlimmstenfalls nehm ich halt den Assembler-Code von ftoa_engine.S Aber die Funktion schaut so verlockend aus...
Michael Reinelt schrieb: > Allerschlimmstenfalls nehm ich halt den Assembler-Code von ftoa_engine.S Ja, dazu würde ich dir eher raten.
Manno, diese avr-libc zu verstehen kostet ganz schön Hirnschmalz! anyway, meine private ftoa-funktion ist fertig und in mein Spezial-printf eingebunden. Falles das wer brauchen kann:
1 | /* from ftoa_engine.h */
|
2 | #define FTOA_MINUS 1
|
3 | #define FTOA_ZERO 2
|
4 | #define FTOA_INF 4
|
5 | #define FTOA_NAN 8
|
6 | #define FTOA_CARRY 16
|
7 | |
8 | int __ftoa_engine(double val, char *buf, unsigned char prec, unsigned char maxdgs); |
9 | |
10 | static char *my_ftoa(char *buffer, const uint8_t size, const double value, const uint8_t width, const uint8_t prec, const uint8_t fill0) |
11 | {
|
12 | /* width is the total (maximum) field width */
|
13 | /* if width is zero, resulting string grows as needed up to <size> */
|
14 | /* prec is limited to 12 by caller */
|
15 | /* INF => ###.##, NAN => ---.-- according to width and prec */
|
16 | /* if output does not fit into width, it will be displayed as ###.## */
|
17 | /* we DO NOT reduce precision because of rounding issues */
|
18 | |
19 | |
20 | char result[9]; |
21 | int8_t exp = __ftoa_engine(value, result, 7, prec + 1); |
22 | uint8_t flag = result[0]; |
23 | uint8_t sign = flag & FTOA_MINUS ? 1 : 0; |
24 | |
25 | int8_t len = (exp > 0 ? exp + 1 : 1); |
26 | if (sign) |
27 | len += 1; |
28 | if (prec) |
29 | len += prec + 1; |
30 | |
31 | if ((width && len > width) || len > size - 1) |
32 | flag |= FTOA_INF; |
33 | |
34 | if (flag & (FTOA_NAN | FTOA_INF)) { |
35 | char *p = buffer + size - 1; |
36 | *p = '\0'; |
37 | char pad = (flag & FTOA_NAN) ? '-' : '#'; |
38 | uint8_t l = width ? width : prec ? prec + 2 : 1; |
39 | uint8_t d = prec; |
40 | while (l--) { |
41 | *--p = d-- ? pad : '.'; |
42 | }
|
43 | return p; |
44 | }
|
45 | |
46 | |
47 | char *p = buffer; |
48 | |
49 | int8_t pad = width > len ? width - len : 0; |
50 | if (!fill0) { |
51 | while (pad) { |
52 | *p++ = ' '; |
53 | pad--; |
54 | }
|
55 | }
|
56 | |
57 | if (sign) |
58 | *p++ = '-'; |
59 | |
60 | while (pad) { |
61 | *p++ = '0'; |
62 | pad--; |
63 | }
|
64 | |
65 | int8_t ndigs = prec + 1 + exp; |
66 | if ((flag & FTOA_CARRY) && result[1] == '1') |
67 | ndigs--; |
68 | if (ndigs < 1) |
69 | ndigs = 1; |
70 | else if (ndigs > 8) |
71 | ndigs = 8; |
72 | |
73 | char c; |
74 | int8_t n = exp > 0 ? exp : 0; |
75 | do { |
76 | if (n == -1) |
77 | *p++ = '.'; |
78 | c = (n <= exp && n > exp - ndigs) ? result[exp - n + 1] : '0'; |
79 | if (--n < -prec) |
80 | break; |
81 | *p++ = c; |
82 | } while (1); |
83 | if (n == exp && (result[1] > '5' || (result[1] == '5' && !(flag & FTOA_CARRY)))) { |
84 | c = '1'; |
85 | }
|
86 | *p++ = c; |
87 | *p = '\0'; |
88 | |
89 | return buffer; |
90 | }
|
damit kann ich mit dem Parameter <width> die Länge des Strings verläßlich beschränken, kann der Wert damit nicht dargestellt werden, wird "###.##" ausgegeben (im Prinzip wie INF also unendlich) Was die Funktion auch macht, weil ich das oft brauche: NAN wird als "---.--" (angepasst an width und prec) ausgegeben. ich arbeite gerne mit NAN um zu kennzeichnen dass ein Wert nicht verfügbar ist (Sensor ausgefallen, nicht aktiv, Messwert steht nicht zur Verfügung etc), das schöne ist dass sich NAN dann durch alle weiteren Berechnungen durchzieht, alle Zwischen- und Endergebnisse sind dann auch NAN, und das wird am Display oder im Logfile oder wo auch immer "elegant" ausgegeben.
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
Mit Google-Account einloggen
Noch kein Account? Hier anmelden.