Доступ к "значению" переменной сценария компоновщика неопределенное поведение в C? - PullRequest
0 голосов
/ 11 апреля 2019

Руководство по GNU ld (скрипт компоновщика) Раздел 3.5.5 Справочник по исходному коду содержит некоторую действительно важную информацию о том, как получить доступ к "переменным" скрипта компоновщика (которые на самом деле являются просто целочисленными адресами) в исходном коде C , Я использовал эту информацию. широко использовать переменные сценария компоновщика, и я написал этот ответ здесь: Как получить значение переменной, определенной в сценарии компоновщика ld, из C .

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

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

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

Вопрос: Итак, если вы делаете пытаетесь получить доступ к значению переменной сценария компоновщика, это "неопределенное поведение"?

Быстрая переподготовка:

Представьте, что в скрипте компоновщика (например: STM32F103RBTx_FLASH.ld ) у вас есть:

/* Specify the memory areas */
MEMORY
{
    FLASH (rx)      : ORIGIN = 0x8000000,  LENGTH = 128K
    RAM (xrw)       : ORIGIN = 0x20000000, LENGTH = 20K
}

/* Some custom variables (addresses) I intend to access from my C source code */
__flash_start__ = ORIGIN(FLASH);
__flash_end__ = ORIGIN(FLASH) + LENGTH(FLASH);
__ram_start__ = ORIGIN(RAM);
__ram_end__ = ORIGIIN(RAM) + LENGTH(RAM);

И в вашем исходном коде C вы делаете:

// 1. correct way A:
extern uint32_t __flash_start__;
printf("__flash_start__ addr = 0x%lX\n", (uint32_t)&__flash_start__);

// OR 2. correct way B (my preferred approach):
extern uint32_t __flash_start__[]; // not a true array; [] is required to access linker script variables (addresses) as though they were normal variables
printf("__flash_start__ addr = 0x%lX\n", (uint32_t)__flash_start__);

// OR 3. COMPLETELY WRONG WAY TO DO IT!
// - IS THIS UNDEFINED BEHAVIOR?
extern uint32_t __flash_start__;
printf("__flash_start__ addr = 0x%lX\n", __flash_start__);

Образец печатной продукции

(это реальный вывод: он был фактически скомпилирован, запущен и напечатан с помощью STM32 mcu):

  1. __flash_start__ addr = 0x8000000
  2. __flash_start__ addr = 0x8000000
  3. __flash_start__ addr = 0x20080000 <== УВЕДОМЛЕНИЕ, КАК Я СКАЗАЛ ВЫШЕ: это <em>совершенно неправильно (даже если он компилируется и работает)!

Обновление:

Ответ на первый комментарий @Eric Postpischil:

Стандарт C вообще ничего не определяет в символах сценария компоновщика. Любая спецификация поведения зависит от инструментов GNU. Тем не менее, если символ сценария компоновщика идентифицирует место в памяти, где хранится некоторый действительный объект, я ожидаю, что доступ к значению этого объекта будет работать, если к нему был получен доступ с его надлежащим типом. Предположим, что flash_start является нормально доступной памятью, и, за исключением любых требований вашей системы относительно того, что находится на flash_start , теоретически вы можете поместить uint32_t (используя соответствующий ввод для компоновщика) и затем получить к нему доступ через flash_start .

Да, но это не мой вопрос. Я не уверен, что вы понимаете тонкость моего вопроса. Посмотрите на примеры, которые я привожу. Это правда, что вы можете получить доступ к этому местоположению очень хорошо, но убедитесь, что вы понимаете , как вы делаете это, и тогда мой вопрос станет очевидным. Посмотрите особенно на пример 3 выше, который является неправильным , хотя для программиста на C это выглядит правильным . Чтобы прочитать uint32_t, например, в __flash_start__, вы должны сделать следующее:

extern uint32_t __flash_start__;
uint32_t u32 = *((uint32_t *)&__flash_start__); // correct, even though it *looks like* you're taking the address (&) of an address (__flash_start__)

ИЛИ это:

extern uint32_t __flash_start__[];
uint32_t u32 = *((uint32_t *)__flash_start__); // also correct, and my preferred way of doing it because it looks more correct to the trained "C-programmer" eye

Но определенно НЕ это:

extern uint32_t __flash_start__;
uint32_t u32 = __flash_start__; // incorrect; <==UPDATE: THIS IS ALSO CORRECT! (and more straight-forward too, actually; see comment discussion under this question)

и НЕ это:

extern uint32_t __flash_start__;
uint32_t u32 = *((uint32_t *)__flash_start__); // incorrect, but *looks* right

Связанный:

1 Ответ

1 голос
/ 11 апреля 2019

Я сказал в вопросе:

// 1. correct way A:
extern uint32_t __flash_start__;
printf("__flash_start__ addr = 0x%lX\n", (uint32_t)&__flash_start__);

// OR 2. correct way B (my preferred approach):
extern uint32_t __flash_start__[]; // not a true array; [] is required to access linker script variables (addresses) as though they were normal variables
printf("__flash_start__ addr = 0x%lX\n", (uint32_t)__flash_start__);

// OR 3. COMPLETELY WRONG WAY TO DO IT!
// - IS THIS UNDEFINED BEHAVIOR?
extern uint32_t __flash_start__;
printf("__flash_start__ addr = 0x%lX\n", __flash_start__);

(см. Обсуждение под вопросом о том, как я пришел к этому).

Особо глядя на № 3 выше :

Ну, на самом деле, если ваша цель - прочитать адрес из __flash_start__, который в данном случае равен 0x8000000, тогда да, это совершенно неправильно. Но это НЕ неопределенное поведение! Вместо этого он фактически читает содержимое (значение) этого адреса (0x8000000) как тип uint32_t. Другими словами, он просто читает первые 4 байта раздела FLASH и интерпретирует их как uint32_t. содержимое (uint32_t значение по этому адресу) в данном случае просто равно 0x20080000.

Чтобы дополнительно доказать эту точку зрения, следующие абсолютно идентичные:

// Read the actual *contents* of the __flash_start__ address as a 4-byte value!
// The 2 techniques should be the same.
extern uint32_t __flash_start__;
uint32_t u32_1 = __flash_start__;
uint32_t u32_2 = *((uint32_t *)&__flash_start__);
printf("u32_1 = 0x%lX\n", u32_1);
printf("u32_2 = 0x%lX\n", u32_2);

Вывод:

u32_1 = 0x20080000
u32_2 = 0x20080000

Обратите внимание, что они дают одинаковый результат. Каждый из них выдает действительное значение uint32_t -типа, которое хранится по адресу 0x8000000.

Оказывается, однако, что техника u32_1, показанная выше, является более прямым и прямым способом чтения значения all, и опять же, это , а не неопределенное поведение. Скорее, он правильно читает значение (содержимое) этого адреса.

Кажется, я говорю по кругу. Во всяком случае, сумасшедший, но теперь я понял. Я был убежден, прежде чем должен был использовать только технику u32_2, показанную выше, но оказалось, что они оба просто хороши, и опять же, техника u32_1 явно более прямолинейна (там я снова говорю кругами ). :)

Приветствие.


Копаем глубже: Откуда взято значение 0x20080000, хранящееся в самом начале моей флэш-памяти?

Еще один маленький кусочек. Я на самом деле запустил этот тестовый код на STM32F777 MCU, который имеет 512 КБ ОЗУ. Поскольку RAM начинается с адреса 0x20000000, это означает, что 0x20000000 + 512K = 0x20080000. Так же бывает и содержимое ОЗУ с нулевым адресом, потому что Руководство по программированию PM0253 Ред. 4 , стр. 42, «Рисунок 10. Таблица векторов» показывает, что первые 4 байта таблицы векторов содержат «Начальное значение SP [Указатель стека]». Смотрите здесь:

enter image description here

Я знаю, что Vector Table находится прямо в начале памяти программ, которая находится во Flash, так что это означает, что 0x20080000 - это мое начальное значение указателя стека. Это имеет смысл, потому что Reset_Handler - это начало программы (и, кстати, ее вектор просто является вторым 4-байтовым значением в начале таблицы векторов), и первое, что он делает, как показано в моем файле запуска сборки " startup_stm32f777xx.s ", указатель стека (sp) установлен в _estack:

Reset_Handler:  
  ldr   sp, =_estack      /* set stack pointer */

Кроме того, _estack определяется в моем скрипте компоновщика следующим образом:

/* Highest address of the user mode stack */
_estack = ORIGIN(RAM) + LENGTH(RAM);    /* end of RAM */

Итак, вот оно! Первое 4-байтовое значение в моей Таблице векторов, в самом начале Flash, задано как начальное значение указателя стека, которое определено как _estack прямо в моем файле сценария компоновщика, а _estack - это адрес в конец моей оперативной памяти, которая составляет 0x20000000 + 512K = 0x20080000. Итак, все это имеет смысл! Я только что доказал, что прочитал правильное значение!

...