Является ли доступ к регистрам через предопределенные статические адреса неопределенным поведением в C++?

Я компилирую программу на C++ для работы в автономной среде, а процессор, на котором я работаю, определяет доступный 32-разрядный периферийный регистр (редактировать: отображение памяти) в 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) неопределенное поведение? Если да, могу ли я это исправить?

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

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

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

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

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

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

Я запутался, регистры - это не память, это локальное хранилище внутри процессора. Если вам не нужен адрес int / double ... тогда компилятор может вместить объект в один, иначе он сохранит его в памяти, где можно взять указатель.

Matthieu Brucher 08.11.2018 19:14

Может быть, вы можете безопасно прочитать его, используя какую-нибудь встроенную функцию компилятора? Может быть, компилятор даже предлагает встроенные функции для этого конкретного случая?

user7860670 08.11.2018 19:16

Доступ @MatthieuBrucher к регистру - это чтение / запись из памяти во многих микрочипах.

SergeyA 08.11.2018 19:24

@MatthieuBrucher OP говорит, что они работают в определенной среде, где регистр логически отображается на адрес памяти. Существуют системы, в которых определенные адреса памяти фактически не относятся к памяти, а вместо этого относятся к другим объектам. Разработчики оборудования могут делать это как угодно, поэтому на машине с памятью 1 КБ адрес 2000 может относиться к регистру, 2001 - к другому регистру, 2002 - к оборудованию ввода-вывода, к которому подключено какое-то произвольное устройство, для 2003 - биты все могут относиться к разным однобитным выводам ввода / вывода или к состояниям прерывания или к чему угодно, на самом деле.

Loduwijk 08.11.2018 20:09

Связанные Какое правило строгого псевдонима

Shafik Yaghmour 08.11.2018 20:37

Я не совсем понимаю суть структуры. Почему не только const volatile uint32_t& value1 = *reinterpret_cast<const volatile uint32_t*>(PERIPH_ADDRESS)?

molbdnilo 08.11.2018 20:46

Честно говоря, отображение памяти имеет смысл.

Matthieu Brucher 08.11.2018 20:49

Вещи могут быть неопределенными в соответствии со стандартом, но хорошо определены в соответствии с конкретной реализацией - такие вещи вполне допустимы в реализации, но вы не можете обратиться к стандарту для интерпретации. («Допустимое неопределенное поведение варьируется от полного игнорирования ситуации с непредсказуемыми результатами до поведения во время трансляции или выполнения программы документированным образом, характерным для среды [...]»)

molbdnilo 08.11.2018 20:51

@molbdnilo Меня действительно волнует возможность доступа через нефундаментальный объектный тип, поэтому я могу создать полезный драйвер, используя несколько подрегистров. Но я не хотел, чтобы вопрос касался макета структуры.

palotasb 08.11.2018 21:14

@palotasb А, значит, структура «покрывает» целый диапазон адресов? В этом есть большой смысл.

molbdnilo 08.11.2018 21:20

Да, например, см. Блок управления системой ARM Cortex-M4 typedef и использование, аналогичный моему примеру 1. (__IOM и т. д. Являются определениями const / volatile.)

palotasb 08.11.2018 22:03

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

supercat 08.11.2018 23:00

По определению значение чтения / записи изменчивого объекта определяется ABI, а не стандартом языка программирования.

curiousguy 09.11.2018 03:30

@curiousguy: Что значит "по определению". Стандарт C не признает концепцию ABI и не запрещает реализации, ориентированной на одну платформу, обрабатывать изменчивые записи способом, имитирующим другую платформу. Концепция реализации, которая нацелена на определенную платформу и генерирует код, соответствующий ABI этой платформы, безусловно, полезна, но Стандарт не признает такой вещи.

supercat 13.11.2018 20:40
Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
4
14
311
7
Перейти к ответу Данный вопрос помечен как решенный

Ответы 7

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

A value of integral type or enumeration type can be explicitly converted to a pointer. A pointer converted to an integer of sufficient size (if any such exists on the implementation) and back to the same pointer type will have its original value; mappings between pointers and integers are otherwise implementation-defined.

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

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

† By definition, a valid pointer points to an object, implying subsequent indirections are also well-defined. Care should be exercised in making sure the object is within its lifetime.

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

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

Тег вопроса очень ясно показывает, что ищет OP.

SergeyA 08.11.2018 19:23

