Undefiniertes Verhalten beim Lesen eines Objekts mit einem Nicht-Zeichentyp, wenn es zuletzt mit einem Zeichentyp geschrieben wurde

Lesezeit: 6 Minuten

Benutzer-Avatar
zwöl

Vorausgesetzt unsigned int keine Trap-Darstellungen hat, eine oder beide der unten mit (A) und (B) gekennzeichneten Aussagen undefiniertes Verhalten hervorrufen, warum oder warum nicht, und (insbesondere wenn Sie denken, dass eine davon wohldefiniert ist, die andere jedoch nicht) , halten Sie das für einen Mangel im Standard? Ich interessiere mich hauptsächlich für die aktuelle Version des C-Standards (dh C2011), aber ob dies in älteren Versionen des Standards oder in C++ anders ist, würde mich auch darüber interessieren.

(_Alignas wird in diesem Programm verwendet, um jede Frage von UB aufgrund einer unzureichenden Ausrichtung zu beseitigen. Die Regeln, die ich in meiner Interpretation bespreche, sagen jedoch nichts über die Ausrichtung aus.)

#include <stdlib.h>
#include <string.h>

int main(void)
{
    unsigned int v1, v2;
    unsigned char _Alignas(unsigned int) b1[sizeof(unsigned int)];
    unsigned char *b2 = malloc(sizeof(unsigned int));

    if (!b2) return 1;

    memset(b1, 0x55, sizeof(unsigned int));
    memset(b2, 0x55, sizeof(unsigned int));

    v1 = *(unsigned int *)b1; /* (A) */
    v2 = *(unsigned int *)b2; /* (B) */

    return !(v1 == v2);
}

Meine Interpretation von C2011 ist, dass (A) undefiniertes Verhalten provoziert, aber (B) wohldefiniert ist (um einen nicht spezifizierten Wert darin zu speichern v2), Weil:

  • memset ist definiert (§7.24.6.1), um in sein erstes Argument als-ob durch einen lvalue mit Zeichentyp zu schreiben, was für beide erlaubt ist b1 und b2 gemäß dem Sonderfall am Ende von §6.5p7.

  • Das Objekt b1 hat einen deklarierten Typ, unsigned char[n]. Daher ist auch sein effektiver Typ für Zugriffe unsigned char[n] pro 6,5p6. Aussage (A) lautet b1 über einen lvalue-Ausdruck, dessen Typ ist unsigned intwas nicht die effektive Art von ist b1 noch eine der anderen Ausnahmen in 6.5p7, daher ist das Verhalten undefiniert.

  • Das Objekt, auf das von gezeigt wird b2 hat keinen deklarierten Typ. Der darin gespeicherte Wert (by memset) wurde (als ob) durch einen lvalue mit Zeichentyp, sodass der zweite Fall von 6.5p6 nicht zutrifft. Der Wert war es nicht kopiert von überall, also trifft der dritte Fall von 6.5p6 auch nicht zu. Daher ist der effektive Typ des Objekts der Typ des lvalue, der für den Zugriff verwendet wird, also unsigned intund die Regeln von 6.5p7 sind erfüllt.

  • Schließlich gemäß 6.2.6.1, vorausgesetzt unsigned int hat keine Fallendarstellungen, die memset Betrieb hat die Darstellung einiger nicht näher erstellt unsigned int Wert in jedem b1 und b2. Wenn also weder (A) noch (B) undefiniertes Verhalten hervorrufen, dann sind die tatsächlichen Werte in v1 und v2 sind nicht spezifiziert, aber sie sind gleich.

Kommentar:

Die Asymmetrie der „typbasierten Aliasing“-Regeln (d. h. 6.5p7), die es einem lvalue mit Zeichentyp erlaubt, auf ein Objekt mit einem beliebigen effektiven Typ zuzugreifen, aber nicht umgekehrt, ist eine ständige Quelle der Verwirrung. Der zweite Fall von 6.5p6 scheint speziell hinzugefügt worden zu sein, um zu verhindern, dass es ein undefiniertes Verhalten beim Lesen eines initialisierten Werts ist memset (oder übrigens calloc), aber da es nur für Objekte ohne deklarierten Typ gilt, ist es selbst eine zusätzliche Quelle der Verwirrung.

  • A ist eindeutig eine Aliasing-Verletzung; aber der Standard scheint nicht klar zu sagen, welche Wirkung memset hat einen effektiven Typ

    – MM

    21. Juni 2015 um 22:42 Uhr

  • Ich habe eine weitere Frage gestellt, die speziell darauf abzielt, den effektiven Typ von Daten zu beschreiben, von denen geschrieben wird memset

    – MM

    21. Juni 2015 um 23:14 Uhr

