AUDACIA Software

Typsichere Format-Strings

Ein Beitrag zur Rehabilitierung von printf.
Moritz Beutel, 06.07.2009



  1. printf() in C
  2. Die C++-Alternative: Streams
  3. CbdeFormat: printf() typsicher
  4. Beispiel 1: Migration nach C++Builder 2009/Unicode
  5. Beispiel 2: Debuggen komplizierterer Format-Strings
  6. Beispiel 3: Übersetzer-Fehler
  7. Für eigene Funktionen einsetzen
  8. Referenzen
  9. Kommentare



printf() in C


Die Möglichkeit, Strings mit formatierten Parametern ausgeben, ist grundlegender Bestandteil praktisch jeder etablierten Hochsprachenumgebung. In C war dafür die printf()-Funktion in all ihren Varianten zuständig. In ihrer Flexibilität und Simplizität diente die Funktion als Vorbild für die Äquivalente der meisten Sprachen, ob in PHP, Delphi, C#/.NET oder Java.

Jedoch hat printf() in C einen gravierenden Nachteil: die Funktion ist nicht typsicher. Der Format-String wird erst zur Laufzeit analysiert, doch verfügt printf() nicht über Typinformationen zu den Argumenten und muß daher annehmen, daß deren Typen den im Format-String angegebenen Typen entsprechen. Dies ist eine obskure und unter Umständen schwer auffindbare Fehlerquelle, im Extremfall läßt sie sich sogar zum Einschleusen von Fremdcode mißbrauchen. Es folgen einige eher harmlose Beispiele:
#include <stddef.h>
#include <stdio.h>
#include <string.h>

int bar (const char* arg)
{
  char buf[200];
  wchar_t wbuf[200];
  int i;

    /* Die Funktion erwartet zwei Parameter, erhält aber nur einen.
     * *(int*)arg wird als erstes Argument betrachtet, die Rücksprungadresse
     * als String-Zeiger. Die Bandbreite möglicher Resultate rangiert zwischen
     * einem undefinierten Inhalt von buf, einem Buffer overflow und
     * einer AV.
     */
  sprintf (buf, "%*s", arg);

    /* Die Sicherheit ist trügerisch: die Funktion erwartet const wchar_t*,
     * erhält aber const char*. Ein wchar_t-String muß mit zwei (oder vier, je
     * nach Plattform) Null-Bytes terminiert werden, die in einem char-String
     * nicht unbedingt vorkommen müssen. Auch hier undefinierter Inhalt bis AV.
     */
  if (strlen (arg) < 200)
    swprintf (wbuf, L"%s", arg);

    /* Erwartet zwei Parameter, erhält aber nur einen. Da die Funktion versucht,
     * in die Rücksprungadresse zu schreiben, erhalten wir mit ziemlicher
     * Sicherheit eine AV.
     */
  sscanf (buf, "%*d", &i);
}


Darüber hinaus sind die Varianten von printf() selbst bei korrekter Parameterisierung anfällig für weitere Fehler. Insbesondere können sprintf(), swprintf() und alle Varianten von scanf() mit Leichtigkeit Pufferüberläufe verursachen:

#include <stdio.h>
#include <string.h>

void foo (const char* arg)
{
  char buf[200];

    /* Korrumpiert den Stack, wenn strlen (arg) > 192.
     */
  sprintf (buf, "arg: '%s'", arg);

    /* Korrumpiert den Stack, wenn der Benutzer mehr als 200 Zeichen eingibt.
     */
  scanf ("%s\n", buf);
}



Die C++-Alternative: Streams


In den meisten Programmiersprachen, die das printf-Konzept im Grundsatz übernommen haben, sind die Formatierungsfunktionen jedoch weder anfällig für Pufferüberläufe (durch das dynamische String-Management) noch fehlt ihnen die Typsicherheit, da sie das Problem nicht wie in C über eine variable Anzahl von Stackargumenten, sondern gewöhnlich mit Arrays der einen oder anderen Art lösen.

