Forum: Compiler & IDEs printf format strings prüfen?


Announcement: there is an English version of this forum on EmbDev.net. Posts you create there will be displayed on Mikrocontroller.net and EmbDev.net.
von Bauform B. (bauformb)


Lesenswert?

Mahlzeit!

-Wformat und besonders -Wformat-security sind ja nicht verkehrt. Ein 
nicht-konstanter format string wird dann angemeckert. Seht gut, aber es 
gibt eine Grauzone, zum Beispiel finde ich, dass diese format strings 
durchaus konstant sind. Kann ich dem gcc außer "const" noch einen Tipp 
geben?
1
int
2
my_mount (unsigned long flags, const char *source)
3
{
4
  static const char  *opttab[] = {
5
    "errors=remount-ro,check=strict,showexec,uid=%d,gid=%d,time_offset=%ld",
6
    "errors=remount-ro,uid=%d,gid=%d,time_offset=%ld",
7
    "errors=remount-ro",
8
    NULL
9
  };
10
  const char  *fsopts;
11
// blkid usw. findet msdos, exfat, ext4 und dann...
12
  fsopts = opttab[type];
13
  asprintf (&options, fsopts, uid, uid, timezone / 60L);
14
  err = mount (source, default_mnt, fstype, flags, options);

Im nächsten Beispiel ist im Endeffekt auch alles konstant, aber da ist 
die Warnung wohl unvermeidlich. Gibt es vielleicht "eine lib", mit der 
ich den format string prüfen könnte? Und wie sage ich dann dem 
strftime(), dass der ok ist?
1
char *
2
isotime (const char *format, time_t utc)
3
{
4
  static char  text[40];
5
  time_t       unixtime;
6
7
  if (utc == 0) {
8
    return (char *)"";
9
  }
10
  if (utc == 1) {
11
    unixtime = time (NULL);
12
  } else {
13
    unixtime = utc;
14
  }
15
  if (format == NULL || *format == '\0') {
16
    format = "%F %T";
17
  }
18
  strftime (text, sizeof text, format, localtime (&unixtime));
19
  return text;
20
}

Ja, natürlich kann ich das mount auch anders formulieren, da gibt es nur 
vier bekannte Dateisysteme. Aber wie man ein Datum schreibt, ist ja 
leider völlig undefiniert.

von Johann L. (gjlayde) Benutzerseite


Lesenswert?

Bauform B. schrieb:
> zum Beispiel finde ich, dass diese format strings durchaus
> konstant sind. Kann ich dem gcc außer "const" noch einen Tipp
> geben?

Damit ein Format-String gecheckt werden kann, muss er natürlich zur 
Compilezeit bekannt sein, und das ist
1
>   const char  *fsopts = opttab[type];
Nun mal nicht, weil type nicht bekannt ist zur Laufzeit.

Ich nehme mal an, dass die Obfuskation über das Array nicht super 
Performance-kritisch ist, also einfach die 3 Fälle per if+else oder 
switch+case abhandeln mit FMT-Strings die zur Compilezeit bekannt sind.

von Bauform B. (bauformb)


Lesenswert?

Johann L. schrieb:
> Damit ein Format-String gecheckt werden kann, muss er natürlich zur
> Compilezeit bekannt sein, und das ist
>>   const char  *fsopts = opttab[type];
> Nun mal nicht, weil type nicht bekannt ist zur Laufzeit.

Ach was, natürlich ist type bekannt ;) Wenn es zu groß würde, wäre das 
ja undefined! Aber an der Stelle geht if/else problemlos, also gut.

Und jetzt auf einmal geht's für das Datumsformat genauso einfach: ich 
erlaube nur den Default oder %x "The preferred date representation for 
the current locale without the time". Wer sich ein Fantasieformat 
ausdenkt, oder in Italien die norwegische Schreibweise will, bekommt 
automatisch RFC 3339. Das muss reichen, manche Formate gehören sowieso 
verboten.

