Validierung von PVS-Studio mit Clang

    PVS-Studio mit Clang überprüfen
    Ja Ja. Du hast richtig gehört. Diesmal ist der Artikel "umgekehrt". Wir überprüfen kein Projekt, sondern unseren Analysator mit einem anderen Tool. Tatsächlich haben wir das schon einmal gemacht. Zum Beispiel haben wir PVS-Studio mit Cppcheck überprüft, den in Visual Studio integrierten Analyzer verwendet und die Intel C ++ - Warnungen überprüft. Aber vorher gab es keinen Grund, einen Artikel zu schreiben. Es wurde nichts Interessantes gefunden. Aber Clang konnte sich für seine Diagnosemeldungen interessieren.

    Wir haben Clang zweimal mit PVS-Studio getestet [ 1 , 2] und jedes Mal fanden sie etwas Interessantes. Wir konnten PVS-Studio jedoch in keiner Weise verifizieren. Clang-Entwickler berichten seit langem, dass sie gut in der Erstellung von Windows-Projekten sind, die mit Visual C ++ entwickelt wurden. Aber in der Praxis klappt es irgendwie nicht. Oder wir haben kein Glück.

    Und neulich wurde uns plötzlich klar, dass wir unseren Analysator problemlos mit Clang testen können. Es war nur notwendig, sich der Aufgabe von der anderen Seite zu nähern. Jede Nacht erhalten wir eine Kommandozeilenversion von PVS-Studio für Linux mit GCC. Und der GCC-Compiler lässt sich sehr leicht durch Clang ersetzen. Dementsprechend können wir einfach versuchen, PVS-Studio zu überprüfen. Und wirklich. Am selben Tag, als einem der Mitarbeiter eine gute Idee einfiel, hatten wir einen Bericht über die Überprüfung von PVS-Studio. Jetzt sitze ich und spreche über den Inhalt des Berichts und meine Eindrücke.

    Eindruck von HTML-Berichten


    Natürlich habe ich mich bereits mit Clang beschäftigt. Es ist jedoch schwierig, die Qualität der Analyse für Drittanbieterprojekte zu beurteilen. Ich kann oft nicht verstehen, ob ein Fehler vorliegt oder nicht. Besonders beängstigend, wenn Clang anzeigt, dass Sie einen Pfad von 37 Punkten im Quellcode anzeigen müssen.

    Der Code von PVS-Studio ist mir hingegen vertraut, und ich konnte endlich mit dem von Clang herausgegebenen Bericht voll und ganz arbeiten. Leider bestätigte dies meine früheren Eindrücke, dass der oft gezeigte Weg, den erkannten Fehler zu erreichen, überflüssig ist und den Programmierer verwirrt. Ich verstehe, dass es äußerst schwierig ist, wichtige Punkte für die Programmausführung und die Erstellung eines solchen Pfades anzugeben. Zum Beispiel haben wir bei PVS-Studio generell Angst, eine so umfangreiche Aufgabe zu übernehmen. Da Clang die Demonstration dieses Weges umsetzt, müssen wir diesen Mechanismus natürlich noch weiter verbessern.

    Ansonsten schlagen solche Punkte nur zu Boden, verschmutzen die Schlussfolgerung und erschweren das Verständnis:



    Die Abbildung zeigt den "Punkt N4". Irgendwo unten ist ein Fehler. Ich verstehe, dass ein Fehler nur auftritt, wenn die Bedingung nicht erfüllt ist. Das berichtet Clang. Aber warum diese Informationen anzeigen? Wenn die Bedingung erfüllt ist, wird die Funktion beendet und es tritt kein Fehler auf. Extra sinnlose Informationen. Es gibt viele solcher redundanten Informationen. Offensichtlich kann und sollte dieser Mechanismus verbessert werden.

    Ich möchte jedoch den Clang-Entwicklern Anerkennung zollen. Das Anzeigen eines solchen Pfads hilft häufig dabei, die Fehlerursache zu verstehen, insbesondere wenn mehrere Funktionen daran beteiligt sind. Und eindeutig stellten die Clang-Entwickler den Weg zur Behebung des Fehlers deutlich besser dar als im statischen Analysator von Visual Studio 2013. Dort ist häufig die Hälfte der aus 500 Linien bestehenden Funktion getönt. Und was diese Färbung ergibt, ist völlig unverständlich.

    Die Kritikalität der gefundenen Fehler


    Die Überprüfung von PVS-Studio ist ein sehr gutes Beispiel dafür, was für eine undankbare Aufgabe es ist, die Vorteile der statischen Analyse an einem funktionierenden, gut getesteten Projekt aufzuzeigen. Tatsächlich kann ich mich von allen Fehlern „losreißen“, indem ich sage:
    • Dieser Code wird derzeit nicht verwendet.
    • Dieser Code wird äußerst selten oder bei der Fehlerbehandlung verwendet.
    • Dieser Fehler führt jedoch nicht zu spürbaren Konsequenzen (seine Korrektur äußert sich in keiner Weise in einer Vielzahl von Regressionstests).


    Mit otmazyvatsya werde ich den Anschein behalten, dass ich keine schwerwiegenden Fehler mache, und mit einem stolzen Blick kann ich sagen, dass Clang nur für Programmieranfänger geeignet ist.

    Das sage ich natürlich nicht! Die Tatsache, dass keine kritischen Fehler gefunden wurden, bedeutet nicht, dass Clang in der Analyse schwach ist. Das Fehlen derartiger Mängel ist das Ergebnis vieler Untersuchungen mit verschiedenen Methoden:
    • interne Komponententests;
    • diagnostische Regressionstests (markierte Dateien);
    • Testen von Mengen von * .i-Dateien, die verschiedene C ++ - Konstrukte und verschiedene Erweiterungen enthalten;
    • Regressionstests an 90 Open-Source-Projekten;
    • und natürlich statische Analyse mit PVS-Studio.


    Nach einer solchen eingehenden Verteidigung ist es schwer zu erwarten, dass Clang 20 Dereferenzen von Nullzeigern und 10 Divisionen durch 0 findet. Denken Sie jedoch darüber nach. Selbst in einem sorgfältig getesteten Projekt hat Clang einige Fehler gefunden. Dies bedeutet, dass Sie bei regelmäßiger Verwendung viele Probleme vermeiden können. Es ist besser, den Fehler zu beheben, wenn Clang ihn findet, als vom * .i-Benutzer die Datei abzurufen, auf der PVS-Studio abstürzt.

    Natürlich haben wir Schlussfolgerungen für uns gezogen. Jetzt richtet mein Kollege nur den Clang-Start auf dem Server ein und sendet Protokolle an die E-Mail, wenn der Analysator etwas findet.

    Über falsche Positive


    Insgesamt erzeugte der Clang-Analysator 45 Warnungen. Ich möchte nicht über die Anzahl der Fehlalarme sprechen. Ich würde besser sagen, dass 12 Stellen korrigiert werden sollten.

    Tatsache ist, dass „falsch positiv“ eine relative Angelegenheit ist. Formal hat der Analysator recht - der Code ist schlecht und misstrauisch. Dies bedeutet jedoch nicht, dass ein Defekt vorliegt. Ich werde mit Beispielen erklären.

    Betrachten Sie zunächst das echte False Positive:
    #define CreateBitMask(bitNum) ((v_uint64)(1) << bitNum)
    unsigned GetBitCountForRepresntValueLoopMethod(
      v_int64 value, unsigned maxBitsCount)
    {
      if (value == 0)
        return 0;
      if (value < 0)
        return maxBitsCount;
      v_uint64 uvalue = value;
      unsigned n = 0;
      int bit;
      for (bit = maxBitsCount - 1; bit >= 0; --bit)
      {
        if ((uvalue & CreateBitMask(bit)) != 0)
         // Clang: Within the expansion of the macro 'CreateBitMask':
         // The result of the '<<' expression is undefined
        {
          n = bit + 1;
          break;
        }
      ....
    }

    Soweit ich weiß, meldet der Analysator, dass der Schichtbetrieb zu undefiniertem Verhalten führen kann. Anscheinend war Clang in der Logik verwirrt oder konnte den möglichen Wertebereich der Variablen maxBitsCount nicht richtig berechnen. Ich habe sorgfältig untersucht, wie die Funktion GetBitCountForRepresntValueLoopMethod () aufgerufen wird, und konnte keine Situation finden, in der der Wert in der Variablen 'maxBitsCount' zu groß ist. Ich verstehe Verschiebungen in [ 3 ]. Ich bin mir also sicher, dass es keinen Fehler gibt.

    Selbstvertrauen ist gut, aber nicht genug. Deshalb habe ich hier so assert () eingetragen:
    ....
    for (bit = maxBitsCount - 1; bit >= 0; --bit)
    {
      VivaAssert(bit >= 0 && bit < 64);
      if ((uvalue & CreateBitMask(bit)) != 0)
    ....

    Und bei allen Tests hat diese assert () nicht funktioniert. Das ist also wirklich ein Beispiel für ein falsches Positiv für den Clang-Analysator.

    Die nette Folge des Hinzufügens von assert () war, dass Clang aufhörte zu berichten. Es konzentriert sich auf assert () - Makros. Sie teilen dem Analysator die möglichen Bereiche variabler Werte mit.

    Es gibt nur wenige solcher wahrer Fehlalarme. Es gibt noch viel mehr dieser Nachrichten:
    static bool G807_IsException1(const Ptree *p)
    {
      ....
        if (kind == ntArrayExpr) {
          p = First(p);
          kind = p->What();
            // Clang: Value stored to 'kind' is never read
      ....

    Die Zuweisung "kind = p-> What ();" wird nicht verwendet. Früher war es so, und nach den Änderungen wurde diese Linie überflüssig. Der Analysator ist richtig. Die Zeile ist überflüssig und sollte zumindest gelöscht werden, um die Person, die diesen Code begleitet, nicht zu verwirren.

    Ein weiteres Beispiel:
    template<> template<>
    void object::test<11>() {
      ....
      // Нулевой nullWalker не будет использоваться в тестах.
      VivaCore::VivaWalker *nullWalker = 0;
      left.m_simpleType = ST_INT;
      left.SetCountOfUsedBits(32);
      left.m_creationHistory = TYPE_FROM_VALUE;
      right.m_simpleType = ST_INT;
      right.SetCountOfUsedBits(11);
      right.m_creationHistory = TYPE_FROM_EXPRESSION;
      result &= ApplyRuleN1(*nullWalker, left, right, false);
        // Clang: Forming reference to null pointer
      ....
    }

    In einem Komponententest wird ein Nullzeiger dereferenziert. Ja, das kannst du nicht und hässlich. Aber wirklich wollen. Tatsache ist, dass es sehr schwierig ist, eine Instanz der VivaWalker-Klasse vorzubereiten. In diesem Fall wird der Verweis auf das Objekt überhaupt nicht verwendet.

    Beide Beispiele zeigen Code, der funktioniert. Ich bezeichne dies jedoch nicht als falsch positiv. Dies sind Mängel, die behoben werden müssen. Ich würde diese Warnungen jedoch auch nicht in die Spalte "Gefundene Fehler" schreiben. Deshalb sage ich, dass ein falsches Positiv ein relativer Begriff ist.

    Fehler gefunden


    Schließlich kamen wir zu dem Abschnitt, in dem ich interessante Codeausschnitte zeigen werde, die Clang in PVS-Studio gefunden hat.

    Gefundene Fehler sind für das Programm nicht kritisch. Ich versuche nicht, Ausreden zu machen. Aber es stellte sich wirklich so heraus. Nach der Bearbeitung aller Warnungen ergaben Regressionstests keine Unterschiede in der Arbeit von PVS-Studio.

    Wir sprechen jedoch über die wahren Fehler und es ist großartig, dass Clang sie finden konnte. Ich hoffe jetzt, dass es mit seinen regelmäßigen Starts in der Lage sein wird, ernstere Fehler im frischen PVS-Studio-Code zu finden.

    Verwendung von zwei nicht initialisierten Variablen


    Der Code war groß und komplex. Es macht keinen Sinn, es in einen Artikel aufzunehmen. Ich habe einen synthetischen Code erstellt, der das Wesentliche des Fehlers widerspiegelt.
    int A, B;
    bool getA, getB;
    Get(A, getA, B, getB);
    int TmpA = A; // Clang: Assigned value is garbage or undefined
    int TmpB = B; // Clang: Assigned value is garbage or undefined
    if (getA)
      Use(TmpA);
    if (getB)
      Use(TmpB);

    Die Funktion Get () kann die Variablen A und B initialisieren. Unabhängig davon, ob sie initialisiert werden oder nicht, werden getA und getB in den Variablen notiert.

    Unabhängig davon, ob die Variablen A und B initialisiert sind, werden ihre Werte nach TmpA, TmpB kopiert. An dieser Stelle wird eine nicht initialisierte Variable verwendet.

    Warum sage ich, dass dies ein unkritischer Fehler ist? In der Praxis bereitet das Kopieren einer nicht initialisierten Variablen vom Typ 'int' keine Probleme. Formal, so wie ich es verstehe, entsteht undefiniertes Verhalten. In der Praxis wird der Müll einfach kopiert. Weitere Variablen mit Garbage werden nicht verwendet.

    Der Code wurde wie folgt umgeschrieben:
    if (getA)
    {
      int TmpA = A;
      Use(TmpA);
    }
    if (getB)
    {
      int TmpB = B;
      Use(TmpB);
    }

    Nicht initialisierte Zeiger


    Schauen wir uns den Aufruf der Funktion GetPtreePos () an. Es übergibt Verweise auf nicht initialisierte Zeiger.
    SourceLocation Parser::GetLocation(const Ptree* ptree)
    {
      const char *begin, *end;
      GetPtreePos(ptree, begin, end);
        return GetSourceLocation(*this, begin);
    }

    Das ist nicht richtig. Die GetPtreePos () - Funktion setzt voraus, dass Zeiger auf nullptr initialisiert werden. So funktioniert es:
    void GetPtreePos(const Ptree *p, const char *&begin, const char *&end)
    {
      while (p != nullptr)
      {
        if (p->IsLeaf())
        {
          const char *pos = p->GetLeafPosition();
          if (....)
          {
            if (begin == nullptr) {
                // Clang: The left operand of '==' is a garbage value
              begin = pos;
            } else {
              begin = min(begin, pos);
            }
            end = max(end, pos);
          }
          return;
        }
        GetPtreePos(p->Car(), begin, end);
        p = p->Cdr();
      }
    }

    Es erspart Ihnen nur die Schande, dass die Funktion GetLocation () aufgerufen wird, wenn im Unit-Test-Subsystem ein bestimmter Code-Parsing-Fehler auftritt. Anscheinend ist das noch nie passiert.

    Ein gutes Beispiel dafür, wie statische Analysen TDD ergänzen [4].

    Unheimlich explizite Besetzungen


    Es gibt drei ähnliche Funktionen mit unheimlichen und falschen Typkonvertierungen. Hier ist eine:
    bool Environment::LookupType(
      CPointerDuplacateGuard &envGuard, const char* name,
      size_t len, Bind*& t, const Environment **ppRetEnv,
      bool includeFunctions) const
    {
      VivaAssert(m_isValidEnvironment);
      //todo:
      Environment *eTmp = const_cast(this);
      Environment **ppRetEnvTmp = const_cast(ppRetEnv);
      bool r = eTmp->LookupType(envGuard, name, len, t,
                                ppRetEnvTmp, includeFunctions);
      ppRetEnv = const_cast(ppRetEnvTmp);
        // Clang: Value stored to 'ppRetEnv' is never read
      return r;
    }

    Sodom und Gomorra. Wir haben versucht, die Konstanz zu entfernen. Und dann den empfangenen Wert zurück. Tatsächlich ändert sich in der Zeile „ppRetEnv = const_cast ....“ einfach die lokale Variable ppRetEnv.

    Lassen Sie mich erklären, woher diese Schande kam und wie sie sich auf das Programm auswirkt.

    Der PVS-Studio Analyzer basiert auf der OpenC ++ Bibliothek. Das Schlüsselwort 'const' wurde praktisch nicht verwendet. Sie können jederzeit und überall Änderungen vornehmen, indem Sie Zeiger auf nicht konstante Objekte verwenden. PVS-Studio hat diesen Fehler geerbt.

    Sie kämpften mit ihm. Aber noch nicht bis zum Ende. Sie schreiben const an einer Stelle, Sie müssen an der zweiten Stelle schreiben, dann an der dritten und so weiter. Dann stellt sich heraus, dass in bestimmten Situationen mit dem Zeiger etwas geändert werden muss und die Funktion in Teile aufgeteilt oder noch umfangreicher überarbeitet werden muss.

    Der letzte heldenhafte Versuch, einen der idealistischen Kollaborateure zu machen, dauerte eine Woche und endete mit einem teilweisen Scheitern. Es wurde klar, dass wir große Änderungen im Code und der Bearbeitung einiger Datenspeicherstrukturen benötigen. Das Einbringen von Licht in das Reich der Dunkelheit wurde gestoppt. Es wurden verschiedene Funktionen von Stubs in Betracht gezogen, deren Zweck darin besteht, kompilierten Code zu erstellen.

    Was betrifft dieser Fehler? Seltsamerweise scheint es egal was. Alle Unit-Tests und Regressionstests ergaben nach Korrekturen keine Änderungen im Verhalten von PVS-Studio. Offensichtlich ist der Rückgabewert in "ppRetEnv" bei der Arbeit nicht sehr notwendig.

    Verwenden einer möglicherweise nicht initialisierten Variablen


    v_uint64 v; // Clang: 'v' declared without an initial value
    verify(GetEscape(p, len - 3, v, notation, &p));
    retValue <<= 8;
    retValue |= v; // Clang: Assigned value is garbage or undefined

    Die GetEscape () - Funktion funktioniert möglicherweise nicht richtig und die Variable 'v' bleibt dann nicht initialisiert. Das Ergebnis von GetEscape () überprüft aus irgendeinem Grund das verify () -Makro. Wie es passiert ist, ist bereits unbekannt.

    Der Fehler bleibt aus folgendem Grund unbemerkt. Die GetEscape () - Funktion initialisiert die Variable nur dann nicht, wenn der PVS-Studio-Analysator mit falschem Programmtext arbeitet. Im richtigen Programmtext stehen immer die richtigen ESC-Sequenzen und die Variable wird immer initialisiert.

    Er selbst war überrascht, wie es funktionierte


    Ptree *varDecl = bind->GetDecl();
    if (varDecl != nullptr)
    {
      if (varDecl->m_wiseType.IsIntegerVirtualValue())
        varRanges.push_back(....);
      else if (varDecl->m_wiseType.IsPointerVirtualValue())
        varRanges.push_back(....);
      else
        varRanges.push_back(nullptr);
    }
    rangeTypes.push_back(varDecl->m_wiseType.m_simpleType);
      // Clang: Dereference of null pointer

    Der varDecl-Zeiger kann nullptr sein. Es wird jedoch immer die letzte Zeile ausgeführt. Und eine Nullzeiger-Dereferenzierung kann auftreten: varDecl-> m_wiseType.m_simpleType.

    Warum es hier noch nie einen Sturz gegeben hat, ist mir ein Rätsel. Die einzige Annahme ist, dass wir hier niemals ankommen, wenn das Objekt keinen Zeiger auf eine Variablendeklaration (Deklarator der Variablen) speichert. Darauf können Sie sich aber trotzdem nicht verlassen.

    Es wurde ein sehr schwerwiegender Fehler gefunden, der sich eines Tages, wenn nicht jetzt, als sicher erweisen würde.

    Überraschenderweise waren auch hier keine Stürze zu sehen.


    Ein weiterer toller Ort. Offensichtlich ist eine Kombination von Faktoren, die zur Dereferenzierung eines Nullzeigers führen können, äußerst unwahrscheinlich. Zumindest ist der Sturz seit dem Schreiben dieser Funktion nicht mehr aufgefallen. Aber danach sind anderthalb Jahre vergangen. Wunder
    
    void ApplyRuleG_657(VivaWalker &walker,
      const BindFunctionName *bind,
      const IntegerVirtualValueArray *pReturnIntegerVirtualValues,
      const PointerVirtualValueArray *pReturnPointerVirtualValues,
      const Ptree *body, const Ptree *bodySrc,
      const Environment *env)
    {
      if (body == nullptr || bodySrc == nullptr)
      {
        VivaAssert(false);
        return;
      }
      if (bind == nullptr)
        return;
      if (pReturnIntegerVirtualValues == nullptr &&
          pReturnPointerVirtualValues == nullptr)
        return;
      ....
      size_t integerValueCount = pReturnIntegerVirtualValues->size();
      // Clang: Called C++ object pointer is null

    Der Zeiger pReturnIntegerVirtualValues ​​kann durchaus nullptr sein.

    Auf den ersten Blick scheint der Fehler in der Bedingung zu liegen und der Operator "||" sollte verwendet werden:
    if (pReturnIntegerVirtualValues == nullptr &&
        pReturnPointerVirtualValues == nullptr)

    Aber das ist nicht so. Der Zustand ist korrekt. Sie müssen nur überprüfen, ob der Zeiger nicht null ist, bevor Sie den Zeiger dereferenzieren. Wenn es Null ist, sollte die Variable integerValueCount auf 0 gesetzt werden. Die richtige Option:
    size_t integerValueCount =
      pReturnIntegerVirtualValues != nullptr ?
        pReturnIntegerVirtualValues->size() : 0;

    Erstaunlich So viele Tests. Läuft auf 90 Open Source-Projekten. Außerdem haben wir in diesem Jahr viele andere Projekte überprüft. Und dennoch lebt ein Fehler im Code. Und er hätte sich bei unserem wichtigen potentiellen Kunden gezeigt.

    Ruhm den statischen Analysatoren! Ehre sei Clang!

    Andere


    Der Analysator hat weitere Fehler festgestellt, die korrigiert werden sollten. Es ist schwierig, sie zu beschreiben, aber ich möchte keine synthetischen Beispiele anführen. Außerdem gibt es ein paar Warnungen, die absolut wahr, aber nutzlos sind. Dort musste ich die Analyse ausschalten.

    Bei der Verwendung der Funktion RunPVSBatchFileMode () machte sich Clang beispielsweise Sorgen um nicht initialisierte Variablen. Fakt ist jedoch, dass der Batch-Start für Linux einfach nicht implementiert ist und es einen Stub gibt. Und wir haben noch nicht vor, es umzusetzen.

    Schlussfolgerungen


    Verwenden Sie bei Ihrer Arbeit statische Analysegeräte.

    Ich finde den PVS-Studio-Core sehr getestet. Der statische Analysator von Clang hat jedoch 12 echte Fehler gefunden. Andere Stellen sind keine Fehler, aber sie waren Code, der riecht, und ich habe alle diese Stellen repariert.

    Gefundene Fehler könnten sich im ungünstigsten Moment als solche herausstellen. Außerdem würde dieser Analysator meiner Meinung nach dazu beitragen, viele Fehler zu finden, die durch Tests festgestellt wurden. Ein Durchlauf der Hauptregressionstests dauert ca. 2 Stunden. Etwas früher zu finden ist in Ordnung.

    Hier ist ein Artikel mit einer Clang-Anzeige. Aber er hat es verdient.

    Denken Sie nur nicht, dass andere Analysegeräte von geringem Nutzen sind. Zum Beispiel mag ich persönlich den Cppcheck- Analysator sehr. Es ist sehr einfach zu bedienen und verfügt über eine sehr klare Diagnose. Es ist jedoch vorgekommen, dass er in PVS-Studio keine Fehler gefunden hat, sodass ich keinen ähnlichen Artikel über ihn schreiben kann.

    Und natürlich empfehle ich bei Ihrer Arbeit den PVS-Studio Analyzer. Es ist äußerst praktisch für Benutzer von Visual C ++ [ 5 ]. Vergessen Sie insbesondere nicht die automatische Analyse, die nach erfolgreicher Kompilierung der geänderten Dateien startet.

    Dieser Artikel ist in englischer Sprache.


    Wenn Sie diesen Artikel mit einem englischsprachigen Publikum teilen möchten, verwenden Sie bitte den Link zur Übersetzung: Andrey Karpov. PVS-Studio mit Clang überprüfen .

    Zusätzliche Links:


    1. Andrey Karpov. PVS-Studio gegen Clang .
    2. Andrey Karpov. Statische Analysen sollten regelmäßig durchgeführt werden .
    3. Andrey Karpov. Ohne die Furt zu kennen, geh nicht ins Wasser. Dritter Teil (über Schichtbetrieb) .
    4. Andrey Karpov. Wie statische Analyse TDD ergänzt .
    5. Andrey Karpov. PVS-Studio für Visual C ++ .


    Haben Sie den Artikel gelesen und eine Frage?
    Oft werden unseren Artikeln die gleichen Fragen gestellt. Hier haben wir Antworten auf diese Fragen gesammelt: Antworten auf Fragen von Lesern von Artikeln über PVS-Studio und CppCat, Version 2014 . Bitte beachten Sie die Liste.

    Jetzt auch beliebt: