Программирование RustUnsafe и памятьРазработчик системного Rust и интеграции с C

Указатель из FFI адресует достаточно большой участок памяти, но его адрес не кратен выравниванию типа. Допу...

Указатель из FFI адресует достаточно большой участок памяти, но его адрес не кратен выравниванию типа. Допустимо ли разыменовать такой raw pointer и чем заменить операцию?

Проходите собеседования с ИИ помощником Hintsage

Краткий ответ

Нет. Прямое разыменование raw pointer требует корректного выравнивания типа; нарушение этого требования приводит к неопределённому поведению даже внутри unsafe. Если данные действительно могут быть не выровнены, для чтения используют std::ptr::read_unaligned, а для записи — std::ptr::write_unaligned, предварительно проверив остальные инварианты указателя.

Исторический контекст

Системное программирование и FFI часто работают с сетевыми пакетами, дисковыми форматами и структурами C с packed-раскладкой. Такие данные могут начинаться по адресу, который не соответствует естественному выравниванию u16, u32 или другой структуры.

Rust сохраняет возможность работать с подобными адресами через raw pointers, но обычные ссылки и операции разыменования сохраняют строгие требования к выравниванию. Это позволяет компилятору и аппаратной платформе безопасно использовать оптимизированные операции доступа к памяти.

Постановка проблемы

Raw pointer может содержать адрес невыравненного поля, и сам факт его создания ещё не является ошибкой. Ошибка возникает при операции, которая трактует этот адрес как корректно выровненный объект типа T, например при прямом разыменовании или создании ссылки.

Проблема может проявиться как аппаратное исключение на некоторых архитектурах, как некорректное чтение либо как неопределённое поведение, позволяющее компилятору сделать неверные для ожиданий программы оптимизации. Блок unsafe не отключает это правило и не превращает неверный адрес в допустимый объект.

Подробное решение

Для обычного чтения через raw pointer необходимы как минимум корректное выравнивание, доступность памяти для чтения, инициализированное значение подходящего типа и отсутствие нарушений правил конкурентного доступа. Для прямого разыменования также нельзя получать ссылку, которая не удовлетворяет требованиям Rust к ссылкам.

read_unaligned выполняет побитовую загрузку значения без требования естественного выравнивания. Он не отменяет остальные требования: указатель должен указывать на доступную область, достаточную для значения, с подходящим происхождением, а прочитанные байты должны образовывать допустимое значение типа.

Минимальный пример чтения поля из упакованной структуры:

#[repr(C, packed)] struct Header { tag: u8, length: u32, } fn read_length(header: *const Header) -> u32 { unsafe { let field = std::ptr::addr_of!((*header).length); std::ptr::read_unaligned(field) } }

addr_of! важен: он получает raw pointer на поле, не создавая промежуточную невыравненную ссылку. Само чтение выполняется через read_unaligned. Для записи применяется аналогичный принцип с write_unaligned, но при этом нужно учитывать, что запись может быть некорректной при наличии других читателей или владельцев данных.

Альтернатива — скопировать отдельные байты в выровненную переменную и явно разобрать их, например с учётом endian-порядка. Такой вариант часто лучше для сетевых протоколов: он безопаснее и явно выражает формат данных, но может потребовать больше кода и дополнительных операций копирования.

Ситуация из практики

C-библиотека передаёт Rust указатель на заголовок бинарного протокола, объявленный в C как упакованная структура. Поле длины расположено после однобайтного поля, поэтому его адрес может быть не выровнен по границе четырёх байт.

Рассматривались три варианта. Прямое разыменование было самым коротким, но нарушало инвариант выравнивания. Копирование четырёх байт с последующим разбором было наиболее явно безопасным и позволяло сразу учесть endian-порядок, но добавляло код. read_unaligned давал простой доступ без требования выравнивания, однако оставлял на вызывающей стороне проверку доступности памяти и корректности остальных инвариантов.

Для внутреннего ABI с уже проверенной длиной буфера выбрали read_unaligned, а для внешнего сетевого формата — копирование и явный разбор байтов. В результате доступ к упакованному полю не зависел от архитектуры, а границы доверия к FFI были оформлены отдельной проверкой.

Что кандидаты часто упускают

  1. Разрешено ли создать raw pointer на невыравненный адрес, если его нельзя разыменовать напрямую?

Ответ: да, создание raw pointer само по себе не требует выравнивания. Ограничение относится к операциям, которые читают или записывают значение как T, а также к созданию ссылок на этот адрес. Поэтому указатель можно передать дальше или использовать с операцией, специально поддерживающей невыравненный доступ, если соблюдены её остальные требования.

  1. Снимает ли read_unaligned требование, чтобы байты представляли допустимое значение типа?

Ответ: нет. Неравномерное выравнивание — только одна из проверяемых предпосылок. Набор байтов должен быть допустимым значением T: например, произвольный байт нельзя без проверки трактовать как любой тип с ограниченным набором допустимых значений. Для внешних данных часто безопаснее читать байты или целые числа, а затем явно проверять диапазон и преобразовывать значение.

  1. Почему нельзя заменить read_unaligned созданием ссылки на упакованное поле?

Ответ: ссылка в Rust обязана быть корректно выровненной для своего типа. Даже если ссылка используется только для немедленного чтения, её создание уже нарушает инвариант, поэтому последующее преобразование ссылки или чтение через неё не исправляет проблему. Нужно получить raw pointer без промежуточной ссылки, например через addr_of!, и выполнить специальную невыравненную операцию.