Haben wir wieder mal ein Problem elegant? umgangen. Trotzdem, so eine 
Format-Prüf-Funktion sollte es eigentlich geben, braucht man das nicht, 
wenn man gettext mit fremden Übersetzungen benutzt?

von Johann L. (gjlayde) Benutzerseite


Lesenswert?

Bauform B. schrieb:
> Johann L. schrieb:
>> Damit ein Format-String gecheckt werden kann, muss er natürlich zur
>> Compilezeit bekannt sein, und das ist
>>>   const char  *fsopts = opttab[type];
>> Nun mal nicht, weil type nicht bekannt ist zur Laufzeit.
>
> Ach was, natürlich ist type bekannt

Was aber nicht aus deinem Code ersichtlich ist.

von Rolf M. (rmagnus)


Lesenswert?

Bauform B. schrieb:
> Johann L. schrieb:
>> Damit ein Format-String gecheckt werden kann, muss er natürlich zur
>> Compilezeit bekannt sein, und das ist
>>>   const char  *fsopts = opttab[type];
>> Nun mal nicht, weil type nicht bekannt ist zur Laufzeit.
>
> Ach was, natürlich ist type bekannt ;)

Wenn type schon zur Compilezeit feststeht, wozu brauchst du es dann?

von Bauform B. (bauformb)


Lesenswert?

