Построение кадра стека на AMD64

/ Просмотров: 1857

Статья является переводом этой статьи.

В этой статье я изложу принципы построения стека на новой 64-разрядной архитектуре AMD64. Фокус внимания будет на системе Linux и других системах, соблюдающий официальный System V AMD64 ABI. Windows использует другой ABI, и я кратко упомяну его в конце статьи. Я не буду подробно описывать полное соглашение о вызовах x64, его вы можете найти в руководстве AMD64 ABI.

Изобилие регистров

В архитектуре x86 есть только 8 регистров общего назначения (eax, ebx, ecx, edx, ebp, esp, esi, edi). В архитектуре x64 они расширены до 64 бит, а вместо префикса "e" используется префикс "r", а также добавлены ещё 8 регистров (r8, r9, r10, r11, r12, r13, r14, r15). Поскольку некоторые регистры x86 имеют особое неявное назначение и не используются как регистры общего назначения (особенно ebp и esp), увеличение их количества и разрядности даже важнее, чем кажется.

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

Передача аргументов

Для упрощения изложения я упомяну лишь назначение целочисленных аргументов и указателей. Согласно ABI, первые 6 целочисленных аргументов или указателей предаются в функцию в регистрах. Первый параметр помещается в rdi, второй - в rsi, третий - в rdx, и затем rcx, r8 и r9. Только начиная с седьмого аргумента параметры начинают помещаться в стек.

Кадр стека

Помня о упомянутом выше, посмотрим, каким будет кадр стека для этой функции:

long myfunc(long a, long b, long c, long d,
            long e, long f, long g, long h)
{
    long xx = a * b * c * d * e * f * g * h;
    long yy = a + b + c + d + e + f + g + h;
    long zz = utilfunc(xx, yy, xx % yy);
    return zz + 20;
}

Кадр выглядит так:

Кадр стека для функции

Итак, первые 6 аргументов оказались в регистрах. Но кроме этого, стек не сильно отличается от того, как это было на x86, за исключением странной "красной зоны". Что она означает?

Красная зона

Для начала я процитирую формальное определение из AMD64 ABI:

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

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

Рассмотрим, как функция myfunc из кода выше вызовет другую функцию utilfunc. Это делается для того, чтобы myfunc не могла использовать красную зону, а utilfunc - могла. Функция выглядит так:

long utilfunc(long a, long b, long c)
{
    long xx = a + 2;
    long yy = b + 3;
    long zz = c + 4;
    long sum = xx + yy + zz;

    return xx * yy * zz + sum;
}

Эта функция действительно не вызывает другие функции. Рассмотрим, как выглядит кадр стека для этой функции:

Кадр стека с использованием красной зоны

Поскольку utilfunc имеет только 3 аргумента, её вызов не требует использования стека для аргументов, они все помещаются в регистры. Поскольку она не вызывает другие функции, её локальные переменные хранятся в красной зоне. Следовательно, регистр RSP не изменяется для размещения пространства локальных переменных.

Сохранение базового указателя

Базовый указатель RBP (и его предшественник EBP на x86) будучи якорем, указывающим на начала кадра стека, в котором происходит выполнение функции, очень удобен при программировании на ассемблере и при отладке. Однако, некоторое время назад было принято решение, что код, генерируемый компилятором, не нуждается в этом регистре (компилятор может легко хранить смещения относительно RSP), а формат отладочной информации DWARF предоставляет такие смещения для доступа к кадрам стека без базового указателя.

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

Компилятор gcc по умолчанию оставляет базовый указатель на x86, но позволяет указать флаг оптимизации -fomit-frame-pointer. Сегодня рекомендации по использованию этого флага - обсуждаемый вопрос, и вы можете найти соответствующую информацию, если это вам интересно.

В любом случае новый AMD64 ABI делает базовый указатель необязательным:

Общепринятое использование регистра RBP в качестве указателя кадра можно упразднить с использованием указателя стека RSP для доступа к переменным в кадре стека. Эта технология сберегает две инструкции в прологе и эпилоге функции и оставляет свободным один регистр общего назначения.

Компилятор gcc придерживается этой рекомендации и по умолчанию пропускает указатель кадра на x64 при компиляции с оптимизацией. Он предоставляет возможность предотвратить такое поведение с помощью флага -fno-omit-frame-pointer. Для ясности, кадры стека, показанные выше, составлены без пропуска указателя кадра.

Windows x64 ABI

Windows на x64 реализует собственный ABI, который несколько отличается от AMD64 ABI. Я кратко опишу Windows x64 ABI, упомяну, как выглядит кадр стека в отличие от AMD64. Вот основные отличия:

  1. С помощью регистров передаются только 4 целочисленных параметра или указателя (rcx, rdx, r8, r9).

  2. Красная зона отсутствует. Фактически, ABI явно констатирует, что область, находящая за указателем rsp рассматривается как volatile и небезопасная к использованию. Операционная система, отладчики, обработчики прерываний могут перезаписывать эту область.

  3. Вместо красной зоны присутствует "область параметров регистров", которая создаётся вызывающей функцией в каждом кадре стека. Когда функция вызывается, последнее, что размещено в стеке перед адресом возврата, это пространство из как минимум 4 регистров (по 8 байт каждый). Эта область доступна для использования в вызываемой функции без явного выделения памяти для неё. Это полезно как для изменяемых аргументов функций, так и для отладки (предоставление известного расположения для параметров, в то время как регистры используются для других целей). Хотя область превоначально создавалась для размещения 4 аргументов, переданных через регистры, в настоящее время компилятор использует её также в целях оптимизации (например, если функции необходимо менее, чем 32 байта стекового пространства для собственных локальных переменных, эту область можно использовать для них, не трогая RSP).

Другое важное изменение, сделанное в Windows x64 ABI - это упразднение соглашений о вызовах. Больше нет безумия вроде cdecl, stdcall, fastcall, thiscall, register, safecall - теперь есть только одно соглашение x64. Ура!

Более подробную информацию об этих и других аспектах Windows x64 ABI можно найти по этим ссылкам:

  1. Официальная страница соглашений о вызовах x64 на MSDN - хорошо структурированная информация, боле лёгкая для усвоения и понимания, чем документ AMD64 ABI.

  2. История соглашений о вызовах, часть 5: AMD64 - статья евангелиста профильного программирования на Windows Раймонда Чена.

  3. Почему Windows использует различные соглашения о вызовах от одной ОС к другой? - интересное обсуждение вопроса, заданного на StackOverflow.

  4. Вызов отладке оптимизированного 64-разрядного кода - фокусируется на "отлаживаемости" (и недостатках) сгенерированного компилятором кода.
Оставьте комментарий!

Комментарий будет опубликован после проверки

Вы можете войти под своим логином или зарегистрироваться на сайте.

(обязательно)