Das Makro std::ptr::addr_of! spielt eine entscheidende Rolle im unsicheren Rust, indem es die Erstellung von rohen Zeigern auf Felder ohne den Zwischenschritt der Erstellung einer Referenz ermöglicht. Im Umgang mit #[repr(packed)] Strukturen können Felder an nicht ausgerichteten Speicheroffsets liegen, was die Ausrichtungsanforderungen der Referenztypen verletzt. Der Versuch, über den &-Operator eine Referenz auf solche nicht ausgerichteten Daten zu erstellen, stellt sofortiges nicht definiertes Verhalten dar, unabhängig davon, ob die Referenz später verwendet wird. Das addr_of!-Makro umgeht dies, indem es direkt einen rohen Zeiger aus der Adresse des Feldes erstellt und die von Referenzen durchgesetzten Ausrichtungs- und Gültigkeitsinvarianten umgeht. Diese Unterscheidung ist entscheidend für sichere FFI-Interaktionen und die niedrige Speicherverwaltung, wo gepackte Datenlayouts verbreitet sind.
Während der Entwicklung eines Hochleistungsparsers für ein veraltetes binäres Protokoll stieß das Ingenieurteam auf eine #[repr(packed)] Struktur, in der ein u32-Feld absichtlich an einem Offset von 1 Byte platziert wurde, um mit einer externen Hardware-Registerkarte übereinzustimmen. Die ursprüngliche Implementierung versuchte, dieses Feld mit &packet.status_register zu leihen, um es einer Validierungsfunktion zu übergeben, ohne zu wissen, dass dies eine nicht ausgerichtete Referenz erzeugte und sofortiges nicht definiertes Verhalten auslöste.
Die erste in Betracht gezogene Lösung bestand darin, das packed-Attribut zu entfernen und manuell Padding-Bytes einzufügen, um die Ausrichtung zu erzwingen. Dieser Ansatz garantierte Sicherheit, indem er die natürliche Referenzbildung ermöglichte, führte jedoch zu einem Verlust der binären Kompatibilität mit der Hardwarespezifikation und verschwendete Speicherbandbreite beim Übertragen großer Arrays dieser Strukturen.
Der zweite Ansatz schlug vor, mit Zeigerarithmetik unsafe { &*(base_ptr.add(1) as *const u32) } zu verwenden, um die Feldadresse manuell zu berechnen. Obwohl dies die direkte Feldzugriffs-Syntax vermied, stellt es immer noch eine Referenz durch den &*-Dereferenzierungsoperator her, was nicht definiertes Verhalten darstellt, wenn der resultierende Zeiger nicht ordnungsgemäß ausgerichtet ist, ohne wobei eine Sicherheitsverbesserung gegenüber dem ursprünglichen naiven Ausleihen angeboten wird und möglicherweise zukünftige Wartende in die Irre führt.
Das Team wählte schließlich die dritte Lösung, indem es std::ptr::addr_of! nutzte, um einen rohen Zeiger auf das nicht ausgerichtete Feld zu erhalten, ohne eine Zwischenreferenz zu erstellen. Dieser Zeiger wurde dann an std::ptr::read_unaligned übergeben, um den Wert sicher in eine ordnungsgemäß ausgerichtete lokale Variable zu kopieren. Diese Strategie bewahrte das erforderliche Speicherlayout und hielt sich strikt an Rust's Speicher-Modell, was zu Code führte, der rigorose Tests mit Miri bestand und über mehrere Zielarchitekturen hinweg korrekt funktionierte, einschließlich ARM und x86_64.
Warum stellt die Erstellung einer Referenz auf nicht ausgerichtete Daten nicht definiertes Verhalten dar, selbst wenn die Referenz sofort in einen rohen Zeiger umgewandelt wird?
In Rust ist der Akt der Erstellung einer Referenz, wie &packed.field, nicht nur eine Zeigerberechnung, sondern eine Behauptung an den Compiler, dass der Ziel-Speicher alle Invarianten dieses Referenztyps erfüllt, einschließlich Ausrichtung und Gültigkeit für Lesevorgänge. Der LLVM-Backend und der Optimierer von Rust gehen davon aus, dass diese Invarianten sofort bei der Erstellung der Referenz gültig sind, was aggressive Optimierungen wie Load-Store-Neuanordnungen oder spekulative Lesevorgänge ermöglicht. Selbst wenn die Referenz sofort in *const T umgewandelt wird, hat der Optimierer möglicherweise bereits Anweisungen emittiert, die an eine ausgerichtete Zugriffsannahme angepasst sind, oder er könnte den Referenzwert in den LLVM-Metadaten als dereferenzierbar kennzeichnen, was zu einer fehlerhaften Kompilierung auf Architekturen mit strikten Ausrichtungsanforderungen führt. Daher tritt das nicht definierte Verhalten bereits zum Zeitpunkt der Referenzherstellung auf, nicht beim Dereferenzieren, wodurch die bloße Existenz einer nicht ausgerichteten Referenz toxisch für die Korrektheit des Programms ist.
Wie unterscheidet sich addr_of! von der Verwendung von as *const _ auf einer bestehenden Referenz, und warum ist das Makro notwendig?
Wenn man &packed.field as *const T schreibt, erstellt der Rust-Compiler zunächst eine Referenz (was Ausrichtungsprüfungen und potentielles UB auslöst) und konvertiert dann erst diese gültige Referenz in einen rohen Zeiger. Im Gegensatz dazu arbeitet std::ptr::addr_of! direkt an dem Platz-Ausdruck (dem Feld) und erzeugt einen rohen Zeiger, ohne jemals eine Zwischenreferenz zu konstruieren. Dies ist entscheidend, da der Compiler den inneren Teil von addr_of! als spezielle Konstruktion betrachtet, die die Gültigkeitsprüfungen der Referenz überspringt, während das as-Schlüsselwort eine Wert-zu-Wert-Konvertierung durchführt, die erfordert, dass der Quellwert (die Referenz) gültig ist. Die Verwendung des Makros stellt sicher, dass die Zeigerableitung selbst kein nicht definiertes Verhalten aufgrund von Ausrichtungsverstößen einführt, und bietet den einzig sicheren Weg, um Adressen von potenziell nicht ausgerichteten Daten zu erhalten.
Welche zusätzlichen Überlegungen sind zu beachten, wenn addr_of_mut! verwendet wird, um Zeiger auf Felder innerhalb einer Struktur zu erhalten, die UnsafeCell enthält?
Wenn eine #[repr(packed)] Struktur ein UnsafeCell<T> enthält, erfordert das Erlangen eines veränderbaren Zeigers auf das Innere eine sorgfältige Handhabung der Aliasierungsregeln von Rust. Das UnsafeCell bietet innere Veränderlichkeit, jedoch verletzt die Erstellung einer veränderbaren Referenz (&mut) auf ein nicht ausgerichtetes UnsafeCell-Feld immer noch die Ausrichtungsanforderungen und ist nicht definiertes Verhalten. Kandidaten gehen oft davon aus, dass UnsafeCell den Zeiger irgendwie von den Ausrichtungsregeln befreit, aber es befreit nur von der Garantie der exklusiven Referenzaliasierung (noalias), nicht von der Ausrichtung. Die Verwendung von addr_of_mut! ergibt einen *mut T, der bei der späteren Dereferenzierung oder der Übergabe an UnsafeCell::raw_get immer noch die Ausrichtung des zugrunde liegenden Typs respektieren muss, was die Verwendung von read_unaligned oder write_unaligned für den tatsächlichen Datenzugriff erfordert.