Я обучаю своих студентов работе с микроконтроллером STM32F411RE, на борту которого имеется аж целых 512 кБайт ROM и 128 кБайт ОЗУ
Обычно на этом микроконтроллере в ROM память записывается программа, а в RAM изменяемые данные и очень часто нужно сделать так, чтобы константы лежали в ROM.
В микроконтроллере STM32F411RE, ROM память расположена по адресам с 0x08000000...0x0807FFFF, а RAM с 0x20000000...0x2001FFFF.
И если все настройки линкера правильные, студент рассчитывает, что вот в таком незамысловатом коде его константа лежит в ROM:
Вы тоже можете пробовать ответить на вопрос: где лежит константа myConstInROM в ROM или в RAM?
Если вы ответили на этот вопрос, что в ROM, поздравляю вас, на самом деле скорее всего вы не правы, константа в общем случае будет лежать в RAM и чтобы разобраться, как правильно и законно расположить ваши константы в ROM — добро пожаловать под кат.
Вначале небольшое отступление, зачем вообще заморачиваться по этому поводу.
При разработке «safety critical» ПО для измерительных устройств, соответствующих стандарту IEC 61508-3:2010 или отечественного аналога ГОСТ IEC 61508-3-2018 приходится принимать во внимание ряд моментов, которые не являются критическими для обычного ПО.
Основной посыл этого стандарт заключается в том, что ПО должно обнаружить любой отказ, влияющий на надежность системы и перевести систему в режим «аварии»
Кроме очевидных механических поломок, например, вывода из строя или деградация сенсора и отказа электронных компонентов, должны выявляться ошибки, вызванные отказом окружением ПО, например, RAM или ROM микроконтроллера.
И если в первых двух случаях, обнаружить ошибку можно только довольно замороченными косвенными способами (есть алгоритмы, определяющие отказ сенсоров, например, Метод оценки состояния термопреобразователя сопротивления), то в случае отказа окружения ПО, это можно сделать намного проще, например, отказ памяти можно проверять, простой проверкой целостности данных. Если целостность данных нарушена, то интерпретировать это как отказ памяти.
Если данные долго лежат в RAM без проверки и обновления, то вероятность того, что с ними что-то случится из-за отказа RAM становится выше с течением времени. Примером могут служить какие-нибудь коэффициенты калибровки для расчета температуры, которые были заданы на производстве и записаны во внешнюю EEPROM, при запуске они считываются и записываются в RAM и находятся там до тех пор, пока питание не отключат. А в жизни, датчик температуры может работать весь период межповерочного интервала, до 3-5 лет. Очевидно, что такие RAM данные необходимо защищать и периодически проверять их целостность.
Но бывают и такие данные, как скажем просто объявленная для читабельности константа, объект драйвера LCD, SPI или I2C, которые не должны изменяться, создаются единожды и не удаляются до выключения питания.
Эти данные лучше держать в ROM. Она надежнее с точки зрения технологии и проверить её намного проще, достаточно, периодически считать контрольную сумму всей постоянной памяти в какой-нибудь низкоприоритетной задаче. При несовпадении контрольной суммы можно просто отрапортовать об отказе ROMи система диагностики выставит аварию.
Если бы эти данные лежали в RAM, определить их целостность было бы проблематично или даже невозможно из-за того, что неясно где в ОЗУ лежат неизменяемые данные, а где изменяемые, линкер размещает их как хочет, а защищать каждый объект ОЗУ контрольной суммой выглядит как паранойя.
Поэтому проще всего — быть уверенным на 100%, что константные данные лежат в ROM. Как это сделать я хочу попробовать объяснить. Но для начала надо рассказать об организации памяти в ARM.
Как известно, ядро ARM имеет гарвардскую архитектуру — шина данных и кода разделены. Обычно это означает, что предполагается наличие отдельной памяти для программ и отдельной памяти для данных. Но дело в том, что ARM является модифицированной гарвардской архитектурой, т.е. доступ к памяти осуществляется по одной шине, а уже устройство управления памятью обеспечивает разделение шин при помощи управляющих сигналов: чтения, записи или выбора области памяти.
Таким образом данные и код могут находится в одной и той же области памяти. В этом едином адресном пространстве может находится и ROM память и RAM и периферия. А это означает, что собственно и код и данные могут попасть хоть куда и это зависит от компилятора и линкера.
Поэтому чтобы различить области памяти для ROM(Flash) и ОЗУ их обычно указывают в настройках линкера, например в IAR 8.40.1 это выглядит вот так:
RAM в данном микроконтроллере находится с адреса 0x20000000...0х2001FFF, а ROM с 0x008000000...0x0807FFFF.
Вы легко можете поменять начальный адрес ROM_start на адрес ОЗУ, скажем RAM_start и конечный адрес ROM_end__ на адрес RAM_end__ и ваша программа будет полностью расположена в ОЗУ.
Вы даже можете сделать наоборот и указать ОЗУ в области памяти ROM, и ваша программа успешно соберется и прошьется, правда работать не будет :)
Некоторые микроконтроллеры, такие как, AVR изначально имеют раздельное адресное пространство для памяти программ, памяти данных и периферии и потому там такие фокусы не пройдут, а программа по умолчанию записывается в ROM память.
Выше я упомянул про сегмент кода, давайте разберемся, что это такое.
На каждый компилируемый модуль создается отдельный объектный файл, который содержит следующую информацию:
Нас интересуют сегменты кода и данных. Сегмент это такой элемент, содержащий часть кода или данных, который должен быть помещен по физическому адресу в памяти. Сегмент может содержать несколько фрагментов, обычно один фрагмент на каждую переменную или функцию. Сегмент может быть помещен как в ROM так и RAM.
Каждый сегмент имеет имя и атрибут, который определяет его содержимое. Атрибут используется для определения сегмента в конфигурации для линкера. Например, атрибуты могут быть:
Конечно есть и другие типы сегментов, например сегменты, содержащие отладочную информацию, но нас будут интересовать только те, которые содержат код или данные нашего приложения.
В общем и целом, сегмент это наименьший линкуемый блок. Однако при необходимости линкеру можно указать и еще более мелкие блоки (фрагменты). Этот вариант рассматривать не будем, обойдемся сегментами.
Во время компиляции данные и функции размещаются в различные сегменты. А во время линковки, линкер назначает реальные физические адреса различным сегментам. В компиляторе IAR есть предопределенные имена сегментов, некоторые из них я приведу ниже:
Для того, чтобы понять где размещаются константы, нам будут интересны только сегменты
.rodata — сегмент в котором хранятся константы,
.data — сегмент в котором хранятся все проинициализированные статические и глобальные переменные,
.bss — сегмент в котором хранятся все проинициализированные нулем(0) статические и глобальные переменные .data,
.text — сегмент для хранения кода.
На практике это означает, что если вы определили переменную
Сегмент .rodata содержит константные данные и включает в себя константные переменные, строки, агрегатные литералы и так далее. И этот сегмент может быть размещена где угодно в памяти.
Теперь становится понятнее, что прописано в настройках линкера и почему:
Т.е все данные помеченные атрибутом readonly должны быть помещены в ROM_region. Таким образом в ROM могут попадать данные из разных сегментов, но помеченные атрибутом readonly.
Перед тем как прояснить ситуацию, вначале давайте вспомним, что глобальные переменные создаются в общей памяти, локальные переменные, т.е. перемененные, объявленные внутри «обычных» функций создаются в стеке или в регистрах, а статические локальные переменные также создаются в общей памяти.
Что это значит на языке С++. Давайте рассмотрим такой пример:
Это все константные данные. Но к любым из них применяется правило создания описанное выше, локальные переменные создаются на стеке. Поэтому, при наших настройках линкера, здесь должно быть так:
Теперь посмотрим на отладочную информацию и сгенерированный map файл. Отладчик показывает по каким адресам лежат эти константы.
![image](https://habrastorage.org/r/w1560/webt/n_/x9/at/n_x9atq0wduhrlaqbrhiudu5qc8.png)
Как я уже говорил раньше, адреса 0x0800… это адреса ROM, а 0x200… это RAM. Давайте посмотрим, в какие сегменты распределил компилятор эти константы:
Четыре глобальных и статических константы попали в сегмент .rodata, а две локальные переменные не попали в map файл поскольку создаются на стеке и их адрес соответствует адресам стека. Сегмент CSTACK начинается по адресу 0x2000'2488 и заканчивается на 0x2000'0488. Как видно из картинки, константы как раз созданы на начале стека.
Стоит отметить еще один важный момент, инициализация. Глобальные и статические переменные, в том числе и константы должны быть проинициализированы. И это может быть сделано несколькими способами. Если это константа лежащая в сегменте .rodata, инициализация происходит на этапе компиляции, т.е. значение сразу же прописывается по адресу, где находится константа. Если это обычная переменная, то инициализация может происходить путем копирования значения из ROM памяти по адресу глобальной переменной:
Например, если определена глобальная переменная
а её значении для инициализации (3), будет лежать в сегменте .rodata по адресу 0x8000190:
Если же написать такой код:
То очевидно, что глобальная константа
Теперь если мы вернемся к нашему
Почему? Потому что в С++ объект данных, который задекларирован как константа, и для которого необходима динамическая инициализация, располагается в read-write памяти и он будет проинициализирован во время создания..
В данном случае, происходит динамическая инициализация,
Как же можно сделать так, чтобы компилятор расположил наш объект в сегменте .rodata? Ответ на этот вопрос прост, нужно всегда выполнять статическую инициализацию. Сделать это можно так:
1. В нашем примере видно, что в принципе компилятор может оптимизировать динамическую инициализацию в статическую, поскольку конструктор довольно прост. Для IAR компилятора можно пометить константу атрибутом __ro_placement
При такой опции, компилятор расположит переменную по адресу в ROM:
Очевидно, что такой подход не универсальный и в общем то очень специфический. Поэтому переходим к правильному способу :)
2. Он заключается в том, чтобы сделать конструктор
Итак, для того, чтобы быть уверенным, что ваш константный объект лежит в ROM, необходимо выполнять простые правила:
Немного слов о constexpr и об constexpr конструкторе. Основное различие между переменными const и constexpr является то, что инициализация переменной const может быть отложена до времени выполнения. Переменная constexpr должна инициализироваться во время компиляции.
Все переменные constexpr относятся к типу const.
Определение constexpr конструктора, должно удовлетворять следующим требованиям:
Неявно определенный конструктор по умолчанию является конструктором constexpr. Теперь давайте посмотрим на примеры:
Так лучше не писать, потому что, как только вы решите проинициализировать атрибут i, объект улетит в RAM
Ну и на последок, константный объект, содержащий массив, с инициализацией массива через constexpr функцию.
Ссылки:
IAR C/C++ Development Guide
Constexpr constructors (C++11)
constexpr (C++)
PS.
После очень полезной дискуссии с Valdaros необходимо добавить следующий момент касательный констант. Согласно стандарту С++ и вот этому документу N1076.pdf
1. Любое изменение константного объекта (за исключением mutable членов класса) в течении его жизни приводит к Undefined Behaviour. Т.е.
2. Проблема заключается в том, что это работает только в период всей жизни константного объекта, а вот в конструкторе и деструкторе это не работает. Поэтому, вполне законно можно сделать так:
И это считается законным. Несмотря на то, что мы использовали constexpr конструктор, и constexpr функцию в нем. Объект идет прямиком в RAM.
Чтобы этого избежать, используйте вместо const — constexpr, тогда будет ошибка компиляции, которая укажет вам, что что-то не так, и объект не может быть константным.
Обычно на этом микроконтроллере в ROM память записывается программа, а в RAM изменяемые данные и очень часто нужно сделать так, чтобы константы лежали в ROM.
В микроконтроллере STM32F411RE, ROM память расположена по адресам с 0x08000000...0x0807FFFF, а RAM с 0x20000000...0x2001FFFF.
И если все настройки линкера правильные, студент рассчитывает, что вот в таком незамысловатом коде его константа лежит в ROM:
class WantToBeInROM
{
private:
int i;
public:
WantToBeInROM(int value): i(value) {}
int Get() const
{
return i;
}
};
const WantToBeInROM myConstInROM(10);
int main()
{
std::cout << &myConstInROM << std::endl ;
}
Вы тоже можете пробовать ответить на вопрос: где лежит константа myConstInROM в ROM или в RAM?
Если вы ответили на этот вопрос, что в ROM, поздравляю вас, на самом деле скорее всего вы не правы, константа в общем случае будет лежать в RAM и чтобы разобраться, как правильно и законно расположить ваши константы в ROM — добро пожаловать под кат.
Введение
Вначале небольшое отступление, зачем вообще заморачиваться по этому поводу.
При разработке «safety critical» ПО для измерительных устройств, соответствующих стандарту IEC 61508-3:2010 или отечественного аналога ГОСТ IEC 61508-3-2018 приходится принимать во внимание ряд моментов, которые не являются критическими для обычного ПО.
Основной посыл этого стандарт заключается в том, что ПО должно обнаружить любой отказ, влияющий на надежность системы и перевести систему в режим «аварии»
Кроме очевидных механических поломок, например, вывода из строя или деградация сенсора и отказа электронных компонентов, должны выявляться ошибки, вызванные отказом окружением ПО, например, RAM или ROM микроконтроллера.
И если в первых двух случаях, обнаружить ошибку можно только довольно замороченными косвенными способами (есть алгоритмы, определяющие отказ сенсоров, например, Метод оценки состояния термопреобразователя сопротивления), то в случае отказа окружения ПО, это можно сделать намного проще, например, отказ памяти можно проверять, простой проверкой целостности данных. Если целостность данных нарушена, то интерпретировать это как отказ памяти.
Если данные долго лежат в RAM без проверки и обновления, то вероятность того, что с ними что-то случится из-за отказа RAM становится выше с течением времени. Примером могут служить какие-нибудь коэффициенты калибровки для расчета температуры, которые были заданы на производстве и записаны во внешнюю EEPROM, при запуске они считываются и записываются в RAM и находятся там до тех пор, пока питание не отключат. А в жизни, датчик температуры может работать весь период межповерочного интервала, до 3-5 лет. Очевидно, что такие RAM данные необходимо защищать и периодически проверять их целостность.
Но бывают и такие данные, как скажем просто объявленная для читабельности константа, объект драйвера LCD, SPI или I2C, которые не должны изменяться, создаются единожды и не удаляются до выключения питания.
Эти данные лучше держать в ROM. Она надежнее с точки зрения технологии и проверить её намного проще, достаточно, периодически считать контрольную сумму всей постоянной памяти в какой-нибудь низкоприоритетной задаче. При несовпадении контрольной суммы можно просто отрапортовать об отказе ROMи система диагностики выставит аварию.
Если бы эти данные лежали в RAM, определить их целостность было бы проблематично или даже невозможно из-за того, что неясно где в ОЗУ лежат неизменяемые данные, а где изменяемые, линкер размещает их как хочет, а защищать каждый объект ОЗУ контрольной суммой выглядит как паранойя.
Поэтому проще всего — быть уверенным на 100%, что константные данные лежат в ROM. Как это сделать я хочу попробовать объяснить. Но для начала надо рассказать об организации памяти в ARM.
Организация памяти
Как известно, ядро ARM имеет гарвардскую архитектуру — шина данных и кода разделены. Обычно это означает, что предполагается наличие отдельной памяти для программ и отдельной памяти для данных. Но дело в том, что ARM является модифицированной гарвардской архитектурой, т.е. доступ к памяти осуществляется по одной шине, а уже устройство управления памятью обеспечивает разделение шин при помощи управляющих сигналов: чтения, записи или выбора области памяти.
Таким образом данные и код могут находится в одной и той же области памяти. В этом едином адресном пространстве может находится и ROM память и RAM и периферия. А это означает, что собственно и код и данные могут попасть хоть куда и это зависит от компилятора и линкера.
Поэтому чтобы различить области памяти для ROM(Flash) и ОЗУ их обычно указывают в настройках линкера, например в IAR 8.40.1 это выглядит вот так:
define symbol __ICFEDIT_region_ROM_start__ = 0x08000000;
define symbol __ICFEDIT_region_ROM_end__ = 0x0807FFFF;
define symbol __ICFEDIT_region_RAM_start__ = 0x20000000;
define symbol __ICFEDIT_region_RAM_end__ = 0x2001FFFF;
define region ROM_region = mem:[from __ICFEDIT_region_ROM_start__ to __ICFEDIT_region_ROM_end__];
define region RAM_region = mem:[from __ICFEDIT_region_RAM_start__ to __ICFEDIT_region_RAM_end__];
RAM в данном микроконтроллере находится с адреса 0x20000000...0х2001FFF, а ROM с 0x008000000...0x0807FFFF.
Вы легко можете поменять начальный адрес ROM_start на адрес ОЗУ, скажем RAM_start и конечный адрес ROM_end__ на адрес RAM_end__ и ваша программа будет полностью расположена в ОЗУ.
Вы даже можете сделать наоборот и указать ОЗУ в области памяти ROM, и ваша программа успешно соберется и прошьется, правда работать не будет :)
Некоторые микроконтроллеры, такие как, AVR изначально имеют раздельное адресное пространство для памяти программ, памяти данных и периферии и потому там такие фокусы не пройдут, а программа по умолчанию записывается в ROM память.
Все адресное пространство в CortexM единое, и код и данные могут размещаться где угодно. С помощью настроек линкера можно задать регион для адресов ROM и RAM памяти. IAR располагает сегмент кода .text в регионе ROM памяти
Объектный файл и сегменты
Выше я упомянул про сегмент кода, давайте разберемся, что это такое.
На каждый компилируемый модуль создается отдельный объектный файл, который содержит следующую информацию:
- Сегменты кода и данных
- Отладочную информацию в формате DWARF
- Таблицу символов
Нас интересуют сегменты кода и данных. Сегмент это такой элемент, содержащий часть кода или данных, который должен быть помещен по физическому адресу в памяти. Сегмент может содержать несколько фрагментов, обычно один фрагмент на каждую переменную или функцию. Сегмент может быть помещен как в ROM так и RAM.
Каждый сегмент имеет имя и атрибут, который определяет его содержимое. Атрибут используется для определения сегмента в конфигурации для линкера. Например, атрибуты могут быть:
- code — исполняемый код
- readonly — константные переменные
- readwrite — инициализируемые переменные
- zeroinit — инициализируемые нулем переменные
Конечно есть и другие типы сегментов, например сегменты, содержащие отладочную информацию, но нас будут интересовать только те, которые содержат код или данные нашего приложения.
В общем и целом, сегмент это наименьший линкуемый блок. Однако при необходимости линкеру можно указать и еще более мелкие блоки (фрагменты). Этот вариант рассматривать не будем, обойдемся сегментами.
Во время компиляции данные и функции размещаются в различные сегменты. А во время линковки, линкер назначает реальные физические адреса различным сегментам. В компиляторе IAR есть предопределенные имена сегментов, некоторые из них я приведу ниже:
- .bss — Содержит статические и глобальные переменные инициализируемые 0
- .CSTACK — Содержит стек используемый программой
- .data — Содержит статические и глобальные инициализируемые переменные
- .data_init — Содержит начальные значения для данных в .data секции, если используется директива инициализации для линкера
- HEAP — Содержит кучу, используемую для размещения динамических данных
- .intvec — Содержит таблицу векторов прерываний
- .rodata — Содержит константные данные
- .text — Содержит код программы
Для того, чтобы понять где размещаются константы, нам будут интересны только сегменты
.rodata — сегмент в котором хранятся константы,
.data — сегмент в котором хранятся все проинициализированные статические и глобальные переменные,
.bss — сегмент в котором хранятся все проинициализированные нулем(0) статические и глобальные переменные .data,
.text — сегмент для хранения кода.
На практике это означает, что если вы определили переменную
int val = 3
, то сама переменная будет расположена компилятором в сегмент .data и помечена атрибутом readwrite, а число 3 может быть помещено либо в сегмент .text, либо в сегмент .rodata или, если применена специальная директива для линкера в .data_init и также помечается им как readonly.Сегмент .rodata содержит константные данные и включает в себя константные переменные, строки, агрегатные литералы и так далее. И этот сегмент может быть размещена где угодно в памяти.
Теперь становится понятнее, что прописано в настройках линкера и почему:
place in ROM_region { readonly }; // Разместить сегменты .rodata и .data_init (константы и инициализаторы) в ROM:
place in RAM_region { readwrite, // Разместить сегменты .data, .bss, и .noinit
block STACK }; // и STACK и HEAP в RAM
Т.е все данные помеченные атрибутом readonly должны быть помещены в ROM_region. Таким образом в ROM могут попадать данные из разных сегментов, но помеченные атрибутом readonly.
Отлично значит все константы должны быть в ROM, но почему же в нашем коде, в начале статьи константный объект все еще лежит в ОЗУ?
class WantToBeInROM
{
private:
int i;
public:
WantToBeInROM(int value): i(value) {}
int Get() const
{
return i;
}
};
const WantToBeInROM myConstInROM(10);
int main()
{
std::cout << &myConstInROM << std::endl ;
}
Константные данные
Перед тем как прояснить ситуацию, вначале давайте вспомним, что глобальные переменные создаются в общей памяти, локальные переменные, т.е. перемененные, объявленные внутри «обычных» функций создаются в стеке или в регистрах, а статические локальные переменные также создаются в общей памяти.
Что это значит на языке С++. Давайте рассмотрим такой пример:
void foo(const int& C1, const int& C2, const int& C3,
const int& C4, const int& C5, const int& C6)
{
std::cout << C1 << C2 << C3 << C4 << C5 << C6 << std::endl;
}
//константа рассчитанная на этапе компиляции
constexpr int Case1 = 1 ;
//глобальная константа (а возможно и рассчитанная на этапе компиляции)
const int Case2 = 2;
int main()
{
//локальная временная константа.
const int Case3 = 3 ;
//статическая константа.
static const int Case4 = 4 ;
//как бы константа рассчитанная на этапе компиляции, но на самом деле нет.
constexpr int Case5 = Case1 + 5 ;
//статическая константа рассчитанная на этапе компиляции.
static constexpr int Case6 = 6 ;
foo(Case1,Case2,Case3,Case4,Case5,Case6);
return 1;
}
Это все константные данные. Но к любым из них применяется правило создания описанное выше, локальные переменные создаются на стеке. Поэтому, при наших настройках линкера, здесь должно быть так:
- Case1 глобальная константа должна лежать в ROM. В сегменте .rodata
- Case2 глобальная константа должна лежать в ROM. В сегменте .rodata
- Case3 локальная константа должна лежать в RAM (константа создалась на стеке в сегменте STACK)
- Case4 статическая константа должна лежать в ROM. В сегменте .rodata
- Case5 локальная константа должна лежать в RAM (интересный случай, но он в точности идентичен Случаю 3.)
- Case6 статическая константа должна лежать в ROM. В сегменте .rodata
Теперь посмотрим на отладочную информацию и сгенерированный map файл. Отладчик показывает по каким адресам лежат эти константы.
![image](https://habrastorage.org/webt/n_/x9/at/n_x9atq0wduhrlaqbrhiudu5qc8.png)
Как я уже говорил раньше, адреса 0x0800… это адреса ROM, а 0x200… это RAM. Давайте посмотрим, в какие сегменты распределил компилятор эти константы:
.rodata const 0x800'4e2c 0x4 main.o //Case1
.rodata const 0x800'4e30 0x4 main.o //Case2
.rodata const 0x800'4e34 0x4 main.o //Case4
.rodata const 0x800'4e38 0x4 main.o //Case6
Четыре глобальных и статических константы попали в сегмент .rodata, а две локальные переменные не попали в map файл поскольку создаются на стеке и их адрес соответствует адресам стека. Сегмент CSTACK начинается по адресу 0x2000'2488 и заканчивается на 0x2000'0488. Как видно из картинки, константы как раз созданы на начале стека.
Глобальные и статические константы компилятор помещает в сегмент .rodata, расположение которого задается в настройках линкера.
Стоит отметить еще один важный момент, инициализация. Глобальные и статические переменные, в том числе и константы должны быть проинициализированы. И это может быть сделано несколькими способами. Если это константа лежащая в сегменте .rodata, инициализация происходит на этапе компиляции, т.е. значение сразу же прописывается по адресу, где находится константа. Если это обычная переменная, то инициализация может происходить путем копирования значения из ROM памяти по адресу глобальной переменной:
Например, если определена глобальная переменная
int i = 3
, то компилятор определил её в сегмент данных .data, линкер положил по адресу 0x20000000: .data inited 0x2000'0000
, а её значении для инициализации (3), будет лежать в сегменте .rodata по адресу 0x8000190:
Initializer bytes const 0x800'0190
Если же написать такой код:
int i = 3;
const int c = i;
То очевидно, что глобальная константа
с
, инициализируется, только после того, как проинициализирована глобальная переменная i
, т. е в runtime. В таком случае константа будет расположена в RAM Теперь если мы вернемся к нашему
начальному примеру
И зададимся вопросом: в какой же сегмент компилятор определил константный объект class WantToBeInROM
{
private:
int i;
public:
WantToBeInROM(int value): i(value) {}
int Get() const
{
return i;
}
};
const WantToBeInROM myConstInROM(10);
int main()
{
std::cout << &myConstInROM << std::endl ;
}
myConstInROM
? И получим ответ: константа будет лежать в сегменте .bss, содержащий статические и глобальные переменные инициализируемые нулем(0)..bss inited 0x2000'0004 0x4
myConstInROM 0x2000'0004 0x4
Почему? Потому что в С++ объект данных, который задекларирован как константа, и для которого необходима динамическая инициализация, располагается в read-write памяти и он будет проинициализирован во время создания..
В данном случае, происходит динамическая инициализация,
const WantToBeInROM myConstInROM(10)
, и компилятор положил этот объект в сегмент .bss, проинициализировав вначале все поля 0, а затем при создании константного объекта, вызвал конструктор для инициализации поля i
значением 10.Как же можно сделать так, чтобы компилятор расположил наш объект в сегменте .rodata? Ответ на этот вопрос прост, нужно всегда выполнять статическую инициализацию. Сделать это можно так:
1. В нашем примере видно, что в принципе компилятор может оптимизировать динамическую инициализацию в статическую, поскольку конструктор довольно прост. Для IAR компилятора можно пометить константу атрибутом __ro_placement
__ro_placement const WantToBeInROM myConstInROM
При такой опции, компилятор расположит переменную по адресу в ROM:
myConstInROM 0x800'0144 0x4 Data
Очевидно, что такой подход не универсальный и в общем то очень специфический. Поэтому переходим к правильному способу :)
2. Он заключается в том, чтобы сделать конструктор
constexpr
. Мы сразу указываем компилятору использовать статическую инициализацию, т.е. на этапе компиляции, когда весь объект будет полностью «вычислен» заранее и все его поля будут известны. Все что нам нужно, это добавить constexpr к конструктору.Объект улетает в ROM
class WantToBeInROM
{
private:
int i;
public:
constexpr WantToBeInROM(int value): i(value) {}
int Get() const
{
return i;
}
};
const WantToBeInROM myConstInROM(10);
int main()
{
std::cout << &myConstInROM << std::endl ;
}
Итак, для того, чтобы быть уверенным, что ваш константный объект лежит в ROM, необходимо выполнять простые правила:
- Сегмент .text в котором размещен код должен находится в ROM. Настраивается в настройках линкера.
- Сегмент .rodata в котором размещены глобальные и статические константы должен быть в ROM. Настраивается в настройках линкера.
- Константа должна быть глобальной или статической
- Атрибуты класса константной переменной не должны быть mutable
- Инициализация объекта должна быть статической, т.е.конструктор класса, объект которого будет константой, должен быть constexpr, либо вообще не определен(отсутствует динамическая инициализация)
- По возможности, если вы уверены, что объект должен храниться в ROM вместо const используйте constexpr
Немного слов о constexpr и об constexpr конструкторе. Основное различие между переменными const и constexpr является то, что инициализация переменной const может быть отложена до времени выполнения. Переменная constexpr должна инициализироваться во время компиляции.
Все переменные constexpr относятся к типу const.
Определение constexpr конструктора, должно удовлетворять следующим требованиям:
- Класс не должен иметь виртуальных базовых классов.
struct D2 : virtual BASE { //error, D2 must not have virtual base class. constexpr D2() : BASE(), mem(55) { } private: int mem; };
- Каждый из типов параметров класса должен быть литеральным типом.
- Тело конструктора должно быть
= delete
или= default
. Или удовлетворять требованиям ниже: - В теле конструктора нет
try catch
блоков. - В теле конструктора может использоваться
nullptr
- В теле конструктора может использоваться
static_assert
- В теле конструктора может использоваться
typedef
, не определяющие классы или перечисления - В теле конструктора может использоваться директива и декларации
using
- Каждый нестатический член класса или базового класса должен быть проинициализирован
- Конструкторы класса или базовых классовых, используемый для инициализации нестатических элементов членов класса и подобъектов базового класса, должны быть
constexpr
. - Инициализаторы для всех нестатических элементов данных, должны быть
constexpr
- При инициализации членов класса все преобразования типов должны быть допустимы в константном выражении. Например недопустимо использование
reinterpret_cast
и приведение изvoid*
в указатель другого типа
Неявно определенный конструктор по умолчанию является конструктором constexpr. Теперь давайте посмотрим на примеры:
Пример 1. Объект в ROM
class Test
{
private:
int i;
public:
Test() {} ;
int Get() const
{
return i + 1;
}
} ;
const Test test; //объект в ROM. Конструктор ничего не инициализирует. i инициализируется 0 по умолчанию.
int main()
{
std::cout << test.Get() << std::endl ;
return 0;
}
Так лучше не писать, потому что, как только вы решите проинициализировать атрибут i, объект улетит в RAM
Пример 2. Объект в RAM
class Test
{
private:
int i = 1; //инициализируем i. Инициализация происходит во время вызова не constexpr конструктора.
public:
Test() {} ; //В данном случае лучше вообще не определять конструктор, тогда по умолчанию он будет восприниматься компилятором, как constexpr
int Get() const
{
return i + 1;
}
} ;
const Test test; //объект в RAM. i инициализируется при создании объекта
int main()
{
std::cout << test.Get() << std::endl ;
return 0;
}
Пример 3. Объект в RAM
class Test
{
private:
int i;
public:
Test(int value): i(value) {} ;
int Get() const
{
return i + 1;
}
} ;
const Test test(10); //объект в RAM. i инициализируется при создании объекта
int main()
{
std::cout << test.Get() << std::endl ;
return 0;
}
Пример 4. Объект в ROM
class Test
{
private:
int i;
public:
constexpr Test(int value): i(value) {} ;
int Get() const
{
return i + 1;
}
} ;
const Test test(10); //объект в ROM. i инициализируется на этапе компиляции constexpr конструктором
int main()
{
std::cout << test.Get() << std::endl ;
return 0;
}
Пример 5. Объект в RAM
class Test
{
private:
int i;
public:
constexpr Test(int value): i(value) {} ;
int Get() const
{
return i + 1;
}
} ;
int main()
{
const Test test(10); //объект в RAM. создается на стеке
std::cout << test.Get() << std::endl ;
return 0;
}
Пример 6. Объект в ROM
class Test
{
private:
int i;
public:
constexpr Test(int value): i(value) {} ;
int Get() const
{
return i + 1;
}
} ;
int main()
{
static const Test test(10); //объект в ROM. Статический константный объект
std::cout << test.Get() << std::endl ;
return 0;
}
Пример 7. Ошибка компиляции
class Test
{
private:
int i;
public:
constexpr Test(int value): i(value) {} ;
int Get() //Метод Get не константный, компилятор думает, что он может менять данные объекта (i), хотя на самом деле нет. Необходимо указывать компилятору, что метод не будет менять состояния объекта.
{
return i + 1;
}
} ;
const Test test(10);
int main()
{
std::cout << test.Get() << std::endl ;
return 0;
}
Пример 8. Объект в ROM, наследование абстрактного класса
class ITest
{
private:
int j;
public:
virtual int Get() const = 0;
constexpr ITest(int value) : j(value)
{
}
int Give() const
{
return j ;
}
};
class Test: public ITest
{
private:
int i;
public:
constexpr Test(int value): i(value), ITest(value+1) {} ;
int Get() const override
{
return i + 1;
}
} ;
const Test test(10); //объект в ROM. i инициализируется на этапе компиляции constexpr конструктором, j также инициализируется constexpr конструктором ITest
int main()
{
std::cout << test.Give() << std::endl ;
return 0;
}
Пример 9. Объект в ROM агрегирует объект, находящийся в RAM
class ITest
{
protected:
int j;
public:
virtual int Get() const = 0;
constexpr ITest(int value) : j(value)
{
}
int Give() const
{
return j ;
}
};
class TestImpl: public ITest
{
private:
int k;
public:
TestImpl(int value): k(value), ITest(value)
{
}
int Get() const override
{
return j + 10;
}
void Set(int value)
{
k = value;
j = value + 10;
}
} ;
TestImpl testImpl(1); //глобальный неконстантный объект в RAM.
class Test: public ITest
{
private:
int i;
TestImpl & obj; //Ссылка на неконстантный объект
public:
constexpr Test(int value, TestImpl & ref): i(value), obj(ref), ITest(value+1)
{
} ;
int Get() const override
{
return i + 1;
}
bool Set() const
{
obj.Set(100) ; //объект по ссылке можно изменять
return true;
}
} ;
constexpr Test test(10, testImpl); //объект в ROM. Инициализируется на этапе компиляции constexpr конструктором
int main()
{
std::cout << test.Set() << std::endl ;
return 0;
}
Пример 10. Тоже самое но статические объект в ROM
class ITest
{
protected:
int j;
public:
virtual int Get() const = 0;
constexpr ITest(int value) : j(value)
{
}
int Give() const
{
return j ;
}
};
class TestImpl: public ITest
{
private:
int k;
public:
TestImpl(int value): k(value), ITest(value)
{
}
int Get() const override
{
return j + 10;
}
void Set(int value)
{
k = value;
j = value + 10;
}
} ;
class Test: public ITest
{
private:
int i;
TestImpl & obj; //Ссылка на неконстантный объект
public:
constexpr Test(int value, TestImpl & ref): i(value), obj(ref),ITest(value+1)
{
} ;
int Get() const override
{
return i + 1;
}
bool Set() const
{
obj.Set(100) ; //объект по ссылке можно изменять
return true;
}
} ;
int main()
{
static TestImpl testImpl(1); //статический объект
static constexpr Test test(10, testImpl); //статический константный объект в ROM. Инициализируется на этапе компиляции constexpr конструктором
std::cout << test.Set() << std::endl ;
return 0;
}
Пример 11. А теперь константный объект нестатический и поэтому в RAM
class ITest
{
protected:
int j;
public:
virtual int Get() const = 0;
constexpr ITest(int value) : j(value)
{
}
int Give() const
{
return j ;
}
};
class TestImpl: public ITest
{
private:
int k;
public:
TestImpl(int value): k(value), ITest(value)
{
}
int Get() const override
{
return j + 10;
}
void Set(int value)
{
k = value;
j = value + 10;
}
} ;
class Test: public ITest
{
private:
int i;
TestImpl & obj; //Ссылка на неконстантный объект
public:
constexpr Test(int value, TestImpl & ref): i(value), obj(ref),ITest(value+1)
{
} ;
int Get() const override
{
return i + 1;
}
bool Set() const
{
obj.Set(100) ; //объект по ссылке можно изменять
return true;
}
} ;
int main()
{
static TestImpl testImpl(1); //статический объект
const Test test(10, testImpl); //локальный константный объект в RAM.
std::cout << test.Set() << std::endl ;
return 0;
}
Пример 12. Ошибка компиляции.
class ITest
{
protected:
int j;
public:
virtual int Get() const = 0;
constexpr ITest(int value) : j(value)
{
}
int Give() const
{
return j ;
}
};
class TestImpl: public ITest
{
private:
int k;
public:
TestImpl(int value): k(value), ITest(value)
{
}
int Get() const override
{
return j + 10;
}
void Set(int value)
{
k = value;
j = value + 10;
}
} ;
class Test: public ITest
{
private:
int i;
TestImpl obj; //Объявляем объект типа TestImpl
public:
constexpr Test(int value): i(value), obj(TestImpl(value)), //попытка вызова не constexpr конструктора класса TestImpl
ITest(value+1)
{
} ;
int Get() const
{
return i + 1;
}
bool Set() const
{
obj.Set(100) ; //объект по ссылке можно изменять
return true;
}
} ;
int main()
{
static TestImpl testImpl(1); //статический объект
static const Test test(10); //статический константный объект
std::cout << test.Set() << std::endl ;
return 0;
}
Пример 13. Ошибка компиляции
class ITest
{
protected:
int j;
public:
virtual int Get() const = 0;
constexpr ITest(int value) : j(value)
{
}
int Give() const
{
return j ;
}
};
class TestImpl: public ITest
{
private:
int k;
public:
constexpr TestImpl(int value): k(value), ITest(value) // Теперь конструктор constexpr
{
}
int Get() const override
{
return j + 10;
}
void Set(int value) //Метод изменяет состояние объекта, поля k и j. Эти поля не могут лежать в ROM, и объект тоже может находиться только в RAM
{
k = value;
j = value + 10;
}
} ;
class Test: public ITest
{
private:
int i;
TestImpl obj;
public:
constexpr Test(int value): i(value), obj(TestImpl(value)), //вызов constexpr конструктора для создания объект obj, который создал объект obj в .rodata сегменте.
ITest(value+1)
{
} ;
int Get() const
{
return i + 1;
}
bool Set() const
{
obj.Set(100) ; //объект нельзя поменять так как он был создан constexpr конструктором во время компиляции
return true;
}
} ;
int main()
{
static TestImpl testImpl(1); //статический объект
static const Test test(10); //статический константный объект
std::cout << test.Set() << std::endl ;
return 0;
}
Пример 14. Объект в ROM
class ITest
{
protected:
int j;
public:
virtual int Get() const = 0;
constexpr ITest(int value) : j(value)
{
}
int Give() const
{
return j ;
}
};
class TestImpl: public ITest
{
private:
int k;
public:
constexpr TestImpl(int value): k(value), ITest(value)
{
}
int Get() const override
{
return j + 10;
}
void Set(int value) const //метод должен быть const
{
//do something
}
} ;
class Test: public ITest
{
private:
int i;
const TestImpl obj; //Константный объект, созданный с помощью constexpr конструктора
public:
constexpr Test(int value): i(value), obj(TestImpl(value)), ITest(value+1)
{
} ;
int Get() const
{
return i + 1;
}
bool Set() const
{
obj.Set(100) ; //вызов константного метода
return true;
}
} ;
int main()
{
//static TestImpl testImpl(1); //статический объект
static const Test test(10); //статический константный объект в ROM. Инициализируется на этапе компиляции constexpr конструктором
std::cout << test.Set() << std::endl ;
return 0;
}
Ну и на последок, константный объект, содержащий массив, с инициализацией массива через constexpr функцию.
class Test
{
private:
int k[100];
constexpr void InitArray()
{
int i = 0;
for(auto& it: k)
{
it = i++ ;
}
}
public:
constexpr Test(): k()
{
InitArray(); //вызываем constexpr функцию для инициализации массива
}
int Get(int index) const
{
return k[index];
}
} ;
int main()
{
static const Test test; //статический константный объект в ROM. Там же находится и массив, проинициализированный constexpr конструктором.
std::cout << test.Get(10) << std::endl ;
return 0;
}
Ссылки:
IAR C/C++ Development Guide
Constexpr constructors (C++11)
constexpr (C++)
PS.
После очень полезной дискуссии с Valdaros необходимо добавить следующий момент касательный констант. Согласно стандарту С++ и вот этому документу N1076.pdf
1. Любое изменение константного объекта (за исключением mutable членов класса) в течении его жизни приводит к Undefined Behaviour. Т.е.
const int ci = 1 ;
int* iptr = const_cast<int*>(&ci); //UB, пытаемся снять константность у константной переменной
*iptr = 2 ;
int i = 1;
const int* ci = &i ;
int* iptr = const_cast<int *> (ci); //можно снять константность
*iptr = 2 ; //не UB, поменяли данные i
2. Проблема заключается в том, что это работает только в период всей жизни константного объекта, а вот в конструкторе и деструкторе это не работает. Поэтому, вполне законно можно сделать так:
class Test
{
public:
int i;
constexpr Test(): i(0) {
foo(this) ;
}
} ;
Test *test1;
constexpr void foo(Test* value)
{
value->i = 1; //во первых поменяли значение константного объекта с 0 на 1
test1 = value ; // во вторых еще и передали указатель на адрес константного объекта
}
const Test test;
int main()
{
test1->i = 2; // а теперь еще и изменили поле константного объекта на 2.
std::cout << &test << std::endl;
}
И это считается законным. Несмотря на то, что мы использовали constexpr конструктор, и constexpr функцию в нем. Объект идет прямиком в RAM.
Чтобы этого избежать, используйте вместо const — constexpr, тогда будет ошибка компиляции, которая укажет вам, что что-то не так, и объект не может быть константным.
class Test
{
public:
int i;
constexpr Test(): i(0) {
foo(this) ;
}
} ;
Test *test1;
constexpr void foo(Test* value)
{
value->i = 1; //во первых поменяли значение константного объекта с 0 на 1
test1 = value ; // во вторых еще и передали указатель на адрес константного объекта
}
constexpr Test test; //ошибка компиляции/ Error[Pe2400]: calling the default constructor for "Test" does not produce a constant value main.cpp 151
int main()
{
test1->i = 2; // а теперь еще и изменили поле константного объекта на 2.
std::cout << &test << std::endl;
}