Макрос std::ptr::addr_of! выполняет важную роль в небезопасном Rust, позволяя создавать сырые указатели на поля без промежуточного шага создания ссылки. При работе со структурами #[repr(packed)] поля могут находиться по невыравненным адресам в памяти, что нарушает требования по выравниванию, присущие типам ссылок. Попытка создать ссылку с помощью оператора & на такие невыравненные данные влечет за собой немедленное неопределенное поведение, независимо от того, будет ли эта ссылка использована впоследствии. Макрос addr_of! обходит это, непосредственно создавая сырой указатель из адреса поля, минуя инварианты выравнивания и валидности, применяемые к ссылкам. Это различие имеет жизненно важное значение для безопасных взаимодействий FFI и низкоуровневых манипуляций с памятью, где распространены упакованные структуры данных.
При разработке высокопроизводительного парсера для устаревшего бинарного протокола инженерная команда столкнулась с структурой #[repr(packed)], где поле u32 было намеренно расположено на смещении в 1 байт, чтобы соответствовать карте регистров внешнего оборудования. Начальная реализация попыталась взять в займы это поле с помощью &packet.status_register, чтобы передать его в функцию валидации, не осознавая, что это создало невыравненную ссылку и вызвало немедленное неопределенное поведение.
Первое решение, которое было рассмотрено, заключалось в удалении атрибута packed и ручном добавлении байтов-заполнителей для принудительного выравнивания. Этот подход гарантировал безопасность, позволяя создавать естественные ссылки, но нарушил бинарную совместимость с аппаратной спецификацией и потратил пропускную способность памяти при передаче больших массивов этих структур.
Второй подход предполагал использование арифметики указателей с unsafe { &*(base_ptr.add(1) as *const u32) } для ручного вычисления адреса поля. Хотя это избегало прямого синтаксиса доступа к полю, он все равно создавал ссылку через оператор разыменования &*, что составляет неопределенное поведение, если результирующий указатель неправильно выровнен, не предлагая никаких улучшений по безопасности по сравнению с изначальным наивным заимствованием и потенциально вводя в заблуждение будущих обслуживающих.
В конечном итоге команда выбрала третье решение, использовав std::ptr::addr_of! для получения сырого указателя на невыравненное поле без создания промежуточной ссылки. Этот указатель затем передавался в std::ptr::read_unaligned для безопасного копирования значения в правильно выровненную локальную переменную. Эта стратегия сохранила необходимую компоновку памяти, строго соблюдая модель памяти Rust, что привело к коду, который прошел строгую проверку с помощью Miri и корректно функционировал на нескольких целевых архитектурах, включая ARM и x86_64.
Почему создание ссылки на невыравненные данные представляет собой неопределенное поведение, даже если ссылка немедленно преобразуется в сырой указатель?
В Rust акт создания ссылки, такой как &packed.field, не просто расчет указателя, но и утверждение для компилятора, что целевая память удовлетворяет всем инвариантам этого типа ссылки, включая выравнивание и валидность для чтения. Бэкэнд LLVM и оптимизатор Rust предполагают, что эти инварианты выполняются немедленно при создании ссылки, что позволяет проводить агрессивные оптимизации, такие как перестановка загрузок и записей или спекулятивные загрузки. Даже если ссылка мгновенно преобразуется в *const T, оптимизатор мог уже сгенерировать инструкции, предполагая выровненный доступ, или он мог пометить значение ссылки как dereferenceable в метаданных LLVM, что приводит к неправильной компиляции на архитектурах с строгими требованиями по выравниванию. Поэтому неопределенное поведение возникает в момент создания ссылки, а не в момент разыменования, делая само существование невыравненной ссылки токсичным для правильности программы.
Как addr_of! отличается от использования as *const _ для существующей ссылки и почему макрос необходим?
При записи &packed.field as *const T компилятор Rust сначала создает ссылку (что приводит к проверкам выравнивания и потенциальному UB) и только затем преобразует эту действительную ссылку в сырой указатель. Напротив, std::ptr::addr_of! действует непосредственно на выражение места (поле), генерируя сырой указатель без создания промежуточной ссылки. Это имеет решающее значение, поскольку компилятор рассматривает внутреннюю часть addr_of! как специальную конструкцию, которая пропускает проверки валидности ссылки, тогда как ключевое слово as выполняет преобразование значения в значение, требуя, чтобы исходное значение (ссылка) было действительным. Использование макроса гарантирует, что сама производная указателя не может вызвать неопределенное поведение из-за нарушений выравнивания, предоставляя единственный безопасный путь для получения адресов потенциально невыравненных данных.
Какие дополнительные соображения применяются при использовании addr_of_mut! для получения указателей на поля в структуре, содержащей UnsafeCell?
Когда структура #[repr(packed)] содержит UnsafeCell<T>, получение изменяемого указателя на внутренности требует тщательной обработки правил алиасинга Rust. UnsafeCell предоставляет внутреннюю изменяемость, но создание изменяемой ссылки (&mut) на невыравненное поле UnsafeCell все равно нарушает требования по выравниванию и является неопределенным поведением. Кандидаты часто предполагают, что UnsafeCell каким-то образом освобождает указатель от правил выравнивания, но это освобождает только от гарантии алиасинга исключительных ссылок (noalias), а не от выравнивания. Использование addr_of_mut! дает *mut T, который все равно должен уважать выравнивание типа при его разыменовании или передаче в UnsafeCell::raw_get, что требует использования read_unaligned или write_unaligned для фактического доступа к данным.