Какова цель регистра RBP в ассемблере x86_64?

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

Я узнал, что всякий раз, когда я пишу программу, я использую эти 3 строки (как я узнал из сравнения кода сборки, сгенерированного из gcc на него эквивалентно C):

pushq   %rbp
movq    %rsp, %rbp
subq    $16, %rsp

У меня есть 2 вопроса:

  1. Прежде всего, почему я использую %rbp? Не проще ли использовать %rsp, так как его содержимое перемещается на %rbp на 2-й строке?
  2. Почему я должен вычитать что-либо из %rsp? Я имею в виду, что это не всегда 16 (когда я printf строки 7 или 8 переменных, затем я вычитал 24 или 28

Я использую Manjaro 64 бит на виртуальной машине (4 ГБ оперативной памяти), процессор Intel 64 бит

Ответ 1

rbp - это указатель на x86_64. В сгенерированном коде он получает моментальный снимок указателя стека (rsp), так что при настройке rsp (т.е. Резервировании пространства для локальных переменных или push значений в стек) локальные переменные и параметры функции по-прежнему доступны из постоянное смещение от rbp.

Многие компиляторы предлагают исключение указателя кадра как вариант оптимизации; это сделает сгенерированный код сборки доступным вместо переменных rsp и освободит rbp как другой регистр общего назначения для использования в функциях.

В случае GCC, который, как я предполагаю, вы используете из синтаксиса ассемблера AT & T, этот переключатель является -fomit-frame-pointer. Попробуйте скомпилировать свой код с этим коммутатором и посмотрите, какой код сборки вы получите. Вы, вероятно, заметите, что при доступе к значениям относительно rsp вместо rbp смещение от указателя изменяется во всей функции.

Ответ 2

Linux использует архитектуру System V ABI для архитектуры x86-64 (AMD64); см. Систему V ABI в Wiki OSDev для деталей.

Это означает, что стек растет; меньшие адреса "выше" в стеке. Типичные функции C скомпилированы для

        pushq   %rbp        ; Save address of previous stack frame
        movq    %rsp, %rbp  ; Address of current stack frame
        subq    $16, %rsp   ; Reserve 16 bytes for local variables

        ; ... function ...

        movq    %rbp, %rsp  ; \ equivalent to the
        popq    %rbp        ; / 'leave' instruction
        ret

Объем памяти, зарезервированный для локальных переменных, всегда кратен 16 байтам, чтобы поддерживать стеки в 16 байт. Если для локальных переменных не требуется пространство стека, нет subq $16, %rsp или аналогичной инструкции.

(Обратите внимание, что адрес возврата и предыдущий %rbp в стек, равны 8 байтам, всего 16 байтов).

Пока %rbp указывает на текущий стек стека, %rsp указывает на вершину стека. Поскольку компилятор знает разницу между %rbp и %rsp в любой точке функции, он может использовать любой из них в качестве базы для локальных переменных.

Фрейм стека - это только локальная игровая площадка: область стека, используемая текущей функцией.

Текущие версии GCC отключают фрейм стека всякий раз, когда используются оптимизации. Это имеет смысл, потому что для программ, написанных на C, фреймы стека наиболее полезны для отладки, но не более того. (Вы можете использовать, например, -O2 -fno-omit-frame-pointer чтобы сохранить фреймы стека, в то же время поддерживая оптимизацию.)

Хотя один и тот же ABI применяется ко всем бинарным файлам, независимо от того, на каком языке они записаны, некоторым другим языкам нужны фреймы стека для "разматывания" (например, для "исключения исключений" для вызывающей функции предка текущей функции); т.е. "раскручивать" стековые кадры, что одна или несколько функций могут быть прерваны, а управление передано некоторой функции предка, не оставляя ненужных вещей в стеке.

Когда кадры стека опущены - -fomit-frame-pointer для GCC - реализация функции существенно меняется

        subq    $8, %rsp    ; Re-align stack frame, and
                            ; reserve memory for local variables

        ; ... function ...

        addq    $8, %rsp
        ret

Поскольку нет фрейма стека (%rbp используется для других целей, и его значение никогда не выталкивается в стек), каждый вызов функции выталкивает только обратный адрес в стек, который представляет собой 8-байтовое количество, поэтому нам нужно вычесть 8 из %rsp чтобы он был кратным 16. (В общем, значение, вычитаемое из %rsp и добавленное к %rsp является нечетным кратным 8.)

Функциональные параметры обычно передаются в регистры. См. Ссылку ABI в начале этого ответа для деталей, но, короче говоря, интегральные типы и указатели передаются в регистры %rdi, %rsi, %rdx, %rcx, %r8 и %r9, с аргументами с плавающей запятой в регистры %xmm0 до %xmm7.

В некоторых случаях вы увидите rep ret вместо rep. Не путайте: rep ret означает то же самое, что и ret; префикс rep, хотя обычно используется со строковыми инструкциями (повторные инструкции), ничего не делает при применении к команде ret. Это просто, что некоторые предсказатели ветки процессоров AMD не любят прыгать в инструкцию ret, и рекомендуемое обходное решение - вместо этого использовать rep ret.

Наконец, я опустил красную зону над вершиной стека (128 байт по адресам меньше %rsp). Это связано с тем, что он не очень полезен для типичных функций: в нормальном случае с корпусом-стоп-кадром вы хотите, чтобы ваши локальные файлы находились в фрейме стека, чтобы сделать отладку возможной. В случае с оцифрованным стеком требования к выравниванию стека уже означают, что нам нужно вычесть 8 из %rsp, поэтому включая память, необходимую локальным переменным, в том, что вычитание ничего не стоит.