Und noch einmal zu Embedded: Suche nach Fehlern im Embox-Projekt

Figur 2

Embox ist ein plattformübergreifendes Multitasking-Echtzeitbetriebssystem für eingebettete Systeme. Es wurde für den Betrieb mit geringen Rechenressourcen entwickelt und ermöglicht es Ihnen, Linux-basierte Anwendungen auf Mikrocontrollern auszuführen, ohne Linux selbst zu verwenden. Natürlich werden auch Embox-Fehler wie bei jeder anderen Anwendung nicht verschont. Dieser Artikel befasst sich mit der Analyse von Fehlern im Code des Embox-Projekts.

Vor einigen Monaten habe ich bereits einen Artikel über die Überprüfung von FreeRTOS geschrieben, einem anderen Betriebssystem für eingebettete Systeme. Ich habe damals keine Fehler darin gefunden, aber ich habe sie in Bibliotheken gefunden, die von den Jungs von Amazon hinzugefügt wurden, als sie ihre eigene Version von FreeRTOS entwickelten.

Der Artikel, den Sie jetzt vor sich sehen, setzt gewissermaßen das Thema des vorherigen fort. Wir haben oft Anfragen erhalten, FreeRTOS zu überprüfen, und wir haben es getan. Dieses Mal gab es keine Anfragen, ein bestimmtes Projekt zu überprüfen, aber ich erhielt Briefe und Kommentare von eingebetteten Entwicklern, denen es gefiel und die mehr wollten.

Nun, die neue Ausgabe des PVS-Studio for Embedded Magazins ist gereift und liegt vor Ihnen. Genieße das Zusehen!

Analyse


Die Analyse wurde mit PVS-Studio durchgeführt - einem statischen Code-Analysator für C, C ++, C # und Java. Vor der Analyse muss das Projekt zusammengestellt werden. Wir stellen also sicher, dass der Projektcode funktioniert, und geben dem Analysator die Möglichkeit, die Baugruppeninformationen zu sammeln, die für eine bessere Codeüberprüfung hilfreich sein können.

Die Anweisungen im offiziellen Embox- Repository bieten die Möglichkeit, unter verschiedenen Systemen (Arch Linux, macOS, Debian) und mit Docker zu erstellen. Ich beschloss, mein Leben etwas abwechslungsreicher zu gestalten und unter Debian zu erstellen und zu analysieren, das ich kürzlich auf meiner virtuellen Maschine installiert hatte.

Die Montage verlief reibungslos. Nun war es notwendig, eine Analyse durchzuführen. Debian ist eines der unterstützten PVS-Studio Linux-basierten Systeme. Eine bequeme Möglichkeit, Projekte unter Linux zu überprüfen, ist das Kompilieren von Trace. Dies ist ein spezieller Modus, in dem der Analysator alle erforderlichen Informationen über die Baugruppe sammelt, sodass Sie die Analyse mit einem Klick starten können. Ich musste lediglich:

1) PVS-Studio herunterladen und installieren;

2) Starten Sie die Baugruppenverfolgung, indem Sie in den Ordner mit Embox gehen und das Terminal eingeben

pvs-studio-analyzer analyze -- make

3) Nachdem Sie auf den Abschluss der Assembly gewartet haben, führen Sie den folgenden Befehl aus:

pvs-studio-analyzer analyze -o /path/to/output.log

4) Konvertieren Sie den Rohbericht in ein für Sie geeignetes Format. Der Analysator wird mit einem speziellen Dienstprogramm PlogConverter geliefert, mit dem Sie dies tun können. Der Befehl zum Konvertieren des Berichts in eine Aufgabenliste (zum Anzeigen beispielsweise in QtCreator) sieht beispielsweise folgendermaßen aus:

plog-converter -t tasklist -o /path/to/output.tasks /path/to/project

Alle! Ich habe nicht länger als 15 Minuten gebraucht, um diese Punkte zu erreichen. Der Bericht ist fertig, jetzt können Sie die Fehler anzeigen. Nun, lass uns anfangen :)

Seltsamer Zyklus


Einer der vom Analysator gefundenen Fehler war die seltsame while-Schleife:

int main(int argc, char **argv) {
  ....

  while (dp.skip != 0 ) {
    n_read = read(ifd, tbuf, dp.bs);
    if (n_read < 0) {
      err = -errno;
      goto out_cmd;
    }
    if (n_read == 0) {
      goto out_cmd;
    }

    dp.skip --;
  } while (dp.skip != 0);       // <=

  do {
    n_read = read(ifd, tbuf, dp.bs);
    if (n_read < 0) {
      err = -errno;
      break;
    }

    if (n_read == 0) {
      break;
    }

    ....

    dp.count --;
  } while (dp.count != 0);
  ....
}

PVS-Studio- Warnung : V715 Der Operator 'while' hat einen leeren Körper. Verdächtiges Muster erkannt: 'while (Ausdruck) {...} while (dp.skip! = 0);'. dd.c 225 hm

. Wirklich seltsame Schleife. Der while- Ausdruck (dp.skip! = 0) wird zweimal geschrieben, einmal direkt über der Schleife und der zweite direkt darunter. Tatsächlich sind dies nun zwei verschiedene Zyklen: Einer enthält Ausdrücke in geschweiften Klammern und der zweite ist leer. In diesem Fall wird der zweite Zyklus niemals ausgeführt.

Unten ist eine do ... while-Schleife mit einer ähnlichen Bedingung, was mich zu der Annahme veranlasst: Die seltsame Schleife war ursprünglich als do ... while gedacht, aber etwas ist schiefgegangen. Ich denke, dass dieser Code mit hoher Wahrscheinlichkeit einen logischen Fehler enthält.

Speicherlecks


Ja, nicht ohne sie.

int krename(const char *oldpath, const char *newpath) {
  
  char *newpatharg, *oldpatharg;

  ....

  oldpatharg =
    calloc(strlen(oldpath) + diritemlen + 2, sizeof(char));
  newpatharg =
    calloc(strlen(newpath) + diritemlen + 2, sizeof(char));
  if (NULL == oldpatharg || NULL == newpatharg) {
    SET_ERRNO(ENOMEM);
    return -1;
  }

  ....
}

PVS-Studio-Warnungen:

  • V773 The function was exited without releasing the 'newpatharg' pointer. A memory leak is possible. kfsop.c 611
  • V773 The function was exited without releasing the 'oldpatharg' pointer. A memory leak is possible. kfsop.c 611

Die Funktion erstellt die lokalen Variablen newpatharg und oldpatharg in sich . Diesen Zeigern werden die Adressen neuer Speicherorte zugewiesen, die intern mit calloc zugewiesen wurden . Wenn beim Zuweisen von Speicher ein Problem auftritt, gibt calloc einen Nullzeiger zurück.

Was ist, wenn nur ein Speicherblock zugeordnet ist? Die Funktion stürzt ab, ohne dass Speicher freigegeben wird. Die Site, die zufällig zugewiesen wurde, bleibt im Speicher, ohne dass die Möglichkeit besteht, erneut darauf zuzugreifen und sie für die weitere Verwendung freizugeben.

Ein weiteres Beispiel für einen Speicherverlust ist etwas heller:

static int block_dev_test(....) {
  int8_t *read_buf, *write_buf;
  
  ....

  read_buf = malloc(blk_sz * m_blocks);
  write_buf = malloc(blk_sz * m_blocks);

  if (read_buf == NULL || write_buf == NULL) {
    printf("Failed to allocate memory for buffer!\n");

    if (read_buf != NULL) {
      free(read_buf);
    }

    if (write_buf != NULL) {
      free(write_buf);
    }

    return -ENOMEM;
  }

  if (s_block >= blocks) {
    printf("Starting block should be less than number of blocks\n");
    return -EINVAL;            // <=
  }

  ....
}

PVS-Studio-Warnungen:

  • V773 The function was exited without releasing the 'read_buf' pointer. A memory leak is possible. block_dev_test.c 195
  • V773 The function was exited without releasing the 'write_buf' pointer. A memory leak is possible. block_dev_test.c 195

Hier hat der Programmierer bereits Genauigkeit gezeigt und den Fall korrekt verarbeitet, in dem es möglich war, nur ein Stück Speicher zuzuweisen. Richtig verarbeitet ... und im nächsten Ausdruck buchstäblich einen weiteren Fehler gemacht.

Dank einer korrekt geschriebenen Prüfung können wir sicher sein, dass zum Zeitpunkt der Ausführung des Ausdrucks -EINVAL zurückgegeben wird. Wir werden definitiv Speicher sowohl für read_buf als auch für write_buf haben . Mit einer solchen Rückkehr von der Funktion haben wir also zwei Lecks gleichzeitig.

Ich denke, dass ein Speicherverlust auf einem eingebetteten Gerät schmerzhafter sein kann als auf einem klassischen PC. Unter Bedingungen, in denen die Ressourcen stark eingeschränkt sind, müssen Sie sie besonders sorgfältig überwachen.

Zeiger Misshandlung


Der folgende Fehlercode ist kurz und einfach genug:

static int scsi_write(struct block_dev *bdev, char *buffer,
                      size_t count, blkno_t blkno) {
  struct scsi_dev *sdev;
  int blksize;

  ....

  sdev = bdev->privdata;
  blksize = sdev->blk_size; // <=

  if (!sdev) {              // <=
    return -ENODEV;
  }

  ....
}

PVS-Studio Warnung: V595 Der Zeiger 'sdev' wurde verwendet, bevor er gegen nullptr verifiziert wurde. Überprüfen Sie die Zeilen: 116, 118. scsi_disk.c 116

Der sdev- Zeiger wird dereferenziert, kurz bevor er auf NULL überprüft wird. Es ist logisch anzunehmen, dass dieser Zeiger null sein kann, wenn jemand eine solche Prüfung geschrieben hat. In diesem Fall haben wir die mögliche Dereferenzierung des Nullzeigers in der Zeichenfolge blksize = sdev-> blk_size .

Der Fehler ist, dass sich die Prüfung nicht dort befindet, wo sie benötigt wird. Es sollte nach der Zeile " sdev = bdev-> privdata; " stehen, aber vor der Zeile " blksize = sdev-> blk_size; ". Dann könnte die mögliche Anziehungskraft auf die Nulladresse vermieden werden.

PVS-Studio hat im folgenden Code zwei weitere Fehler gefunden:

void xdrrec_create(....)
{
  char *buff;

  ....

  buff = (char *)malloc(sendsz + recvsz);
  assert(buff != NULL);

  ....

  xs->extra.rec.in_base = xs->extra.rec.in_curr = buff;
  xs->extra.rec.in_boundry 
    = xs->extra.rec.in_base + recvsz;                    // <=

  ....
  xs->extra.rec.out_base
    = xs->extra.rec.out_hdr = buff + recvsz;             // <= 
  xs->extra.rec.out_curr 
    = xs->extra.rec.out_hdr + sizeof(union xdrrec_hdr);

  ....
}

PVS-Studio-Warnungen:

  • V769 Der Zeiger 'xs-> extra.rec.in_base' im Ausdruck 'xs-> extra.rec.in_base + recvsz' könnte nullptr sein. In diesem Fall ist der resultierende Wert sinnlos und sollte nicht verwendet werden. Überprüfen Sie die Zeilen: 56, 48. xdr_rec.c 56
  • V769 Der Zeiger 'buff' im Ausdruck 'buff + recvsz' könnte nullptr sein. In diesem Fall ist der resultierende Wert sinnlos und sollte nicht verwendet werden. Überprüfen Sie die Zeilen: 61, 48. xdr_rec.c 61

Der Buf-Zeiger wird mit malloc initialisiert , und sein Wert wird dann verwendet, um andere Zeiger zu initialisieren. Die Malloc- Funktion kann einen Nullzeiger zurückgeben, und dies sollte immer überprüft werden. Es scheint , dass hier ein assert Überprüfung buf für NULL , und alles sollte gut funktionieren.

Aber nein! Tatsache ist, dass Assert 's zum Debuggen verwendet werden. Wenn Sie das Projekt in der Release-Konfiguration erstellen , wird diese Assert gelöscht. Es stellt sich heraus, dass das Programm bei der Arbeit in Debug korrekt funktioniert und beim Erstellen in Release der Nullzeiger weiter "geht".

Verwenden Sie nullin arithmetischen Operationen ist falsch, weil das Ergebnis einer solchen Operation keinen Sinn ergibt und ein solches Ergebnis nicht verwendet werden kann. Darum warnt uns der Analysator.

Einige könnten argumentieren, dass es furchtlos ist , den Zeiger nach malloc / realloc / calloc nicht zu überprüfen . Wie beim ersten Zugriff durch einen Nullzeiger tritt ein Signal / eine Ausnahme auf und es ist nichts falsch. In der Praxis ist alles viel komplizierter, und wenn Ihnen die fehlende Überprüfung nicht gefährlich erscheint, empfehlen wir Ihnen, den Artikel „ Warum ist es wichtig zu überprüfen, was die Malloc-Funktion zurückgegeben hat “ zu lesen .

