Исправлена ​​переменная адреса в C - PullRequest
16 голосов
/ 25 марта 2009

Для встроенных приложений часто необходим доступ к фиксированным ячейкам памяти для периферийных регистров. Стандартный способ, который я нашел для этого, выглядит примерно так:

// 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 и т. Д.), Поэтому в настоящее время меня больше всего интересует специфика этого набора инструментов.

Дополнительная информация о моей архитектуре для уточнения вопроса:

Мой процессор взаимодействует с ПЛИС через набор регистров, сопоставленных с обычным пространством данных (где живут переменные) процессора. Поэтому мне нужно указать на эти регистры и заблокировать соответствующее адресное пространство. В прошлом я использовал компилятор, который имел расширение для поиска переменных из кода Си. Я бы сгруппировал регистры в структуру, а затем разместил бы структуру в соответствующем месте:

typedef struct
{ 
   BYTE reg1;
   BYTE reg2;
   ...
} Registers;

Registers regs _at_ 0x100;

regs.reg1 = 0;

На самом деле создание структуры 'Registers' резервирует место в глазах компилятора / компоновщика.

Теперь, используя инструменты 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();
    }
}

Ответы [ 10 ]

12 голосов
/ 25 марта 2009

Ваш компоновщик и компилятор не знают об этом (конечно, вы ничего не сказали). Разработчик ABI вашей платформы должен указать, что они не размещают объекты по этим адресам.

Итак, иногда (на платформе, над которой я работал) был диапазон в виртуальном адресном пространстве, который отображается непосредственно на физические адреса, и другой диапазон, который может использоваться процессами пространства пользователя для увеличения стека или выделения кучи. объем памяти.

Вы можете использовать опцию defsym с GNU ld для выделения некоторого символа по фиксированному адресу:

--defsym symbol=expression

Или, если выражение сложнее, чем простая арифметика, используйте пользовательский скрипт компоновщика. Это место, где вы можете определить области памяти и сообщить компоновщику, какие области должны быть заданы для каких разделов / объектов. См. здесь для объяснения. Хотя обычно это работа автора цепочки инструментов, которую вы используете. Они берут спецификации ABI, а затем пишут сценарии компоновщика и бэкэнды ассемблера / компилятора, которые отвечают требованиям вашей платформы.

Кстати, у GCC есть атрибут section, который вы можете использовать для помещения своей структуры в определенный раздел. Затем вы можете указать компоновщику разместить этот раздел в регионе, где находятся ваши регистры.

Registers regs __attribute__((section("REGS")));
9 голосов
/ 25 марта 2009

Компоновщик обычно использует скрипт компоновщика, чтобы определить, где будут размещаться переменные. Это называется разделом «данные» и, конечно, должен указывать на расположение ОЗУ. Поэтому невозможно разместить переменную по адресу, который не находится в ОЗУ.

Подробнее о скриптах компоновщика вы можете прочитать в GCC здесь .

5 голосов
/ 25 марта 2009

Ваш компоновщик обрабатывает размещение данных и переменных. Он знает о вашей целевой системе через скрипт компоновщика. Сценарий компоновщика определяет регионы в макете памяти , например .text (для постоянных данных и кода) и .bss (для глобальных переменных и кучи), а также создает корреляцию между виртуальным и физический адрес (если он нужен). Работа сопровождающего скрипта компоновщика заключается в том, чтобы убедиться, что секции, используемые компоновщиком, не переопределяют ваши адреса ввода-вывода.

3 голосов
/ 26 марта 2009

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

  1. Вам почти наверняка понадобится настроить модуль запуска времени выполнения C. Это модуль сборки (часто называемый чем-то вроде crt0.s), который отвечает за инициализацию инициализированных данных, очистку BSS, вызов конструкторов для глобальных объектов, если включены модули C ++ с глобальными объектами, и т. Д. Типичные настройки включают необходимость настройки аппаратное обеспечение для фактического обращения к ОЗУ (возможно, включая настройку контроллера DRAM), чтобы было место для размещения данных и стека Некоторые процессоры должны выполнять эти действия в определенной последовательности: например, ColdFire MCF5307 имеет один выбор чипа, который отвечает на каждый адрес после загрузки, который в конечном итоге должен быть сконфигурирован так, чтобы охватывать только область карты памяти, запланированную для подключенного чипа.

  2. Ваша аппаратная группа (или вы, возможно, с другой шляпой) должны иметь карту памяти, на которой записано, что находится по разным адресам. ПЗУ по адресу 0x00000000, ОЗУ по адресу 0x10000000, устройство регистрируется по адресу 0xD0000000 и т. Д. В некоторых процессорах команда аппаратного обеспечения могла подключить только микросхему выбора из ЦП к устройству и оставить на ваше усмотрение решение, какие триггеры адреса выбирают вывод .

  3. GNU ld поддерживает очень гибкий язык сценариев компоновщика, который позволяет размещать различные разделы исполняемого образа в определенных адресных пространствах. Для обычного программирования вы никогда не увидите скрипт компоновщика, так как стандартный пакет поставляется gcc, который настроен на предположения вашей ОС для обычного приложения.

  4. Вывод компоновщика в перемещаемом формате, который предназначен для загрузки в ОЗУ операционной системой. Он, вероятно, имеет исправления перемещения, которые необходимо завершить, и может даже динамически загружать некоторые библиотеки. В системе ПЗУ динамическая загрузка (обычно) не поддерживается, поэтому вы не будете этого делать. Но вам все еще нужно сырое двоичное изображение (часто в формате HEX, подходящем для программиста PROM какой-либо формы), поэтому вам нужно будет использовать утилиту objcopy из binutil для преобразования вывода компоновщика в подходящий формат.

Итак, чтобы ответить на вопрос, который вы задали ...

Вы используете скрипт компоновщика, чтобы указать целевые адреса каждого раздела изображения вашей программы. В этом сценарии у вас есть несколько вариантов работы с регистрами устройств, но все они включают в себя размещение сегментов текста, данных, bss-стека и кучи в диапазонах адресов, которые избегают аппаратных регистров. Существуют также механизмы, которые могут гарантировать, что ld выдает ошибку, если вы переполняете ПЗУ или ОЗУ, и вы должны также использовать их.

Фактически получение адресов устройств в ваш код на C можно сделать с помощью #define, как в вашем примере, или путем объявления символа непосредственно в скрипте компоновщика, который разрешается в базовый адрес регистров, с соответствием extern объявление в заголовочном файле C.

Хотя можно использовать атрибут GCC section для определения экземпляра неинициализированного struct как находящегося в определенном разделе (например, FPGA_REGS), я обнаружил, что в реальных системах он не очень хорошо работает , Это может создать проблемы с обслуживанием, и это становится дорогим способом описания полной карты регистров встроенных устройств. Если вы используете эту технику, скрипт компоновщика будет отвечать за отображение FPGA_REGS на его правильный адрес.

В любом случае вам потребуется хорошее понимание концепций объектных файлов, таких как «разделы» (в частности, разделы «текст», «данные» и «bss»), и вам, возможно, придется искать детали, которые соединяют разрыв между аппаратным и программным обеспечением, таким как таблица векторов прерываний, приоритеты прерываний, режимы супервизор и пользовательский режим (или звонки от 0 до 3 в вариантах x86) и т. п.

3 голосов
/ 25 марта 2009

Когда встроенная операционная система загружает приложение в память, оно обычно загружает его в определенном месте, скажем, 0x5000. Вся локальная память, которую вы используете, будет относиться к этому адресу, то есть int x будет где-то примерно 0x5000 + размер кода + 4 ... при условии, что это глобальная переменная. Если это локальная переменная, она находится в стеке. Когда вы ссылаетесь на 0x100, вы ссылаетесь на пространство системной памяти, то же пространство, за которое операционная система отвечает за управление, и, возможно, очень специфическое место, которое она отслеживает.

Компоновщик не будет размещать код в определенных местах памяти, он работает в «относительно того места, где находится мой программный код».

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

Ура!

1 голос
/ 28 марта 2009

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

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

1 голос
/ 25 марта 2009

Чтобы расширить ответ от litb, вы также можете использовать опцию --just-symbols= {symbolfile}, чтобы определить несколько символов, если у вас более пары устройств с отображением в памяти. Файл символов должен быть в формате

symbolname1 = address;
symbolname2 = address;
...

(пробелы вокруг знака равенства кажутся необходимыми.)

1 голос
/ 25 марта 2009

Если расположение памяти имеет особое значение в вашей архитектуре, компилятор должен это знать и не помещать туда никаких переменных. Это было бы похоже на отображаемое пространство ввода-вывода на большинстве архитектур. Он не знает, что вы используете его для хранения значений, он просто знает, что обычные переменные не должны идти туда. Многие встроенные компиляторы поддерживают языковые расширения, которые позволяют вам объявлять переменные и функции в определенных местах, обычно используя #pragma. Кроме того, обычно я видел, как люди реализуют тот тип отображения памяти, который вы пытаетесь сделать, - это объявить int в нужном месте памяти, а затем просто обработать его как глобальную переменную. Кроме того, вы можете объявить указатель на int и инициализировать его по этому адресу. Оба из них обеспечивают большую безопасность типов, чем макрос.

1 голос
/ 25 марта 2009

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

0 голосов
/ 25 марта 2009

Это немного зависит от того, какую ОС вы используете. Я предполагаю, что вы используете что-то вроде 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
...