X86_64: указатель кадра стека почти бесполезен?


  • Linux x86_64.
  • gcc 5.x

Я изучал вывод двух кодов, с -fomit-frame-pointer и без (gcc на "-O3" включил эту опцию по умолчанию).

pushq    %rbp
movq     %rsp, %rbp
...
popq     %rbp

Мой вопрос:

Если во всем мире отключить этот параметр, даже для, в крайнем случае, компиляции операционной системы, есть ли улов?

Я знаю, что прерывания используют эту информацию, так что этот параметр хорош только для пользовательского пространства?

Ответ 1

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

Прерывания не используют информацию указателя кадра, они могут использовать текущий указатель стека для сохранения минимального контекста, но это зависит от типа прерывания и ОС (аппаратное прерывание использует стек Ring 0, вероятно).
Вы можете посмотреть в руководствах Intel для получения дополнительной информации об этом.

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

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


Мне нравится использовать указатель кадра, писать пролог и эпилог функции, но мне также нравятся прямые неудобные ответы, поэтому вот как я это вижу:

Да, указатель кадра в x86_64 практически бесполезен

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

Вернуться в реальном режиме (16 бит) дней

Когда процессоры Intel поддерживали только "16-битный режим", были некоторые ограничения на доступ к стеку, особенно эта инструкция была (и остается) недопустимой

mov ax, WORD [sp+10h]

потому что sp не может быть использован в качестве базового регистра. Для этой цели можно использовать только несколько назначенных регистров, например, bx или более известный bp.
В настоящее время это не та деталь, на которую все смотрят, но bp имеет преимущество перед другим базовым регистром в том, что он неявно подразумевает использование ss в качестве регистра сегмента/селектора, точно так же, как неявное использование sp (push, pop и т.д.), И как esp на более поздних 32-битных процессорах.
Даже если ваша программа была разбросана по всей памяти, причем каждый регистр сегмента указывал на отдельную область, bp и sp действовали одинаково, в конце концов, это было намерением проектировщиков.

Таким образом, стековый фрейм обычно был необходим и, следовательно, указатель фрейма.
bp эффективно разделил стек на четыре части: область аргументов, адрес возврата, старый bp (просто WORD) и область локальных переменных. Каждая область, идентифицируемая смещением, используется для доступа к нему: положительный для аргументов и адреса возврата, ноль для старого bp, отрицательный для локальных переменных.

Расширенные эффективные адреса

По мере развития процессоров Intel были добавлены более широкие 32-битные режимы адресации.
В частности, возможность использовать любой 32-битный регистр общего назначения в качестве базового регистра, это включает использование esp.
Будучи такими инструкциями

mov eax, DWORD [esp+10h]

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

Без указателя кадра push или pop изменили бы аргумент или смещение локальной переменной относительно esp, давая форму коду, который на первый взгляд выглядит не интуитивно понятным. Рассмотрим, как реализовать следующую подпрограмму C с соглашением о вызовах cdecl:

void my_routine(int a, int b)
{  
    return my_add(a, b); 
}

без и с рамой

my_routine:      
  push DWORD [esp+08h]
  push DWORD [esp+08h]
  call my_add
  ret

my_routine:
  push ebp
  mov ebp, esp

  push DWORD [ebp+0Ch]
  push DWORD [ebp+08h]
  call my_add

  pop ebp
  ret 

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

Если вы добавляете локальные переменные (особенно их много), тогда ситуация быстро становится трудной для чтения: mov eax, [esp+0CAh] ссылаются на локальную переменную или на аргумент? С помощью стекового фрейма мы зафиксировали смещения для аргументов и локальных переменных.

Даже компиляторы поначалу все еще предпочитали фиксированные смещения, задаваемые использованием указателя базы кадра. Я вижу, что это поведение меняется в первую очередь с gcc.
В отладочной сборке фрейм стека эффективно добавляет ясности в код и позволяет (опытному) программисту следить за происходящим и, как указано в комментарии, позволяет им легче восстанавливать фрейм стека.
Современные компиляторы, однако, хорошо разбираются в математике и могут легко вести подсчет перемещений указателя стека и генерировать соответствующие смещения из esp, опуская кадр стека для более быстрого выполнения.

Когда CISC требует выравнивания данных

До введения инструкций SSE процессоры Intel никогда не просили у программистов многого по сравнению с их братьями по RISC.
В частности, они никогда не запрашивали выравнивание данных, мы могли получить доступ к 32-битным данным по адресу, не кратному 4, без каких-либо серьезных претензий (в зависимости от ширины данных DRAM это может привести к увеличению задержки).
SSE использовал 16-байтовый операнд, к которому необходимо было получить доступ на 16-байтовой границе, поскольку парадигма SIMD эффективно реализуется в аппаратном обеспечении и становится более популярной, выравнивание на 16-байтовой границе становится важным.

Основные 64-битные ABI теперь требуют этого, стек должен быть выровнен по абзацам (т.е. 16 байтов).
Теперь нас обычно называют такими, что после пролога стек выровнен, но предположим, что мы не наделены этой гарантией, нам нужно сделать одно из этого

push rbp                   push rbp
mov rbp, rsp               mov rbp, rsp             

and spl, 0f0h              sub rsp, xxx
sub rsp, 10h*k             and spl, 0f0h

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

Таким образом, мы можем использовать указатель кадра для доступа к аргументам в стеке и указатель стека для локальных переменных, достаточно справедливо.
Увы, роль стека для передачи аргументов была уменьшена, и для небольшого количества аргументов (в настоящее время четыре) он даже не используется, и в будущем он, вероятно, будет использоваться еще меньше.

Таким образом, мы не используем указатель кадра для локальных переменных (в основном) и для аргументов (в основном), для чего мы его используем?

  1. Сохраняет копию исходного rsp, поэтому для восстановления указателя стека при выходе из функции достаточно mov. Если стек выровнен с and, который не является обратимым, необходима оригинальная копия.

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

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

  4. Некоторые функции требуют более четырех параметров.

Резюме

Указатель кадра - это рудиментарная парадигма из 16-битных программ, которая доказала свою эффективность на 32-битных машинах благодаря своей простоте и ясности при доступе к локальным переменным и аргументам.
Однако на 64-битных машинах строгие требования исчезают из-за большей простоты и ясности, однако указатель кадра остается в режиме отладки.


О том, что указатель кадра можно использовать для забавных вещей: это правда, я думаю, я никогда не видел такого кода, но я могу представить, как он будет работать.
Я, однако, сосредоточился на вспомогательной роли указателя кадра, так как я всегда видел это.
Все сумасшедшие вещи могут быть выполнены с любым указателем, установленным на то же значение указателя кадра, я даю последнему более "особую" роль.
Например, VS2013 иногда использует rdi в качестве "указателя кадра", но я не считаю его указателем реального кадра, если он не использует rbp/ebp/bp.
Для меня использование rdi означает оптимизацию rdi кадров указателя :)