Как заставить gcc генерировать стек в среде с «голым металлом»?

Когда я использую GCC для разработки операционной системы ARM, я не могу использовать локальную переменную, потому что стек не был инициализирован, так как же мне указать компилятору инициализировать SP?


person Alan Jian    schedule 03.10.2020    source источник
comment
Я думаю, что вы обычно пишете ассемблер вручную, чтобы инициализировать вещи, включая стек, перед вызовом или переходом к функции, созданной компилятором. Или, если ваше ядро ​​имеет метаданные, считанные загрузчиком, оно может указать стек? IDK, это будет зависеть от того, какой загрузчик вы используете.   -  person Peter Cordes    schedule 03.10.2020
comment
Обычно вы не сообщаете компилятору. Вы говорите компоновщику. Вы связываетесь с фрагментом ассемблерного кода, который инициализирует стек и все, что вам нужно, а затем переходите к вашему коду. Если вы хотите сообщить компилятору, вам нужно написать встроенный ассемблер как первое, что делает ваша программа.   -  person n. 1.8e9-where's-my-share m.    schedule 03.10.2020
comment
@PeterCordes, но если я использую asm (mov sp, # 0x8000);, сгенерированный компилятором код будет использовать push перед инструкцией, как мне заставить компилятор сделать это первым?   -  person Alan Jian    schedule 03.10.2020
comment
@AlanJian Пожалуйста, покажите код, о котором идет речь, а также точные параметры, с которыми вы компилируете. Обычно можно использовать __attribute__((naked)), но это действительно зависит от вашего варианта использования.   -  person fuz    schedule 03.10.2020
comment
Вы неправильно поняли, что я сказал: вы пишете код на языке ассемблера в отдельном файле .S, который настраивает машину на чистом ассемблере, а затем вызывает ваш C как bl main, как n.'pronouns'm. сказал. Не оператор asm внутри вашего C. Он не может вызывать ваш C, потому что он уже находится внутри функции C, как вы указали. (Или, как сказал fuz, вы можете использовать оператор __attribute__((naked)) или asm("") в глобальной области видимости, но, насколько мне известно, у них нет особого преимущества перед отдельным файлом для вашего asm.)   -  person Peter Cordes    schedule 03.10.2020


Ответы (3)


Мой опыт связан с Cortex-M, и, как сказал @n-pronouns-m, стек настраивается компоновщиком, а не компилятором или ассемблером. Все, что необходимо, — это поместить начальное значение указателя стека по адресу 0x0 в памяти программы. Обычно это (самый высокий адрес ОЗУ + 4). Поскольку разные процессоры имеют разный объем оперативной памяти, правильный адрес зависит от процессора и обычно является литералом в файле компоновщика.

person Elliot Alderson    schedule 03.10.2020
comment
Не могли бы вы показать мне пример того, как это сделать в компоновщике на Cortex-M? Интересно, что делает начальный сегмент значения SP в векторной таблице? - person Alan Jian; 03.10.2020
comment
Что означает самый высокий адрес ОЗУ? например, разные модели raspberry pi имеют разную оперативную память, такую ​​как pi4 2g, 4g и 8g, но их процессор одинаковый, поэтому максимальный адрес RAM означает размер кеша? - person Alan Jian; 03.10.2020
comment
@AlanJian Я могу, но для этого мне нужно знать точную модель микроконтроллера, для которой вы программируете. Каждый немного отличается. Обратите внимание, что Raspberry Pi — это Cortex A, а не Cortex M, поэтому он работает совершенно иначе. - person fuz; 03.10.2020
comment
Я использую Raspberry Pi 0 с Arm 6 (Arm1176JZF-S), какой у него максимальный адрес оперативной памяти? - person Alan Jian; 03.10.2020
comment
Производитель чипа или разработчик платы реализует объем памяти, не задействованный ARM. Число пи-ноль в настоящее время имеет 512 МБ драм, о чем вы уже должны знать. На сайте pi есть очень хороший форум по «голому железу» с множеством хороших ссылок и примеров. В документе Broadcom указано, что часть оперативной памяти предназначена для руки, а часть — для графического процессора, но вы можете отрегулировать это в некоторой степени самостоятельно. Для нуля пи, который обычно копирует вашу программу в 0x8000 в пространстве оружия, я обычно устанавливаю указатель стека туда, а не в верхнюю часть памяти. - person old_timer; 03.10.2020
comment
Если ваш вопрос (вам нужно написать более подробный вопрос в следующий раз) о нуле пи, это не cortex-m. Этот ответ предназначен специально для ядер cortex-m, а не для arm11 (armv6) в нуле числа пи. Это не будет работать для пи ноль. Я могу восстановить свой ответ или написать новый (как и Эллиот, если вы действительно спрашиваете о нуле пи), но есть десятки и сотни тысяч примеров для armv4t на ядрах up с таблицей исключений, такой как armv6, поэтому на самом деле нет необходимости ни в вопросе здесь, ни в ответе. - person old_timer; 03.10.2020
comment
вместо векторной таблицы вы просто записываете значение в указатель стека, но есть несколько указателей стека, почти по одному для каждого режима процессора, поэтому какой бы режим вам ни нужно было использовать, вы захотите установить указатель стека в этом случае тот же ответ, что и выше, начните с верхней части памяти и работайте вниз, выберите размер для каждого и просто запишите эти значения в соответствующий указатель стека. - person old_timer; 03.10.2020
comment
@old_timer Очень жаль, ваш ответ был неплохим. Возможно, стоит рассмотреть возможность публикации его отдельно по новому вопросу, который только что задали, чтобы опубликовать его? - person fuz; 05.10.2020

Это вариация кода, который я использую на глобальном уровне в моем чистом металлическом коде C, aarch64, Pi3. Он вызывает функцию C с именем enter, настроив простой стек, учитывая переменную stacks и размер стека, который вы хотите для каждого ядра STACK_SIZE (вы не можете использовать sizeof).

asm (
    "\n.global  _start"
    "\n.type    _start, %function"
    "\n.section .text"
    "\n_start:"
    "\n\tmrs     x0, mpidr_el1"
    "\n\ttst     x0, #0x40000000"
    "\n\tand     x1, x0, #0xff"
    "\n\tcsel    x1, x1, xzr, eq" // core
    "\n\tadr     x0, stacks"
    "\n\tmov     x3, #"STACK_SIZE                                                                                       
    "\n\tmul     x2, x1, x3"
    "\n\tadd     x0, x0, x2"
    "\n\tadd     sp, x0, x3"
    "\n\tb     enter"
    "\n\t.previous"
    "\n.align 10" ); // Alignment to avoid GPU overwriting code
person Simon Willcocks    schedule 12.10.2020

Ваш вопрос сбивает с толку, поскольку вы не указываете цель, для разных разновидностей архитектуры ARM есть разные ответы. Но независимо от этого gcc не имеет к этому никакого отношения. Gcc - это компилятор C, и поэтому вам в идеале нужен загрузчик, написанный на каком-то другом языке (иначе это выглядит плохо, и вы все равно боретесь с проблемой курицы и яйца). Обычно делается на ассемблере.

Для armv4t до ядер armv7-a у вас есть разные режимы процессора, пользовательский, системный, супервизор и т. д. Когда вы смотрите на Architectural Reference Manual, вы видите, что указатель стека сгруппирован, по одному для каждого режима или, по крайней мере, много из режимов есть свой один плюс небольшой обмен. Это означает, что вам нужен способ доступа к этому регистру. Для тех ядер, как это работает, вам нужно переключить режимы, установить режим переключения указателя стека, установить указатель стека, пока у вас не будет всех тех, которые вы собираетесь использовать (см. десятки-сотни тысяч примеров в Интернете относительно как это сделать). А затем часто возвращайтесь в режим супервизора, чтобы затем загрузить приложение/ядро, как вы хотите его назвать.

Затем с armv8-a и, я думаю, с armv7-a также у вас есть режим гипервизора, который отличается. И, конечно же, armv8-a, который является 64-битным ядром (имеет ядро, совместимое с armv7-a, для выполнения aarch32).

Все вышеперечисленное, хотя вам нужно установить указатель стека в вашем коде

reset:
    mov sp,=0x8000

или что-то в этом роде. На ранних версиях Pi это то, что вы могли сделать, так как загрузчик поместил бы ваш kernel.img в 0x8000, если не указано иное, поэтому от чуть ниже точки входа до чуть выше ATAG есть свободное место и после загрузки, если вы используете ATAG, то вы можете перейти к таблице исключений (которую вам нужно настроить, самый простой способ — позволить инструментам работать на вас и сгенерировать адреса, а затем просто скопировать их в нужное место. Такого рода вещи.

.globl _start
_start:
    ldr pc,reset_handler
    ldr pc,undefined_handler
    ldr pc,swi_handler
    ldr pc,prefetch_handler
    ldr pc,data_handler
    ldr pc,unused_handler
    ldr pc,irq_handler
    ldr pc,fiq_handler
reset_handler:      .word reset
undefined_handler:  .word hang
swi_handler:        .word hang
prefetch_handler:   .word hang
data_handler:       .word hang
unused_handler:     .word hang
irq_handler:        .word irq
fiq_handler:        .word hang

reset:
    mov r0,#0x8000
    mov r1,#0x0000
    ldmia r0!,{r2,r3,r4,r5,r6,r7,r8,r9}
    stmia r1!,{r2,r3,r4,r5,r6,r7,r8,r9}
    ldmia r0!,{r2,r3,r4,r5,r6,r7,r8,r9}
    stmia r1!,{r2,r3,r4,r5,r6,r7,r8,r9}


    ;@ (PSR_IRQ_MODE|PSR_FIQ_DIS|PSR_IRQ_DIS)
    mov r0,#0xD2
    msr cpsr_c,r0
    mov sp,#0x8000

    ;@ (PSR_FIQ_MODE|PSR_FIQ_DIS|PSR_IRQ_DIS)
    mov r0,#0xD1
    msr cpsr_c,r0
    mov sp,#0x4000

    ;@ (PSR_SVC_MODE|PSR_FIQ_DIS|PSR_IRQ_DIS)
    mov r0,#0xD3
    msr cpsr_c,r0
    mov sp,#0x8000000

    ;@ SVC MODE, IRQ ENABLED, FIQ DIS
    ;@mov r0,#0x53
    ;@msr cpsr_c, r0

В armv8-m есть таблица исключений, но исключения разнесены, как показано в документации ARM.

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

Cortex-ms, которые являются armv6-m, armv7-m и armv8-m (до сих пор совместимыми с одним или другим), используют векторную таблицу. Это означает, что известные адреса являются векторами, адресами обработчика, а не инструкциями, поэтому вы должны сделать что-то вроде этого

.thumb

.globl _start
_start:
.word 0x20001000
.word reset
.word loop
.word loop
.word loop

.thumb_func
reset:
    bl main
    b .
.thumb_func
loop:
    b .

Как задокументировано ARM, в векторной таблице cortex-m есть запись для инициализации указателя стека, поэтому вам не нужно добавлять код, просто поместите туда адрес. При сбросе логика считывает из 0x00000000, помещает это значение в указатель стека, читает из 0x00000004, проверяет и удаляет lsbit и начинает выполнение по этому адресу (lsbit должен быть установлен в векторной таблице, пожалуйста, не делайте сброс + 1 вещь, правильно пользоваться инструментами).

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

Когда вы читаете справочное руководство по архитектуре, любое из них, вы замечаете, что описание инструкции stm/push сначала выполняет декремент, а затем сохраняет, поэтому, если вы установите 0x20001000, то первое, что будет отправлено, будет по адресу 0x20000FFC, а не 0x20001000, не обязательно верно для не-ARM, поэтому, как всегда, сначала получите и прочитайте документы, а затем начните кодировать.

Вы, программист на «голом железе», несете полную ответственность за карту памяти в реализации поставщика чипа. Итак, если есть 64 КБ оперативной памяти от 0x20000000 до 0x20010000, вы решаете, как это разделить. Очень легко просто пойти с традиционным стеком, сходящим сверху, данными внизу, кучей посередине, хотя зачем вам когда-либо иметь кучу на микроконтроллере, если вы говорите об этом микроконтроллере (вы сделали не указать). Таким образом, для 64-килобайтного ram cortex-m вы, вероятно, просто захотите поместить 0x20010000 в первую запись векторной таблицы, вопрос инициализации указателя стека выполнен. Некоторым людям нравится чрезмерно усложнять сценарии компоновщика в целом, и по какой-то причине я не могу понять, определить стек в сценарии компоновщика. В этом случае вы просто используете переменную, определенную в скрипте компоновщика, чтобы указать вершину стека, и вы используете ее в своей векторной таблице для cortex-m или в коде начальной загрузки для полноразмерного ARM.

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

Для коры-м может быть что-то вроде этого

MEMORY
{
    /* rom : ORIGIN = 0x08000000, LENGTH = 0x1000 *//*AXIM*/
    rom : ORIGIN = 0x00200000, LENGTH = 0x1000 /*ITCM*/
    ram : ORIGIN = 0x20000000, LENGTH = 0x1000
}
SECTIONS
{
    .text   : { *(.text*)   } > rom
    .rodata : { *(.rodata*) } > rom
    .bss    : { *(.bss*)    } > ram
}

Для Pi Zero может быть что-то вроде этого:

MEMORY
{
    ram : ORIGIN = 0x8000, LENGTH = 0x1000
}
SECTIONS
{
    .text : { *(.text*) } > ram
    .rodata : { *(.rodata*) } > ram
    .bss : { *(.bss*) } > ram
    .data : { *(.data*) } > ram
}

и вы можете усложнить это оттуда.

Указатель стека — это простая часть начальной загрузки, в которую вы просто вставили число, выбранное вами при разработке карты памяти. Инициализация .data и .bss более сложна, хотя для |Pi Zero, если вы знаете, что делаете, сценарий компоновщика может быть таким, как указано выше, а начальная загрузка может быть такой простой

reset:
    ldr sp,=0x8000
    bl main
hang: b hang

Если вы не меняете режимы и не используете argc/argv. Вы можете усложнить это оттуда.

Для коры-м можно сделать проще

reset:
    bl main
hang: b hang

Или, если вы не используете .data или .bss или не нуждаетесь в их инициализации, технически вы можете сделать это:

.word 0x20001000
.word main
.word handler
.word handler
...

Но большинство людей, кроме меня, полагаются на то, что .bss будет равен нулю, а .data будет инициализирован. Вы также не можете вернуться из main, что отлично подходит для системы с «голым железом», такой как микроконтроллер, если дизайн вашего программного обеспечения основан на событиях и нет необходимости в переднем плане после настройки всего. Большинство людей думают, что вы не можете вернуться из main.

gcc не имеет ничего общего со всем этим, gcc — это просто компилятор, который он не может собрать, он не может связать, он даже не может скомпилировать, gcc — это внешний интерфейс, который вызывает другие инструменты, выполняющие эту работу, синтаксический анализатор, компилятор, ассемблер и компоновщик, если только сказал не делать этого. Парсер и компилятор являются частью gcc. Ассемблер и компоновщик являются частью другого пакета, называемого binutils, который содержит множество бинарных утилит, а также включает ассемблер gnu или gas. Он также включает компоновщик gnu. Языки ассемблера специфичны для ассемблера, а не цели, скрипты компоновщика специфичны для компоновщика, а встроенная сборка специфична для компилятора, поэтому не предполагается, что эти вещи переносятся из одной цепочки инструментов в другую. Как правило, неразумно использовать встроенную сборку, вы должны быть в отчаянии, лучше использовать настоящую сборку или вообще не использовать ее, зависит от того, в чем заключается реальная проблема. Но да, с gnu вы можете встроить бутстрап, если действительно чувствуете в этом необходимость.

Если это вопрос о Raspberry Pi, загрузчик графического процессора копирует программу ARM в оперативную память для вас, поэтому все это находится в оперативной памяти, что делает ее намного проще по сравнению с другим «голым железом». Для mcu, хотя логика просто загружается с использованием задокументированного решения, вы несете ответственность за инициализацию оперативной памяти, поэтому, если у вас есть какие-либо .data или .bss, которые вы хотите инициализировать, вы должны сделать это в начальной загрузке. Информация должна быть в энергонезависимой оперативной памяти, поэтому вы используете компоновщик, чтобы сделать две вещи: поместить эту информацию в энергонезависимую память (ПЗУ/флеш-память), а также указать, где вы собираетесь хранить ее в оперативной памяти, если вы используете инструменты правильно, компоновщик сообщит вам, помещал ли он каждую вещь во флэш-память/память, и затем вы можете программно использовать переменные для инициализации этих пространств. (перед вызовом main, конечно).

По этой причине существует очень тесная связь между начальной загрузкой и сценарием компоновщика для платформы, где вы отвечаете за .data и .bss (плюс другие сложности, которые вы создаете, для решения которых вы используете компоновщик). Конечно, с gnu, когда вы используете свой дизайн карты памяти, чтобы указать, где будут жить разделы .text, .data, .bss, вы создаете переменные в скрипте компоновщика, чтобы знать начальную точку, конечную точку и/или размер, и эти переменные используется загрузчиком для копирования/инициализации этих разделов. Поскольку asm и скрипт компоновщика зависят от инструмента, ожидается, что они не будут переносимыми, поэтому вам придется переделывать его, возможно, для каждого инструмента (где C более переносим, ​​если вы не используете встроенный asm и прагмы и т. д. (нет необходимости в тех в любом случае)) поэтому чем проще решение, тем меньше кода вам нужно портировать, если вы хотите попробовать приложение на разных инструментах, хотите поддерживать разные инструменты, чтобы конечный пользователь мог использовать приложение и т. д.

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

person old_timer    schedule 03.10.2020