Является ли доступ к регистрам через предопределенные статические адреса неопределенным поведением в C ++? - PullRequest
0 голосов
/ 08 ноября 2018

Я компилирую программу на C ++ для работы в автономной среде, а процессор, на котором я работаю, определяет 32-разрядный периферийный регистр, который будет доступен ( edit: memory-mapped ) на PERIPH_ADDRESS (выровнено правильно и не перекрывается с любым другим объектом C ++, стеком и т. д.).

Я компилирую следующий код с предопределенным PERIPH_ADDRESS, позже связываю его с полной программой и запускаю его.

#include <cstdint>

struct Peripheral {
    const volatile uint32_t REG;
};

static Peripheral* const p = reinterpret_cast<Peripheral*>(PERIPH_ADDRESS);

uint32_t get_value_1() {
    return p->REG;
}

static Peripheral& q = *reinterpret_cast<Peripheral*>(PERIPH_ADDRESS);

uint32_t get_value_2() {
    return q.REG;
}

extern Peripheral r;
// the address of r is set in the linking step to PERIPH_ADDRESS

uint32_t get_value_3() {
    return r.REG;
}

Имеет ли какая-либо из функций get_value (напрямую или через p / q) неопределенное поведение? Если да, я могу это исправить?

Я думаю, что эквивалентный вопрос был бы: может ли какой-либо соответствующий компилятор отказаться от компиляции ожидаемой программы для меня? Например, один с включенным UB sanitezer.