Falsche Behandlung von Arrays


Der folgende Fehler ist dem vorletzten Beispiel sehr ähnlich:


int fat_read_filename(struct fat_file_info *fi,
                      void *p_scratch,
                      char *name) {
  int offt = 1;

  ....

  offt = strlen(name);
  while (name[offt - 1] == ' ' && offt > 0) { // <=
    name[--offt] = '\0';
  }
  log_debug("name(%s)", name);

  return DFS_OK;
}

PVS-Studio Warnung: V781 Der Wert des 'offt'-Index wird nach seiner Verwendung überprüft. Möglicherweise liegt ein Fehler in der Programmlogik vor. fat_common.c 1813 Die

Variable offt wird zuerst innerhalb der Indizierungsoperation verwendet, und erst dann wird überprüft, ob ihr Wert größer als Null ist. Aber was passiert, wenn sich herausstellt, dass der Name eine leere Zeichenfolge ist? Die Funktion strlen () gibt 0 zurück , dann einen lauten Schuss ins Bein. Das Programm greift mit einem negativen Index zu, was zu undefiniertem Verhalten führt. Alles kann passieren, einschließlich eines Programmabsturzes. Chaos!

Bild 1

Verdächtige Bedingungen


Und wo ohne sie? Wir finden solche Fehler buchstäblich in jedem Projekt, das wir überprüfen.

int index_descriptor_cloexec_set(int fd, int cloexec) {
  struct idesc_table *it;

  it = task_resource_idesc_table(task_self());
  assert(it);

  if (cloexec | FD_CLOEXEC) {
    idesc_cloexec_set(it->idesc_table[fd]);
  } else {
    idesc_cloexec_clear(it->idesc_table[fd]);
  }
  return 0;
}

PVS-Studio Warnung: V617 Überprüfen Sie den Zustand. Das Argument '0x0010' des '|' Die bitweise Operation enthält einen Wert ungleich Null. index_descriptor.c 55

Um zu verstehen, worin der Fehler liegt, sehen Sie sich die Definition der FD_CLOEXEC- Konstante an :

#define FD_CLOEXEC 0x0010

Es stellt sich heraus, dass im Ausdruck if (cloexec | FD_CLOEXEC) rechts vom bitweisen „oder“ immer eine Konstante ungleich Null vorhanden ist. Das Ergebnis einer solchen Operation ist immer eine Zahl ungleich Null. Somit ist dieser Ausdruck immer äquivalent zum if (true) -Ausdruck , und wir werden immer nur den then-Zweig des if-Ausdrucks verarbeiten.

Ich vermute, dass diese Makrokonstante verwendet wird, um das Embox-Betriebssystem vorzukonfigurieren, aber selbst dann sieht dieser immer wahre Zustand seltsam aus. Vielleicht wollten sie hier den Operator & verwenden , aber sie haben einen Tippfehler gemacht.

Ganzzahlige Division


Der folgende Fehler hängt mit einer Funktion der C-Sprache zusammen:

#define SBSIZE    1024

static int ext2fs_format(struct block_dev *bdev, void *priv) {
  size_t dev_bsize;
  float dev_factor;

  ....

  dev_size = block_dev_size(bdev);
  dev_bsize = block_dev_block_size(bdev);
  dev_factor = SBSIZE / dev_bsize;            // <=

  ext2_dflt_sb(&sb, dev_size, dev_factor);
  ext2_dflt_gd(&sb, &gd);

  ....
}

PVS-Studio- Warnung : V636 Der Ausdruck '1024 / dev_bsize' wurde implizit vom Typ 'int' in den Typ 'float' umgewandelt. Erwägen Sie die Verwendung eines expliziten Typgusses, um den Verlust eines Bruchteils zu vermeiden. Ein Beispiel: double A = (double) (X) / Y; ext2.c 777

Diese Funktion lautet wie folgt: Wenn wir zwei ganzzahlige Werte teilen, ist das Ergebnis der Division eine ganze Zahl. Somit erfolgt die Teilung ohne Rest, oder mit anderen Worten, der Bruchteil wird aus dem Teilungsergebnis verworfen.

Manchmal vergessen Programmierer es und es werden solche Fehler gemacht. Die SBSIZE-Konstante und die Variable dev_bsize haben einen ganzzahligen Typ (int bzw. size_t). Daher das Ergebnis des Ausdrucks SBSIZE / dev_bsizewird auch vom Typ Integer sein.

