Frage Begründung für Zeigervergleiche außerhalb eines Arrays als UB


Also, der Standard (Bezug auf N1570) sagt folgendes über den Vergleich von Zeigern:

C99 6.5.8 / 5 Vergleichsoperatoren

Wenn zwei Zeiger verglichen werden, hängt das Ergebnis vom relativen Wert ab   Orte im Adressraum der Objekte, auf die verwiesen wird.   ... [schnippte offensichtliche Definitionen des Vergleichs innerhalb von Aggregaten] ...    In allen anderen Fällen   Das Verhalten ist nicht definiert.

Was ist der Grund für diese Instanz von UB, im Gegensatz zur Angabe von (zB) Konvertierung zu intptr_t und Vergleich dessen?

Gibt es eine Maschinenarchitektur, in der eine vernünftige Gesamtordnung auf Zeigern schwer zu konstruieren ist? Gibt es eine Klasse von Optimierung oder Analyse, die uneingeschränkte Zeigervergleiche behindern würde?

Eine gelöschte Antwort auf diese Frage erwähnt, dass dieser Teil von UB es ermöglicht, Vergleiche von Segmentregistern zu überspringen und nur Offsets zu vergleichen. Ist das besonders wertvoll zu bewahren?

(Die gleiche gelöschte Antwort, sowie eine hier, beachten Sie, dass in C ++, std::less und dergleichen werden benötigt, um eine Gesamtreihenfolge in Zeigern zu implementieren, unabhängig davon, ob der normale Vergleichsoperator dies tut oder nicht.)


5
2017-07-01 01:19


Ursprung


Antworten:


Verschiedene Kommentare in der ub Mailingliste Diskussion  Begründung für <nicht eine Gesamtordnung für Zeiger? weisen stark auf segmentierte Architekturen hin. Einschließlich der folgenden Kommentare, 1:

Getrennt davon glaube ich, dass die Kernsprache einfach die Tatsache anerkennen sollte, dass alle Maschinen heutzutage ein flaches Speichermodell haben.

und 2:

Dann brauchen wir vielleicht einen neuen Typ, der eine Gesamtbestellung garantiert   konvertiert von einem Zeiger (z. B. in segmentierten Architekturen, Umwandlung   würde erfordern, die Adresse des Segmentregisters zu nehmen und das hinzuzufügen   Offset im Zeiger gespeichert).

und 3:

Zeiger, obwohl historisch nicht vollständig geordnet, sind praktisch so   für alle heute existierenden Systeme mit Ausnahme des Elfenbeinturms   Köpfe des Ausschusses, so ist der Punkt strittig.

und 4:

Aber selbst wenn es segmentierte Architekturen gibt, so unwahrscheinlich es auch ist, kommen Sie doch   zurück muss das Bestellproblem noch angesprochen werden, so wie es std :: less ist   wird benötigt, um Zeiger vollständig zu ordern. Ich will nur Operator <sein   alternative Schreibweise für diese Eigenschaft.

Warum sollten alle anderen vorgeben zu leiden (und ich meine, so zu tun,   weil außerhalb eines kleinen Kontingents des Ausschusses schon Leute sind   gehe davon aus, dass die Zeiger in Bezug auf den Operator <) vollständig geordnet sind   erfüllen die theoretischen Bedürfnisse einiger derzeit nicht existent   die Architektur?

Gegen den Trend der Kommentare von der ub Mailing Liste, FUZxxl weist darauf hin, dass die Unterstützung von DOS ein Grund ist, nicht vollständig geordnete Zeiger zu unterstützen.

Aktualisieren

Dies wird auch unterstützt von der Kommentiertes C ++ Referenzhandbuch(ARM) was besagt, dass dies auf die Last zurückzuführen war, dies auf segmentierten Architekturen zu unterstützen:

Der Ausdruck kann bei segmentierten Architekturen nicht als falsch bewertet werden   [...] Dies erklärt, warum Addition, Subtraktion und Vergleich von   Zeiger sind nur für Zeiger in ein Array und ein Element definiert   über das Ende hinaus. [...] Benutzer von Maschinen mit einer nicht segmentierten Adresse   Raum entwickelte Idiome, die sich jedoch auf die Elemente dahinter bezogen   Das Ende des Arrays [...] war für segmentierte Architekturen nicht portierbar   es sei denn, es wurden besondere Anstrengungen unternommen. [...] Erlauben [...] wäre kostspielig   und dienen nur wenigen nützlichen Zwecken.