Die Entwickler der Sprache C++ jedoch wählten einen vollkommen unterschiedlichen Ansatz. Zwar sind die herkömmlichen C-Funktionen in C++ sämtlich nutzbar, und es wäre auch mit den neuen Sprachmitteln leicht möglich gewesen, die Pufferüberläufe durch die konsequente Nutzung einer String-Klasse zu vermeiden. Doch hatte das Konzept der IO-Streams, das eine sehr einfache Erweiterbarkeit für eigene Datentypen bietet, den Eindruck gestärkt, printf() sei durch seine fehlende Erweiterbarkeit ohnehin nicht mehr modern genug für die Sprache.

So werden denn printf() und die verwandten Funktionen in C++ weithin als veraltet angesehen; die verschiedenen Stream-Typen sind die bevorzugte Lösung für die Formatierung. Sie kombinieren Typsicherheit und Erweiterbarkeit mit der Flexibilität von printf(): man kann dank ADL die Stream-Operatoren für eigene Klassen überladen, mittels Manipulatoren Streams formatieren, beliebige Objekte als Streams verwenden (z.B. Dateistreams, Standard-I/O-Streams, Stringstreams) und so weiter und so fort.

Das Problem ist nur: Es sieht immer furchtbar aus.

Man betrachte die Übertragung eines konzisen Beispieles von C nach C++:
int iVal = 2;
int hVal = 0x0C;
const char* filename = "test.fil";
const char* username = "juser";
float fValue = 23.8;

    // C
#include <stdio.h>

void doItWithPrintf (void)
{
    printf ("%4d  0x%02X  Testfile: [%10s]  user: [%-10s]\nTrial %8.2f \n",
            iVal, hVal, filename, username, fValue);
}


    // C++
#include <iostream>
#include <iomanip>
#include <cstdio>

int doItWithIOStreams (void)
{
  cout << setw(4) << iVal;
  cout << "  0x" << hex << uppercase << setprecision (2) << setfill ('0') << setw (2) << hVal;
  cout << setfill (' ') << dec; // reset things
  cout << "  Testfile: [" << setw (10) << filename << "]";
  cout << "  user: [" << setw (10) << left << username << right << "]";
  cout << "\n";
  cout << "Trial " << setw (8) << fixed << setprecision (2) << fValue;
  cout << endl;
}


Aber selbst in weniger extremen Fällen ist augenfällig, wie schwer die Stream-Variante es beispielsweise macht, Strings vernünftig zu internationalisieren. Als Beispiel betrachten wir eine Compiler-Fehlermeldung, die wir mit gettext lokalisieren:
void e2094 (const char* op, const char* lhstype, const char* rhstype)
{
        // C-Variante
    std::fprintf (stderr, gettext ("E2094: Operator '%s' not implemented in "
                                   "type '%s' for arguments of type '%s'"),
                  op, lhstype, rhstype);

        // C++-Variante
    std::cerr << gettext ("E2094: Operator '") << op
              << gettext ("' not implemented in type '") << lhstype
              << gettext ("' for arguments of type '") << rhstype
              << '\'' << std::endl;
}
Anstelle eines einzigen Strings mit Platzhaltern darin haben wir nun mehrere Teilstrings. Das freut den Übersetzer, ganz besonders dann, wenn er über keine oder nur vage Kontextinformationen verfügt. Noch problematischer wird es, wenn man sich die deutsche Übersetzung ansieht:
E2094: Operator 'operator' ist im Typ 'typ' für Argumente des Typs 'typ' nicht implementiert

Im deutschen Zustandspassiv wird das Partizip dem zugehörigen Substantiv üblicherweise nachgestellt. Das ist aber mit einer Internationalisierung obiger Einzelteile mit gettext schlicht nicht möglich.

Auch darüber hinaus gibt es gute Gründe, C++-Streams bei der Formatierung von Strings aus dem Wege zu gehen. Eine sehr flexible Alternative ist die Boost Format library, die tatsächlich die meisten Vorteile der beiden Varianten kombiniert - und darüber hinaus das eine oder andere Defizit des klassischen printf() beseitigt: beispielsweise ermöglicht es, Parameter mehrfach zu verwenden und die Reihenfolge zu verändern.