Aber warte einen Moment. Die Variable dev_factor ist vom Typ float ! Offensichtlich erwartete der Programmierer ein Bruchteilungsergebnis. Dies kann weiter überprüft werden, wenn Sie auf die weitere Verwendung dieser Variablen achten. Beispielsweise hat die Funktion ext2_dflt_sb , bei der dev_factor als dritter Parameter übergeben wird, die folgende Signatur:

static void ext2_dflt_sb(struct ext2sb *sb, size_t dev_size, float dev_factor);

Ebenso an anderen Stellen, an denen die Variable dev_factor verwendet wird : Alles zeigt an, dass eine Gleitkommazahl erwartet wird.

Um diesen Fehler zu korrigieren, reicht es aus, nur einen der Divisionsoperanden in einen realen Typ umzuwandeln. Zum Beispiel:

dev_factor = float(SBSIZE) / dev_bsize;

Dann ist das Ergebnis der Division eine Bruchzahl.

Nicht überprüfte Eingabe


Der folgende Fehler hängt mit der Verwendung nicht verifizierter Daten zusammen, die von außerhalb des Programms empfangen wurden.

int main(int argc, char **argv) {
  int ret;
  char text[SMTP_TEXT_LEN + 1];

  ....

  if (NULL == fgets(&text[0], sizeof text - 2, /* for \r\n */
      stdin)) { ret = -EIO; goto error; }
    text[strlen(&text[0]) - 1] = '\0'; /* remove \n */    // <=

  ....
}

PVS-Studio Warnung: V1010 Nicht aktivierte fehlerhafte Daten werden im Index verwendet: 'strlen (& text [0])'. sendmail.c 102 Überlegen Sie zunächst

, was die Funktion fgets zurückgibt . Bei erfolgreichem Lesen einer Zeile gibt die Funktion einen Zeiger auf diese Zeile zurück. Wenn die Lese ein antrifft end of file bevor mindestens ein Element gelesen wird , oder wenn ein Eingangsfehler auftritt, die fgets Funktion kehrt NULL .

Der Ausdruck lautet also NULL == fgets (....)prüft, ob die empfangene Eingabe korrekt ist. Aber es gibt eine Einschränkung. Wenn Sie ein Null-Terminal als erstes zu lesendes Zeichen übertragen (dies kann beispielsweise durch Drücken von Strg + 2 im Legacy-Modus der Windows-Befehlszeile erfolgen), berücksichtigt die Funktion fgets dies, ohne NULL zurückzugeben . In diesem Fall hat die Zeile, in der die Aufnahme gemacht wird, nur ein Element - ' \ 0 '.

Was wird als nächstes passieren? Der Ausdruck strlen (& text [0]) gibt 0 zurück . Als Ergebnis erhalten wir einen negativen Indexaufruf:

text[ 0 - 1 ] = '\0';

Infolgedessen können wir das Programm „umkippen“, indem wir einfach das Zeilenabschlusszeichen übergeben. Dies ist umständlich und kann möglicherweise verwendet werden, um Systeme mit Embox anzugreifen.

Mein Kollege, der diese Diagnoseregel entwickelte, machte sich sogar ein Beispiel für einen solchen Angriff auf das NcFTP-Projekt:


Ich empfehle zu sehen, ob Sie immer noch nicht an eine solche Gelegenheit glauben :)

Außerdem hat der Analysator zwei weitere Stellen mit demselben Fehler gefunden:

  • V1010 Nicht aktivierte fehlerhafte Daten werden im Index 'strlen (& from [0])' verwendet. sendmail.c 55
  • V1010 Nicht aktivierte fehlerhafte Daten werden im Index 'strlen (& to [0])' verwendet. sendmail.c 65

Misra


MISRA ist eine Reihe von Richtlinien und Richtlinien zum Schreiben von sicherem C- und C ++ - Code für verantwortungsbewusste eingebettete Systeme. In gewisser Weise handelt es sich hierbei um ein Schulungshandbuch, mit dem Sie nicht nur die sogenannten "Code-Gerüche" beseitigen, sondern auch Ihr Programm vor Schwachstellen schützen können.

MISRA wird dort eingesetzt, wo Menschenleben von der Qualität Ihres eingebetteten Systems abhängen: in der Medizin-, Automobil-, Flugzeug- und Militärindustrie.