4
2017-07-01 02:47



Der 8086 ist ein Prozessor mit 16 Bit Registern und einem 20 Bit Adressraum. Um dem Mangel an Bits in seinen Registern zu begegnen, wurde eine Menge von Segmentregister existiert. Beim Speicherzugriff wird die dereferenzierte Adresse wie folgt berechnet:

address = 16 * segment + register

Beachten Sie, dass eine Adresse unter anderem mehrere Möglichkeiten zur Darstellung hat. Das Vergleichen zweier beliebiger Adressen ist mühsam, da der Compiler zuerst beide Adressen normalisieren und dann die normalisierten Adressen vergleichen muss.

Viele Compiler spezifizieren (in den Speichermodellen, wo dies möglich ist), dass bei der Zeigerarithmetik der Segmentteil unberührt bleiben soll. Dies hat mehrere Konsequenzen:

  • Objekte können eine Größe von maximal 64 kB haben
  • Alle Adressen in einem Objekt haben denselben Segmentteil
  • das Vergleichen von Adressen in einem Objekt kann einfach durch Vergleichen des Registerteils erfolgen; das kann in einer einzigen Anweisung erfolgen

Dieser schnelle Vergleich funktioniert natürlich nur, wenn die Zeiger von derselben Basisadresse abgeleitet sind, was einer der Gründe ist, warum der C-Standard Zeigervergleiche nur dann definiert, wenn beide Zeiger auf dasselbe Objekt zeigen.

Wenn Sie einen geordneten Vergleich für alle Zeiger wünschen, sollten Sie die Zeiger auf konvertieren uintptr_t Werte zuerst.


3
2017-07-01 07:18



Ich glaube, es ist undefiniert, so dass C auf Architekturen ausgeführt werden kann, wo "intelligente Zeiger" hardwaremäßig implementiert werden, mit verschiedenen Überprüfungen, um sicherzustellen, dass Zeiger niemals versehentlich außerhalb der Speicherbereiche zeigen, auf die sie sich beziehen. Ich habe nie eine solche Maschine persönlich benutzt, aber die Art, darüber nachzudenken, ist, dass die Berechnung eines ungültigen Zeigers genau so verboten ist wie die Division durch 0; Sie erhalten wahrscheinlich eine Laufzeitausnahme, die Ihr Programm beendet. Außerdem ist verboten rechnen Der Zeiger muss nicht einmal dereferenziert werden, um die Ausnahme zu erhalten.

Ja, ich glaube, die Definition erlaubte auch effizientere Vergleiche von Offset-Registern im alten 8086-Code, aber das war nicht der einzige Grund.

Ja, ein Compiler für eine dieser geschützten Zeigerarchitekturen könnte theoretisch die "verbotenen" Vergleiche implementieren, indem er in vorzeichenlos oder das Äquivalent umwandelt, aber (a) wäre es wahrscheinlich wesentlich weniger effizient, dies zu tun, und (b) wäre dies mutwillig vorsätzliche Umgehung des beabsichtigten Schutzes der Architektur, Schutz, den zumindest einige der C-Programmierer der Architektur vermutlich aktiviert (nicht deaktiviert) wollen würden.


1
2017-07-01 04:00



In der Vergangenheit bedeutete die Aussage, dass die Aktion Undefined Behaviour aufgerufen hat, dass jedes Programm, das solche Aktionen verwendet, korrekt erwartet werden kann nur für diejenigen Implementierungen, die für diese Aktion ein Verhalten definiert haben, das ihren Anforderungen entspricht. Die Angabe, dass eine Aktion Undefined Behavior aufgerufen hat, bedeutet nicht, dass Programme, die eine solche Aktion verwenden, als "illegitim" betrachtet werden, sondern C auf Programmen, die keine solchen Aktionen erfordern, auf Plattformen, die nicht effizient arbeiten können unterstütze sie.

Im Allgemeinen war die Erwartung, dass ein Compiler entweder die Befehlsfolge ausgeben würde, die die angegebene Aktion in den vom Standard geforderten Fällen am effizientesten ausführen würde, und was auch immer diese Befehlsfolge in anderen Fällen tun würde, oder eine Sequenz ausgeben würde von Anweisungen, deren Verhalten in solchen Fällen als "nützlicher" angesehen wurde als die natürliche Sequenz. In Fällen, in denen eine Aktion eine Hardware-Trap auslösen könnte oder wenn das Auslösen eines Betriebssystem-Traps in einigen Fällen möglicherweise als besser als die Ausführung der "natürlichen" Befehlsfolge angesehen wird und ein Trap ein Verhalten außerhalb der Kontrolle des C-Compilers verursachen kann, Der Standard enthält keine Anforderungen. Solche Fälle werden daher als "Undefiniertes Verhalten" bezeichnet.