CbdeFormat: printf() typsicher


Mit boost::format() existiert zwar eine schöne Lösung, die ich nachdrücklich empfehle, doch nicht immer kann sie eingesetzt werden, beispielsweise, wenn man die Abhängigkeit eines Projektes von Boost vermeiden möchte, oder wenn größere Mengen vorliegenden Codes, der weiterer Wartung und Migration bedarf, bereits ausgiebig die printf-Syntax benutzt.

Interessanterweise ist es in durchaus möglich, die oben aufgezählten Probleme systematisch zu vermeiden. Zunächst kann die Speicherverwaltung automatisiert werden, um die Gefahr von Pufferüberläufen bei sprintf() zu beseitigen:
#include <string>
#include <cstdarg>
#pragma hdrstop

std::string str_printf (const char* format, ...)
{
    std::va_list args;
    std::string retval;

    va_start (args, format);

        // Bei der Übergabe von 0 als Puffer ermittelt die Funktion
        // nur die Länge des entstehenden Strings.
    retval.resize (std::vsnprintf (0, 0, format, args));

    std::vsnprintf (&retval[0], retval.size () + 1, format, args);
    va_end (args);

    return retval;
}
Leider existiert kein derartiges Konstrukt in der Standardbibliothek. Wie auch in vielen anderen Fällen fehlender grundlegender Library-Features bieten die meisten 3rd-Party-Bibliotheken jedoch eine entsprechende Implementation: Beispiele sind die C++Builder häufig genutzte Variante String::sprintf(), analog existiert CString::Format() in ATL/MFC. (Darüber hinaus existiert in C++Builder auch SysUtils::Format() und String::Format(), die allerdings die abweichende Delphi-Formatsyntax benutzen, zudem in C++-Code nicht maximal effizient sind. Dafür sind sie ohne zusätzlichen Aufwand typsicher.)

Dabei bleibt zunächst das Problem der Typsicherheit. Zwar bietet der GCC die Option -Wformat, die ihn veranlaßt, Format-Strings zur Kompilierzeit zu überprüfen - doch sobald man den Format-String über eine Indirektion wie beispielsweise gettext() gewinnt, funktioniert das natürlich nicht mehr.

Die neuen Sprachmittel von C++, insbesondere Templates und Typinferenz, ermöglichen allerdings eine (laufzeit-)typsichere Implementation der printf-Funktionen. Meine Implementation für C++Builder, CbdeFormat, möchte ich im Weiteren vorstellen.

CbdeFormat funktioniert mit C++Builder 2006, 2007 und 2009 (frühere Compilerversionen beherrschen keine Variadic Macros). Obgleich ich es für C++Builder geschrieben und mit C++Builder getestet habe, ist es aber nicht abhängig von Compiler-Spezifika. Im Grunde dürfte es auch mit jedem anderen C++-Compiler funktionieren.

Die Implementation erfüllt die meisten meiner Anforderungen an eine typsichere Implementation der printf-Familie:
  • In gewöhnlichem Anwendungscode sind keine Anpassungen notwendig.
  • Für Debugging-Zwecke ist der Informationsgehalt einer Fehlermeldung möglichst umfangreich (Position des Auftretens im Code, Format-String, tatsächliche und erwartete Parameter).
  • Nicht selten ist es sinnvoll, die Typüberprüfungen auch in der Release-Version durchzuführen. Der für jeden printf-Aufruf zusätzlich generierte Code hat daher einen möglichst geringen Laufzeit-Overhead und verursacht nur geringfügig umfangreicheren Code.
  • Der Formatstring-Parser ist einfach anpaßbar, so daß Erweiterungen der Formatsyntax leicht zu integrieren sind.
  • Auch andere Funktionen, die Formatstrings benutzen, können mit geringem Aufwand typsicher gemacht werden.