Я рассмотрел [ basic.stc.dynamic.safety ] и [ basic.compound # def: object_pointer_type ], но это ограничивает допустимость указателей динамическими объекты. Я не думаю, что это применимо к этому коду, потому что «объект» в PERIPH_ADDRESS никогда не считается динамическим. Я думаю, что я могу с уверенностью сказать, что хранилище, обозначенное p, никогда не достигает конца своего срока хранения, его можно считать статическим .

Я также посмотрел на Почему C ++ запрещает создание действительных указателей с действительного адреса и типа? и ответы на этот вопрос. Они также относятся только к адресам динамических объектов и их действительности, поэтому они не отвечают на мой вопрос.

Другие вопросы, которые я рассмотрел, но не мог ответить сам, которые могли бы помочь с основным вопросом:

  • Могу ли я столкнуться с какими-либо проблемами UB, поскольку объект никогда не создавался в абстрактной машине C ++?
  • Или я действительно могу считать, что объект со статической продолжительностью хранения «сконструирован» должным образом?

Очевидно, я бы предпочел ответы, которые ссылаются на любой недавний стандарт C ++.

Ответы [ 7 ]

0 голосов
/ 09 ноября 2018

Это краткое изложение очень полезных ответов, первоначально опубликованных @curiousguy @Passer By, @Pete Backer и другими. В основном это основано на стандартном тексте (отсюда тег language-lawyer) со ссылками, предоставленными другими ответами. Я сделал это вики-сообществом, потому что ни один из ответов не был полностью удовлетворительным, но у многих были хорошие моменты. Не стесняйтесь редактировать.

Код определяется реализацией в лучшем случае, но может иметь неопределенное поведение.

Части, определенные реализацией:

  1. reinterpret_cast от целочисленного типа к типу указателя определяется реализацией. [ expr.reinterpret.cast / 5]

    Значение целочисленного типа или типа перечисления может быть явно преобразовано в указатель. Указатель, преобразованный в целое число достаточного размера (если таковое существует в реализации) и обратно в тот же тип указателя, будет иметь свое первоначальное значение; Отображения между указателями и целыми числами определяются реализацией . [Примечание: за исключением случаев, описанных в [basic.stc.dynamic.safety], результатом такого преобразования не будет значение указателя, полученное безопасно. - конец примечания]

  2. Доступ к энергозависимым объектам определяется реализацией. [ dcl.type.cv / 5]

    Семантика доступа через volatile glvalue определяется реализацией. Если предпринята попытка получить доступ к объекту, определенному с volatile-квалифицированным типом, с помощью энергонезависимого glvalue, поведение не определено.

Части, где следует избегать UB:

  1. Указатели должны указывать на действительный объект в C ++ абстрактной машине , в противном случае программа имеет UB.

    Насколько я могу судить, если реализация абстрактной машины - это программа, созданная разумным, соответствующим компилятором и компоновщиком, работающая в среде, в которой регистр отображен в памяти, как описано, то реализация * Можно сказать, что 1046 * имеет объект C ++ uint32_t в этом месте, и нет UB с какой-либо из функций. Это, по-видимому, разрешено [ intro.compliance / 8] :

    Соответствующая реализация может иметь расширения (включая дополнительные библиотечные функции), при условии, что они не изменяют поведение любой правильно сформированной программы. [...]

    Это все еще требует либеральной интерпретации [ intro.object / 1] , потому что объект не создан ни одним из перечисленных способов:

    Объект создается по определению ([basic.def]), выражению new, при неявном изменении активного члена объединения ([class.union]) или при создании временного объекта ([[ conv.rval], [class.tevent]).

    Если реализация абстрактной машины имеет компилятор с дезинфицирующим средством (-fsanitize=undefined, -fsanitize=address), то, возможно, придется добавить дополнительную информацию в компилятор, чтобы убедить его в том, что есть a действительный объект в этом месте.

    Конечно, ABI должен быть правильным, но это подразумевалось в вопросе (правильное выравнивание и отображение в памяти).

  2. Определяется реализацией, имеет ли реализация строгий или смягченный указатель безопасности [ basic.stc.dynamic.safety / 4] . При строгой безопасности указателей доступ к объектам с динамической длительностью хранения возможен только через безопасный указатель [ basic.stc.dynamic.safety] . Значения p и &q не являются таковыми, но объекты, на которые они ссылаются, не имеют динамической длительности хранения, поэтому этот пункт не применяется.

    Реализация может иметь ослабленную безопасность указателя, и в этом случае достоверность значения указателя не зависит от того, является ли оно безопасно полученным значением указателя. AlteВ сущности, реализация может иметь строгую безопасность указателя, и в этом случае значение указателя, ссылающееся на объект с динамической продолжительностью хранения, который не является безопасным значением указателя, является недопустимым значением указателя [...].[Примечание: эффект от использования недопустимого значения указателя (включая передачу его функции освобождения) не определен, см. [Basic.stc].

Практический вывод кажетсяБудь то, что определенная реализация поддержка необходима, чтобы избежать UB.Для здравомыслящих компиляторов результирующая программа не содержит UB или может иметь UB, на которую можно очень положиться (в зависимости от того, как вы на нее смотрите).Однако дезинфицирующие средства могут обоснованно жаловаться на код, если только им явно не сообщают, что в ожидаемом месте существует правильный объект.Вывод указателя не должен быть практической проблемой.

0 голосов
/ 09 ноября 2018

На практике, из предложенных вами конструкций, эта

struct Peripheral {
    volatile uint32_t REG;  // NB: "const volatile" should be avoided
};

extern Peripheral r;
// the address of r is set in the linking step to PERIPH_ADDRESS

uint32_t get_value_3() {
    return r.REG;
}

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

Поскольку r в контексте get_value_3 является объектом с внешней связью, который не определен в этом модуле перевода, компилятор должен предположить, что этот объект существует и уже был правильно сконструирован при генерации кода для get_value_3. Peripheral - это объект POD, поэтому не нужно беспокоиться о статическом упорядочении конструктора. Функция определения объекта для жизни по определенному адресу во время соединения является воплощением поведения, определяемого реализацией: это официально документированная особенность реализации C ++ для оборудования, с которым вы работаете, но она не покрывается стандартом C ++.

Предупреждение 1: абсолютно не пытайтесь сделать это с не POD-объектом; в частности, если бы у Peripheral был нетривиальный конструктор или деструктор, это, вероятно, вызвало бы неправильные записи по этому адресу при запуске.

Предупреждение 2: Объекты, которые должным образом объявлены как const и volatile, крайне редки, и поэтому компиляторы, как правило, имеют ошибки в обработке таких объектов. Я рекомендую использовать только volatile для этого аппаратного регистра.

Предупреждение 3: Как указывает суперкат в комментариях, в любой момент времени в определенной области памяти может быть только один объект C ++. Например, если есть несколько наборов регистров, мультиплексированных в блок адресов, вам нужно выразить это как-то одним объектом C ++ (возможно, объединением), а не несколькими объектам, которым назначен один и тот же базовый адрес.

0 голосов
/ 09 ноября 2018

Ни стандарт C, ни стандарт C ++ формально не охватывают даже действия по связыванию объектных файлов, скомпилированных различными компиляторами. Стандарт C ++ не дает никаких гарантий того, что вы можете взаимодействовать с модулями, скомпилированными с любым компилятором C или даже что означает взаимодействие с такими модулями; язык программирования C ++ даже не подчиняется стандарту C для какой-либо базовой функции языка; не существует класса C ++, формально гарантированно совместимого со структурой C. (Язык программирования C ++ даже формально не признает, что существует язык программирования C с некоторыми фундаментальными типами с таким же написанием, как в C ++.)

Все взаимодействие между компиляторами по определению выполняется ABI: двоичный интерфейс приложения.

Использование объектов, созданных вне реализации, должно выполняться после ABI; это включает системные вызовы, которые создают представление объектов в памяти (например, mmap) и volatile объектов.

0 голосов
/ 08 ноября 2018

Код, подобный приведенному выше, эффективно стремится использовать C как форму «ассемблера высокого уровня». Хотя некоторые люди настаивают на том, что C не является ассемблером высокого уровня, авторы Стандарта C сказали это в своем опубликованном документе Rationale:

Хотя он стремился дать программистам возможность писать По-настоящему переносимые программы, Комитет C89 не хотел заставлять программистов писать переносимо, чтобы исключить использование C в качестве «высокоуровневого ассемблера»: способность писать машинный код - одна из сильных сторон языка C. этот принцип в значительной степени мотивирует проведение различия между строго соответствующей программой и соответствующей программой (§4).

Стандарты C и C ++ сознательно избегают требования, чтобы все реализации были пригодны для использования в качестве ассемблеров высокого уровня, и не пытаются определить все поведения, необходимые для того, чтобы сделать их подходящими для таких целей. Следовательно, поведение таких конструкций, как ваша, которые эффективно рассматривают компилятор как высокоуровневый ассемблер, не определяется Стандартом. Однако авторы Стандарта явно признают ценность способности некоторых программ использовать язык в качестве высокоуровневого ассемблера и, таким образом, явно предполагают, что такой код, как ваш, может использоваться в реализациях, предназначенных для поддержки таких конструкций, - сбой определение поведения никоим образом не подразумевает, что такой код «сломан».

Еще до того, как был написан стандарт, реализации, предназначенные для низкоуровневого программирования на платформах, где было бы целесообразно обрабатывать преобразования между указателями и целыми числами одинакового размера, просто интерпретируя их биты, по существу, единодушно обрабатывали бы такие преобразования таким образом. Такая обработка значительно облегчает низкоуровневое программирование на таких платформах, но авторы Стандарта не видят причин для этого. На платформах, где такое поведение не имеет смысла, такой мандат будет вредным, а на тех, где это имеет смысл, разработчики компиляторов будут вести себя соответствующим образом с этим или без него, что делает его ненужным.

К сожалению, авторы Стандарта были слишком самонадеянны. Опубликованное Обоснование заявляет о желании поддерживать Дух C, принципы которого включают «Не мешайте программисту делать то, что нужно сделать». Это может означать, что на платформе с естественным упорядочением памяти может потребоваться область хранения, которая в разное время «принадлежит» разным контекстам выполнения, - качественная реализация, предназначенная для низкоуровневого программирования на такой платформе, учитывая что-то вроде:

extern volatile uint8_t buffer_owner;
extern volatile uint8_t * volatile buffer_address;

buffer_address = buffer;
buffer_owner = BUFF_OWNER_INTERRUPT;
... buffer might be asynchronously written at any time here
while(buffer_owner != BUFF_OWNER_MAINLINE)
{  // Wait until interrupt handler is done with the buffer and...
}  // won't be accessing it anymore.
result = buffer[0];

должен прочитать значение из buffer[0] после код прочитал object_owner и получил значение BUFF_OWNER_MAINLINE. К сожалению, некоторые реализации думают, что было бы лучше попытаться использовать некоторое ранее наблюдавшееся значение buffer[0], чем рассматривать изменчивый доступ как возможное освобождение и повторное приобретение права собственности на рассматриваемое хранилище.

В общем, компиляторы будут надежно обрабатывать такие конструкции с отключенными оптимизациями (и фактически будут делать это с volatile) или без него, но не смогут эффективно обрабатывать такой код без использования специфических для компилятора директив (что также приведет к volatile ненужно). Я думаю, что дух C должен прояснить, что качественные компиляторы, предназначенные для низкоуровневого программирования, должны избегать оптимизаций, которые ослабили бы семантику volatile таким образом, чтобы низкоуровневые программисты могли делать то, что может понадобиться на цели платформа, но, видимо, это не совсем понятно.

0 голосов
/ 08 ноября 2018

Имеет ли какая-либо из функций get_value (напрямую или через p / q) неопределенное поведение?

Да. Все они. Все они обращаются к значению объекта (типа Peripheral), который в отношении объектной модели C ++ не существует . Это определено в [basic.lval / 11] , AKA: правило строгого наложения имен:

Если программа пытается получить доступ к сохраненному значению объекта через glvalue другого, чем один из следующих типов, поведение не определено:

Проблема не в «броске»; это использование результатов этого броска. Если там есть объект указанного типа, то поведение четко определено. Если нет, то он не определен.

А поскольку там нет Peripheral, это UB.

Теперь, если ваша среда выполнения обещает, что является объектом типа Peripheral по этому адресу, тогда это хорошо определенное поведение. В противном случае, нет.

Если да, можно ли это исправить?

Нет. Просто положитесь на UB.

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

0 голосов
/ 08 ноября 2018

Я не знаю, ищете ли вы здесь ответ юриста или практический ответ. Я дам вам практический ответ.

Определение языка не говорит вам, что делает этот код. Вы получили ответ, который говорит, что поведение определяется реализацией. Я не уверен, так или иначе, но это не имеет значения. Предположим, что поведение не определено. Это не значит, что будут плохие вещи. Это означает only , что определение языка C ++ не говорит вам, что делает этот код. Если компилятор использует документы, что он делает, это нормально. И если компилятор не документирует это, но все знают, что он делает, это тоже хорошо. Код, который вы показали, является разумным способом доступа к отображенным в памяти регистрам во встроенных системах; если это не сработает, многие будут расстроены.

0 голосов
/ 08 ноября 2018

Это определяется реализацией, что означает приведение от указателя [expr.reinterpret.cast]

Значение целочисленного типа или типа перечисления может быть явно преобразовано в указатель. Указатель, преобразованный в целое число достаточного размера (если таковое существует в реализации) и обратно в тот же тип указателя, будет иметь свое первоначальное значение; в противном случае отображения между указателями и целыми числами определяются реализацией.

Поэтому это четко определено. Если ваша реализация обещает вам, что результат приведения действителен, у вас все в порядке.

Связанный вопрос касается арифметики указателей, которая не связана с рассматриваемой проблемой.

† По определению действительный указатель указывает на объект, подразумевая, что последующие косвенные указания также четко определены. Следует проявлять осторожность, чтобы убедиться, что объект является в течение его жизни .

...