PVS-Studio verfügt über umfangreiche Diagnoseregeln, mit denen Sie Ihren Code auf Übereinstimmung mit den Standards MISRA C und MISRA C ++ überprüfen können. Standardmäßig ist der Modus mit dieser Diagnose deaktiviert, aber da wir im Projekt nach Fehlern für eingebettete Systeme suchen, könnte ich einfach nicht auf MISRA verzichten.

Folgendes habe ich gefunden:

/* find and read symlink file */
static int ext2_read_symlink(struct nas *nas,
                             uint32_t parent_inumber,
                             const char **cp) {
  char namebuf[MAXPATHLEN + 1];

  ....

  *cp = namebuf;              // <=
  if (*namebuf != '/') {
    inumber = parent_inumber;
  } else {
    inumber = (uint32_t) EXT2_ROOTINO;
  }
  rc = ext2_read_inode(nas, inumber);

  return rc;
} 

PVS-Studio Warnung: V2548 [MISRA C 18.6] Die Adresse des lokalen Arrays 'namebuf' sollte nicht außerhalb des Bereichs dieses Arrays gespeichert werden. ext2.c 298

Der Analysator hat eine verdächtige Zuordnung festgestellt, die möglicherweise zu undefiniertem Verhalten führen kann.

Schauen wir uns den Code genauer an. Hier ist namebuf ein Array, das im lokalen Bereich der Funktion erstellt wird, und der cp- Zeiger wird per Zeiger an die Funktion übergeben.

Gemäß der C-Syntax ist der Name des Arrays ein Zeiger auf das erste Element in dem Speicherbereich, in dem das Array gespeichert ist. Es stellt sich heraus, dass der Ausdruck * cp = namebuf der Variablen, auf die cp zeigt , die Adresse des Arrays namebuf zuweist. Da cp per Zeiger an die Funktion übergeben wird, wird eine Änderung des Werts, auf den sie zeigt, an der Stelle wiedergegeben, an der die Funktion aufgerufen wurde.

Es stellt sich heraus, dass der dritte Parameter nach Abschluss der Arbeit der Funktion ext2_read_symlink den Bereich angibt, den das namebuf- Array einmal belegt hat .

Es gibt nur ein "aber": Da namebuf ein auf dem Stapel reserviertes Array ist, wird es beim Beenden der Funktion gelöscht. Ein Zeiger, der außerhalb der Funktion vorhanden ist, zeigt somit auf den freigegebenen Speicher.

Was wird an dieser Adresse sein? Es ist unmöglich vorherzusagen. Es ist möglich, dass sich der Inhalt des Arrays noch einige Zeit im Speicher befindet, oder dass das Programm diesen Bereich sofort mit etwas anderem löscht. Im Allgemeinen gibt der Zugriff auf eine solche Adresse einen undefinierten Wert zurück, und die Verwendung eines solchen Werts ist ein grober Fehler.

Der Analysator hat mit derselben Warnung auch einen anderen Fehler gefunden:

  • V2548 [MISRA C 18.6] Die Adresse der lokalen Variablen 'dst_haddr' sollte nicht außerhalb des Bereichs dieser Variablen gespeichert werden. net_tx.c 82

Abbildung 6

Fazit


Ich habe gerne mit dem Embox-Projekt gearbeitet. Trotz der Tatsache, dass ich nicht alle im Artikel gefundenen Fehler ausgeschrieben habe, war die Gesamtzahl der Warnungen relativ gering, und im Allgemeinen wurde der Projektcode mit hoher Qualität geschrieben. Daher danke ich sowohl den Entwicklern als auch denen, die im Namen der Community zum Projekt beigetragen haben. Du bist großartig!

Ich nutze diese Gelegenheit, um den Entwicklern von Tula Grüße zu übermitteln. Ich werde glauben, dass es in St. Petersburg momentan nicht sehr kalt ist :)

Hier endet mein Artikel. Ich hoffe, es hat Ihnen Spaß gemacht, es zu lesen, und Sie haben etwas Neues für sich gefunden.

Wenn Sie an PVS-Studio interessiert sind und ein Projekt unabhängig davon überprüfen möchten, laden Sie es herunter und probieren Sie es aus . Dies dauert nicht länger als 15 Minuten.



Wenn Sie diesen Artikel einem englischsprachigen Publikum zugänglich machen möchten, verwenden Sie bitte den Link zur Übersetzung: George Gribkov. Wieder eingebettet: Suche nach Fehlern im Embox-Projekt .

All Articles