Die Autoren des Standards erkennen in der Begründung an, dass eine Implementierung zwar konform, aber nutzlos sein könnte. Da sie erwarteten, dass Implementierer sich bemühen würden, ihre Implementierungen nützlich zu machen, hielten sie es nicht für notwendig, jedes Verhalten vorzuschreiben, das erforderlich sein könnte, um eine Implementierung für einen bestimmten Zweck geeignet zu machen.

Der Standard stellt keine Anforderungen an das Verhalten von Code, der auf ein ausgerichtetes Objekt vom Typ Zeichenfeld wie auf einen anderen Typ zugreift. Das bedeutet nicht, dass sie beabsichtigten, dass Implementierungen etwas anderes tun sollten, als das Array als nicht typisierten Speicher zu behandeln in Fällen, in denen Code die Adresse des Arrays einmal übernimmt, aber nie direkt darauf zugreift. Die grundlegende Natur des Aliasing besteht darin, dass auf ein Element auf zwei verschiedene Arten zugegriffen werden muss. Wenn auf ein Objekt immer nur in eine Richtung zugegriffen wird, gibt es per Definition kein Aliasing. Jede Qualitätsimplementierung, die für Low-Level-Programmierung geeignet sein soll, sollte sich in Fällen, in denen a char[] dient nur als untypisierter Speicher, ob der Standard es verlangt oder nicht, und es ist schwer vorstellbar, dass ein nützlicher Zweck durch eine solche Behandlung behindert würde. Der einzige Zweck, dem das Standard-Mandat für ein solches Verhalten dienen würde, wäre, Compiler-Autoren daran zu hindern, das Fehlen eines Mandats an und für sich als Grund zu behandeln, solchen Code nicht auf die offensichtlich nützliche Weise zu verarbeiten.

  • Ja, das ist gesunder Menschenverstand. Aber der gesunde Menschenverstand versagt oft, wenn es um UB- und Real-World-Compiler geht. Deshalb die Programmierer tun wollen, dass der Standard solche vernünftigen Dinge vorschreibt.

    – Ruslan

    13. Juli 2017 um 8:18 Uhr


  • @Ruslan: Der Standard sollte vermeiden, so zu tun, als würde er alle Dinge des gesunden Menschenverstands vorgeben, es sei denn, er wird vollständig überarbeitet, um jegliches Vertrauen auf den gesunden Menschenverstand zu beseitigen. Stattdessen sollte es offen verkünden sein Vertrauen auf den gesunden Menschenverstand mit etwas Analogem zur neunten Änderung der Verfassung und auch zu der damit verbundenen Beobachtung, dass es ein moralisches und tugendhaftes Volk voraussetzt und für jede andere Art völlig ungeeignet ist.

    – Superkatze

    13. Juli 2017 um 16:42 Uhr


Benutzer-Avatar
Phil Müller

Bei einer oberflächlichen Prüfung würde ich Ihrer Einschätzung zustimmen (A ist UB, B ist in Ordnung) und kann eine konkrete Begründung dafür liefern, warum dies so sein sollte (vor der einzuschließenden Bearbeitung _Alignas()): Ausrichtung.

Das char[] auf dem Stack kann an jeder Adresse beginnen, unabhängig davon, ob dies eine gültige Ausrichtung für eine ist unsigned int oder nicht. Im Gegensatz, malloc() ist erforderlich, um Speicher zurückzugeben, der die strengsten Ausrichtungsanforderungen aller nativen Typen auf der betreffenden Plattform erfüllt.

Der Standard will offensichtlich keine Alignment-Anforderungen auferlegen char[] über die von chardaher muss der typbasierte Zugriff darauf als potenziell undefiniert belassen werden.

  • In C11, das explizite Ausrichtungskontrollen hinzufügt, ist der Wortlaut in 6.5 unverändert – Zugriff auf unsigned char _Alignas(unsigned int) b1[sizeof(unsigned int)] über einen lvalue mit type unsigned int ist noch undefiniert, wie ich den Standard lese. Dies könnte ein Versehen sein, oder es könnte bedeuten, dass die Angleichung nicht der einzige Grund dafür ist, dass der Standard so formuliert wird, wie er ist.

    – zol

    21. Juni 2015 um 18:58 Uhr

  • Das ist ein guter Punkt. Es kann noch andere Gründe geben, oder dies kann als Versehen belassen werden. Ich werde sicherlich nicht behaupten, dass die Ausrichtung der einzige Grund ist, warum dieser Unterschied bestehen sollte.

    – Phil Müller

    21. Juni 2015 um 22:39 Uhr

  • Ausrichtung hat nichts mit Aliasing zu tun

    – MM

    21. Juni 2015 um 22:43 Uhr

1228450cookie-checkUndefiniertes Verhalten beim Lesen eines Objekts mit einem Nicht-Zeichentyp, wenn es zuletzt mit einem Zeichentyp geschrieben wurde

This website is using cookies to improve the user-friendliness. You agree by using the website further.

Privacy policy