Программные соглашения x64

Тема в разделе "WASM.X64", создана пользователем Mikl___, 4 дек 2022.

  1. Mikl___

    Mikl___ Супермодератор Команда форума

    Публикаций:
    14
    Регистрация:
    25 июн 2008
    Сообщения:
    3.709
    Программные соглашения x64
    Перевод статей MSDN
    В данном разделе рассматривается методология соглашения о вызовах Visual C++ x64 для 64-битового расширения в архитектуре x86.
    Следующий параметр компилятора позволяет оптимизировать приложение для x64:
    • /favor (оптимизация для особенностей архитектуры)

    Общие сведения о соглашениях о вызовах для архитектуры x64

    Двумя важнейшими различиями между архитектурами x86 и x64 является возможность 64-битной адресации и набор из 16 64-битных регистров общего назначения. Предоставляя расширенный набор регистров, x64 использует только соглашение о вызовах __fastcall и модель RISC-архитектуры обработки исключений. Модель соглашения о вызовах __fastcall использует регистры для первых четырех аргументов, а для передачи других параметров используется кадр стека.

    Типы данных и размещение в памяти

    В этом разделе рассматривается перечисление и хранение типов данных архитектуры x64.

    Скалярные типы

    Несмотря на то, что обращение к данным возможно при любом выравнивании, в целях повышения производительности рекомендуется использовать выравнивание данных в исходном диапазоне. Перечисления представляют собой константы целого типа и обрабатываются как 32-разрядные целые числа. В следующей таблице приводится определение типа и рекомендуемый для него объем памяти в случае выравнивания с использованием следующих значений:
    • Byte — 8 бит
    • Word — 16 бит
    • Double Word — 32 бит
    • Quad Word — 64 бит
    • Octa Word — 128 бит
    Скалярный
    тип
    Тип данных CОбъем
    памяти
    (в байтах)
    Рекомендуемое
    выравнивание
    INT8​
    char​
    1​
    Byte​
    UINT8​
    unsigned char​
    1​
    Byte​
    INT16​
    short​
    2​
    Word​
    UINT16​
    unsigned short​
    2​
    Word​
    INT32​
    int, long​
    4​
    Doubleword​
    UINT32​
    unsigned int,
    unsigned long​
    4​
    Doubleword​
    INT64​
    __int64​
    8​
    Quadword​
    UINT64​
    unsigned __int64​
    8​
    Quadword​
    FP32 (одинарной
    точности)​
    float​
    4​
    Doubleword​
    FP64 (двойной
    точности)​
    double​
    8​
    Quadword​
    pointer​
    *​
    8​
    Quadword​
    __m64​
    struct __m64​
    8​
    Quadword​
    __m128​
    struct __m128​
    16​
    Octaword​

    Статические выражения и объединения

    К другим типам, таким как массивы, структуры и объединения, предъявляются более строгие требования к выравниванию, обеспечивающие согласованность хранения статических выражений и объединений и извлечения данных. Далее приведены определения массива, структуры и объединения.
    • Массив
      Содержит упорядоченную группу смежных объектов данных. Каждый объект именуется элементом. Все элементы массива должны быть одного размера и принадлежать одному типу данных.
    • Структура
      Содержит упорядоченную группу объектов данных. В отличие от элементов массива, объекты данных внутри структуры могут принадлежать разным типам и иметь разный размер. Каждый объект данных в структуре называется членом.
    • Union
      Объект, содержащий любое из множества поименованных членов. Члены этого именованного набора могут быть любого типа. Область хранения, выделенная для объединения, равна области хранения, требующейся для члена этого объединения, имеющего наибольший размер, плюс заполнение, необходимое для выравнивания.
    В таблице представлены требования по выравниванию для скалярных членов объединений и структур.
    Скалярный
    тип
    Тип
    данных C
    Обязательное
    выравнивание
    INT8​
    char​
    Byte​
    UINT8​
    unsigned char​
    Byte​
    INT16​
    short​
    Word​
    UINT16​
    unsigned short​
    Word​
    INT32​
    int, long​
    Doubleword​
    UINT32​
    unsigned int,
    unsigned long​
    Doubleword​
    INT64​
    __int64​
    Quadword​
    UINT64​
    unsigned __int64​
    Quadword​
    FP32 (одинарной
    точности)​
    float​
    Doubleword​
    FP64 (двойной
    точности)​
    double​
    Quadword​
    pointer​
    *​
    Quadword​
    __m64​
    struct __m64​
    Quadword​
    __m128​
    struct __m128​
    Octaword​
    Применяются следующие правила выравнивания статических выражений:
    • Выравнивание массива аналогично выравниванию одного из элементов массива.
    • Выравнивание начальной части структуры или объединения является максимальным выравниванием любого отдельного члена. Каждый член внутри структуры или объединения должен быть размещен в соответствии со своим выравнивании, как определено в предыдущей таблице, для чего может потребоваться неявное внутреннее заполнение в зависимости от предыдущего члена.
    • Размер структуры должен быть целым числом, кратным его выравниванию, для чего может потребоваться заполнение после последнего члена. Поскольку структуры и объединения могут быть сгруппированы в массивы, каждый элемент массива в структуре или объединении должен начинаться и завершаться соответствующим предварительно определенным выравниванием.
    • Возможно выравнивание данных с превышением требований к выравниванию при условии соблюдения ранее установленных правил.
    • Отдельный компилятор может регулировать упаковку структуры из соображений ее размера. Например, /Zp (Выравнивание члена структуры) позволяет регулировать упаковку структур.

    Примеры выравнивания структуры

    В каждом из следующих примеров содержится объявление выровненной структуры или объединения. Порядок размещения таких структур или объединений в памяти показан на соответствующих рисунках. Каждый столбец на рисунке соответствует байту в памяти. Номер столбца определяет смещение указанного байта. Имя второго столбца на каждом рисунке соответствует имени переменной в объявлении. Затененные столбцы определяют заполнение, необходимое для указанного типа выравнивания.

    Пример 1

    Код (C):
    1. // Total size = 2 bytes, alignment = 2 bytes (word).
    2.  _declspec(align(2)) struct {
    3. short a; // +0; size = 2 bytes
    4. }
    [​IMG]

    Пример 2

    Код (C):
    1. // Total size = 24 bytes, alignment = 8 bytes (quadword).
    2.  _declspec(align(8)) struct {
    3.  int a; // +0; size = 4 bytes
    4.  double b; // +8; size = 8 bytes
    5.  short c; // +16; size = 2 bytes
    6.  }
    [​IMG]

    Пример 3

    Код (C):
    1. // Total size = 12 bytes, alignment = 4 bytes (doubleword).
    2.  _declspec(align(4)) struct {
    3. char a; // +0; size = 1 byte
    4.  short b; // +2; size = 2 bytes
    5.  char c; // +4; size = 1 byte
    6.  int d; // +8; size = 4 bytes
    7.  }
    [​IMG]

    Пример 4

    Код (C):
    1. // Total size = 12 bytes, alignment = 4 bytes (doubleword).
    2.  _declspec(align(4)) struct {
    3. char a; // +0; size = 1 byte
    4.  short b; // +2; size = 2 bytes
    5.  char c; // +4; size = 1 byte
    6.  int d; // +8; size = 4 bytes
    7.  }
    [​IMG]

    Разряды

    Структура битовых полей ограничивается 64 битами и может быть следующих типов: signed int, unsigned int, int64 или unsigned int64. Битовые поля, которые пересекают границу типов, пропустят биты, чтобы выровнять разряды до уровня следующего типа. Например, разряды целого числа (integer) не могут пересечь 32-разрядную границу.

    Конфликты с компилятором x86

    Типы данных, размер которых превышает 4 байта, не выравниваются в стеке автоматически при компиляции приложения с помощью компилятора x86. Поскольку архитектура компилятора x86 представляет собой выровненный 4-байтовый стек, что-либо большее, чем 4 байта, например, 64-разрядное целое число, не может автоматически выравниваться по 8-байтовому адресу.

    Работа с данными без выравнивания имеет два ограничения.
    • Доступ к невыровненным расположениям может занимать слишком много времени по сравнению с доступом к выровненным расположениям.
    • Невыровненные расположения не могут использоваться в блокируемых операциях.
    Если требуется более строгое выравнивание, используйте __declspec(align(N)) при объявлении ваших переменных. Это заставляет компилятор динамически выравнивать стек в соответствии с требованиями. Тем не менее динамическая настройка стека во время выполнения может привести к замедлению выполнения приложения.

    Использование регистров

    Архитектура x64 поддерживает 16 регистров общего назначения (в дальнейшем называемых целочисленными регистрами), а также 16 регистров XMM, используемых для операций с плавающей запятой. Содержимое "изменяемых регистров" не может быть изменено в процессе выполнения вызова процедуры. Содержимое "неизменяемого регистра" гарантированно сохраняет свое значение в процессе выполнения функции и должно сохраняться вызываемым объектом в случае использования.
    В следующей таблице описываются способы использования каждого регистра в процессе выполнения вызова функции:
    РегистрстатусПрименение
    RAXИзменяемыйРегистр возвращаемого значения
    RCXПервый целочисленный аргумент
    RDXВторой целочисленный аргумент
    R8Третий целочисленный аргумент
    R9Четвертый целочисленный аргумент
    R10:R11Должны сохраняться вызывающим объектом. Используется в инструкциях syscall/sysret
    R12:R15НеизменяемыйДолжны сохраняться вызываемым объектом
    RDIДолжен сохраняться вызываемым объектом
    RSIДолжен сохраняться вызываемым объектом
    RBXДолжен сохраняться вызываемым объектом
    RBPМожет использоваться как указатель кадра. Должен сохраняться вызываемым объектом
    RSPУказатель стека
    XMM0ИзменяемыйПервый аргумент с плавающей запятой
    XMM1Второй аргумент с плавающей запятой
    XMM2Третий аргумент с плавающей запятой
    XMM3Четвертый аргумент с плавающей запятой
    XMM4:XMM5Должны сохраняться вызывающим объектом
    XMM6:XMM15НеизменяемыйДолжны сохраняться вызываемым объектом

    Соглашение о вызовах

    Двоичный интерфейс для 64-разрядных приложений Application Binary Interface (ABI) по умолчанию использует соглашение о вызове функций с передачей первых четыре параметров через регистры. Под содержимое этих регистров выделяется место в стеке ("теневое хранилище"). Существует однозначное соответствие между аргументами в функции и регистрами для этих аргументов. Любой аргумент, который не умещается в 8 байтах, передается по ссылке. Один аргумент никогда не разделяется по нескольким регистрам. Регистровый стек x87 не используется. Его можно использовать, но он рассматривается как изменяемый при вызове функций. Все операции с плавающей запятой выполняются с помощью 16 регистров XMM. Целочисленные аргументы передаются в регистрах RCX, RDX, R8 и R9. Вещественные аргументы типа float или double передаются в регистрах XMM0L, XMM1L, XMM2L и XMM3L. 16-байтовые аргументы передаются по ссылке. Передача параметров подробно описана в разделе "Передача параметров". Регистры RAX, R10, R11, XMM4 и XMM5 являются изменяемыми. Все остальные регистры являются неизменяемыми. Использование регистров подробно описано в разделах "Использование регистров" и "Сохраняемые регистры вызываемого и вызывающего объектов".

    Вызывающая функция отвечает за выделение пространства для параметров вызываемой функции и должна всегда выделять достаточное пространство для 4 параметров, даже если вызываемая функция не содержит такого количества параметров. Это помогает упростить поддержку функций без прототипов и функций с переменным количеством аргументов (vararg) C/C++. Для функций с переменным количеством аргументов или для функций без прототипов любое значение типа float должно быть продублировано в соответствующем регистре общего назначения. Любые параметры, следующие после первых 4, до вызова должны сохраняться в стеке над размещенными в памяти для первыми четырьмя параметрами. Сведения о функции с переменным количеством аргументов представлены в разделе "Функции с переменным количеством аргументов (Varargs)". Сведения о функции без прототипов представлены в разделе "Функции без прототипа".

    Выравнивание

    Большинство структур выровнены в соответствии с естественным выравниванием. Главными исключениями являются указатели стека и функции распределения памяти malloc или alloca, которые выровнены на 16 байт для сохранения производительности. Выравнивание свыше 16 байт должно выполняться вручную, но начиная с 16 байт выполняется общее выравнивание размера для операций XMM, которого должно хватать для большей части кода. Дополнительные сведения о структуре и выравнивании см. в разделе "Типы данных и размещение их в памяти". Дополнительные сведения о стеке см. в разделе "Использование стека".

    Unwindability (Способность очищаться, раскрутка)

    Все конечные функции (функции, которые никогда не вызывают функцию, а также никогда не выделяют пространство в стеке) должны дополняться данными (относится к типам данных xdata или pdata, на которые есть указатель из pdata), которые объясняют ОС, каким образом выполнять их очищение для сохранения неизменяемых регистров. Прологи и эпилоги строго ограничены, следовательно, они могут быть правильно описаны в xdata. Указатель стека должен быть выровнен на 16 байт, за исключением конечных функций, в любой области кода, которая не является частью эпилога или пролога. Дополнительные сведения о структуре функции пролога и эпилога см. в разделе "Пролог и эпилог".

    Соглашение о вызовах

    Передача параметров

    Первые четыре целочисленных аргумента передаются через регистры. Целочисленные значения передаются слева направо в регистры RCX, RDX, R8 и R9. Аргументы начиная с пятого и далее передаются через стек. Все 32-разрядные аргументы получают в регистрах знаковое расширение. Это делается для того, чтобы вызываемый мог игнорировать старшие разряды регистра при необходимости и получить доступ только к необходимой части регистра.

    Вещественные 64-разрядные аргументы передаются в регистры XMM0 – XMM3 (до 4) Значения с плавающей запятой помещаются в регистры целочисленных значений RCX, RDX, R8 и R9 только при наличии в них аргументов varargs, регистры XMM0–XMM3 игнорируются, если соответствующий аргумент является целым числом или указателем.

    Типы __m128, массивы и строки никогда не передаются непосредственным значением. Вместо этого указатель передается в память, выделенную вызывающим объектом. Структуры и объединения размером в 8, 16, 32 или 64 бита и __m64, передаются так как если бы они были целыми числами одного и того же размера. Структуры или объединения, отличные от этих размеров передаются как указатель на выделенную память вызывающим объектом. Для этих агрегатных типов, передаваемых в качестве указателя (включая __m128), память должна кратна 16 байт.

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

    Тот, кто вызывает функцию, несет ответственность за сбрасывание при необходимости параметров регистра в теневое пространство.

    В следующей таблице подведены итоги передачи параметров:
    Тип параметраКак передается
    Вещественные параметрыПервые 4 через регистры от XMM0 к XMM3. Остальные передаются
    через стек.
    Целочисленные параметрыПервые 4 через регистры RCX, RDX, R8, R9. Остальные передаются
    через стек.
    Агрегатные параметры (8, 16, 32 или 64-
    разрядные) и __m64
    Первые 4 через регистры RCX, RDX, R8, R9. Остальные передаются
    через стек.
    Агрегатные параметры (другие)
    __m128Указатель. Первые 4 параметра, передаются в качестве
    указателей через RCX, RDX, R8 и R9
    Примеры:
    1. все аргументы — целые числа
      func1(int a, int b, int c, int d, int e);
      // a в RCX, b в RDX, c в R8, d в R9, e передается через стек
    2. все аргументы - числа с плавающей запятой
      func2(float a, double b, float c, double d, float e);
      // a в XMM0, b в XMM1, c в XMM2, d в XMM3, e передается через стек
    3. аргументы — вперемешку целые и вещественные числа
      func3(int a, double b, int c, float d);
      // a в RCX, b в XMM1, c в R8, d в XMM3
    4. аргументы — __m64, __m128 и агрегатные параметры
      func4(__m64 a, _m128 b, struct c, float d);
      // a в RCX, указатель на b в RDX, указатель на c в R8, d в XMM3

    Функции с переменным количеством аргументов (Varargs)

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

    Функции без прототипа

    Для функций без прототипа вызывающий объект передает целые числа в виде значений типа Integer, а значения с плавающей запятой — в виде чисел двойной точности. (Только для значений с плавающей запятой) Если вызываемый объект предполагает наличие значения в регистре операций с целыми числами, в регистрах операций с целыми числами и числами с плавающей запятой одновременно будут содержаться значения с плавающей запятой.
    Код (C):
    1. func1();
    2. func2() {   // RCX = 2, RDX = XMM1 = 1.0, R8 = 7
    3.    func1(2, 1.0, 7);
    4. }

    Возвращаемые значения

    Возвращаемое значение, которое может быть размещен в 64—разрядном регистре RAX это включает типы __m64, но __m128, __m128i, __m128d, расположенном и типы double возвращаются в XMM0. Если возвращаемое значение пользовательского типа, который нельзя разместить в 64—разрядах и вызывающий объект принимает за выделение и передача указателя для возвращаемого значения в качестве первого аргумента. Последующие аргументы перемещают на один аргумент вправо. Тот же самый указатель возвращается вызываемой стороной в RAX. Пользовательские типы, которые должны возвращать непосредственно от 1, 2, 4, 8, 16, 32 и 64 — в длину.

    Примеры:

    1. возвращаемое значение 64—разрядный результат
      __int64 func1 (int a, float b, int c, int d, int e);
      объект вызывающий функцию передает a в RCX, b в XMM1, c в R8, d в R9, e отправляет в стек,
      вызываемая функция возвращает результат __int64 через RAX.
    2. возвращаемое значение — 128-битый результата
      __m128 func2 (float a, double b, int c, __m64 d);
      объект вызывающий функцию передает a в XMM0, b в XMM1, c в R8, d в R9,
      вызываемая функция возвращает результат __m128 в XMM0.
    3. возвращаемое значение — результат пользовательского типа
      struct1 func3 (int a, double b, int c, float d);
      объект вызывающий функцию выделяет память для возвращаемой структуры struct1 и передает указатель в RCX,
      //a в RDX, b в XMM2, c в R9, d отправлен в стек,
      функция возвращает указатель на struct1 через RAX.

    Сохраняемые регистры вызываемого и вызывающего объектов

    Регистры RAX, RCX, RDX, R8, R9, R10 и R11 считаются изменяемыми и их содержимое может модифицироваться при вызове функции (если иное не требуется, исходя из соображений безопасности, например в процессе оптимизации программы).

    Регистры RBX, RBP, RDI, RSI, RSP, R12, R13, R14 и R15 считаются неизменяемыми. Значение в этих регистров должно сохраняться и восстанавливаться в использующей их функции.

    Указатели функций

    Указатели функций указывают на метку соответствующей функции. Требования к оглавлению для указателей функций не предусмотрены.

    Использование стека

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

    Выделение памяти в стеке

    Выделение памяти в стеке для локальных переменных, сохраненных регистров, а также параметров стека и регистра осуществляется в прологе функции.

    Область параметров всегда располагается в нижней части стека (даже если используется функция alloca) и всегда примыкает к адресу, возвращаемому при вызове функции. В области параметров содержится как минимум четыре записи и всегда выделяется объем памяти, достаточный для хранения всех параметров любой вызываемой функции. Обратите внимание, что для параметров регистра память выделяется всегда, даже если они никогда не размещаются в стеке. Также гарантированно выделяется память для всех параметров вызываемого объекта. Чтобы обеспечить выделение непрерывной области памяти при вызове функции, принимающей адрес списка аргументов (va_list) или отдельного аргумента, необходимо предоставить внутренние адреса аргументов регистра. В этой области также хранятся аргументы регистра во время выполнения преобразователя. Кроме того, в ней удобно хранить аргументы регистра, используемые в качестве параметров отладки (например поиск аргументов в процессе отладки является более эффективным, если они хранятся по внутреннему адресу в коде пролога). Даже если вызываемая функция принимает менее четырех параметров, для нее выделяется четыре ячейки стека, которые могут использоваться не только для хранения значений регистра параметров, но и для других целей. Таким образом, во время вызова функции вызывающий объект не может сохранять данные в этой области стека.

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

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

    В следующем примере описывается структура стека, в которой функция A вызывает неоконченную функцию B. В прологе функции A уже выделена память в нижней части стека для всех параметров регистра и стека, необходимых для выполнения функции B. В результате вызова в стек помещается возвращаемый адрес, а в прологе функции B выделяется память для ее локальных переменных и защищенных регистров, а также память, необходимая для вызова функций из функции B. Если для функции B используется функция alloca, память выделяется между областью для хранения локальных переменных или защищенного регистра и областью стека параметра.
    [​IMG]
    При вызове другой функции из функции B возвращаемый адрес помещается непосредственно под внутренним адресом регистра RCX.
     
    Последнее редактирование: 2 мар 2023
  2. Mikl___

    Mikl___ Супермодератор Команда форума

    Публикаций:
    14
    Регистрация:
    25 июн 2008
    Сообщения:
    3.709

    Динамическое создание параметра области стека

    Если используется указатель кадров, то параметр используется для динамического создания параметра области стека. В настоящее время в компиляторе x64 это не выполняется.

    Типы функций

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

    Выравнивание с помощью функции malloc

    Visual C++ допускает типы, имеющие расширенное выравнивание, также известные как сверх-выровненные типы. Например, типы SSE __m128 и __m256, а также типы, объявленные с помощью __declspec(align(n)), где n больше 8, имеют расширенное выравнивание. Выравнивание памяти на границе, которая подходит для объекта, который требует расширенного выравнивания, не гарантированного malloc. Чтобы выделить память для избыточно выровненных типов, используйте _aligned_malloc и соответствующие функции.

    alloca

    Функции _alloca требуется выравнивание по 16-байтовой границе и использование указателя кадра стека.
    Выделяемый стек должен включать расположенное под ним пространство для параметров функций, вызываемых позднее, как описано в разделе Выделение памяти в стеке.
    malloc гарантированно возвращает память, которая подходит для хранения любого объекта с базовым выравниванием, который мог бы поместиться в выделенной памяти. Базовое выравнивание — это выравнивание, которое меньше или равно наибольшему выравниванию, которое поддерживается реализацией без задания выравнивания. (В Visual C++ это основное выравнивание, необходимое для double или 8 байт. В коде, который нацелен на 64-разрядные платформы, это 16 байт.) Например, выделение 4 байт будет выровнено по границе, которая поддерживает все любой четырехбайтовый или меньший объект.

    Пролог и эпилог

    Для каждой функции, в которой выделяется память в стеке, вызываются другие функции, сохраняются защищенные регистры или выполняется обработка исключений, необходимо использовать пролог. На адрес пролога накладываются ограничения, описываемые в данных завершения, которые связаны с соответствующей записью таблицы функций. При необходимости в прологе сохраняются регистры аргумента (по внутренним адресам), помещаются в стек защищенные регистры, выделяется фиксированная часть стека для локальных и временных переменных, а также создается указатель кадра. В связанных данных завершения описывается действие пролога, а также предоставляются сведения, используемые для отмены результатов выполнения кода пролога.
    Если выделяемая фиксированная часть стека занимает более одной страницы (более 4096 байт), выделяемая область стека может располагаться на нескольких страницах виртуальной памяти. В этом случае необходимо проверить выделяемую область перед ее фактическим выделением. Для этих целей используется специальная процедура, которая вызывается из пролога и не уничтожает регистры аргумента.
    При сохранении защищенных регистров рекомендуется перемещать их в стек до выделения фиксированной части стека. Если выделение фиксированной части стека выполняется до сохранения защищенных регистров, для обращения к области сохраненного регистра в большинстве случаев требуется 32-разрядное смещение. Производительность функций помещения и перемещения регистров примерно одинакова и будет оставаться такой в ближайшем будущем, независимо от предполагаемой зависимости между функциями помещения. Защищенные регистры могут сохраняться в любом порядке. Однако в качестве первой операции с защищенным регистром в прологе необходимо выполнять сохранение регистра.
    типичный код пролога
    Код (ASM):
    1. mov       [RSP + 8], RCX
    2.  push   R15
    3.  push   R14
    4.  push   R13
    5.  sub      RSP, fixed-allocation-size
    6.  lea      R13, [RSP+128]
    7.  ...
    В этом прологе аргумент регистра RCX сохраняется по внутреннему адресу, сохраняются защищенные регистры R13-R15, выделяется кадр фиксированной части кадра стека, а также создается указатель кадра, который указывает на выделенную фиксированную область размером 128 байт. Благодаря использованию смещения обеспечивается обращение к большему числу адресов выделенной фиксированной области с помощью однобайтовых смещений.
    Если размер фиксированной области памяти превышает размер одной страницы памяти или равен ему, перед изменением RSP следует вызвать вспомогательную функцию. Вызываемая функция __chkstk обеспечивает проверку подлежащей выделению области стека на предмет допустимости расширения стека. В этом случае приведенный выше пример пролога будет выглядеть следующим образом:
    Код (ASM):
    1. mov       [RSP + 8], RCX
    2.  push   R15
    3.  push   R14
    4.  push   R13
    5.  mov      RAX,  fixed-allocation-size
    6.  call   __chkstk
    7.  sub      RSP, RAX
    8.  lea      R13, [RSP+128]
    9.  ...
    Вспомогательная функция __chkstk изменяет только регистры R10 и R11. Другие регистры и коды условий не изменяются. В частности, при ее выполнении регистр RAX возвращается без изменений. Все защищенные регистры и регистры передачи аргументов также не изменяются.

    Код эпилога

    Код эпилога существует для каждого выхода в функции. В большинстве случаев используется один пролог, но допускается использование нескольких эпилогов. В коде эпилога выполняется усечение стека до размера фиксированной выделяемой области (при необходимости), отменяется выделение фиксированной части стека, восстанавливаются значения защищенных регистров (посредством извлечения их сохраненных значений из стека), после чего управление возвращается вызывающей функции.
    В коде эпилога необходимо придерживаться строгого набора правил, применяемых к коду завершения, что позволяет обеспечить безопасное завершение без вызова исключений и прерываний. Это позволяет уменьшить объем используемых данных завершения, поскольку не используются дополнительные данные для описания каждого эпилога. Вместо этого выполнение эпилога определяется в коде завершения посредством прямого просмотра потока кода для идентификации эпилога.
    Если в функции не используется указатель кадра, в эпилоге сначала отменяется выделение фиксированной части стека, затем извлекаются сохраненные значения защищенных регистров, после чего управление возвращается вызывающей функции. Например:
    Код (ASM):
    1. add      RSP,fixed-allocation-size
    2.  pop      R13
    3.  pop      R14
    4.  pop      R15
    5.  ret
    Если в функции используется указатель кадра, перед выполнением эпилога необходимо выполнить усечение стека до размера фиксированной выделяемой области. С технической точки зрения эта операция не входит в состав эпилога. Ниже приведен пример эпилога, который может использоваться для отмены ранее выполненного пролога:
    Код (ASM):
    1. lea      RSP,[R13-128] ; epilogue proper starts here
    2.  add      RSP, fixed-allocation-size
    3.  pop      R13
    4.  pop      R14
    5.  pop      R15
    6.  ret
    На практике, если используется указатель кадра, не имеет смысла выполнять изменение регистра RSP в два этапа, поэтому вместо приведенного выше можно использовать следующий эпилог:
    Код (ASM):
    1. lea      RSP,[R13128+fixed-allocation-size]
    2.  pop      R13
    3.  pop      R14
    4.  pop      R15
    5.  ret
    Выше приведены единственно допустимые формы эпилога. Эпилог должен включать выражение add RSP,constant или lea RSP,[FPReg+constant], за которым следуют последовательность из нескольких (или ни одной) команд извлечения 8-байтовых регистров (pop), а также команды return или jmp. В эпилоге допускается использование не всех операторов jmp. К допустимым относятся только операторы jmp со ссылками на память ModRM, в которых значение поля mod ModRM равно 00. Использование в эпилоге операторов jmp, для которых значение поля mod ModRM равно 01 или 10, не допускается. Дополнительные сведения о допустимых ссылках ModRM см. в таблице A-15 в разделе, посвященном инструкциям общего и системного назначения, руководства программиста архитектуры процессора AMD x86-64 (том 3). Использование другого кода в эпилоге не допускается. В частности, в эпилоге не допускается планирование каких-либо задач, в том числе загрузки возвращаемого значения.
    Обратите внимание, что если указатель кадра не используется, в эпилоге необходимо использовать выражение add RSP,constant для отмены выделения фиксированной части стека. Использование вместо него выражения lea RSP,[RSP+constant] не допускается. Это ограничение позволяет уменьшить число шаблонов, распознаваемых при поиске эпилогов.
    Если эти правила соблюдаются, в коде завершения определяется выполняемый в данный момент эпилог и имитируется выполнение оставшейся части эпилога, что позволяет воссоздать контекст вызывающей функции.

    Обработка исключений (x64)

    В данном разделе рассматриваются структурная обработка исключений и поведение приложений C++ на платформах x64 при обработке исключений.

    Данные раскрутки для обработки исключений и поддержки отладчика

    В данном подразделе описываются структуры данных, необходимые для обработки исключений, а также поддержки отладчика.

    структура RUNTIME_FUNCTION

    Для табличной обработки исключений требуется запись в таблице для каждой функции, выделяющей место в стеке или вызывающей другую функцию (например, неконечные функции). Записи в таблице функций имеют следующий формат:
    РазмерЗначение
    ULONGНачальный адрес функции
    ULONGКонечный адрес функции
    ULONGАдрес очистки
    Структура RUNTIME_FUNCTION должна быть выровнена в памяти по типу DWORD. Все адреса задаются относительно образа, то есть, они представляют собой 32-разрядные смещения относительно стартового адреса образа, содержащего запись в таблице функций. Эти записи сортируются и помещаются в раздел .pdata образа PE32+. Для динамически создаваемых функций [JIT-компиляторов] среда выполнения для поддержки этих функций должна использовать RtlInstallFunctionTableCallback или RtlAddFunctionTable, чтобы предоставлять эти сведения операционной системе. Невыполнение этого требования приведет к ненадежной обработке исключений и отладке процессов.

    структура UNWIND_INFO

    Информационная структура очищения данных используется для записи эффектов функции на указатель стека и места в стеке, где сохраняются неизменяемые регистры:
    РазмерЗначение
    UBYTE: 3Версия
    UBYTE: 5Флаги
    UBYTEРазмер пролога
    UBYTEСчетчик кодов очистки
    UBYTE: 4Регистр кадра
    UBYTE: 4Смещение регистра кадра (масштабированное)
    USHORT * nМассив кодов очистки
    переменнаяМожет находиться в форме (1) или (2) ниже
    (1) Обработчик исключений
    РазмерЗначение
    ULONGАдрес обработчика исключений
    переменнаяДанные языкового обработчика (необязательно)
    (2) цепочка Unwind Info
    РазмерЗначение
    ULONGНачальный адрес функции
    ULONGКонечный адрес функции
    ULONGАдрес очистки
    Структура UNWIND_INFO должна быть выровнена в памяти по DWORD. Каждое поле имеет следующее значение:
    • Версия
      Номер версии данных возврата, текущая версия — 1.
    • Флаги
      В настоящее время определены три флага:
      • UNW_FLAG_EHANDLER функция содержит обработчик исключений, который должен вызываться и функции, которые необходимо проанализировать исключения.
      • UNW_FLAG_UHANDLER функция содержит обработчик завершения, который должен вызываться развертывание исключение.
      • UNW_FLAG_CHAININFO это разматывает структуру нет основные сведения и процедуры. Вместо этого запись информации очистки является содержимым предыдущей записи RUNTIME_FUNCTION. Просмотрите дальнейший текст, поясняющий структуры зависимой информации очистки. Если этот флаг установлен, тогда флаги UNW_FLAG_EHANDLER и UNW_FLAG_UHANDLER должны быть сняты. Кроме того, регистр кадра и фиксированные поля выделения стека должны иметь значения, совпадающие с значениями основной информации очистки.
    • Размер пролога
      Длина пролога функции в байтах.
    • Счетчик кодов очистки
      Количество гнезд в массиве кодов очистки. Обратите внимание на то, что некоторые коды очистки (например, UWOP_SAVE_NONVOL) требуют больше одного гнезда в массиве.
    • Регистр кадра
      Если значение отличается от нулевого, то функция использует указатель кадра, а это поле является номером постоянного регистра, используемого в качестве указателя кадра с той же кодировкой, что и для поля информации об операции узлов UNWIND_CODE.
    • Смещение регистра кадра (масштабированное)
      Если поле регистра кадра отлично от нуля, то это поле содержит масштабированное смещение от RSP, примененного к FP reg после его установки. Действительное значение FP reg задается как RSP + 16 * это число, что делает возможными смещения от 0 до 240. Это разрешает направление FP reg в середину локального выделения стека для динамических кадров стека, обеспечивая лучшую плотность кода за счет использования более коротких инструкций (больше инструкций могут использовать для смещения 8-разрядное знаковое число).
    • Массив кодов очистки
      Это массив элементов, объясняющий степень воздействия пролога на постоянные регистры и RSP. Просмотрите подраздел об использовании UNWIND_CODE для значений индивидуальных элементов. Чтобы выполнить выравнивание, этот массив всегда будет иметь четное количество записей с потенциально неиспользуемой последней записью (в таком случае массив будет на одну запись длиннее, чем указано счетчиком полей кодов очистки).
    • Адрес обработчика исключений
      Это относительный указатель на обработчик языковых исключений или обработчик завершений функции (если флаг UNW_FLAG_CHAININFO снят, а один из флагов UNW_FLAG_EHANDLER или UNW_FLAG_UHANDLER установлен).
    • Данные обработчика определенного языка
      Это данные обработчика исключений выбранного языка функции. Формат этих данных не указан и полностью определяется конкретным используемым обработчиком исключений.
    • Зависимая информация очистки
      Если флаг UNW_FLAG_CHAININFO установлен, тогда структура UNWIND_INFO завершается тремя UWORD. Эти UWORD представляют связанную информацию RUNTIME_FUNCTION для обращаемой функции.

    структура UNWIND_CODE

    Массив кода раскрутки используется для записи последовательности операций в прологе, оказывающих влияние на неизменяемые регистры и RSP. Каждый элемент кода имеет следующий формат:
    РазмерЗначение
    UBYTEСмещение в прологе
    UBYTE: 4Код операции очистки
    UBYTE: 4Сведения об операции
    Элементы массива располагаются в убывающем порядке в соответствии с положением в прологе.

    Смещение в прологе

    Смещение от начала пролога конца инструкции, которая выполняет данную операцию, плюс 1 (это расположение следующей инструкции).
     
    Последнее редактирование: 6 дек 2022
    Intro нравится это.
  3. Mikl___

    Mikl___ Супермодератор Команда форума

    Публикаций:
    14
    Регистрация:
    25 июн 2008
    Сообщения:
    3.709

    Код завершающей операции

    Примечание. В кодах некоторых операций необходимо использовать смещение без учета знака в качестве значения в локальном кадре стека. Это смещение отсчитывается от начала (нижнего адреса) выделенного фиксированного пространства стека. Если поле регистра кадра стека в структуре UNWIND_INFO является пустым, то смещение отсчитывается от RSP. Если поле регистра кадра стека не является пустым, смещение отсчитывается от расположения RSP в момент установки регистра FP. Оно рассчитывается как регистр FP минус смещение регистра FP (16×масштабированное смещение регистра кадра стека в UNWIND_INFO). При использовании регистра FP любой код раскрутки со смещением должен использоваться только после установки в прологе регистра FP.
    Для всех кодов операций кроме UWOP_SAVE_XMM128 и UWOP_SAVE_XMM128_FAR смещение будет кратным 8, поскольку хранимые в стеке значения выравниваются до 8 байт (сам стек всегда выравнивается до 16 байт). Для кодов операций с коротким смещением (менее 512 Кбайт) завершающий USHORT в узлах кода содержит смещение, разделенное на 8. Для кодов операций с длинным смещением (от 512 Кбайт до 4 Гбайт) два завершающих узла USHORT кода содержат смещение (с прямым порядком следования байтов).
    Для кодов операций UWOP_SAVE_XMM128 и UWOP_SAVE_XMM128_FAR смещение будет кратным 16, поскольку все 128-битные операции XMM должны выполняться в памяти, выровненной до 16 байт. Поэтому для операции UWOP_SAVE_XMM128 используется масштабный коэффициент 16, который позволяет использовать смещение до 1 МБ.
    Кодом завершающей операции может быть:
    • 1 узел UWOP_PUSH_NONVOL (0)
      Отправка неизменяемого целочисленного регистра с уменьшением значения RSP на 8 байт. Значение в поле сведений об операции является числом регистра. Обратите внимание, что из-за ограничений, накладываемых на эпилог, коды раскрутки UWOP_PUSH_NONVOL должны использоваться первыми в прологе и, соответственно, последними в массиве кодов раскрутки. Этот относительный порядок применяется ко всем другим кодам раскрутки операций, за исключением UWOP_PUSH_MACHFRAME.
    • 2 или 3 узла UWOP_ALLOC_LARGE (1)
      Выделение для стека большого объема памяти. Существуют два варианта. Если в поле сведений об операции содержится значение ноль, в соседнюю ячейку записывается размер выделенной памяти, поделенный на 8. Если в поле сведений об операции содержится значение 1, то в следующие две ячейки записывается размер выделенной памяти без масштабирования с прямым порядком следования байтов. Это позволяет выделять до 4 ГБ - 8.
    • 1 узел UWOP_ALLOC_SMALL (2)
      Выделение для стека небольшого объема памяти. Размер выделения рассчитывается как число в поле сведений об операции * 8 + 8, что позволяет выделять от 8 до 128 байт.
      Код раскрутки должен всегда использовать наиболее короткую кодировку для выделения памяти в стеке.
      Размер выделения Код раскрутки
      От 8 до 128 байтUWOP_ALLOC_SMALL
      От 136 до 512 КБ - 8 байтUWOP_ALLOC_LARGE, operation info = 0
      От 512 КБ до 4 ГБ – 8 байтUWOP_ALLOC_LARGE, operation info = 1
    • 1 узел UWOP_SET_FPREG (3)
      Установите регистр указателя кадра стека, задав для регистра какое-либо смещение текущего RSP. Это смещение рассчитывается как значение (масштабированное) смещения в поле регистра указателя кадра стека UNWIND_INFO * 16, что разрешает смещение со значением от 0 до 240. Использование смещения позволяет установить указатель кадра стека по центру фиксированного выделения стека, что способствует повышению плотности кода и разрешает использовать большее количество форм коротких инструкций. Обратите внимание, что поле сведений об операции является зарезервированным и не должно использоваться.
    • 2 узлаUWOP_SAVE_NONVOL (4)
      Сохраните неизменяемый регистр целых чисел в стеке, используя функцию MOV вместо PUSH. Как правило, она применяется для создания изолированного кода, при котором неизменяемый регистр сохраняется в стеке в том положении, которое было ранее выделено. Значение в поле сведений об операции является числом регистра. Смещение стека (масштабированное по 8) записывается в следующей ячейке кода завершающей операции, как описано выше в примечании.
    • 3 узла UWOP_SAVE_NONVOL_FAR (5)
      Сохраните неизменяемый целочисленный регистр в стеке с длинным смещением, используя функцию MOV вместо PUSH. Как правило, она применяется для создания изолированного кода, при котором неизменяемый регистр сохраняется в стеке в том положении, которое было ранее выделено. Значение в поле сведений об операции является числом регистра. Смещение стека (немасштабированное) записывается в следующих двух ячейках кода завершающей операции, как описано выше в примечании.
    • 2 узла UWOP_SAVE_XMM128 (8)
      Сохраните все 128 байт неизменяемого регистра XMM в стеке. Значение в поле сведений об операции является числом регистра. Смещение стека (масштабированное на 16) записывается в следующую ячейку.
    • 3 узла UWOP_SAVE_XMM128_FAR (9)
      Сохраните все 128 байт неизменяемого регистра XMM в стеке с длинным смещением. Значение в поле сведений об операции является числом регистра. Смещение стека (немасштабированное) записывается в следующие две ячейки.
    • 1 узел UWOP_PUSH_MACHFRAME (10)
      Передача машинного кадра Используется для записи воздействия аппаратного вмешательства или исключения. Существуют два варианта. Если в поле сведений об операции значение ноль, значит в стек были переданы следующие данные:
    РасположениеЗначение
    RSP+32SS
    RSP+24Old RSP
    RSP+16EFLAGS
    RSP+8CS
    RSPRIP
    Если в поле сведений об операции значение "1", значит в стек были переданы следующие данные:
    РасположениеЗначение
    RSP+40SS
    RSP+32Old RSP
    RSP+24EFLAGS
    RSP+16CS
    RSP+8RIP
    RSPКод ошибки
    Этот код раскрутки должен всегда присутствовать в фиктивном прологе, который никогда не выполняется, но присутствует перед реальной точкой входа подпрограммы прерывания и существует, только чтобы предоставить место для имитации передачи машинного кадра. UWOP_PUSH_MACHFRAME записывает эту имитацию, что означает, что компьютер концептуально выполнил следующее:
    1. Перемещение адреса возврата RIP с вершины стека в Temp
    2. Передача SS
    3. Передача старого значения в RSP
    4. Передача EFLAGS
    5. Передача CS
    6. Передача Temp
    7. Передача кода ошибки (если в поле сведений об операции значение "1")
    Смоделированная операция UWOP_PUSH_MACHFRAME уменьшает RSP на 40 (если в поле сведений об операции значение "0") или 48 (если в поле сведений об операции значение "1")

    Сведения об операции

    Значение этих 4 байт зависит от кода операции. Чтобы выполнить кодирование целочисленного регистра общего назначения, используется следующее сопоставление.
    Разряд
    0​
    1​
    2​
    3​
    4​
    5​
    6​
    7​
    8-15​
    РегистрRAXRCXRDXRBXRSPRBPRSIRDIR8-R15

    структуры связанных данных раскрутки

    Если установлен флаг UNW_FLAG_CHAININFO, то структура информации очистки является вторичной и общее поле обработчика исключений/связанных данных содержит первичную информацию раскрутки. Следующий код извлекает основные сведения очистки, при условии что unwindInfo — структура, имеющая установленный флаг UNW_FLAG_CHAININFO.
    Код (C):
    1. PRUNTIME_FUNCTION primaryUwindInfo =
    2. (PRUNTIME_FUNCTION) &
    3. (unwindInfo-]UnwindCode[( unwindInfo-]CountOfCodes + 1 ) & ~1]);
    Связанные данные используются в двух случаях. Во-первых, они используется в несмежных сегментах кода. Используя связанные сведения, можно уменьшить размер требуемой информации раскрутки, поскольку нет необходимости дублировать массив кодов раскрутки из основной информации раскрутки.
    Связанные сведения можно также использовать для группировки сохраненных данных неизменяемых регистров. Компилятор может отложить сохранение некоторых неизменяемых регистров до выхода из пролога записи функции. Они могут быть записаны перед группированным кодом посредством использования основных данных раскрутки для части функции и последующей установки связанных данных с ненулевым размером пролога. При этом коды раскрутки в связанных данных будут отражать сохраненные данных неизменяемых регистров. В этом случае все коды раскрутки являются экземплярами UWOP_SAVE_NONVOL. Команда, которая сохраняет содержимое неизменяемых регистров с помощью PUSH или изменить регистр RSP с помощью дополнительного фиксированного выделение стека не поддерживается.
    Элемент UNWIND_INFO, имеющий набор UNW_FLAG_CHAININFO, может содержать запись RUNTIME_FUNCTION, чей элемент UNWIND_INFO также имеет набор UNW_FLAG_CHAININFO(множественный изолированный код). В конечном счете, указатели связанных данных раскрутки достигнут элемент UNWIND_INFO, для которого флаг UNW_FLAG_CHAININFO не установлен. Этот элемент будет являться основным элементом UNWIND_INFO, указывающим на фактическую точку входа процедуры.

    Процедура очистки

    Массив кода очистки сортируется в убывающем порядке. При возникновении исключения полный контекст сохраняется операционной системой в записи контекста. После этого вызывается логика обработки исключений, несколько раз выполняющая следующие операции по поиску обработчика исключения.
    1. Для поиска записи в таблице RUNTIME_FUNCTION, описывающей текущую функцию (или часть функции, в случае связанных записей UNWIND_INFO) следует использовать текущую версию защиты остаточных данных (RIP), сохраняемых в записи контекста.
    2. Если записи в таблице функций не найдено, то она находится в конечной функции, а RSP обращается напрямую к указателю возврата. Указатель возврата в [RSP] сохраняется в обновленном контексте, смоделированный RSP получает приращение на 8, и шаг 1 повторяется.
    3. Если запись в таблице функций найдена, то RIP может лежать в трех областях — a) в заключительной части, b) в прологе или c) в коде, доступном обработчику исключений.
      • В первом случае, если RIP находится в заключительной части, то элемент управления выходит из функции, отсутствует обработчик исключения для этой функции, а результаты заключительной части должны обрабатываться и далее до вычисления контекста вызывающей функции. Чтобы определить, лежит ли RIP в заключительной части, необходимо исследовать поток кода из включенного RIP. Если поток кода может соответствовать конечной части допустимого эпилога, то это будет эпилог, а остальная часть эпилога будет смоделирована, при этом запись контекста будет обновляться при обработке каждой инструкции. После этого повторяется выполнение шага 1.
      • Во втором случае, если RIP находится в прологе, то элемент управления не вошел в функцию, отсутствует обработчик исключения для этой функции, а результаты пролога должны быть отменены для вычисления контекста вызывающей функции. RIP лежит в прологе, если расстояние от начала функции до RIP меньше либо равно размеру пролога, закодированному в информации об очистке. Результаты в прологе очищаются при просмотре вперед по массиву кода очистки для первой записи со смещением, меньшим либо равным смещению RIP от начала функции, после чего выполняется отмена результата для всех остальных элементов в массиве кода очистки. После этого повторяется выполнение шага 1.
      • В третьем случае, если RIP не лежит в прологе или в заключительной части, и для функции имеется обработчик исключений (установлен флаг UNW_FLAG_EHANDLER), то вызывается обработчик конкретного языка. Обработчик просматривает данные и вызывает соответствующие функции фильтра. Обработчик, специфичный для конкретного языка, может возвращать результат, указывающий на то, что исключение было обработано, либо на то, что поиск следует продолжить. Он может также инициировать очистку напрямую.
    4. Если обработчик, специфичный для конкретного языка, возвращает состояние "обработано", то выполнение продолжается с использованием исходной записи контекста.
    5. Если обработчик, специфичный для конкретного языка, отсутствует, либо если возвращает результат "продолжить поиск", запись контекста должна быть очищена до состояния вызывающего объекта. Эта выполняется путем обработки всех элементов массива кода очистки с отменой результата для каждого элемента. После этого повторяется выполнение шага 1.
    Если используются связанные данные очистки, то выполнение этих основных операций продолжается. Единственное отличие заключается в том, что при прохождении массива кода очистки с целью отмены результатов для пролога, как только достигнут конец массива, он присоединяется к главной информации по очистке, и выполняется прохождение по всему обнаруженному массиву очистки. Это присоединение продолжается до состояния очистки без флага UNW_CHAINED_INFO и завершения прохождения по массиву кода очистки.
    Наименьший набор данных очистки имеет размер в 8 байтов. Это позволило бы создать функцию, занимающую в стеке не более 128 байтов, и сэкономить независимый регистр. Это также показывает размер связанной структуры данных очистки для пролога нулевой длины без кодов очистки.

    Обработчик конкретного языка

    Относительный адрес обработчика языка присутствует в UNWIND_INFO, когда бы ни были установлены флаги UNW_FLAG_EHANDLER или UNW_FLAG_UHANDLER. Как описано в предыдущем разделе, обработчик языка вызывается как часть поиска обработчика исключения или часть раскрутки. Он имеет следующий прототип:
    Код (C):
    1. typedef EXCEPTION_DISPOSITION (*PEXCEPTION_ROUTINE) (
    2.      IN PEXCEPTION_RECORD ExceptionRecord,
    3.     IN ULONG64 EstablisherFrame,
    4.      IN OUT PCONTEXT ContextRecord,
    5.      IN OUT PDISPATCHER_CONTEXT DispatcherContext
    6.  );
    • ExceptionRecord предоставляет указатель на запись исключения, имеющий стандартное определение Win64.
    • EstablisherFrame представляет собой адрес базы фиксированного расположения стека для данной функции.
    • ContextRecord указывает на контекст исключения во время его возникновения (в случае если задействован обработчик событий) или текущий контекст "раскрутки" (в случае если задействован обработчик завершения).
    • DispatcherContext указывает на контекст диспетчера для данной функции. Он имеет следующее определение:
    Код (C):
    1. typedef struct _DISPATCHER_CONTEXT {
    2.      ULONG64 ControlPc;
    3.      ULONG64 ImageBase;
    4.      PRUNTIME_FUNCTION FunctionEntry;
    5.      ULONG64 EstablisherFrame;
    6.      ULONG64 TargetIp;
    7.      PCONTEXT ContextRecord;
    8.      PEXCEPTION_ROUTINE LanguageHandler;
    9.      PVOID HandlerData;
    10.  } DISPATCHER_CONTEXT, *PDISPATCHER_CONTEXT;
    • ControlPc представляет собой значение RIP в рамках данной функции. Это может быть адрес исключения или адрес, на котором элемент прекратил функцию установления. Этот RIP будет использоваться для определения, находится ли элемент управления в рамках защищенной конструкции в данной функции (например, блок __try для __try/__except или __try/__finally).
    • ImageBase представляет собой основу образа (адрес загрузки) модуля, содержащего данную функцию, которую необходимо добавить в 32-битные смещения, используемые в записи функции, а также в информации раскрутки для записи относительных адресов.
    • FunctionEntry предоставляет указатель записи функции RUNTIME_FUNCTION, содержащей саму функцию и относительные адреса информации раскрутки основного образа для данной функции.
    • EstablisherFrame представляет собой адрес базы фиксированного расположения стека для данной функции.
    • TargetIp предоставляет адреса выборочных инструкций, указывающие дополнительные адреса раскрутки. Этот адрес пропускается, если не было указано EstablisherFrame.
    • ContextRecord указывает на контекст исключения, используемый кодом диспетчеризации или раскрутки системного исключения.
    • LanguageHandler указывает на подпрограмму вызванного языкового обработчика.
    • HandlerData указывает на данные языкового обработчика для данной функции.

    Завершение вспомогательных процедур для MASM

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

    Необработанные псевдооперации

    В этом разделе перечислены псевдооперации.
    ПсевдооперацияОписание
    PROC FRAME [:ehandler]Приводит к тому, что компилятор MASM создает запись в таблице функции в PDATA и раскручивает информацию в XDATA для структурной обработки исключений функции при раскрутке. При наличии обработчика ошибок данная процедура вводится в XDATA как языковой обработчик.
    При использовании атрибута FRAME за ним обязательно должна следовать директива .ENDPROLOG. Если функция является конечной (в соответствии с разделом Типы функций), атрибут FRAME является необязательным, так же как и остатки этих псевдоопераций.
    .PUSHREG regСоздает в коде завершения UWOP_PUSH_NONVOL запись для указанного номера регистра с помощью текущего смещения в прологе.
    Следует применять эту операцию только к защищенным целочисленным регистрам. Для передачи временных регистров следует использовать ALLOCSTACK 8.
    .SETFRAME reg, offsetЗаполняет поле регистра для фреймов и указывает смещение в информации для раскрутки с помощью указанного регистра и смещения. Смещение должно быть кратным 16 и меньшим или равным 240. Данная директива также создает в коде завершения UWOP_SET_FPREG запись для указанного регистра с помощью текущего смещения в прологе.
    .ALLOCSTACK sizeСоздает код UWOP_ALLOC_SMALL или UWOP_ALLOC_LARGE с указанным размером текущего смещения в прологе.
    Операнд size должен быть кратным 8.
    .SAVEREG reg, offsetСоздает запись в коде завершения UWOP_SAVE_NONVOL или UWOP_SAVE_NONVOL_FAR для указанного регистра и смещения, используя текущее смещение в прологе. Компилятор MASM выберет наиболее подходящий способ кодировки.
    Смещение должно иметь положительное значение и быть кратным 8. Смещение указывается относительно кадра процедуры (как правило, в RSP) или указателя на кадр (немасштабированный).
    .SAVEXMM128 reg, offsetСоздает запись в коде завершения UWOP_SAVE_XMM128 или UWOP_SAVE_XMM128_FAR для указанного регистра XMM и смещения, используя текущее смещение в прологе. Компилятор MASM выберет наиболее подходящий способ кодировки.
    Смещение должно иметь положительное значение и быть кратным 16. Смещение указывается относительно кадра процедуры (как правило, в RSP) или указателя на кадр (немасштабированный).
    .PUSHFRAME [код]Создает запись в коде завершения UWOP_PUSH_MACHFRAME. Если указан дополнительный код, к записи в код завершения добавляется модификатор 1. В противном случае используется модификатор 0.
    .ENDPROLOGСообщает об окончании объявлений в прологе. Находится в первых 255 байтах функции.
    Ниже представлен пример пролога функции, демонстрирующий допустимое использование большинства кодов операций.
    Код (ASM):
    1. sample PROC FRAME
    2.       db      048h; emit a REX prefix, to enable hot-patching
    3.  push rbp
    4.  .pushreg rbp
    5.  sub rsp, 040h
    6.  .allocstack 040h
    7.    lea rbp, [rsp+020h]
    8.  .setframe rbp, 020h
    9.  movdqa [rbp], xmm7
    10.  .savexmm128 xmm7, 020h;the offset is from the base of the frame
    11. ;not the scaled offset of the frame
    12.  mov [rbp+018h], rsi
    13.  .savereg rsi, 038h
    14.  mov [rsp+010h], rdi
    15.  .savereg rdi, 010h; you can still use RSP as the base of the frame
    16.  ; or any other register you choose
    17.  .endprolog   ; you can modify the stack pointer outside of the prologue (similar to alloca)
    18. ; because we have a frame pointer.
    19. ; if we didn’t have a frame pointer, this would be illegal
    20. ; if we didn’t make this modification,
    21. ; there would be no need for a frame pointer
    22.    sub rsp, 060h   ; we can unwind from the following AV because of the frame pointer
    23.    mov rax, 0
    24.  mov rax, [rax] ; AV!
    25.    ; restore the registers that weren’t saved with a push
    26. ; this isn’t part of the official epilog, as described in section 2.5
    27.    movdqa xmm7, [rbp]
    28.  mov rsi, [rbp+018h]
    29.  mov rdi, [rbp-010h]
    30.    ; Here’s the official epilog
    31.    lea rsp, [rbp-020h]
    32.  pop rbp
    33.  ret
    34. sample ENDP

    Макросы MASM

    Для упрощения использования операций, описанных в разделе Необработанные псевдооперации, в файле ksamd64.inc определен набор макросов, которые можно использовать для создания типичных прологов и эпилогов процедур.
    МакросОписание
    alloc_stack(n)Выделяет кадр стека размером в n байт (с помощью команды "sub rsp, n") и помещает соответствующую информацию для раскрутки (".allocstack b").
    save_reg reg, locСохраняет защищенный регистр "reg" в стеке по RSP-смещению "loc" и помещает соответствующую информацию для раскрутки (".savereg reg, loc").
    push_reg regСохраняет защищенный регистр "reg" в стеке и помещает соответствующую информацию для раскрутки (".pushreg reg").
    rex_push_reg regСохраните содержимое неизменяемого регистра в стеке использование внедрения 2 байт, и выведите соответствующее размотайте сведения (reg .pushreg) это должно использоваться, если внедрения первая инструкция в функции убедиться, что функция высокий - patchable.
    save_xmm128 reg, locСохраняет защищенный XMM-регистр "reg" в стеке по RSP-смещению "loc" и помещает соответствующую информацию для раскрутки (".savexmm128 reg, loc").
    set_frame reg, offsetПрисваивает регистру стекового кадра "reg" значение RSP + offset (с помощью команды mov или lea) и помещает соответствующую информацию для раскрутки (".set_frame reg, offset").
    push_eflagsСохраняет регистр "eflags" в стек с помощью команды pushfq и помещает соответствующую информацию для раскрутки (".alloc_stack 8").
    Ниже представлен пример пролога функции, в котором должным образом используются описанные макросы.
    Код (ASM):
    1. SkFrame struct
    2.  Fill    dq ?; fill to 8 mod 16
    3.  SavedRdi dq ?; saved register RDI
    4.  SavedRsi dq ?; saved register RSI
    5.  SkFrame ends
    6.  
    7. sampleFrame struct
    8.  Filldq?; fill to 8 mod 16
    9.  SavedRdidq?; Saved Register RDI
    10.  SavedRsi  dq?; Saved Register RSI
    11.  sampleFrame ends
    12.  
    13. sample2 PROC FRAME
    14.  alloc_stack(sizeof sampleFrame)
    15.  save_reg rdi, sampleFrame.SavedRdi
    16.  save_reg rsi, sampleFrame.SavedRsi
    17.  .end_prolog
    18. ; function body
    19.    mov rsi, sampleFrame.SavedRsi[rsp]
    20.  mov rdi, sampleFrame.SavedRdi[rsp]
    21. ; Here’s the official epilog
    22.    add rsp, (sizeof sampleFrame)
    23.  ret
    24. sample2 ENDP

    Описание раскрутки данных в языке C

    Далее следует описание раскрутки данных в языке С.
    Код (C):
    1. typedef enum _UNWIND_OP_CODES {
    2.      UWOP_PUSH_NONVOL = 0, /* info == register number */
    3.      UWOP_ALLOC_LARGE,     /* no info, alloc size in next 2 slots */
    4.      UWOP_ALLOC_SMALL,     /* info == size of allocation / 8 - 1 */
    5.      UWOP_SET_FPREG,       /* no info, FP = RSP + UNWIND_INFO.FPRegOffset*16 */
    6.      UWOP_SAVE_NONVOL,     /* info == register number, offset in next slot */
    7.      UWOP_SAVE_NONVOL_FAR, /* info == register number, offset in next 2 slots */
    8.      UWOP_SAVE_XMM128,     /* info == XMM reg number, offset in next slot */
    9.      UWOP_SAVE_XMM128_FAR, /* info == XMM reg number, offset in next 2 slots */
    10.      UWOP_PUSH_MACHFRAME   /* info == 0: no error-code, 1: error-code */
    11.  } UNWIND_CODE_OPS;
    12.    typedef union _UNWIND_CODE {
    13.      struct {
    14.          UBYTE CodeOffset;
    15.          UBYTE UnwindOp : 4;
    16.          UBYTE OpInfo   : 4;
    17.      };
    18.      USHORT FrameOffset;
    19.  } UNWIND_CODE, *PUNWIND_CODE;
    20. #define UNW_FLAG_EHANDLER  0x01
    21. #define UNW_FLAG_UHANDLER  0x02
    22. #define UNW_FLAG_CHAININFO 0x04
    23. typedef struct _UNWIND_INFO {
    24.      UBYTE Version       : 3;
    25.      UBYTE Flags         : 5;
    26.      UBYTE SizeOfProlog;
    27.      UBYTE CountOfCodes;
    28.      UBYTE FrameRegister : 4;
    29.      UBYTE FrameOffset   : 4;
    30.      UNWIND_CODE UnwindCode[1]; /*  UNWIND_CODE MoreUnwindCode[((CountOfCodes + 1) & ~1) - 1];
    31.  *   union {
    32.  *       OPTIONAL ULONG ExceptionHandler;
    33. *       OPTIONAL ULONG FunctionEntry;
    34. *   };
    35. *   OPTIONAL ULONG ExceptionData[]; */
    36.  } UNWIND_INFO, *PUNWIND_INFO;
    37. typedef struct _RUNTIME_FUNCTION {
    38.      ULONG BeginAddress;
    39.      ULONG EndAddress;
    40.      ULONG UnwindData;
    41.  } RUNTIME_FUNCTION, *PRUNTIME_FUNCTION;
    42. #define GetUnwindCodeEntry(info, index) \
    43.      ((info)-]UnwindCode[index])
    44. #define GetLanguageSpecificDataPtr(info) \
    45.      ((PVOID)&GetUnwindCodeEntry((info),((info)-]CountOfCodes + 1) & ~1))
    46. #define GetExceptionHandler(base, info) \
    47.      ((PEXCEPTION_HANDLER)((base) + *(PULONG)GetLanguageSpecificDataPtr(info)))
    48. #define GetChainedFunctionEntry(base, info) \
    49.      ((PRUNTIME_FUNCTION)((base) + *(PULONG)GetLanguageSpecificDataPtr(info)))
    50. #define GetExceptionDataPtr(info) \
    51.      ((PVOID)((PULONG)GetLanguageSpecificData(info) + 1)
     
    Последнее редактирование: 6 дек 2022
    HESH и mantissa нравится это.
  4. Mikl___

    Mikl___ Супермодератор Команда форума

    Публикаций:
    14
    Регистрация:
    25 июн 2008
    Сообщения:
    3.709

    /favor (оптимизация для особенностей архитектуры)

    /favor:option создает код, оптимизированный для определенной архитектуры или для особенностей микроархитектуры в архитектурах AMD и Intel.
    /favor:{Blend | Atom | AMD64 | Intel64}
    • /favor: Blend
      (x86 и x64) создает код, оптимизированный для конкретных особенностей микроархитектуры в архитектурах AMD и Intel. Хотя /favor: Blend может не обеспечить максимальную производительность конкретного процессора, он предназначен для обеспечения лучшей производительности в широком спектре процессоров x86 и x64. По умолчанию действует /favor: Blend
    • /favor: ATOM
      (x86 и x64) создает код, оптимизированный для особенностей процессора Intel Atom и технологии Intel Centrino Atom. Код, созданный с помощью /favor: Atom , также может формировать инструкции Intel SSSE3, SSE3, SSE2 и SSE для процессоров Intel.
    • /favor: AMD64
      (только для x64) оптимизирует созданный код для процессоров AMD Opteron и Athlon, поддерживающих 64-разрядные расширения. Оптимизированный код может выполняться на всех платформах, совместимых с x64. Код, созданный с помощью /favor: AMD64 может привести к ухудшению производительности процессоров Intel, поддерживающих Intel64.
    • /favor: INTEL64
      (только x64) оптимизирует созданный код для процессоров Intel, поддерживающих Intel64, что обычно повышает производительность этой платформы. Полученный код может выполняться на любой платформе x64. Код, созданный с помощью /favor: Intel64 , может привести к ухудшению производительности на процессорах AMD Opteron и Athlon, поддерживающих 64-разрядные расширения.
     
  5. ormoulu

    ormoulu Well-Known Member

    Публикаций:
    0
    Регистрация:
    24 янв 2011
    Сообщения:
    1.208
  6. Mikl___

    Mikl___ Супермодератор Команда форума

    Публикаций:
    14
    Регистрация:
    25 июн 2008
    Сообщения:
    3.709
    А вы ее внимательно читали? ;)
     
  7. ormoulu

    ormoulu Well-Known Member

    Публикаций:
    0
    Регистрация:
    24 янв 2011
    Сообщения:
    1.208
    Нет, я мануалы обычно на языке вероятного противника читаю :blush2:
     
  8. Mikl___

    Mikl___ Супермодератор Команда форума

    Публикаций:
    14
    Регистрация:
    25 июн 2008
    Сообщения:
    3.709
    Счастливчик! Может быть поможете с вычиткой "Программных соглашений..."? С переводом на нормальный русский...
     
  9. ormoulu

    ormoulu Well-Known Member

    Публикаций:
    0
    Регистрация:
    24 янв 2011
    Сообщения:
    1.208
    Довольно сложно на самом деле, нужно думать практически над каждым термином. Например "макет хранилища" надо переводить как "размещение данных в памяти", что может быть сразу очевидно если только профессиональному техпереводчику :dntknw:
     
  10. R81...

    R81... Active Member

    Публикаций:
    0
    Регистрация:
    1 фев 2020
    Сообщения:
    141
    "Двумя важнейшими различиями между архитектурами x86 и x64 является возможность 64-битной адресации и набор из 16 64-битных регистров общего назначения."
    Хоть не работал с 64, но для меня еще существенной является возможность 4-х байтной адресации памяти данных относительно счетчика команд (RIP).

    "Довольно сложно на самом деле, нужно думать практически над каждым термином."
    Так если собрались учебник писать...
     
  11. mantissa

    mantissa Мембер Команда форума

    Публикаций:
    0
    Регистрация:
    9 сен 2022
    Сообщения:
    138
    Почему только после этого устанавливается RBP согласно скрину MSDN? А разве не наоборот должно быть:
    Код (Text):
    1.  
    2. push rbp
    3. mov rbp, rsp
    4. sub rsp, n
    5. ; Функция работает
    6. leave ; тоже самое что mov rsp, rbp а потом pop rbp
    7. ret
    8.  
    upload_2023-3-13_21-33-54.png
    После установки RBP будем обращаться к локальным переменным по отрицательному смещению, а к параметрам по положительному. А для установки параметров для следующей функции:
    1. первые 4 уйдут в регистры
    2. остальные по rsp + 20h и так далее по +8 байт
     
  12. aa_dav

    aa_dav Active Member

    Публикаций:
    0
    Регистрация:
    24 дек 2008
    Сообщения:
    441
    Тут косячок перевода - "неоконченная" в оригинале "non-leaf", т.е. "не листовая", смысл чего "функция которая может вызывать другие функции" и потому не может обращаться со стеком каким то оптимизированным образом отличающимся от каноничного описываемого.

    Что касается картинки - давно её уже разглядывал и на ней, имхо, есть конкретный косяк - где именно расположена пунктирная линия "call B". Она должна быть перед "B stack parameter stack area". Т.е. изображённые локальные переменные на картинке - это локальные переменные A, а не B. Всё внутреннее хозяйство B лежит ниже всего изображённого на картинке, а всё что до "alloca" включительно - это всё хозяйство A и тогда картинка становится логичной. C rbp возможно тоже косяк потому что рисующий пытался как то адаптировать его к неправильной линии с "call B".
     
    Последнее редактирование: 14 мар 2023
  13. mantissa

    mantissa Мембер Команда форума

    Публикаций:
    0
    Регистрация:
    9 сен 2022
    Сообщения:
    138
    Тут картинка нарисована с перевернутым стеком (т.е сверху находятся старшие адреса, а снизу младшие), растет, то он правильно, по направлению к 0 адресам, но они почему-то снизу, а не сверху, как положено. Поэтому и путаница создается для понимания. А вообще пунктирная линия нарисована правильно, функция A в рамках пролога выделяет место для своих локальных переменных (на рисунке нет), после чего выделяет место для параметров, которые будут переданы функции B, после чего помещает адрес возврата и B вызывается. Если бы было так, как вы говорите, то функции B пришлось бы обращаться к своим параметрам, перепрыгивая через локальные переменные A, что невозможно, так как B не знает о размере этой области.
    Почитал насчет этого, оказывается компиляторы на Windows x64 предпочитают вообще не использовать RBP, а высчитывать все смещения относительно RSP, имея какие-то метаданные по стеку. RBP используется лишь в случае, когда используется alloca и тогда он находится там, где указано на картинке. Док-во на GodBolt
    Источник: мой вопрос на stackoverflow
     
  14. aa_dav

    aa_dav Active Member

    Публикаций:
    0
    Регистрация:
    24 дек 2008
    Сообщения:
    441
    Я лично именно к такому соглашению изображения стека и привык - сверху старшие адреса и пуши помещают данные сверху-вниз. Тут чётко нарисовано где область функции A, а где область функции B - параметры B находятся в вершине сейчас стека, но разграничительная линия между A и B проведена неправильно.
    По порядку сверху-вниз:
    1. сперва идут параметры которые передали в A
    2. адрес куда вернётся A
    3. локальные переменные A
    4. alloca вызывается в динамике поэтому ей остаётся только еще прибавить к стеку чтобы разместить своё хранилище заранее неизвестного размера ниже локальных переменных. это alloca функции A
    Пояснение про то, что RBP в x64 используется именно при использовании alloca тут вправляет мозги и понятно почему RBP здесь - он отделяет статику сверху от динамически выделенной области alloca ниже. Этого я не знал. Прикольно. Теперь чтобы адресовать локалки и параметры по заранее известным смещениям надо использовать RBP, а RSP ушёл на заранее неизвестный размер вниз.
    5. И вот на самом деле тут происходит вызов call B и в стек помещаются (по необходимости) параметры в B (B stack parameters area). Далее мы уже просто не видим какие есть локальные переменные и прочее в B ибо для объяснения того как A вызывает B это уже не важно.
    Разграничительная линия между стеком A и стеком B проведена категорически неправильно.
     
    Последнее редактирование: 14 мар 2023
  15. mantissa

    mantissa Мембер Команда форума

    Публикаций:
    0
    Регистрация:
    9 сен 2022
    Сообщения:
    138
    В данном случае я думаю, что порядок таков:
    1. параметры, которые A передает в B
    2. адрес куда вернет А
    3. локальные переменные B
    4. alloca B
    5. параметры, которые B передаст в другую функцию
    --- Сообщение объединено, 14 мар 2023 ---
    они в любом случае должны находиться до параметров, которые A будет передавать в другую функцию. Тут они не изображены, зато изображены у B.
    параметры которые передали в A должны находится рядом с адресом возврата функции, вызвавшей A
     
  16. aa_dav

    aa_dav Active Member

    Публикаций:
    0
    Регистрация:
    24 дек 2008
    Сообщения:
    441
    А, кстати, точно, в этом есть смысл. Если трактовать зоны A и B здесь не как "переменные с которыми функция имеет дело", а как "области стека которые растут или уменьшаются пока функция выполняется", а "A stack parameters" читать не как "параметры в стеке функции A", а "параметры которая записала в стек A для следующей функции", то картинка срастается идеально. Годами меня эта картинка такими фразами вводила в заблуждение. :)
    --- Сообщение объединено, 14 мар 2023 ---
    P.S.
    Кстати странно, решил проверить то высказывание со стековерфлоу, что мол MSVC уводит RBP в конец локальных переменных чтобы как то оптимальнее использовать адресацию с байтовым смещением, но смысла в это не углядел.
    И построил контрпример где такой увод контрпродуктивен: https://godbolt.org/z/q4MjEff8h
    И да, он всё-равно увёл RBP за большой локальный массив и оффсеты до самых часто используемых переменных ушли далеко за знаковые байты, хотя казалось бы и ежу понятно, что параметры должны адресоваться часто.
    Странновато. Но вот так почему то MSVC сделан.