@SergeyA - Я запуталась. Если ты веришь, что это вопрос строго языкового юриста, почему вы ответили написать комментарий на вопрос о поведении некоторых микрочипов?

Pete Becker 08.11.2018 19:35

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

SergeyA 08.11.2018 19:45

@SergeyA верен, что OP помечен как language-lawyer, но это все еще действительный ответ. «На самом деле это не имеет значения, потому что ...» может быть неприемлемым ответом, но он по-прежнему действителен и полезен. тем не мение, я не голосовал за то, что «если компилятор не документирует это, но все знают, что он делает, это тоже нормально» ... Я видел много ошибок, внесенных из-за людей, полагающихся на это мышление . Если это задокументировано, хорошо. Если это не так, пожалуйста, не полагайтесь на это, так как это может измениться.

Loduwijk 08.11.2018 20:13

Это действительно вопрос языкового юриста, я могу разумно полагаться на то, что GCC / Clang хорошо справляется с этим кодом. UB Sanitizer в GCC также обрабатывает это так, как я ожидал (без UB).

palotasb 08.11.2018 20:36

ИМХО, это все еще полезный ответ для тех, кто ищет практического результата.

palotasb 08.11.2018 20:56

@Aaron: Неправильно говорить, что если компилятор не документирует это, вы не можете на это полагаться. Потому что большинство разработчиков компиляторов не знают, что такое спецификация. Они не могли бы написать спецификацию, если бы от этого зависела их жизнь. У вас нет спецификации и вы не можете полагаться ни на что, кроме интуиции.

curiousguy 11.11.2018 06:48

@curiousguy: Большинство спецификаций компилятора плохие, потому что большинство спецификаций практически для всего плохого. Однако более серьезная проблема заключается в том, что конструкции, которые поддерживались повсеместно, за исключением реализаций, нацеленных на необычные процессоры до C89, рассматриваются авторами стандарта как «популярные расширения», но разработчики компиляторов никогда не считали их расширениями, достойными документации, потому что такая поддержка была нормально, и ненормальным было только необычное поведение процессора.

supercat 11.11.2018 17:14

Does any of the get_value functions (either directly or through p/q) have undefined behavior?

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

If a program attempts to access the stored value of an object through a glvalue of other than one of the following types the behavior is undefined:

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

А так как там нет Peripheral, то это UB.

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

If yes, can I fix it?

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

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

Я согласен с тем, что фрагмент не содержит кода, определяющего объект Peripheral по адресу. Разрешено ли реализации предполагать, что она не определена где-то еще (например, в другой единице перевода, которая позже будет связана)?

palotasb 08.11.2018 20:35

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

Nicol Bolas 08.11.2018 20:41

Да, но это вызвано нарушенными инвариантами, которые компилятор или компоновщик считает истинными, не так ли? Компилятор должен сгенерировать правильный код для случая, когда инварианты сохранены, и я стараюсь полагаться на этот факт.

palotasb 08.11.2018 20:47

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

palotasb 08.11.2018 20:52

@palotasb: "Да, но это вызвано нарушенными инвариантами, которые компилятор или компоновщик считает истинными, не так ли?" UB вызван тем, что ваш код делает что-то, что, по словам спецификации, не имеет определенного поведения. Вопросы «языковой юрист» не заботятся о том, что делает «компилятор или компоновщик»; это для вопросов, где вас волнует, что говорит язык.

Nicol Bolas 08.11.2018 21:01

@palotasb: "использование компоновщика для установки адреса объявленного extern объекта" Что определяет, является ли это UB или нет, так это то, есть ли объект этого типа по этому адресу в то время, когда вы обращаетесь к нему. Если есть, то это четко определено. Если нет, то это УБ. Ваша гипотетическая другая единица перевода, которая определяет r, могла легко уничтожить этот объект (через прямой вызов деструктора) до того, как основной попытается получить к нему доступ, и это все равно будет UB, даже если по этому адресу объявлена ​​переменная.

Nicol Bolas 08.11.2018 21:03

Я упомянул компиляцию и компоновку, потому что в стандарте это упоминается в [basic.link] и [dcl.link]. Там также написано: "Связь из C++ с объектами, определенными в других языках [...], определяется реализацией [...]. Такая связь может быть достигнута только там, где стратегии размещения [...] объектов достаточно похожи." Это могло быть то, что я искал с "если да, как я могу это исправить?" вопрос. На самом деле объект является определен вне C++, и стратегия компоновки может быть определена так же, как C++. Может ли это быть UB-free?