Ach, keiner versteht mich :( Natürlich ist das erst zur Laufzeit 
bekannt. Aber sonst heißt es immer, UB gibt es nicht. Also könnte der 
gcc sagen, es muss eins von den drei Formaten sein und die sind alle ok. 
Ich benutze allerdings auch -O, vielleicht würde er es mit -O3 sogar 
finden.

von Johann L. (gjlayde) Benutzerseite


Lesenswert?

Johann L. schrieb:
> Nun mal nicht, weil type nicht bekannt ist zur Laufzeit.

Mein Fehler.  Sollte natürlich heissen "nicht bekannt zur COMPILEzeit".

von Rolf M. (rmagnus)


Lesenswert?

Bauform B. schrieb:
> Ach, keiner versteht mich :( Natürlich ist das erst zur Laufzeit
> bekannt. Aber sonst heißt es immer, UB gibt es nicht. Also könnte der
> gcc sagen, es muss eins von den drei Formaten sein und die sind alle ok.

Das könnte er möglicherweise (Wo type herkommt und wie es zu seinem Wert 
kommt, sieht man deinem Code oben nicht), aber das wäre recht komplex, 
und so ein Verhalten müsste extra da reinprogrammiert werden, für einen 
äußerst seltenen Anwendungsfall.

> Ich benutze allerdings auch -O, vielleicht würde er es mit -O3 sogar
> finden.

Höhere Optimierungsstufen sind für sowas definitv besser, weil der 
Compiler dann eine ausführliche Codeanalyse machen muss, durch die 
solche Sonderfälle eher gefunden werden können.

von Frank M. (ukw) (Moderator) Benutzerseite


Lesenswert?

Bauform B. schrieb:
> Natürlich ist das erst zur Laufzeit bekannt.

Da type nur die Werte 0, 1, 2 annehmen kann, mache einen switch über die 
Variable type und schreibe das printf jedesmal explizit mit dem 
konkreten Format-String hin.

Dann kannst Du Dir das array "opttab" sparen und der Code wird lesbarer. 
Außerdem kann der Compiler dann zur Compilezeit die drei Formatstrings 
prüfen.

von Bauform B. (bauformb)


Lesenswert?

Frank M. schrieb:
> mache einen switch über die
> Variable type und schreibe das printf jedesmal explizit mit dem
> konkreten Format-String hin.

Ja, natürlich ist das in diesem Fall besser. Rein optisch sieht es jetzt 
garnicht so übel aus. Aber wenn es mehr Fälle werden, ist mir ein Liste 
mit structs (hier fstype und options) doch lieber.
1
static const char dosopts[] = "check=strict,showexec";
2
3
snprintf (fatopts, sizeof fatopts, "time_offset=%ld,uid=%d,gid=%d",
4
          timezone / 60L, uid, uid);
5
if (strcmp (fstype, "vfat") == 0 || strcmp (fstype, "msdos") == 0) {
6
   asprintf (&options, "%s,%s,%s", errors, fatopts, dosopts);
7
} else {
8
   if (strcmp (fstype, "exfat") == 0) {
9
      asprintf (&options, "%s,%s", errors, fatopts);
10
   } else {
11
      if (strcmp (fstype, "ext4") == 0) {
12
         asprintf (&options, "%s", errors);
13
      } else {
14
         fail (__func__, 0, "Unknown fstype '%s'", fstype);
15
      }
16
   }
17
}

von Johann L. (gjlayde) Benutzerseite


Lesenswert?

Bauform B. schrieb:
> Rein optisch

Viele Editoren unterstützen auch folgende Einrückung:
1
if (strcmp (fstype, "vfat") == 0 || strcmp (fstype, "msdos") == 0) {
2
   asprintf (&options, "%s,%s,%s", errors, fatopts, dosopts);
3
} else if (strcmp (fstype, "exfat") == 0) {
4
   asprintf (&options, "%s,%s", errors, fatopts);
5
} else if (strcmp (fstype, "ext4") == 0) {
6
   asprintf (&options, "%s", errors);
7
} else {
8
   fail (__func__, 0, "Unknown fstype '%s'", fstype);
9
}

Wobei der grauslige Stil, } anders einzurücken als das zugehörige {, 
grauslig ist.

von Frank M. (ukw) (Moderator) Benutzerseite


Lesenswert?

Johann L. schrieb:
> Wobei der grauslige Stil, } anders einzurücken als das zugehörige {,
> grauslig ist.

Deshalb schreibe ich die { immer auf eine extra Zeile. Dann ist rechts 
daneben auch noch jede Menge Platz für einen passenden Kommentar. :-)

P.S.

Auch für die } gebe ich mir eine extra Zeile und schreibe auch das "else 
if" darunter. Also:
1
if (! strcmp (fstype, "vfat") || ! strcmp (fstype, "msdos"))
2
{
3
   asprintf (&options, "%s,%s,%s", errors, fatopts, dosopts);
4
}
5
else if (! strcmp (fstype, "exfat"))
6
{
7
   asprintf (&options, "%s,%s", errors, fatopts);
8
}
9
else if (! strcmp (fstype, "ext4"))
10
{
11
   asprintf (&options, "%s", errors);
12
}
13
else
14
{
15
   fail (__func__, 0, "Unknown fstype '%s'", fstype);
16
}

Aber das ist Geschmackssache. Bei den heutigen Monitorgrößen muss man 
aber nicht unbedingt mit Platz geizen. Noch ein Vorteil: Ich kann mit 
dem Editor jederzeit einen else-if-Block zeilenweise rausschneiden oder 
einfügen, ohne die Formatierung in unmittelbarer Umgebung anpassen zu 
müssen.

EDIT:

strcmp-Vergleich gekürzt.

: Bearbeitet durch Moderator
von Bauform B. (bauformb)


Lesenswert?

Das war ja klar :) Aber so geht es natürlich auch nicht. Euch fehlen ein 
paar { { {. Entweder elif (oder so) oder die Klammern müssen sein. Spart 
ihr die { auch beim einzelnen if oder for mit nur einem Statement? Das 
ist ja nun total illegal
;)

von Daniel A. (daniel-a)


Lesenswert?

Eventuell etwas overengineert, mit X-Macros die Structs und Funktionen 
generieren: https://godbolt.org/z/3KPKcE5e3

von Bruno V. (bruno_v)


Lesenswert?

Bauform B. schrieb:
> Euch fehlen ein paar { { {.
Nein. Zu else if gibt es einen breiten Konsens, dass nur hier auf {} 
verzichtet werden darf, ja sogar sollte. Dafür ist das abschließende 
else Pflicht.

Die Klammern würden hier obfuskieren. Die anweisungsblöcke hingegen 
haben auch hier Klammern, egal ob 0, 1 oder mehr Anweisungen.

: Bearbeitet durch User
von Daniel A. (daniel-a)


Lesenswert?

Bruno V. schrieb:
> Bauform B. schrieb:
>> Euch fehlen ein paar { { {.
> Nein. Zu else if gibt es einen breiten Konsens, dass nur hier auf {}
> verzichtet werden darf. Dafür ist das abschließende else Pflicht.

Ach wo, wenn es legal ist darf man es schreiben. Ich habe auch schon so 
Sachen wie "do if(){ }while();" oder "if(){}else for(;;){}" 
hingeschrieben. Das ist völlig OK.

von Oliver S. (oliverso)


Lesenswert?

Bruno V. schrieb:
> Zu else if gibt es einen breiten Konsens, dass nur hier auf {}
> verzichtet werden darf, ja sogar sollte.

Natürlich. Genauso wie einen breiten Konsens darüber gibt, wo man 
öffnende und schließende Klammern zu setzen hat...

Immerhin werfen gcc und Clang inzwischen eine Warnung, wenn die auf else 
ohne Klammern und potentiell missverständliche Formatierungen treffen.

Klammern. Immer. Ausnahmen bestätigen die Regel.

Oliver

von Frank M. (ukw) (Moderator) Benutzerseite


Lesenswert?

Oliver S. schrieb:
> Immerhin werfen gcc und Clang inzwischen eine Warnung, wenn die auf else
> ohne Klammern und potentiell missverständliche Formatierungen treffen.

Das ist ja auch sinnvoll. Aber hier wird garantiert nicht gewarnt:
1
if (foo)
2
{
3
    bar();
4
}
5
else if (foo2)
6
{
7
    bar2();
8
}
9
else if (foo3)
10
{
11
    bar3();
12
}
13
else
14
{
15
    baz();
16
}

Grund: Das auf else folgende if() mit geklammerten Block dahinter ist 
für den Compiler grundsätzlich ein Statement, so dass auf die 
geschweifte Klammer zwischen else und if immer(!) verzichtet werden 
kann - wie man es vom switch() gewohnt ist. Das ist auch keine 
C-Sonderregel oder ähnliches, sondern ergibt sich klar aus der 
C-Grammatik.

Auf diese Klammer- bzw. Einrückungs-Orgie hier:
1
if (foo)
2
{
3
    bar();
4
}
5
else
6
{
7
    if (foo2)
8
    {
9
        bar2();
10
    }
11
    else
12
    {
13
        if (foo3)
14
        {
15
            bar3();
16
        }
17
        else
18
        {
19
            baz();
20
        }
21
    }
22
}

kann also immer verzichtet werden. Es reicht eine lineare Aufzählung 
aller Bedingungen. Die obere lineare Lösung ist auch wesentlich lesbarer 
als ein derart ineinander geschachtelter Block. Der wird nämlich, wenn 
eine neue Bedingung hinzukommt, immer weiter verschachtelt und nach 
rechts immer weiter eingerückt. Bei 20 Bedingungen ist man dann "ganz 
weit draußen".

: Bearbeitet durch Moderator
von Bruno V. (bruno_v)


Lesenswert?

Oliver S. schrieb:
> Immerhin werfen gcc und Clang inzwischen eine Warnung, wenn die auf else
> ohne Klammern und potentiell missverständliche Formatierungen treffen.

Wirklich bei else if -Reihen mit abschließendem else? (Ich nehme an, Du 
hast das ausprobiert und mit MISRA und Co bisher nichts zu tun gehabt. 
Da ist dann wohl was schiefgelaufen)

von Daniel A. (daniel-a)


Lesenswert?

Oh Gott, jetzt auch noch MISRA. Das ist nun wirklich nur noch 
verschwendete Arbeitszeit und herausgeworfenes Geld.
Wer glaubt solche stumpfsinnige generelle Einschränkungen brächten 
irgendwas, sollte gleich Rust nehmen.

: Bearbeitet durch User
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.