Forum: Compiler & IDEs dtostrf: maximale Buffergröße?


von Michael R. (Firma: Brainit GmbH) (fisa)


Lesenswert?

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
von Jörg W. (dl8dtl) (Moderator) Benutzerseite


Lesenswert?

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. :-)

von Michael R. (Firma: Brainit GmbH) (fisa)


Lesenswert?

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...

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


Lesenswert?

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.

von Peter II (Gast)


Lesenswert?

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.

von Karl H. (kbuchegg)


Lesenswert?

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

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


Lesenswert?

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.

von Karl H. (kbuchegg)


Lesenswert?

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)

von Michael R. (Firma: Brainit GmbH) (fisa)


Lesenswert?

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...

von Michael R. (Firma: Brainit GmbH) (fisa)


Lesenswert?

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

von Oliver (Gast)


Lesenswert?

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

von Michael R. (Firma: Brainit GmbH) (fisa)


Lesenswert?

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

von Oliver (Gast)


Lesenswert?

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

von Oliver (Gast)


Lesenswert?

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

von Michael R. (Firma: Brainit GmbH) (fisa)


Lesenswert?

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.

von Oliver (Gast)


Lesenswert?

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

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


Lesenswert?

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.)

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


Lesenswert?

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.

von Michael R. (Firma: Brainit GmbH) (fisa)


Lesenswert?

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...

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


Lesenswert?

Michael Reinelt schrieb:
> Allerschlimmstenfalls nehm ich halt den Assembler-Code von ftoa_engine.S

Ja, dazu würde ich dir eher raten.

von Michael R. (Firma: Brainit GmbH) (fisa)


Lesenswert?

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
Noch kein Account? Hier anmelden.