Почему локальные переменные стека вызывающих абонентов сохраняются в регистрах стека Callee? - PullRequest
0 голосов
/ 29 августа 2018

Я стараюсь изо всех сил узнать о стеке вызовов и структуре фреймов стека в ARM Cortex-M0, это оказывается немного сложным, но с терпением я учусь. У меня есть несколько вопросов на этот вопрос, так что, надеюсь, вы, ребята, сможете помочь мне во всех областях. Вопросы, которые у меня есть, будут выделены жирным шрифтом в этом объяснении.

Я использую ARM Cortex-M0 с GDB и просто программу для отладки. Вот моя программа:

int main(void) {
    static uint16_t myBits;
    myBits = 0x70;

    halInit();

    return 0;
}

У меня установлена ​​точка останова на halInit(). Затем я выполняю команду info frame на своем терминале GDB, чтобы получить этот вывод:

Stack level 0, frame at 0x20000400:
pc = 0x80000d8 in main (src/main.c:63); saved pc 0x8002dd2
source language c.
Arglist at 0x200003e8, args: 
Locals at 0x200003e8, Previous frame's sp is 0x20000400
Saved registers:
 r0 at 0x200003e8, r1 at 0x200003ec, r4 at 0x200003f0, r5 at 0x200003f4, r6 at 0x200003f8, lr at 0x200003fc

Я объясню, как я это интерпретирую, пожалуйста, дайте мне знать, если я прав.

Stack level 0: текущий уровень кадра стека. 0 всегда будет представлять вершину стека, другими словами текущий используемый кадр стека.

frame at 0x20000400: представляет расположение стекового кадра во флэш-памяти.

pc = 0x80000d8 in main (src/main.c:63);: Это представляет следующее выполнение, которое должно быть выполнено, то есть значение счетчика программы. Поскольку программный счетчик всегда представляет следующую команду, которая должна быть выполнена.

saved pc 0x8002dd2: Это немного сбивает меня с толку, но я думаю, что это означает адрес возврата, по сути, инструкцию, которая будет выполнена, когда он вернется из выполнения функции halInit(). Однако, если я наберу команду info reg в моем терминале GDB, я вижу, что регистр связи - это не это значение, а следующий адрес: lr 0x8002dd3. Почему это?

source language c.: представляет используемый язык.

Arglist at 0x200003e8, args:: Это представляет начальный адрес моих аргументов, которые были переданы в кадр стека. Поскольку args: пусто, это означает, что аргументы не переданы. Это объясняется двумя причинами: это первый кадр стека в стеке вызовов, и моя функция не имеет аргументов int main(void).

Locals at 0x200003e8: Это начальный адрес моих локальных переменных. Как вы можете видеть в моем исходном фрагменте кода, у меня должна быть одна локальная переменная myBits. Мы вернемся к этому позже.

Previous frame's sp is 0x20000400: это указатель стека, который указывает на верхнюю часть фрейма стека вызывающих. Поскольку это первый кадр стека, я ожидаю, что это значение должно равняться адресу текущего кадра, который он делает.

Saved registers:
r0 at 0x200003e8
r1 at 0x200003ec
r4 at 0x200003f0
r5 at 0x200003f4
r6 at 0x200003f8
lr at 0x200003fc

Это регистры, которые были помещены в стек для последующего сохранения в текущем кадре стека. Эта часть мне интересна, потому что это первый кадр стека, так почему бы он сохранил так много регистров? Если я выполню команду info reg, я получу следующий вывод:

r0             0x20000428   0x20000428
r1             0x0  0x0
r2             0x0  0x0
r3             0x70 0x70
r4             0x80000c4    0x80000c4
r5             0x20000700   0x20000700
r6             0xffffffff   0xffffffff
r7             0xffffffff   0xffffffff
r8             0xffffffff   0xffffffff
r9             0xffffffff   0xffffffff
r10            0xffffffff   0xffffffff
r11            0xffffffff   0xffffffff
r12            0xffffffff   0xffffffff
sp             0x200003e8   0x200003e8
lr             0x8002dd3    0x8002dd3
pc             0x80000d8    0x80000d8 <main+8>
xPSR           0x21000000   0x21000000

Это говорит мне, что если я проверю значения, сохраненные в каждом из адресов памяти сохраненных регистров, выполнив команду p/x *(register), то значения должны быть равны значениям, показанным в выходных данных выше.

Saved registers:
r0 at 0x200003e8 -> 0x20000428
r1 at 0x200003ec -> 0x0
r4 at 0x200003f0 -> 0x80000c4
r5 at 0x200003f4 -> 0xffffffff
r6 at 0x200003f8 -> 0xffffffff
lr at 0x200003fc -> 0x8002dd3

Работает, значения в каждом адресе представляют значения, показанные командой info reg. Однако я заметил одну вещь. У меня есть одна локальная переменная myBits со значением 0x70, и, похоже, она хранится в r3. Однако r3 не помещается в стек для сохранения.

Если мы перейдем к следующей инструкции, для функции halInit() будет создан новый кадр стека. Это показано выполнением команды bt на моем терминале. Он генерирует следующий вывод:

#0  halInit () at src/hal/src/hal.c:70
#1  0x080000dc in main () at src/main.c:63

Если я выполню команду info frame, то получу следующий вывод:

Stack level 0, frame at 0x200003e8:
pc = 0x8001842 in halInit (src/hal/src/hal.c:70); saved pc 0x80000dc
called by frame at 0x20000400
source language c.
Arglist at 0x200003e0, args: 
Locals at 0x200003e0, Previous frame's sp is 0x200003e8
Saved registers:
 r3 at 0x200003e0, lr at 0x200003e4

Теперь мы видим, что регистр r3 был помещен в этот кадр стека. Этот регистр содержит значение переменной myBits. Почему r3 помещается в этот кадр стека, если для этого регистра требуется кадр стека вызывающего абонента?

Извините за длинный пост, я просто хочу охватить все области необходимой информации.

Обновление

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

Это потому, что функция halInit() будет изменять значение в r3?

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

Ответы [ 2 ]

0 голосов
/ 29 августа 2018

Я стараюсь изо всех сил узнать о стеке вызовов и о том, как стековые кадры структурированы в ARM Cortex-M0

Итак, исходя из этой цитаты, сначала у cortex-m0 нет стековых фреймов, процессоры действительно очень глупы. Компилятор генерирует стековые фреймы, которые являются компилятором, а не набором команд. Понятие функции - вещь компилятора, а не нечто более низкое. Компилятор использует соглашение о вызовах или некоторый базовый набор правил, разработанных таким образом, чтобы для этого языка функции вызывающей и вызываемой функций точно знали, где находятся параметры, возвращаемые значения, и никто не удаляет другие данные.

Авторы компилятора могут делать все, что хотят, если они работают и соответствуют правилам набора команд, как в логике, а не на языке ассемблера. (Автор на ассемблере может создавать любой язык ассемблера по своему усмотрению, мнемонику независимо от того, что машинный код соответствует правилам логики). И они делали это, производители процессоров начали давать рекомендации, скажем так, и компиляторы им соответствуют. Речь идет не о том, чтобы делить объекты между компиляторами, а о том, что это 1) мне не нужно придумывать свой собственный 2) мы доверяем поставщику ip свой процессор и надеемся, что их соглашение о вызовах было разработано для производительности и других причин, которые мы хотим .

gcc до сих пор пытался соответствовать ABI ARM по мере своего развития и развития gcc.

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

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

Компиляторы, такие как GCC, имеют оптимизаторы, и, несмотря на то, что они создают путаницу в отношении мертвого кода, изучение из оптимизированной версии проще, чем неоптимизированной версии. Давайте погрузимся в

extern unsigned int more_fun ( unsigned int, unsigned int );
unsigned int fun ( unsigned int a, unsigned int b )
{
    return(a+b);
}

Оптимизированный

 <fun>:
   0:   1840        adds    r0, r0, r1
   2:   4770        bx  lr

не

00000000 <fun>:
   0:   b580        push    {r7, lr}
   2:   b082        sub sp, #8
   4:   af00        add r7, sp, #0
   6:   6078        str r0, [r7, #4]
   8:   6039        str r1, [r7, #0]
   a:   687a        ldr r2, [r7, #4]
   c:   683b        ldr r3, [r7, #0]
   e:   18d3        adds    r3, r2, r3
  10:   0018        movs    r0, r3
  12:   46bd        mov sp, r7
  14:   b002        add sp, #8
  16:   bd80        pop {r7, pc}

Прежде всего, почему функция по адресу ноль? Поскольку я разобрал объект, а не связанный двоичный файл, возможно, я сделаю это позже. А зачем разбирать против компиляции на сборку? Если дизассемблер хорош, то вы на самом деле можете увидеть, что было произведено, а не сборку, которая будет содержать, конечно же, скомпилированный код, много языка без инструкций, а также псевдокод, который изменяется после окончательной сборки.

ИМК стека - это когда есть второй указатель, указатель кадра. Вы часто видите это с наборами команд, которые имеют инструкции или ограничения, которые склоняются к этому. Например, набор инструкций может иметь регистр указателя стека, но вы не можете адресовать его, может быть другой указатель регистра фрейма, и это возможно. Таким образом, типичной записью будет сохранение указателя кадра в стеке, поскольку вызывающая сторона, возможно, использовала его для своего кадра, и мы хотим вернуть его как найденный, затем скопировать адрес указателя стека в указатель кадра, а затем переместить указатель стека настолько, насколько это необходимо для этой функции, чтобы прерывать или вызывать другие функции указатель стека находится на границе между используемым и неиспользуемым пространством стека, как это должно быть всегда. В этом случае указатель кадра будет использоваться для доступа к любым переданным в параметрах или адресам возврата в указателе кадра плюс мода смещения (для стеков, растущих вниз) и в направлении отрицательного смещения для локальных данных.

Теперь похоже, что компилятор использует указатель фрейма, что за пустая трата времени, давайте попросим его не делать:

00000000 <fun>:
   0:   b082        sub sp, #8
   2:   9001        str r0, [sp, #4]
   4:   9100        str r1, [sp, #0]
   6:   9a01        ldr r2, [sp, #4]
   8:   9b00        ldr r3, [sp, #0]
   a:   18d3        adds    r3, r2, r3
   c:   0018        movs    r0, r3
   e:   b002        add sp, #8
  10:   4770        bx  lr

поэтому сначала компилятор определил, что в стеке нужно сохранить 8 байтов. Неоптимизированный почти все занимает место в стеке, переданные параметры, а также локальные переменные, в этом случае не было никаких локальных элементов, поэтому мы просто передали единицы, два 32-битных числа и 8 байтов. Соглашение о вызовах использовало попытки использовать r0 для первого параметра и r1 для второго, если они подходят, в этом случае они делают. таким образом, кадр стека формируется, когда 8 вычитается из указателя стека, указатель кадра стека в этом случае является указателем стека. Соглашение о вызовах, используемое здесь, позволяет r0-r3 быть изменчивым в функции. Компилятору не нужно возвращаться к вызывающей стороне с этими регистрами, поскольку они были найдены, они могут быть использованы внутри функции по желанию. В этом случае компилятор выбрал извлечение из стека операндов сложения, используя регистры рядом с регистрами, а не первыми для освобождения. Как только r0 и r1 сохранены в стеке, тогда «пул» свободных регистров можно было бы начать с r0, r1, r2, r3. Так что да, похоже, что он не работает, но это то, что есть, это функционально правильно, и это задача компилятора создавать код, который функционально реализует скомпилированный код. Соглашение о вызовах, используемое этим компилятором, гласит, что возвращаемое значение помещается в r0, если оно соответствует, что и делает.

Таким образом, кадр стека настроен, 8 вычитается из sp. Переданные параметры сохраняются в стек. Теперь функция начинается с извлечения переданных параметров из стека, добавления их и помещения результата в регистр возврата.

Затем для возврата используется bx lr, ищите эту инструкцию вместе с pop (для armv6m, для armv4t pop нельзя использовать для переключения режимов, поэтому компиляторы будут, если они могут перейти к lr, затем bx lr).

armv4t thumb, не могу использовать pop для возврата в случае, если этот код смешан с arm, поэтому возвращение появляется в энергозависимом регистре и выполняет bx lr, вы не можете сразу же войти в lr большим пальцем. Вполне возможно, что вы сможете сказать компилятору, что я не смешиваю это с кодом arm, поэтому его можно сохранить с помощью pop для возврата. Зависит от компилятора.

00000000 <fun>:
   0:   b580        push    {r7, lr}
   2:   b082        sub sp, #8
   4:   af00        add r7, sp, #0
   6:   6078        str r0, [r7, #4]
   8:   6039        str r1, [r7, #0]
   a:   687a        ldr r2, [r7, #4]
   c:   683b        ldr r3, [r7, #0]
   e:   18d3        adds    r3, r2, r3
  10:   0018        movs    r0, r3
  12:   46bd        mov sp, r7
  14:   b002        add sp, #8
  16:   bc80        pop {r7}
  18:   bc02        pop {r1}
  1a:   4708        bx  r1

чтобы увидеть указатель кадра

00000000 <fun>:
   0:   b580        push    {r7, lr}
   2:   b082        sub sp, #8
   4:   af00        add r7, sp, #0
   6:   6078        str r0, [r7, #4]
   8:   6039        str r1, [r7, #0]
   a:   687a        ldr r2, [r7, #4]
   c:   683b        ldr r3, [r7, #0]
   e:   18d3        adds    r3, r2, r3
  10:   0018        movs    r0, r3
  12:   46bd        mov sp, r7
  14:   b002        add sp, #8
  16:   bd80        pop {r7, pc}

сначала вы сохраняете указатель кадра в стек как вызывающий или вызывающий, и т. Д., Возможно, он использует его, его регистр, который мы должны сохранить. теперь некоторые соглашения о вызовах вступают в силу с самого начала. Мы знаем, что компилятор знает, что мы не вызываем другую функцию, поэтому нам не нужно сохранять адрес возврата (хранится в регистре ссылок r14), поэтому зачем помещать его в стек, зачем тратить пространство и такты? Что ж, соглашение не так давно изменилось, чтобы стек должен быть выровнен на 64 бита, так что вы в основном помещаете и извлекаете пары регистров (четное количество регистров). Иногда они используют более одной инструкции для пары, как мы видим в возвращении armv4t. Таким образом, компилятору нужно было выдвинуть другой регистр, он мог бы, и вы увидите, что он просто выбирает какой-то регистр, который он не использует, и помещает его в стек, возможно, мы сможем сделать это здесь чуть позже. В этом случае, будучи armv6-m, вы можете переключать режимы с помощью pop, чтобы было безопасно генерировать возврат с использованием pop pc, поэтому вы сохраняете инструкцию, используя регистр связи здесь, а не какой-либо другой регистр. Небольшая оптимизация, несмотря на то, что код не оптимизирован.

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

Пока здесь один из ваших вопросов, и я до сих пор прокомментировал это косвенно. Полноразмерные процессоры arm от armv4t до armv7 поддерживают как инструкции arm, так и команды thumb. Не все поддерживают все, что было в эволюции, но вы можете сослаться на инструкции «рука-палец» как часть правил, определенных логикой для этого ядра. Конструкция ARM для поддержки этого заключается в том, что инструкции на руку должны быть выровнены по слову, младшие два бита адреса инструкции на руку всегда равны нулю. Желаемый 16-битный набор команд, также выровненный, всегда будет иметь младший бит адреса ноль. Так почему бы не использовать lsbit адреса как способ переключения режимов. И это то, что они решили сделать. Сначала с несколькими инструкциями, а затем стало больше, что разрешено архитектурой armv7, если адрес ветви (сначала посмотрите bx, обмен ветвями) имеет lsbit 1, то процессор переключается в режим большого пальца, когда он начинает выборку инструкции по этому адресу, счетчик программы не сохраняет этот, он удаляется командой, это просто сигнал, используемый для указания инструкции на переключение режимов. если lsbit равен 0, то процессор переключается в режим охраны. Если он уже был в указанном режиме, он просто остается в этом режиме.

Теперь идут эти ядра Cortex-M, которые являются машинами только для большого пальца, без режима руки. Инструменты на месте, все работает без причин для изменения, если вы пытаетесь перейти в режим охраны на кортекс-м, вы получаете ошибку.

Теперь посмотрите на приведенный выше код, иногда мы возвращаемся с bx lr, а иногда с pop pc, в обоих случаях lr содержит «адрес возврата». чтобы сработал случай bx lr, необходимо установить lsbit для lr. Вызывающая сторона не может знать, какую инструкцию мы будем использовать для возврата, и вызывающая сторона не должна, но, скорее всего, использует bl для выполнения вызова, поэтому логика фактически устанавливает бит, а не компилятор. Вот почему ваш обратный адрес отключен на один байт.

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

00000000 <fun>:
   0:   1840        adds    r0, r0, r1
   2:   4770        bx  lr

r0 и r1 - передаваемые параметры, r0 - куда возвращается значение, регистр связи - адрес возврата. Это то, на что вы надеетесь, что компилятор создаст такую ​​функцию.

Теперь давайте попробуем что-нибудь более сложное.

extern unsigned int more_fun ( unsigned int, unsigned int );
unsigned int fun ( unsigned int a, unsigned int b )
{
    return(more_fun(a,b));
}

00000000 <fun>:
   0:   b510        push    {r4, lr}
   2:   f7ff fffe   bl  0 <more_fun>
   6:   bd10        pop {r4, pc}

Несколько вещей, во-первых, почему оптимизатор не сделал этого:

fun:
   b more_fun

Я не знаю.

почему он говорит, что bl 0, больше веселья не в нуле? Это объект, не связанный код, после того, как связанный компоновщик изменит эту инструкцию bl, чтобы она указала на more_fun ().

В-третьих, у нас уже есть компилятор для отправки регистра, который мы не использовали. Он нажимает и выталкивает r4, так что он может поддерживать стек в соответствии с соглашением о вызовах, используемым этим компилятором. Он мог бы выбрать почти любой из регистров, и вы можете найти версию gcc или llvm / clang, которая использует скажем r3 вместо r4. gcc уже некоторое время использует r4. Это первый в списке регистров, который вы должны сохранить первым в списке регистров, который, если они хотят сохранить что-то в вызове, который они будут использовать (как мы увидим через секунду). так что, может быть, поэтому, кто знает, спросите автора.

extern unsigned int more_fun ( unsigned int, unsigned int );
unsigned int fun ( unsigned int a, unsigned int b )
{
    more_fun(a,b);
    return(a);
}

00000000 <fun>:
   0:   b510        push    {r4, lr}
   2:   0004        movs    r4, r0
   4:   f7ff fffe   bl  0 <more_fun>
   8:   0020        movs    r0, r4
   a:   bd10        pop {r4, pc}

Теперь мы делаем успехи. Таким образом, мы сообщаем компилятору, что он должен сохранить переданный параметр через вызов функции. Каждая функция запускает правила заново, поэтому каждая вызываемая функция может удалить r0-r3, поэтому, если вы используете r0-r3 для чего-то, вам нужно их где-то сохранить. Таким образом, очень разумный выбор, вместо сохранения переданного параметра в стеке и, возможно, необходимости делать несколько дорогостоящих циклов памяти для доступа к нему. Вместо этого сохраните значение вызываемого или вызываемого и т. Д. В стеке и используйте регистр в нашей функции для сохранения этого параметра, так как в этом проекте сохраняется много потерянных циклов. В любом случае нам нужно было выровнять стек, так что все это сработало, сохранив r4 и сохранив обратный адрес, так как мы сами делаем вызов, который его ударит. Сохраните нужный нам параметр после вызова в r4. Сделайте вызов, поместите возвращаемое значение в регистр возврата и вернитесь. Очистка стека, как вы идете. Таким образом, кадр стека здесь минимален, если вообще. Не очень много пользуюсь стеком.

extern unsigned int more_fun ( unsigned int, unsigned int );
unsigned int fun ( unsigned int a, unsigned int b )
{
    b<<=more_fun(a,b);
    return(a+b);
}

00000000 <fun>:
   0:   b570        push    {r4, r5, r6, lr}
   2:   0005        movs    r5, r0
   4:   000c        movs    r4, r1
   6:   f7ff fffe   bl  0 <more_fun>
   a:   4084        lsls    r4, r0
   c:   1960        adds    r0, r4, r5
   e:   bd70        pop {r4, r5, r6, pc}

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

extern unsigned int more_fun ( unsigned int, unsigned int );
unsigned int fun ( unsigned int a, unsigned int b, unsigned int c, unsigned int d )
{
    b<<=more_fun(b,c);
    c<<=more_fun(c,d);
    d<<=more_fun(b,d);
    return(a+b+c+d);
}


 0: b5f8        push    {r3, r4, r5, r6, r7, lr}
   2:   000c        movs    r4, r1
   4:   0007        movs    r7, r0
   6:   0011        movs    r1, r2
   8:   0020        movs    r0, r4
   a:   001d        movs    r5, r3
   c:   0016        movs    r6, r2
   e:   f7ff fffe   bl  0 <more_fun>
  12:   0029        movs    r1, r5
  14:   4084        lsls    r4, r0
  16:   0030        movs    r0, r6
  18:   f7ff fffe   bl  0 <more_fun>
  1c:   0029        movs    r1, r5
  1e:   4086        lsls    r6, r0
  20:   0020        movs    r0, r4
  22:   f7ff fffe   bl  0 <more_fun>
  26:   4085        lsls    r5, r0
  28:   19a4        adds    r4, r4, r6
  2a:   19e4        adds    r4, r4, r7
  2c:   1960        adds    r0, r4, r5
  2e:   bdf8        pop {r3, r4, r5, r6, r7, pc}

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

extern unsigned int more_fun ( unsigned int, unsigned int );
unsigned int fun ( unsigned int a, unsigned int b, unsigned int c, unsigned int d, unsigned int e, unsigned int f )
{
    b<<=more_fun(b,c);
    c<<=more_fun(c,d);
    d<<=more_fun(b,d);
    e<<=more_fun(e,d);
    f<<=more_fun(e,f);
    return(a+b+c+d+e+f);
}

00000000 <fun>:
   0:   b5f0        push    {r4, r5, r6, r7, lr}
   2:   46c6        mov lr, r8
   4:   000c        movs    r4, r1
   6:   b500        push    {lr}
   8:   0011        movs    r1, r2
   a:   0007        movs    r7, r0
   c:   0020        movs    r0, r4
   e:   0016        movs    r6, r2
  10:   001d        movs    r5, r3
  12:   f7ff fffe   bl  0 <more_fun>
  16:   0029        movs    r1, r5
  18:   4084        lsls    r4, r0
  1a:   0030        movs    r0, r6
  1c:   f7ff fffe   bl  0 <more_fun>
  20:   0029        movs    r1, r5
  22:   4086        lsls    r6, r0
  24:   0020        movs    r0, r4
  26:   f7ff fffe   bl  0 <more_fun>
  2a:   4085        lsls    r5, r0
  2c:   9806        ldr r0, [sp, #24]
  2e:   0029        movs    r1, r5
  30:   f7ff fffe   bl  0 <more_fun>
  34:   9b06        ldr r3, [sp, #24]
  36:   9907        ldr r1, [sp, #28]
  38:   4083        lsls    r3, r0
  3a:   0018        movs    r0, r3
  3c:   4698        mov r8, r3
  3e:   f7ff fffe   bl  0 <more_fun>
  42:   9b07        ldr r3, [sp, #28]
  44:   19a4        adds    r4, r4, r6
  46:   4083        lsls    r3, r0
  48:   19e4        adds    r4, r4, r7
  4a:   1964        adds    r4, r4, r5
  4c:   4444        add r4, r8
  4e:   18e0        adds    r0, r4, r3
  50:   bc04        pop {r2}
  52:   4690        mov r8, r2
  54:   bdf0        pop {r4, r5, r6, r7, pc}
  56:   46c0        nop         ; (mov r8, r8)

Хорошо, вот как это будет ...

extern unsigned int more_fun ( unsigned int, unsigned int );
extern void not_dead ( unsigned int *);
unsigned int fun ( unsigned int a, unsigned int b )
{
    unsigned int x[16];
    unsigned int ra;
    for(ra=0;ra<16;ra++)
    {
        x[ra]=more_fun(a+ra,b);
    }
    not_dead(x);
    return(ra);
}


00000000 <fun>:
   0:   b5f0        push    {r4, r5, r6, r7, lr}
   2:   0006        movs    r6, r0
   4:   b091        sub sp, #68 ; 0x44
   6:   0004        movs    r4, r0
   8:   000f        movs    r7, r1
   a:   466d        mov r5, sp
   c:   3610        adds    r6, #16
   e:   0020        movs    r0, r4
  10:   0039        movs    r1, r7
  12:   f7ff fffe   bl  0 <more_fun>
  16:   3401        adds    r4, #1
  18:   c501        stmia   r5!, {r0}
  1a:   42b4        cmp r4, r6
  1c:   d1f7        bne.n   e <fun+0xe>
  1e:   4668        mov r0, sp
  20:   f7ff fffe   bl  0 <not_dead>
  24:   2010        movs    r0, #16
  26:   b011        add sp, #68 ; 0x44
  28:   bdf0        pop {r4, r5, r6, r7, pc}
  2a:   46c0        nop         ; (mov r8, r8)

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

Кстати, это то, что сделал Clang с этим кодом.

00000000 <fun>:
   0:   b5b0        push    {r4, r5, r7, lr}
   2:   af02        add r7, sp, #8
   4:   b090        sub sp, #64 ; 0x40
   6:   460c        mov r4, r1
   8:   4605        mov r5, r0
   a:   f7ff fffe   bl  0 <more_fun>
   e:   9000        str r0, [sp, #0]
  10:   1c68        adds    r0, r5, #1
  12:   4621        mov r1, r4
  14:   f7ff fffe   bl  0 <more_fun>
  18:   9001        str r0, [sp, #4]
  1a:   1ca8        adds    r0, r5, #2
  1c:   4621        mov r1, r4
  1e:   f7ff fffe   bl  0 <more_fun>
  22:   9002        str r0, [sp, #8]
  24:   1ce8        adds    r0, r5, #3
  26:   4621        mov r1, r4
  28:   f7ff fffe   bl  0 <more_fun>
  2c:   9003        str r0, [sp, #12]
  2e:   1d28        adds    r0, r5, #4
  30:   4621        mov r1, r4
  32:   f7ff fffe   bl  0 <more_fun>
  36:   9004        str r0, [sp, #16]
  38:   1d68        adds    r0, r5, #5
  3a:   4621        mov r1, r4
  3c:   f7ff fffe   bl  0 <more_fun>
  40:   9005        str r0, [sp, #20]
  42:   1da8        adds    r0, r5, #6
  44:   4621        mov r1, r4
  46:   f7ff fffe   bl  0 <more_fun>
  4a:   9006        str r0, [sp, #24]
  4c:   1de8        adds    r0, r5, #7
  4e:   4621        mov r1, r4
  50:   f7ff fffe   bl  0 <more_fun>
  54:   9007        str r0, [sp, #28]
  56:   4628        mov r0, r5
  58:   3008        adds    r0, #8
  5a:   4621        mov r1, r4
  5c:   f7ff fffe   bl  0 <more_fun>
  60:   9008        str r0, [sp, #32]
  62:   4628        mov r0, r5
  64:   3009        adds    r0, #9
  66:   4621        mov r1, r4
  68:   f7ff fffe   bl  0 <more_fun>
  6c:   9009        str r0, [sp, #36]   ; 0x24
  6e:   4628        mov r0, r5
  70:   300a        adds    r0, #10
  72:   4621        mov r1, r4
  74:   f7ff fffe   bl  0 <more_fun>
  78:   900a        str r0, [sp, #40]   ; 0x28
  7a:   4628        mov r0, r5
  7c:   300b        adds    r0, #11
  7e:   4621        mov r1, r4
  80:   f7ff fffe   bl  0 <more_fun>
  84:   900b        str r0, [sp, #44]   ; 0x2c
  86:   4628        mov r0, r5
  88:   300c        adds    r0, #12
  8a:   4621        mov r1, r4
  8c:   f7ff fffe   bl  0 <more_fun>
  90:   900c        str r0, [sp, #48]   ; 0x30
  92:   4628        mov r0, r5
  94:   300d        adds    r0, #13
  96:   4621        mov r1, r4
  98:   f7ff fffe   bl  0 <more_fun>
  9c:   900d        str r0, [sp, #52]   ; 0x34
  9e:   4628        mov r0, r5
  a0:   300e        adds    r0, #14
  a2:   4621        mov r1, r4
  a4:   f7ff fffe   bl  0 <more_fun>
  a8:   900e        str r0, [sp, #56]   ; 0x38
  aa:   350f        adds    r5, #15
  ac:   4628        mov r0, r5
  ae:   4621        mov r1, r4
  b0:   f7ff fffe   bl  0 <more_fun>
  b4:   900f        str r0, [sp, #60]   ; 0x3c
  b6:   4668        mov r0, sp
  b8:   f7ff fffe   bl  0 <not_dead>
  bc:   2010        movs    r0, #16
  be:   b010        add sp, #64 ; 0x40
  c0:   bdb0        pop {r4, r5, r7, pc}

Теперь вы использовали термин стек вызовов. Соглашение о вызовах, используемое этим компилятором, гласит, что используйте r0-r3, когда это возможно, для передачи первых параметров, а затем используйте стек после этого.

unsigned int fun ( unsigned int a, unsigned int b, unsigned int c, unsigned int d, unsigned int e )
{
    return(a+b+c+d+e);
}
00000000 <fun>:
   0:   b510        push    {r4, lr}
   2:   9c02        ldr r4, [sp, #8]
   4:   46a4        mov r12, r4
   6:   4463        add r3, r12
   8:   189b        adds    r3, r3, r2
   a:   185b        adds    r3, r3, r1
   c:   1818        adds    r0, r3, r0
   e:   bd10        pop {r4, pc}

, поэтому, имея более четырех параметров, первые четыре находятся в r0-r3, а затем «стек вызовов», если предположить, что это то, на что вы ссылались, это пятый параметр. Набор команд thumb использует bl в качестве основной инструкции вызова, которая использует r14 в качестве адреса возврата, в отличие от других наборов команд, которые могут использовать стек для хранения адреса возврата, arm использует регистр. А популярные соглашения о вызовах рук используют регистры для первых нескольких операндов, а затем используют стек после этого.

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

00000000 <_fun>:
   0:   1d80 0008       mov 10(sp), r0
   4:   6d80 000a       add 12(sp), r0
   8:   6d80 0006       add 6(sp), r0
   c:   6d80 0004       add 4(sp), r0
  10:   6d80 0002       add 2(sp), r0
  14:   0087            rts pc
0 голосов
/ 29 августа 2018

В системах ARM многие авто хранятся в регистрах, а не выделяют место в стеке. ARM имеет много регистров, по сравнению с другими процессорами. Когда функция (контекст) вызывает другую функцию, эти регистры могут быть перезаписаны. Создатели компилятора имеют два варианта: 1) сохранить все регистры при входе в (в верхней части) каждой функции или 2) сохранить регистры, которые использует функция в любой точке, которую она вызывает, в другую функцию.

Вызывающая сторона имеет полный контекст, поэтому эффективнее сохранять только те регистры, которые используются. ARM ABI определяет соглашения, используемые большинством компиляторов. Это позволяет библиотекам функций из разных компиляторов взаимодействовать.

...