palotasb 08.11.2018 21:48

@palotasb: Вы слишком много думаете об этом. Вопрос очень простой: есть ли там объект в то время, когда вы обращаетесь к lvalue? Если ответ «да», то поведение четко определено. Если ответ «нет», то поведение не определено. Связь - не имеющий отношения; важно состояние абстрактной машины во время вызова кода.

Nicol Bolas 08.11.2018 22:10

Конечно, я пометил это как языковый юрист. :) Подводя итог, вы говорите, что это UB, хотя в этом месте не существует объекта правильного типа (с точки зрения абстрактной машины), но, по крайней мере, определяется реализацией, если он есть? Я все еще предполагаю, что это должно быть статическая продолжительность хранения, потому что [basic.stc.dynamic.safety] относится к «динамические объекты», указатели которого все еще необходимо безопасно извлекать. Связывание актуально, потому что это может быть одним из способов легального перевода объекта, не относящегося к C++, в C++.

palotasb 08.11.2018 23:12

@NicolBolas: С точки зрения Стандарта единственная разница между UB и IDB заключается в том, являются ли реализации требуется для определения поведения. Если бы 95% платформ могли дешево и с пользой указать полезное поведение для некоторого действия, но сказать что-либо полезное об этом было бы непрактично для оставшихся 5%, то вред от требования реализаций для этих 5% уменьшился бы их способ вести себя в согласованная мода, которая может быть задокументирована, независимо от стоимости или того, служит ли поведение каким-либо полезным целям, будет больше, чем выгода от требования этой реализации ...

supercat 08.11.2018 23:20

... к тому, что они будут делать с явным требованием или без него. В Стандарте не удается определить такие термины, как «объект», таким образом, чтобы можно было разумно обрабатывать все угловые случаи, когда часть Стандарта описывала поведение некоторых действий, а другая часть говорит, что перекрывающаяся категория действий вызывает UB, и, как правило, путает, ожидая, что реализации могут вести себя разумно, не требуя этого.

supercat 08.11.2018 23:24

@NicolBolas Ваше определение «существует» слишком узко: вы, кажется, говорите, что объект должен существовать в той же объектной единице (например, исполняемой). Однако мы уже знаем, что он может существовать, например, в связанной библиотеке, в том числе предоставляться через привязку к коду, написанному на каком-то другом языке. Нет никаких реальных оснований утверждать, что объект не может существовать косвенно в каком-то другом месте (в данном случае на оборудовании), если он совместим с объектной моделью C++, а это так. Вот почему стандарт намеренно расплывчато описывает такие вещи, а не говорит, что «определение должно быть видимым».

Lightness Races in Orbit 09.11.2018 18:08

@LightnessRacesinOrbit: "похоже, вы говорите, что объект должен существовать в той же объектной единице (например, исполняемой)." Где я это сказал? Я сказал это должен существовать; Я не уточнил где. Пока объект существует в этом месте в соответствии со стандартной спецификацией того, как объекты возникают, код является правильно сформированным. Я сказал или подразумевал ничего такого относительно «объектных единиц», «исполняемых файлов», «определение должно быть видимым» или чего-либо в этом роде.

Nicol Bolas 09.11.2018 19:04

@NicolBolas Вы это имели в виду, утверждая, что объект не существует. Он существует. Поскольку мы исключили все очевидные причины, по которым вы можете полагать, что объект не существует, я совершенно запутался в вашей точке зрения!

Lightness Races in Orbit 10.11.2018 21:17

@LightnessRacesinOrbit: "Он существует." [intro.object] / 1 описывает способы возникновения объекта в C++. Если с этим адресом памяти не произошло ничего из этого, значит, там нет объекта. OP не заявлял и не подразумевал, что что-либо из этого было сделано с этой памятью (действительно, тот факт, что это даже вопрос, строго подразумевает, что ничего из этого не было сделано). Следовательно, если компилятор сам не создает там объект, что касается объектной модели C++, там нет объекта.

Nicol Bolas 10.11.2018 21:19

@LightnessRacesinOrbit: не я должен доказывать, что объект не существует; Я просто говорю вам, каковы правила. Когда вы обращаетесь к этому адресу, там должен быть объект. В противном случае вы получите UB.

Nicol Bolas 10.11.2018 21:20

@Nicol Нет, это еще не все, что ты делаешь. Ваш ответ буквально говорит о том, что объекта не существует, а в программе есть UB. Это ваше утверждение. Итак, да, вы должны доказать, что это правда; однако вы будете бороться, потому что (как я вам объясняю) это неверно. Ясно, что эта попытка помочь вам улучшить ваш ответ неконструктивна, и поэтому дальнейшее обсуждение не будет полезным.

Lightness Races in Orbit 11.11.2018 01:53

@LightnessRacesinOrbit: "Ваш ответ буквально говорит о том, что объекта не существует, а в программе есть UB." Потому что ничто из того, что представлено ни в коде, ни в тексте, описывающем его, не говорит о том, что объект существует в этой области памяти. Действительно, в тексте вопроса ничего не говорится и не подразумевается, что объект существует в соответствии с C++. Если OP хочет заявить, что там есть объект, они могут создать код, который помещает его туда, или другую документацию, в которой говорится, что Peripheral живет по этому адресу. В противном случае пример кода и его описание следует воспринимать как есть.

Nicol Bolas 11.11.2018 03:09

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

Although it strove to give programmers the opportunity to write truly portable programs, the C89 Committee did not want to force programmers into writing portably, to preclude the use of C as a “high-level assembler”: the ability to write machine-specific code is one of the strengths of C. It is this principle which largely motivates drawing the distinction between strictly conforming program and conforming program (§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 таким образом, чтобы не дать программистам низкого уровня делать то, что может потребоваться на целевой платформе. но, видимо, этого недостаточно.

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

palotasb 08.11.2018 22:08

@palotasb: Перезагрузка buffer[0] будет бесполезна для реализаций, которые используются для некоторых целей, но необходима для тех, которые используются для некоторых других. Авторы Стандарта ожидают, что люди, пишущие реализации, предназначенные для различных целей, будут лучше, чем Комитет, смогут определить, требует ли код, написанный для таких целей, чтение buffer[0] после buffer_owner.

supercat 08.11.2018 22:26

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

Flip 09.11.2018 07:20

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

supercat 09.11.2018 17:02

@Flip: в тех случаях, когда программисту на языке ассемблера необходимо добавить зависящие от процессора барьеры памяти, для компилятора C было бы разумно потребовать использования расширения, зависящего от производителя, чтобы сделать то же самое. Но если все, что необходимо, - это предотвратить переупорядочение компилятор загрузок / хранилищ объектов, которые были выставлены внешнему миру в частях кода, которые, как известно, взаимодействуют с внешним миром способами, которые компилятор не может понять, нет причина, по которой должен требоваться синтаксис, специфичный для компилятора.

supercat 09.11.2018 17:07

@supercat См. Gcc. Последовательности изменчивых данных не переупорядочиваются компилятором. Но нелетучие вещества переупорядочиваются и могут быть переупорядочены относительно летучих. Так что нестабильности недостаточно. Тогда есть еще процессор, который может изменить порядок ваших летучих веществ. Вот почему был представлен atomic. Атомика дает явные гарантии порядка памяти.

Flip 12.11.2018 09:12

@Flip: если неоптимизирующий компилятор для конкретной платформы сможет, например, создать мьютекс, не требуя синтаксиса, специфичного для компилятора, компилятор, предназначенный для низкоуровневого программирования на той же платформе, которая уважает Дух C (который, по мнению авторов Стандарта, включает принцип «Не мешайте программисту делать то, что нужно сделать ") должны иметь возможность делать то же самое. Авторы компилятора C Standard думали, что авторы компилятора могли бы распознать, какая семантика понадобится программистам на их целевых платформах, но не смогли подчеркнуть это ...

supercat 12.11.2018 16:47

... Причина, по которой Стандарт не требует таких вещей, заключается в том, что они относятся к вопросам качества выполнения, выходящим за рамки юрисдикции Стандарта.

supercat 12.11.2018 16:49

@supercat На вышедших из строя процессорах невозможно создать мьютекс без использования барьеров памяти. Который до тех пор, пока атомикс не был частью стандарта. Если у вас нет компилятора, создающего неявные барьеры памяти вокруг всех летучих компонентов (например, MSVC). Что в большинстве случаев, без нужды, намного медленнее. Что также противоречит «Духу Си».

Flip 13.11.2018 09:08

@Flip: В абзаце перед примером кода я специально сказал на платформе с естественным упорядочением памяти [категория, которая, кстати, включает более 99% микроконтроллеров, используемых во встроенных системах]. Разумно сказать, что если для платформы требуются барьеры памяти для корректности, то ответственность за такие барьеры лежит на программисте, а не на реализации. Однако, если платформа может гарантировать безупречную корректность, качественный компилятор не должен быть единственным препятствием для этого.

supercat 13.11.2018 21:02

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

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

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

Ответ принят как подходящий

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

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

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

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

    A value of integral type or enumeration type can be explicitly converted to a pointer. A pointer converted to an integer of sufficient size (if any such exists on the implementation) and back to the same pointer type will have its original value; mappings between pointers and integers are otherwise implementation-defined. [ Note: Except as described in [basic.stc.dynamic.safety], the result of such a conversion will not be a safely-derived pointer value. — end note ]

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

    The semantics of an access through a volatile glvalue are implementation-defined. If an attempt is made to access an object defined with a volatile-qualified type through the use of a non-volatile glvalue, the behavior is undefined.

Части, в которых следует избегать UB:

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

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

    A conforming implementation may have extensions (including additional library functions), provided they do not alter the behavior of any well-formed program. [...]

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

    An object is created by a definition ([basic.def]), by a new-expression, when implicitly changing the active member of a union ([class.union]), or when a temporary object is created ([conv.rval], [class.temporary]).

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

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

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

    An implementation may have relaxed pointer safety, in which case the validity of a pointer value does not depend on whether it is a safely-derived pointer value. Alternatively, an implementation may have strict pointer safety, in which case a pointer value referring to an object with dynamic storage duration that is not a safely-derived pointer value is an invalid pointer value [...]. [ Note: The effect of using an invalid pointer value (including passing it to a deallocation function) is undefined, see [basic.stc].

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

Забавная история: in «Объект создается с помощью определения ([basic.def]), с помощью нового выражения, при неявном изменении активного члена объединения ([class.union]) или при создании временного объекта. ([conv.rval], [class.porary]) "текст о союзе новый. Предыдущий C++ std не включал текст объединения, который формально означает, что союзы фактически не поддерживались в C++ раньше был бы позицией нулевого члена комитета. Это многое говорит о разнице между намерением и тем, что комитет C++ излагает на бумаге.

curiousguy 10.11.2018 01:29

(...) За первый стандарт C++ проголосовали в 1997 году. Все последующие версии содержали тот же текст без объединяющей части. Итак, все эти годы люди использовали семантику языка, которая позволяла создавать объекты только с помощью определения, new или неявно компилятором (временный объект). Это, похоже, не беспокоило ни одного эксперта по C++, который полагается на стандартную семантику C++, что вызывает беспокойство.

curiousguy 10.11.2018 01:35

@palotasb: "то можно сказать, что реализация имеет объект C++ uint32_t в этом месте, и нет UB с какой-либо из функций" Но у вас нет доступа к uint32_t; вы получаете доступ к Peripheral, который содержит - это uint32_t. Даже если реализация обещала, что по этому адресу находится uint32_t, она не обещает, что там есть Peripheral. Так что доступ через такой объект по-прежнему UB.

Nicol Bolas 10.11.2018 21:28

@NicolBolas "это не обещает" Отличие без разницы!

curiousguy 11.11.2018 04:07

@curiousguy: В тех случаях, когда всем было очевидно, как должен вести себя фрагмент кода, тот факт, что Стандарт не запрещал компиляторам делать что-то глупое, не рассматривался как проблема, пока авторы компилятора не начали рассматривать тот факт, что Стандарт позволяет обрабатывать этот код определенным образом в качестве доказательства того, что это не сделает реализацию менее полезной.

supercat 11.11.2018 17:08

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

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: как указывает supercat в комментариях, в любой момент времени может быть только один объект C++ в определенной области памяти. Например, если есть несколько наборов регистров, мультиплексированных в блок адресов, вам нужно каким-то образом выразить это с помощью одного объекта C++ (возможно, будет служить объединение), а не несколькими объектами, которым назначен один и тот же базовый адрес.

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

supercat 12.11.2018 23:10

@supercat Я не думаю, что этот код делает что-либо, что могло бы быть нарушено компилятором того типа, который вы описываете.

zwol 13.11.2018 01:51

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

supercat 13.11.2018 05:13

... но это не значит, что будущих компиляторов не будет.

supercat 13.11.2018 05:14

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

zwol 13.11.2018 13:45

Другие вопросы по теме