Wie andere bemerkt haben, gibt es einige Plattformen, wo p1 < p2Für nicht verwandte Zeiger p1 und p2 könnte garantiert 0 oder 1 ergeben, aber wo das effizienteste Mittel zum Vergleichen von p1 und p2, das in den durch den Standard definierten Fällen funktionieren würde, könnte die übliche Erwartung, dass p1 < p2 || p2 > p2 || p1 != p2. Wenn ein Programm, das für eine solche Plattform geschrieben wurde, weiß, dass es niemals unzusammenhängende Zeiger absichtlich vergleicht (was bedeutet, dass ein solcher Vergleich einen Programmfehler darstellen würde), kann es hilfreich sein, dass Stresstest- oder Fehlerbehebungs-Builds Code generieren, der solche Vergleiche auffängt. Die einzige Möglichkeit für den Standard, solche Implementierungen zuzulassen, besteht darin, solche Vergleiche zu einem nicht definierten Verhalten zu machen.

Bis vor kurzem würde die Tatsache, dass eine bestimmte Aktion ein Verhalten auslösen würde, das nicht durch den Standard definiert wurde, im Allgemeinen nur Schwierigkeiten für Personen darstellen, die versuchen, Code auf Plattformen zu schreiben, wo die Aktion unerwünschte Konsequenzen hätte. Darüber hinaus war es auf Plattformen, auf denen eine Aktion nur unerwünschte Folgen haben konnte, wenn ein Compiler aus dem Weg ging, um dies zu tun, allgemein anerkannte Praxis für Programmierer, sich darauf zu verlassen, dass sich eine solche Aktion vernünftig verhält.

Wenn man die Begriffe akzeptiert, die

  1. Die Autoren des Standards erwarteten, dass Vergleiche zwischen nicht verwandten Zeigern auf diesen Plattformen nützlich sein würden, und nur die Plattformen, auf denen die natürlichsten Mittel zum Vergleichen verwandter Zeiger auch mit nicht verwandten Zeigern funktionieren würden, und

  2. Es gibt Plattformen, bei denen das Vergleichen nicht verwandter Zeiger problematisch wäre

Dann macht es für den Standard durchaus Sinn, Vergleiche nicht verwandter Zeiger als Undefiniertes Verhalten zu betrachten. Hätten sie erwartet, dass sogar Compiler für Plattformen, die ein disjunktes globales Ranking für alle Zeiger definieren, Vergleiche von nicht verwandten Pointern die Gesetze von Zeit und Kausalität negieren könnten (z. B. gegeben:

int needle_in_haystack(char const *hs_base, int hs_size, char *needle)
{ return needle >= hs_base && needle < hs_base+hs_size; }

Ein Compiler kann folgern, dass das Programm niemals irgendwelche Eingaben erhält, die dies verursachen würden needle_in_haystack ohne Bezugspunkte gegeben werden, und jeder Code, der nur relevant wäre, wenn das Programm solche Eingaben erhält, könnte eliminiert werden. Ich denke, sie hätten die Dinge anders spezifiziert. Compiler-Autoren würden wahrscheinlich argumentieren, dass die richtige Art zu schreiben needle_in_haystack wäre:

int needle_in_haystack(char const *hs_base, int hs_size, char *needle)
{
  for (int i=0; i<size; i++)
    if (hs_base+i == needle) return 1;
  return 0;
}

da ihre Compiler erkennen würden, was die Schleife tut, und auch erkennen, dass sie auf einer Plattform läuft, auf der nicht verwandte Zeigervergleiche laufen, und somit den gleichen Maschinencode erzeugen, wie ältere Compiler für die früher genannte Formulierung erzeugt hätten. Ob es besser wäre, Compiler vorzuschreiben, ein Mittel zur Verfügung zu stellen, dass Code, der der früheren Version ähnelt, entweder sinnvoll auf Plattformen, die ihn unterstützen, oder auf Kompilierung auf denjenigen, die nicht wollen, oder besser, dass Programmierer die frühere Semantik voraussetzen, sollte Ich sollte das letztere schreiben und hoffe, dass Optimierer es in etwas Nützliches verwandeln, überlasse ich dem Leser das Urteil.


1
2017-07-01 15:30