Фиксированная переменная адреса в C
Для встроенных приложений часто требуется доступ к ячейкам фиксированной памяти для периферийных регистров. Стандартный способ, который я нашел для этого, выглядит примерно так:
// access register 'foo_reg', which is located at address 0x100
#define foo_reg *(int *)0x100
foo_reg = 1; // write to foo_reg
int x = foo_reg; // read from foo_reg
Я понимаю, как это работает, но я не понимаю, как распределяется пространство для foo_reg (т.е. что удерживает компоновщик от размещения другой переменной в 0x100?). Может ли пространство зарезервировано на уровне C, или же должна быть опция компоновщика, которая указывает, что ничего не должно быть расположено на 0x100. Я использую инструменты GNU (gcc, ld и т.д.), Поэтому меня в основном интересует специфика этого набора инструментов на данный момент.
Некоторая дополнительная информация о моей архитектуре, чтобы прояснить вопрос:
Мой процессор взаимодействует с FPGA через набор регистров, отображаемых в обычное пространство данных (где живут переменные) процессора. Поэтому мне нужно указать на эти регистры и заблокировать связанное адресное пространство. Раньше я использовал компилятор с расширением для определения переменных из C-кода. Я бы сгруппировал регистры в структуру, а затем разместил структуру в соответствующем месте:
typedef struct
{
BYTE reg1;
BYTE reg2;
...
} Registers;
Registers regs _at_ 0x100;
regs.reg1 = 0;
Фактически создание структуры "Регистры" резервирует пространство в глазах компилятора/компоновщика.
Теперь, используя инструменты GNU, я, очевидно, не имею расширения. Использование метода указателя:
#define reg1 *(BYTE*)0x100;
#define reg2 *(BYTE*)0x101;
reg1 = 0
// or
#define regs *(Registers*)0x100
regs->reg1 = 0;
Это простое приложение без ОС и без расширенного управления памятью. По существу:
void main()
{
while(1){
do_stuff();
}
}
Ответы
Ответ 1
Ваш компоновщик и компилятор не знают об этом (конечно, вы ничего не говорите). Это для дизайнера ABI вашей платформы, чтобы указать, что они не выделяют объекты по этим адресам.
Итак, иногда (на платформе, над которой я работал, был) диапазон в виртуальном адресном пространстве, который отображается непосредственно на физические адреса и другой диапазон, который может использоваться процессами пространства пользователя для увеличения стека или выделения кучи Память.
Вы можете использовать опцию defsym с GNU ld для выделения некоторого символа по фиксированному адресу:
--defsym symbol=expression
Или, если выражение сложнее простой арифметики, используйте собственный компоновщик script. Это место, где вы можете определить области памяти и рассказать компоновщику, какие регионы должны быть предоставлены тем разделам/объектам. См. здесь для объяснения. Хотя это, как правило, именно работа писателя в цепочке инструментов, которую вы используете. Они берут спецификацию ABI, а затем записывают сценарии компоновщика и ассемблерные/компиляторы, которые отвечают требованиям вашей платформы.
Кстати, GCC имеет атрибут section
, который можно использовать для размещения вашей структуры в определенном разделе. Затем вы можете рассказать компоновщику, чтобы разместить этот раздел в регионе, где живут ваши регистры.
Registers regs __attribute__((section("REGS")));
Ответ 2
Линкером обычно будет использовать компоновщик script, чтобы определить, где будут выделяться переменные. Это называется секцией "данные" и, конечно же, должно указывать на местоположение ОЗУ. Поэтому невозможно, чтобы переменная была выделена по адресу не в ОЗУ.
Подробнее о скриптах-линкерах в GCC здесь.
Ответ 3
Ваш компоновщик обрабатывает размещение данных и переменных. Он знает о вашей целевой системе через компоновщик script. Компонент script определяет области в макете памяти, например .text
(для постоянных данных и кода) и .bss
(для вашего глобальные переменные и кучу), а также создает корреляцию между виртуальным и физическим адресом (если это необходимо). Это задача компоновщика компоновщика script, чтобы убедиться, что разделы, используемые компоновщиком, не переопределяют ваши адреса IO.
Ответ 4
Когда встроенная операционная система загружает приложение в память, он будет загружать его, как правило, в определенном месте, допустим, 0x5000. Вся используемая вами локальная память будет относиться к этому адресу, то есть int x будет где-то вроде 0x5000 + размер кода + 4... если это глобальная переменная. Если это локальная переменная, то она находится в стеке. Когда вы ссылаетесь на 0x100, вы ссылаетесь на пространство системной памяти, то же пространство, на которое операционная система отвечает за управление, и, возможно, на очень конкретное место, которое оно контролирует.
Компонент не будет размещать код в определенных ячейках памяти, он работает в области "относительно моего программного кода".
Это немного сокращается, когда вы попадаете в виртуальную память, но для встроенных систем это имеет тенденцию оставаться верным.
Ура!
Ответ 5
Получение инструментальной цепочки GCC для получения изображения, подходящего для использования непосредственно на оборудовании без загрузки ОС, возможно, но включает в себя несколько шагов, которые обычно не требуются для обычных программ.
-
Вам почти наверняка потребуется настроить модуль запуска времени выполнения C. Это модуль сборки (часто называемый как crt0.s
), который отвечает за инициализацию инициализированных данных, очистку BSS, вызывающих конструкторов для глобальных объектов, если включены модули С++ с глобальными объектами и т.д. Типичные настройки включают в себя необходимость настройки (возможно, включая настройку контроллера DRAM), так что есть место для размещения данных и стека. Некоторым ЦП необходимо, чтобы эти вещи выполнялись в определенной последовательности: например. В ColdFire MCF5307 есть один выбор микросхемы, который отвечает на каждый адрес после загрузки, который в конечном итоге должен быть сконфигурирован для покрытия только области карты памяти, запланированной для присоединенного чипа.
-
В вашей группе оборудования (или у вас есть еще одна шляпа, возможно) должна быть карта памяти, документирующая то, что находится на разных адресах. ROM на 0x00000000, оперативная память 0x10000000, регистры устройств на 0xD0000000 и т.д. В некоторых процессорах команда оборудования могла только подключить чип-выбор от CPU к устройству и оставить его вам решать, какие триггеры адресов выбирают вывод.
-
GNU ld поддерживает очень гибкий язык компоновщика script, который позволяет размещать различные разделы исполняемого изображения в определенных адресных пространствах. Для нормального программирования вы никогда не видите компоновщик script, так как запас один поставляется gcc, который настроен на ваши предположения ОС для обычного приложения.
-
Результат компоновщика находится в перемещаемом формате, который предназначен для загрузки в ОЗУ операционной системой. Вероятно, у него есть исправления для перемещения, которые необходимо выполнить, и даже динамически загружать некоторые библиотеки. В ROM-системе динамическая загрузка (обычно) не поддерживается, поэтому вы не будете этого делать. Но вам все равно нужен необработанный двоичный образ (часто в формате HEX, подходящий для программиста PROM той или иной формы), поэтому вам нужно будет использовать утилиту objcopy из binutil, чтобы преобразовать вывод компоновщика в подходящий формат.
Итак, чтобы ответить на фактический вопрос, который вы задали...
Вы используете компоновщик script, чтобы указать целевые адреса каждой части вашего образа программы. В этом script у вас есть несколько вариантов работы с регистрами устройств, но все они включают в себя размещение сегментов текста, данных, bss и кучи в диапазонах адресов, которые избегают аппаратных регистров. Существуют также доступные механизмы, которые могут гарантировать, что ld выдает ошибку, если вы переполняете свой ПЗУ или ОЗУ, и вы также должны использовать их.
Фактически получение адресов устройств в вашем C-коде можно сделать с помощью #define
, как в вашем примере, или путем объявления символа непосредственно в компоновщике script, который разрешен к базовому адресу регистров, с соответствующими extern
в файле заголовка C.
Хотя атрибут GCC section
можно использовать для определения экземпляра неинициализированного struct
как расположенного в определенном разделе (например, FPGA_REGS
), я обнаружил, что не работает в реальных системах, Он может создавать проблемы обслуживания, и он становится дорогостоящим способом описания полной карты регистров встроенных устройств. Если вы используете этот метод, компоновщик script будет отвечать за сопоставление FPGA_REGS
с его правильным адресом.
В любом случае вам нужно будет получить хорошее представление о концепциях объектных файлов, таких как "разделы" (в частности, текстовые, данные и секции bss, как минимум), и, возможно, потребуется преследовать детали, которые разрыв между оборудованием и программным обеспечением, таким как таблица векторов прерываний, приоритеты прерываний, режимы супервизора или пользователя (или кольца от 0 до 3 по вариантам x86) и т.п.
Ответ 6
Обычно эти адреса недоступны для вашего процесса. Итак, ваш компоновщик не посмел бы разместить там вещи.
Ответ 7
Если ячейка памяти имеет особое значение в вашей архитектуре, компилятор должен знать это и не помещать туда никаких переменных. Это будет похоже на сопоставленное пространство ввода-вывода на большинстве архитектур. Он не знает, что вы используете его для хранения значений, он просто знает, что обычные переменные не должны туда идти. Многие встроенные компиляторы поддерживают языковые расширения, которые позволяют объявлять переменные и функции в определенных местах, обычно используя #pragma
. Кроме того, как правило, я видел, как люди реализуют вид сопоставления памяти, который вы пытаетесь сделать, это объявить int в нужном месте памяти, а затем просто рассматривать его как глобальную переменную. В качестве альтернативы вы можете объявить указатель на int и инициализировать его по этому адресу. Оба они обеспечивают большую безопасность типов, чем макрос.
Ответ 8
Чтобы расширить флажок, вы также можете использовать опцию --just-symbols=
{symbolfile}, чтобы определить несколько символов, если у вас есть более чем несколько устройств с отображением памяти. Файл символа должен быть в формате
symbolname1 = address;
symbolname2 = address;
...
(Кажется, что требуются пробелы вокруг знака равенства.)
Ответ 9
Часто для встроенного программного обеспечения вы можете определить в файле компоновщика одну область ОЗУ для переменных, назначенных линкером, и отдельную область для переменных в абсолютных местоположениях, которые линкер не коснется.
Несоблюдение этого должно привести к ошибке компоновщика, поскольку он должен заметить, что он пытается поместить переменную в местоположение, которое уже используется переменной с абсолютным адресом.
Ответ 10
Это зависит от того, какую ОС вы используете. Я предполагаю, что вы используете что-то вроде DOS или vxWorks. Как правило, система будет иметь области сертификации в памяти, зарезервированные для аппаратного обеспечения, а компиляторы для этой платформы всегда будут достаточно умны, чтобы избежать этих областей для своих собственных распределений. В противном случае вы будете постоянно записывать случайный мусор на дисковые или линейные принтеры, когда вы хотите получить доступ к переменным.
В случае, если что-то еще вас сбивает с толку, я должен также указать, что #define
- это препроцессорная директива. Для этого не создается код. Он просто сообщает компилятору текстовую замену любого foo_reg
, который он видит в вашем исходном файле с помощью *(int *)0x100
. Это не что иное, как просто набрав *(int *)0x100
в себе везде, где у вас есть foo_reg
, кроме того, что он может выглядеть более чистым.
Вместо этого я бы вместо этого (в современном компиляторе C):
// access register 'foo_reg', which is located at address 0x100
const int* foo_reg = (int *)0x100;
*foo_reg = 1; // write to foo_regint
x = *foo_reg; // read from foo_reg