Natürlich hat sie auch Nachteile:
  • Die Typprüfung geschieht in keinem Falle zur Übersetzungszeit, auch nicht in Fällen, wo dies möglich wäre. Grund dafür ist einerseits die Konsistenz, andererseits die Tatsache, daß Laufzeitfehlermeldungen exakt über Datei und Zeile des Auftretens informieren können, wohingegen statische Typprüfungen aufgrund des Fehlens von Concepts gewöhnlich mit einer grenzwertig kryptischen Fehlermeldung in irgendeiner Headerdatei scheitern.
  • CbdeFormat verwaltet eine (beliebig erweiterbare) Liste von Funktionen wie printf(), sprintf(), scanf() etc., die mithilfe von Makros durch Aufrufe der typsicheren Varianten ersetzt werden. Das impliziert, daß für jede Funktion gleichen Namens ebenfalls eine sichere Version deklariert werden muß. Auch bedeutet es, daß Funktionen gleichen Namens, aber mit vollkommen unterschiedlichen Parametern unbenutzbar werden. (Das dürfte jedoch in der Praxis so gut wie nicht begegnen.)
  • Code Completion und Parameter Insight funktionieren innerhalb von Makro-Aufrufen nicht.

Die Bibliothek kann in der C++Builder-Sektion heruntergeladen werden. Der mitgelieferte Installer integriert CbdeFormat auf Wunsch in C++Builder 2006, 2007 und 2009. Dabei führt er folgende Schritte durch:
  • Zunächst kopiert er die benötigten Headerdateien in das Include-Verzeichnis der IDE.
  • Da es in C++ noch keine Variadic Templates gibt, implementierte ich eine variable Argumentzahl in der üblichen, auch bei std::tr1::function oder Variant::OleProcedure() eingesetzten Methode: mit mehrfacher Überladung. Mithilfe des für diesen Zweck erstellten Programmes formatgen erzeugt der Installer anhand einer Parameterdatei drei weitere Headerdateien im jeweiligen Include-Verzeichnis.
  • Weiter paßt er die Deklarationen in einigen Headerdateien (stdio.h, dstring.h, wstring.h, ustring.h, crtdbg.h) mithilfe von patch an.
  • Sind die Headerdateien aktualisiert, kompiliert er die notwendigen Bibliotheken mit dem jeweiligen Compiler.
  • Bei C++Builder 2006 existieren noch zentrale PCH-Dateien in $(BDS)\lib (vcl100.csm, vcl100.#??), die der Installer dann beseitigt (sie werden beim nächsten Build neu erstellt), damit die Änderungen an den Headerdateien wirksam werden. Bei C++Builder 2007 und 2009 ist das nicht notwendig.

Sodann ist CbdeFormat installiert, einsatzbereit und darüber hinaus für Debug-Builds standardmäßig aktiviert.

Im Weiteren betrachte ich einige mehr oder weniger praxisnahe Anwendungsbeispiele.


Beispiel 1: Migration nach C++Builder 2009/Unicode



Gerade bei der anstehenden Migration nach C++Builder 2009 beginnt die fehlende Typsicherheit von Format-Strings sich gerne bemerkbar zu machen. Betrachten wir ein exemplarisches Stück Code, das sich in ähnlicher Weise in vielen C++Builder-Anwendungen befinden dürfte:
    const unsigned majorVersion = 1, minorVersion = 2;

    AnsiString theMessage = AnsiString ().sprintf ("Hello %s!\n"
        "This machine is running since %d minutes. "
        "High time for a coffee break!",
        EdtUserName->Text.c_str (), GetTickCount () / 1000 / 60);
    AnsiString theTitle = AnsiString ().sprintf (
        "%s v%d.%2d Professional Edition",
        Application->Title, majorVersion, minorVersion);

    MessageBox (Handle, theMessage.c_str (), theTitle.c_str (),
        MB_ICONINFORMATION);


Öffnet man ältere Projekte in C++Builder 2009, so setzt die IDE beim Konvertieren das TCHAR-Mapping auf "char". Das hat zur Folge, daß weder UNICODE noch _UNICODE definiert sind und daher auch sämtliche Windows-Funktionen auf die ANSI-Varianten verweisen. Da in C++Builder anders als in Delphi meist explizit der Typ AnsiString anstelle von String benutzt wird und darüber hinaus die meisten nicht-VCL-spezifischen Funktionen nicht mit wchar_t-, sondern mit char-Strings arbeiten, vereinfacht das die Migration älterer Projekte etwas. (Soll eine Anwendung aber Unicode-fähig gemacht werden, so ist es natürlich sinnvoll, das Mapping auf "wchar_t" umzustellen und den Code anzupassen.)

In Delphi verläuft die Umstellung auf Unicode allerdings etwas anders als in C++Builder. Delphi-Programmierer verwendeten selten direkt AnsiString, sondern gewöhnlich den String-Typen. Dieser wurde in der Vergangenheit schon einmal geändert, nämlich von dem auf 255 Zeichen begrenzten ShortString, der bis Delphi 1 verwendet wurde, auf den referenzgezählten, dynamisch allozierten AnsiString. (Dieser Zusammenhang ist auch dafür verantwortlich, daß alle Delphi-Stringtypen nicht wie sonst üblich ab 0, sondern ab 1 indiziert werden: da die Entwickler von Turbo Pascal die Probleme der in C üblichen ASCIIZ-Strings und die umständliche String-Speicherverwaltung vermeiden wollten, wurde einfach im Index 0 des Strings die Stringlänge untergebracht, was dessen Länge effektiv auf 255 Zeichen beschränkte. In der DOS-Zeit war das ein tragbarer Kompromiß, in Win32 nicht mehr.) Da ab Delphi 2009 die Typen String und Char aber UTF-16-basiert sind, ist die Unicode-Migration deutlich weniger aufwendig. Problematisch sind hier eher subtilere Dinge, etwa die Annahme, daß Char immer ein Byte groß sei, Zeigerarithmetik mit PChar und das Unterbringen von binären Daten in Strings (was in Delphi eine Zeitlang nicht unüblich war, da erst Delphi 4 dynamische Arrays einführte).

Obiges Codebeispiel jedenfalls kommt real existierendem C++Builder-Code recht nahe und eignet sich daher, um die Migration mithilfe der Unterstützung von CbdeFormat beispielhaft zu erläutern. Aufgrund des TCHAR-Mappings wird obiger Code zwar fehlerfrei übersetzt, jedoch tut er nicht das Gewünschte, denn von beiden Strings ist nur der erste Buchstabe sichtbar:



Also installieren wir CbdeFormat und kompilieren das Programm neu. Nun ist CbdeFormat aktiv und überprüft sämtliche Format-Strings auf Fehler. Führen wir obigen Code dann erneut aus, so wirft CbdeFormat eine Exception, und wir sehen diesen Dialog:



Nach dem Hinzufügen von SystemCppException wird die Fehlermeldung schon etwas informativer:



Hieraus entnehmen wir, daß der erste Format-String-Parameter offenbar von Typ wchar_t* ist, obgleich er char* hätte sein müssen. (Auch darüber hinausgehende Abweichungen sind festzustellen, aber CbdeFormat berücksichtigt bei der Typprüfung ein gewisses Toleranzschema, beispielsweise akzeptiert es den impliziten const_cast<> oder die Umwandlung zwischen verschieden typisierten, aber auf gleiche Weise übergebenen Integerwerten wie int und unsigned long.)

Grund für die Diskrepanz ist, daß unabhängig von irgendwelchen Einstellungen in unserem C++Builder-Projekt aller Delphi-Code, auch die VCL, nun standardmäßig Unicode-Strings verwendet. Auch das von uns verwendete TEdit::Text ist mittlerweile nicht mehr AnsiString, sondern UnicodeString.
Das Problem läßt sich umgehen, indem wir entweder den Format-String anpassen und "%s" durch "%ls" ersetzen oder aber das Argument explizit nach AnsiString casten: AnsiString (EdtUserName->Text).c_str ().

Beheben wir das Problem, so werden wir sogleich auf das nächste aufmerksam gemacht:



Tatsächlich sah ich schon hin und wieder, daß Strings direkt an sprintf() übergeben wurden. Das funktioniert zwar zufällig, weil AnsiString und UnicodeString entsprechend implementiert sind (die Klassen sind nur vier Bytes groß und enthalten einen Zeiger auf den nullterminierten String), aber verwenden sollte man es trotzdem nicht - man verläßt sich auf ein Implementationsdetail, für das mit String::c_str() eine wohldokumentierte Alternative existiert.

Auch diesen Fehler beseitigen wir genau wie oben, und schon funktioniert alles:




Beispiel 2: Debuggen komplizierterer Format-Strings


Kürzlich verwendete ich in einer Anwendung, die ich in C++Builder 2009 schrieb, etwa folgendes:
    std::vector <String> values;
    values.push_back (String ().sprintf (_D ("File path: %s"),
        image->getImagePath ().c_str ()));
    values.push_back (String ().sprintf (_D ("Width: %f %s"),
        ppimage.getXLength (), ppimage.getXUnit ().c_str ()));
    values.push_back (String ().sprintf (_D ("Height: %f %s"),
        ppimage.getYLength (), ppimage.getYUnit ().c_str ()));
    values.push_back (String ().sprintf (_D ("Range: %.3f %s"),
        ppimage.getMaxVal () - ppimage.getMinVal (),
        ppimage.getZUnit ().c_str ()));
    values.push_back (String ().sprintf (_D ("X Resolution: %.3f %s/px"),
        ppimage.getXLength () / ppimage.getWidth (),
        ppimage.getXUnit ().c_str ()));
    values.push_back (String ().sprintf (_D ("Y Resolution: %.3f %s/px"),
        ppimage.getYLength () / ppimage.getHeight (),
        ppimage.getYUnit ().c_str ()));

Hier ist die Situation etwas unübersichtlich. image ist beispielsweise vom Typ ImageManager*, einer Klasse, die Bilder speichert und verwaltet. ImageManager verwendet einige Klassen aus der Delphi-RTL und daher aus Konsistenzgründen System::String als String-Typ. ppimage hingegen ist vom Typ pp::Image&. Diese Klasse speichert primär die Pixeldaten des Bildes in einem double-Array und bietet Methoden zum Laden und Speichern, sie ist in portablem C++ implementiert und benutzt dementsprechend std::string. Weiter gibt ppimage.getXLength() einen double-Wert (in ppimage.getXUnit()) zurück, ppimage.getWidth() aber ein unsigned int für die Breite in Pixeln. Demensprechend sehen auch die Ergebnisse aus:



Ist CbdeFormat aktiviert, so erhält man bei jedem fehlerhaften printf-Aufruf eine Exception mit der zugehörigen Meldung. Das ist für Einzelfälle gut geeignet, aber in diesem Falle wäre es so beispielsweise nötig, das Programm fünfmal neu zu erstellen und wieder zu starten, damit alle fehlerhaften Zeilen ausgeführt werden. Da dies etwas zu umständlich ist, behelfen wir uns damit, einen anderen Error-Handler zu registrieren. In format_error.hpp gibt es dafür folgende Schnittstellen:
  // format_error.hpp
namespace cbde
{

...

typedef void (*FormatStringErrorHandlerT) (const FormatStringErrorDescriptor& fse);

void setFormatStringErrorHandler (FormatStringErrorHandlerT newHandler);
FormatStringErrorHandlerT getFormatStringErrorHandler (void);

    // This is the default.
void formatStringErrorException (const FormatStringErrorDescriptor& fse);

void formatStringErrorMessageAndAbort (const FormatStringErrorDescriptor& fse);

    // Asks the user what to do next. Suitable for debugging purposes only.
void formatStringErrorDebug (const FormatStringErrorDescriptor& fse);


} // namespace cbde
Der Debug-Handler ist für diesen Fall geeignet. Wir installieren ihn folgendermaßen:
  // Project.cpp
...
#include <cbde/format_error.hpp>

//---------------------------------------------------------------------------
USEFORM("MainUnit.cpp", FrmMain);
...
//---------------------------------------------------------------------------
WINAPI _tWinMain(HINSTANCE, HINSTANCE, LPTSTR, int)
{
    try
    {
        cbde::setFormatStringErrorHandler (cbde::formatStringErrorDebug);
        ...
Nun erhalten wir eine Meldung, die es ermöglicht, den Fehler im Quelltext zu korrigieren, den Programmablauf aber fortzusetzen:



Wir können also, sofern sie nicht zu schwerwiegend sind, die Fehler ohne Neukompilierung beheben. Sodann sieht die Ausgabe aus wie erwartet:




Beispiel 3: Übersetzer-Fehler


Lokalisierungen mit gettext haben unter anderem den Vorteil der einfachen Erweiterbarkeit: jeder Nutzer des Programmes kann poEdit herunterladen und die Ressourcendateien für seine eigene Sprache anpassen. Dabei passieren natürlich unvermeidlich Fehler, etwa wird der folgende, obenstehendem Beispiel entnommene String
"E2094: Operator '%s' not implemented in type '%s' for arguments of type '%s'"
zu
"E2094: Operator '%s' ist im Typ '%s' für Argumente '%s' des Typs '%s' nicht implementiert".
Daher ist es in diesen Fällen sinnvoll, auch in der Release-Version der Anwendung Format-String-Überprüfungen durchzuführen. Diese können im Release-Build durch die Definition der Präprozessorkonstante CBDE_FORMAT_CHECK_DEBUG (mit Positionsinformation, also Dateiname, Zeilennummer und Funktion) oder CBDE_FORMAT_CHECK (ohne Positionsinformation) aktiviert werden.

Aktiviert man CBDE_FORMAT_CHECK, so verursacht die fehlerhafte Übersetzung eine Exception mit folgender Fehlermeldung:



Dabei ist zu beachten, daß sowohl der zusätzlich generierte Code als auch der Laufzeit-Overhead äußerst gering sind. Der Format-String-Parser vergrößert die ausführbare Datei um etwa 2 KB; sofern im Format-String keine Fehler gefunden werden, kommt er ohne Heap-Allokationen aus.

Auch der zusätzlich generierte Code ist minimal. Diesen Code generiert BCC ohne Format-String-Überprüfung:
; MainUnit.cpp.42: std::fprintf (stderr, gettext ("E2094: Operator '%s' is...
004019B4 8B4D10           mov ecx,[ebp+$10]
004019B7 51               push ecx
004019B8 8B450C           mov eax,[ebp+$0c]
004019BB 50               push eax
004019BC 8B5508           mov edx,[ebp+$08]
004019BF 52               push edx
004019C0 68FA114700       push $004711fa
004019C5 E8BAFFFFFF       call _gettext
004019CA 59               pop ecx
004019CB 50               push eax
004019CC 8B0DD0CD4700     mov ecx,[$0047cdd0]
004019D2 83C130           add ecx,$30
004019D5 51               push ecx
004019D6 E8ADF30600       call _fprintf
004019DB 83C414           add esp,$14

Mit CBDE_FORMAT_CHECK wird folgendes generiert:
; MainUnit.cpp.42: std::fprintf (stderr, gettext ("E2094: Operator '%s' is...
004019B7 8B7510           mov esi,[ebp+$10]
004019BA 8B7D0C           mov edi,[ebp+$0c]
004019BD 8B4508           mov eax,[ebp+$08]
004019C0 8945D4           mov [ebp-$2c],eax
004019C3 68FA214700       push $004721fa
004019C8 E8B7FFFFFF       call _gettext
004019CD 8BD8             mov ebx,eax
004019CF A1D0DD4700       mov eax,[$0047ddd0]
004019D4 59               pop ecx
004019D5 83C030           add eax,$30
004019D8 8945D0           mov [ebp-$30],eax
004019DB 6A03             push $03
004019DD 68D0224700       push $004722d0
004019E2 53               push ebx
004019E3 E830280000       call cbde::verifyPrintfFormatString(const char *,...
004019E8 83C40C           add esp,$0c
004019EB 56               push esi
004019EC 57               push edi
004019ED 8B55D4           mov edx,[ebp-$2c]
004019F0 52               push edx
004019F1 53               push ebx
004019F2 8B4DD0           mov ecx,[ebp-$30]
004019F5 51               push ecx
004019F6 E815F60600       call _fprintf
004019FB 83C414           add esp,$14
Das ist äquivalent zu folgendem C++-Code, mithin praktisch optimal:
    static const unsigned argTypeTable[3] = {
        cbde::TypeID <decltype (op)>::value,
        cbde::TypeID <decltype (lhstype)>::value,
        cbde::TypeID <decltype (rhstype)>::value,
    };
    const char* theFormatString = gettext ("E2094: Operator '%s' is not "
                                           "implemented in type '%s' "
                                           "for arguments of type '%s'");
    cbde::verifyPrintfFormatString (theFormatString, argTypeTable,
        sizeof (argTypeTable) / sizeof (unsigned));
    std::fprintf (stderr, theFormatString, op, lhstype, rhstype);


Für eigene Funktionen einsetzen


Der Installer konfiguriert CbdeFormat für eine fixe Menge von Format-String-Funktionen. Es ist aber leicht möglich, diese Menge für eigene Format-Funktionen beliebig anzupassen. Ich will dies hier beispielhaft für obige str_printf()-Funktion zeigen:
  • Zunächst müssen die Headerdateien neu generiert werden. Dazu öffne man im Installationsverzeichnis die Datei "defaultHeaderSettings.dat" und füge die Funktionsnamen, sofern noch nicht enthalten, der Liste hinzu:
    object TFormatHeaderSettings: TPersistenceWrapper
      Persistent.MaxFormatParams = 12
      Persistent.MaxFixedParams = 3
      Persistent.FormatNames.Strings = (
        'str_printf'
        'wstr_printf'
        'printf'
        'wprintf'
        'sprintf'
        'swprintf'
        'fprintf'
        'fwprintf'
        'scanf'
        'wscanf'
        'sscanf'
        'swscanf'
        'fscanf'
        'fwscanf'
        'snprintf'
        'snwprintf'
        '_snprintf'
        '_snwprintf'
        'cat_printf'
        'cat_sprintf')
    end
  • Sodann rufe man aus der Kommandozeile FORMATGEN defaultHeaderSettings.dat "$(BDS)\include\cbde" auf (und ersetze $(BDS) durch den Pfad der C++Builder-Installation).

  • Weiter muß die Headerdatei geringfügig angepaßt werden:
    #ifndef _STR_PRINTF_HPP
    #define _STR_PRINTF_HPP
    
    #include <string>
    
    + #include <cbde/format_definition_begin.hpp>
    
    std::string str_printf (const char* format, ...);
    
    +     // Printf|Scanf, format_string_type, return_type, func_name
    + CBDE_FORMAT_DECLARE_SAFE (Printf, const char*, std::string, str_printf)
    
    + #include <cbde/format_definition_end.hpp>
    
    #endif // _STR_PRINTF_HPP
    Auch in der Quelldatei ist eine kleine Änderung nötig:
    #include <string>
    #include <cstdarg>
    #include <cstdio>
    #pragma hdrstop
    
    #include "str_printf.hpp"
    
    + #include <cbde/format_definition_begin.hpp>
    
    std::string str_printf (const char* format, ...)
    {
        std::va_list args;
        std::string retval;
    
        va_start (args, format);
    
            // Bei der Übergabe von 0 als Puffer ermittelt die Funktion
            // nur die Länge des entstehenden Strings.
        retval.resize (std::vsnprintf (0, 0, format, args));
    
        std::vsnprintf (&retval[0], retval.size () + 1, format, args);
        va_end (args);
    
        return retval;
    }



Referenzen


[1] Wikipedia: Format string vulnerabilities, zum Stand am 08.06.2009
[2] Boost Format library
[3] Using the GNU Compiler Collection (GCC): Options to Request or Suppress Warnings (-Wformat)
[4] Joel Spolsky: Back to Basics, 11.12.2001


Kommentare


Neuer Eintrag:

Name:
E-Mail:
Website:
Datum:
Anzahl der Zeichen in Ihrem Namen:
Text: