ARM: регистр ссылок и указатель кадра

Я пытаюсь понять, как реестр ссылок и указатель фрейма работают в ARM. Я был на нескольких сайтах, и я хотел подтвердить свое понимание.

Предположим, что у меня был следующий код:

int foo(void)
{
    // ..
    bar();
    // (A)
    // ..
}

int bar(void)
{
    // (B)
    int b1;
    // ..
    // (C)
    baz();
    // (D)
}

int baz(void)
{
    // (E)
    int a;
    int b;
    // (F)
}

и я вызываю foo(). Будет ли регистр ссылок содержать адрес для кода в точке (A), а указатель кадра содержит адрес в коде в точке (B)? И указатель стека мог бы быть любым, где внутри bar(), после того, как были объявлены локальные жители?

[edit] Добавлен другой вызов функции baz()

Ответ 1

Некоторые соглашения о вызовах регистров зависят от ABI (двоичный интерфейс приложения). FP требуется в стандарте APCS, а не в более новой AAPCS (2003). Для AAPCS (GCC 5. 0+) FP не нужно использовать, но, безусловно, можно; информация об отладке аннотируется с помощью указателя стека и фрейма для отслеживания стека и разматывания кода с помощью AAPCS. Если функция - static, компилятору действительно не нужно придерживаться каких-либо соглашений.

Как правило, все регистры ARM общего назначения. lr (регистр связи, также R14) и pc (программный счетчик также R15) являются специальными и закреплены в наборе команд. Вы правы, что lr будет указывать на A. pc и lr связаны между собой. Один "где ты", а другой "где ты был". Они являются кодом аспекта функции.

Как правило, у нас есть sp (указатель стека, R13) и fp (указатель кадра, R11). Эти два также связаны. Этот Макет Microsoft хорошо описывает вещи. Стек используется для хранения временных данных или локальных данных в вашей функции. Любые переменные в foo() и bar() хранятся здесь, в стеке или в доступных регистрах. fp отслеживает переменные от функции к функции. Это окно кадра или изображения в стеке для этой функции. ABI определяет макет этого кадра. Обычно lr и другие регистры сохраняются здесь за кулисами компилятором, а также предыдущим значением fp. Это создает связанный список стековых фреймов, и если вы хотите, вы можете проследить его до main(). Корнем является fp, который указывает на один кадр стека (например, struct) с одной переменной в struct, являющейся предыдущей fp. Вы можете идти по списку до финального fp, который обычно NULL.

Таким образом, sp находится там, где находится стек, а fp - там, где был стек, во многом как pc и lr. Каждый старый lr (регистр связи) сохраняется в старом fp (указатель кадра). sp и fp являются аспектом данных функций.

Ваша точка B является активными pc и sp. Точка A на самом деле является fp и lr; если вы не вызовете еще одну функцию, а затем компилятор может подготовиться к настройке fp для указания на данные в B.

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

; Prologue - setup
mov     ip, sp                 ; get a copy of sp.
stmdb   sp!, {fp, ip, lr, pc}  ; Save the frame on the stack. See Addendum
sub     fp, ip, #4             ; Set the new frame pointer.
    ...
; Maybe other functions called here.
; Older caller return lr stored in stack frame. bl baz ... ; Epilogue - return ldm sp, {fp, sp, lr} ; restore stack, frame pointer and old link. ... ; maybe more stuff here. bx lr ; return.
Вот как бы выглядел foo(). Если вы не вызываете bar(), то компилятор выполняет листовую оптимизацию и ему не нужно сохранять фрейм; нужен только bx lr. Скорее всего, это может быть, почему вы запутались в веб-примерах. Это не всегда одно и то же.

Еда на вынос должна быть,

  1. pc и lr связаны кодовыми регистрами. Один - "Где ты", другой - "Где ты был".
  2. sp и fp связаны локальными данными регистрами.
    Один из них "Где локальные данные", другой - "Где последние локальные данные".
  3. Совместная работа с передачей параметров parameter passing для создания механизма функций.
  4. Трудно описать общий случай, потому что мы хотим, чтобы компиляторы были максимально быстрыми, чтобы они использовали все возможные приемы.

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

Смотрите также: Соглашение о вызовах ARM.
              Статья стека MSDN ARM
              Обзор APCS Кембриджского университета
              Блог трассировки стека ARM
              Ссылка Apple ABI

Базовая компоновка кадра:

  • fp [-0] сохранил pc, где мы сохранили этот кадр.
  • fp [-1] сохранил lr, адрес возврата для этой функции.
  • fp [-2] предыдущий sp, до того как эта функция съест стек.
  • fp [-3] предыдущий fp, последний кадр стека.
  • много необязательных регистров...

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

Приложение: Это не ошибка в ассемблере; это нормально. Объяснение в вопросе о прологах, созданных ARM.

Ответ 2

Отказ от ответственности: я думаю, что это примерно правильно; пожалуйста, исправьте по мере необходимости.

Как указано в другом месте этого Q & A, имейте в виду, что компилятору может не потребоваться генерировать код (ABI), который использует указатели кадров. Кадры в стеке вызовов часто требуют размещения бесполезной информации.

Если параметры компилятора вызывают 'no frames' (флаг псевдоопции), то компилятор может генерировать меньший код, который сохраняет данные стека вызовов меньшими. Вызывающая функция компилируется для хранения только необходимой информации о вызове в стеке, а вызываемая функция компилируется для извлечения только необходимой информации вызова из стека.

Это экономит время выполнения и место в стеке, но крайне затрудняет отслеживание в вызывающем коде назад (я перестал пытаться...)

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