Поделиться через


Общие сведения о соглашениях ABI ARM64

Базовый двоичный интерфейс приложений (ABI) для Windows при компиляции и выполнении на процессорах ARM в 64-разрядном режиме (архитектура ARMv8 или более поздних версий) по большей части следует стандартному встроенному ABI (EABI) AArch64 ARM. В этой статье рассматриваются некоторые основные принципы и изменения, описываемые в EABI. Дополнительные сведения о 32-разрядной версии ABI см. в статье Общие сведения о соглашениях ABI ARM. Дополнительные сведения о стандартном встроенном двоичном интерфейсе приложения ARM см. в статье Двоичный интерфейс приложения для архитектуры ARM (внешняя ссылка).

Определения

С внедрением поддержки 64-разрядной архитектуры компания ARM определила ряд терминов:

  • AArch32 — устаревшая 32-разрядная архитектура набора инструкций (ISA), определяемая компанией ARM, включая выполнение в режиме Thumb.
  • AArch64 — новая 64-разрядная архитектура набора инструкций (ISA), определяемая компанией ARM.
  • ARMv7 — спецификация 7-го поколения оборудования ARM, которое поддерживает только архитектуру AArch32. Это первая версия оборудования ARM, поддерживаемая Windows для ARM.
  • ARMv8 — спецификация 8-го поколения оборудования ARM, которое поддерживает архитектуры AArch32 и AArch64.

В Windows также используются следующие термины:

  • ARM — это 32-разрядная архитектура ARM (AArch32), которая иногда называется WoA (Windows на ARM).
  • ARM32 — та же архитектура, что и описываемая выше ARM. Используется в этом документе для большей наглядности.
  • ARM64 — 64-разрядная архитектура ARM (AArch64). WoA64 не существует.

Наконец, в отношении типов данных компания ARM использует следующие определения:

  • Короткий вектор — тип данных, напрямую представляемый в архитектуре с одним потоком команд и многими потоками данных (SIMD). Вектор элементов размером 8 или 16 байтов. Размер выравнивается с размером вектора (8 или 16 байтов), а каждый элемент может иметь размер 1, 2, 4 или 8 байтов.
  • HFA (однородный агрегат с плавающей запятой) — тип данных, содержащий от 2 до 4 идентичных элементов с плавающей запятой типа float или double.
  • HVA (однородный агрегат коротких векторов) — тип данных, содержащий от 2 до 4 идентичных элементов типа "короткий вектор".

Основные требования

Версия Windows для ARM64 предназначена для работы с архитектурой ARMv8 или более поздней версии. Оборудование должно поддерживать вычисления с плавающей запятой и технологию NEON.

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

Порядок байтов

Как и версия Windows для ARM32, Windows для ARM64 выполняется в режиме с прямым порядком байтов. Изменить порядок байтов без поддержки режима ядра в архитектуре AArch64 сложно, поэтому его проще определить принудительно.

Точное понимание

При работе с архитектурой ARM64 Windows позволяет оборудованию ЦП прозрачным образом обрабатывать невыровненные операции доступа. В отличие от AArch32, эта возможность теперь также поддерживается для целочисленных операций доступа (включая многословные) и операций доступа с плавающей запятой.

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

Выравнивание макета по умолчанию для локальных элементов:

Размер (в байтах) Выравнивание (в байтах)
1 1
2 2
3, 4 4
> 4 8

Выравнивание макета по умолчанию для глобальных и статических элементов:

Размер (в байтах) Выравнивание (в байтах)
1 1
2–7 4
8–63 8
>= 64 16

Целочисленные регистры

Архитектура AArch64 поддерживает 32 целочисленных регистра:

Регистр Летучесть Роль
x0-x8 Переменный Регистры с нуля параметров и результатов
x9-x15 Переменный Оперативные регистры
x16–x17 Переменный Оперативные регистры для вызова внутри процедуры
x18 Н/П Зарезервированный регистр платформы: в режиме ядра указывает на KPCR для текущего процессора; В пользовательском режиме указывает на TEB
x19–x28 Неизменяемый Оперативные регистры
x29/fp Неизменяемый Указатель фрейма
x30/lr Оба Регистрация ссылок: функция callee должна сохранить ее для собственного возврата, но значение вызывающего элемента будет потеряно.

Каждый регистр может быть доступен как полное 64-разрядное значение (посредством x0–x30) или как 32-разрядное значение (посредством w0–w30). Результаты 32-разрядных операций дополняются нулями до 64 разрядов.

Дополнительные сведения об использовании регистров параметров см. в разделе "Передача параметров".

В отличие от AArch32, регистры счетчика команд (PC) и указателя стека (SP) не являются индексированными. Кроме того, порядок доступа к ним ограничен. Также обратите внимание на отсутствие регистра x31. Этот код используется в особых целях.

Указатель фрейма (X29) нужен для обеспечения совместимости с быстрой проверкой стека, которая используется службой трассировки событий Windows и другими службами. Он должен указывать на предыдущую пару {x29, x30} в стеке.

Регистры чисел с плавающей запятой и SIMD

Архитектура AArch64 также поддерживает 32 регистра чисел с плавающей запятой и SIMD (см. описание ниже):

Регистр Летучесть Роль
v0-v7 Переменный Регистры с нуля параметров и результатов
v8–v15 Оба Низкая 64-разрядная версия не является переменной. Высокий 64-разрядный уровень — Переменная.
v16–v31 Переменный Оперативные регистры

Каждый регистр может быть доступен как полное 128-разрядное значение (посредством v0–v31 или q0–q31). Кроме того, каждый регистр может быть доступен как 64-разрядное (посредством d0–d31), 32-разрядное (посредством s0–s31), 16-разрядное (посредством h0–h31) или 8-разрядное (посредством b0–b31) значение. Операции доступа размером менее 128 разрядов обращаются только к младшим разрядам полного 128-разрядного регистра. Остальные разряды не затрагиваются, если не указано иное. Отличие архитектуры AArch64 от AArch32 заключается в том, что в последней регистры меньшего размера упаковывались поверх регистров большего размера.

К битовым полям регистра управления операциями с плавающей запятой (FPCR) предъявляются определенные требования:

Bits Значение Летучесть Роль
26 AHP Неизменяемый Альтернативное управление с половинной точностью
25 DN Неизменяемый Управление режимом по умолчанию NaN
24 FZ Неизменяемый Управление режимом обнуления
23—22 RMode Неизменяемый Управление режимом округления
15, 12–8 IDE, IXE и т. п. Неизменяемый Биты отслеживания исключения, должны всегда быть равны 0

Системные регистры

Как и для архитектуры AArch32, в спецификации AArch64 определяются три контролируемых системой регистра идентификаторов потока:

Регистр Роль
TPIDR_EL0 Зарезервировано.
TPIDRRO_EL0 Содержит номер ЦП для текущего процессора
TPIDR_EL1 Указывает на структуру области управления процессором ядра (KPCR) для текущего процессора

Исключения для плавающей запятой

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

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

В отношении функций с фиксированным числом аргументов ABI Windows следует правилам, определенным компанией ARM для передачи параметров. Эти правила взяты напрямую из стандарта вызова процедур для архитектуры AArch64:

Этап А — инициализация

Этот этап выполняется строго один раз до начала обработки аргументов.

  1. В качестве номера следующего регистра общего назначения (NGRN) устанавливается нуль.

  2. В качестве номера следующего регистра SIMD и чисел с плавающей запятой (NSRN) устанавливается нуль.

  3. В качестве адреса следующего помещенного в стек аргумента (NSAA) устанавливается текущее значение указателя стека (SP).

Этап Б — предварительное заполнение и расширение аргументов

Для каждого имеющегося аргумента применяется первое соответствующее правило из следующего списка. Если соответствующее правило не найдено, аргумент используется без изменений.

  1. Если аргумент имеет составной тип и его размер не может быть статически определен как вызывающим, так и вызываемым объектом, этот аргумент копируется в память, а сам аргумент заменяется указателем на его копию. В C/C++ таких типов нет, однако они используются в других языках и расширениях для них.

  2. Если аргумент имеет тип HFA или HVA, он используется без изменений.

  3. Если аргумент имеет составной тип и его размер превышает 16 байтов, этот аргумент копируется в выделяемую вызывающим объектом память, а сам аргумент заменяется указателем на его копию.

  4. Если аргумент имеет составной тип, его размер округляется до ближайшего значения, кратного 8 байтам.

Этап В — назначение аргументов регистрам и стеку

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

  1. Если аргумент имеет тип с плавающей запятой или с плавающей запятой с плавающей запятой или типом короткого вектора, а NSRN меньше 8, аргумент выделяется наименее значительным битам регистра v[NSRN]. Номер NSRN увеличивается на единицу. С этого момента аргумент назначен.

  2. Если аргумент имеет тип HFA или HVA и имеется достаточно неназначенных регистров SIMD и чисел с плавающей запятой (NSRN + число элементов ≤8), аргумент назначается регистрам SIMD и чисел с плавающей запятой (по одному регистру для каждого элемента HFA или HVA). Номер NSRN увеличивается на число использованных регистров. С этого момента аргумент назначен.

  3. Если аргумент имеет тип HFA или HVA, номер NSRN задается равным 8, а размер аргумента округляется до ближайшего значения, кратного 8 байтам.

  4. Если аргумент имеет тип HFA или HVA, тип с плавающей запятой четырехкратной точности, либо является коротким вектором, адрес NSAA округляется до 8 или до естественной границы выравнивания для типа аргумента (в зависимости от того, какая величина больше).

  5. Если аргумент имеет тип с плавающей запятой половинной или одиночной точности, его размер устанавливается равным 8 байтам. Это дает такой же результат, что и копирование аргумента в младшие значащие разряды 64-разрядного регистра с заполнением оставшихся разрядов неуказанными значениями.

  6. Если аргумент имеет тип HFA или HVA, тип с плавающей запятой половинной, одиночной, двойной или четырехкратной точности либо является коротким вектором, аргумент копируется в память и согласуется с адресом NSAA. Адрес NSAA увеличивается на размер аргумента. С этого момента аргумент назначен.

  7. Если аргумент является целочисленным или указательным типом, размер аргумента меньше или равен 8 байтам, а NGRN меньше 8, аргумент копируется в наименее значимые биты x[NGRN]. Номер NGRN увеличивается на единицу. С этого момента аргумент назначен.

  8. Если аргумент имеет выравнивание 16, номер NGRN округляется до следующего четного числа.

  9. Если аргумент является целочисленным типом, размер аргумента равен 16, а NGRN меньше 7, аргумент копируется в x[NGRN] и x[NGRN+1]. x[NGRN] должен содержать нижнее адресное двойное слово представления памяти аргумента. Номер NGRN увеличивается на два. С этого момента аргумент назначен.

  10. Если аргумент является составным типом, а размер аргумента в двойных словах не превышает 8 минус NGRN, то аргумент копируется в последовательные регистры общего назначения, начиная с x[NGRN]. Аргумент передается так, как если бы он был загружен в регистры с адреса с выравниванием с удвоенным словом с использованием соответствующих инструкций LDR, которые загружают последовательные регистры из памяти. Содержимое неиспользуемых частей регистров в этом стандарте не определяется. Номер NGRN увеличивается на число использованных регистров. С этого момента аргумент назначен.

  11. Номер NGRN задается равным 8.

  12. Адрес NSAA округляется до 8 или до естественной границы выравнивания для типа аргумента (в зависимости от того, какая величина больше).

  13. Если аргумент имеет составной тип, он копируется в память по выровненному адресу NSAA. Адрес NSAA увеличивается на размер аргумента. С этого момента аргумент назначен.

  14. Если размер аргумента меньше 8 байтов, его размер устанавливается равным 8 байтам. Это дает такой же результат, что и копирование аргумента в младшие значащие разряды 64-разрядного регистра с заполнением оставшихся разрядов неуказанными значениями.

  15. Аргумент копируется в память по выровненному адресу NSAA. Адрес NSAA увеличивается на размер аргумента. С этого момента аргумент назначен.

Addendum: функции Variadic

Функции, принимающие переменное число аргументов, обрабатываются не так, как описывается выше:

  1. Все составные элементы обрабатываются одинаково (различий между HFA или HVA не делается).

  2. Регистры SIMD и чисел с плавающей запятой не используются.

Фактически соблюдаются правила C.12–C.15 для назначения аргументов мнимому стеку, при котором первые 64 байта стека загружаются в регистры x0–x7, а оставшиеся аргументы стека размещаются обычным образом.

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

Целочисленные значения возвращаются в регистр x0.

Значения с плавающей запятой возвращаются соответственно в регистры s0, d0 или v0.

Тип считается HFA или HVA, если выполняются все следующие условия:

  • он не пуст;
  • у него нет нетривиальных конструкторов по умолчанию, конструкторов копий, деструкторов и операторов присваивания;
  • все его члены имеют одинаковый тип HFA или HVA, либо типы float, double или neon, соответствующие типам HFA или HVA других членов.

Значения HVA с четырьмя или меньшими элементами возвращаются в s0-s3, d0-d3 или v0-v3 в соответствии с соответствующим образом.

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

  • Является агрегатом в соответствии с определением стандарта C++14, то есть массивом или классом без предоставленного пользователем конструктора, без закрытых или защищенных нестатических элементов данных, без базовых классов и виртуальных функций.
  • Имеет тривиальный оператор присваивания копированием.
  • Имеет тривиальный деструктор.

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

  • Типы, которые являются HFA с четырьмя или меньшими элементами, возвращаются в s0-s3, d0-d3 или v0-v3, как это необходимо.
  • Типы, размер которых не превышает 8 байтов, возвращаются в регистр x0.
  • Типы, размер которых не превышает 16 байтов, возвращаются в регистры x0 и x1, причем регистр x0 содержит младшие 8 байтов.
  • Для других агрегатных типов вызывающий объект должен зарезервировать блок памяти достаточного размера и выравнивания для удержания результата. Адрес блока памяти должен передаваться в качестве дополнительного аргумента функции в регистр x8. Вызываемый объект может изменить блок памяти результата в любой момент выполнения подпрограммы. Вызываемый объект не обязан сохранять значение, находящееся в регистре x8.

Для всех остальных типов используется следующее соглашение:

  • Вызывающий объект должен резервировать блок памяти с достаточным размером и соответствующим выравниванием для хранения результата. Адрес блока памяти должен передаваться в качестве дополнительного аргумента функции в регистр x0 или в x1, если в регистр x0 передано $this. Вызываемый объект может изменить блок памяти результата в любой момент выполнения подпрограммы. Вызываемый объект возвращает адрес блока памяти в регистр x0.

Стек

После внедрения ABI компанией ARM стек должен всегда сохранять 16-байтовое выравнивание. В архитектуре AArch64 предусмотрена аппаратная функция, которая возвращает ошибки выравнивания стека во всех случаях, когда выполняются операции загрузки или сохранения относительно указателя стека, который не имеет 16-байтового выравнивания. В Windows эта функция активна на постоянной основе.

Функции, выделяющие 4 КБ или больше в стеке, должны проверять, что каждая страница перед последней обрабатывается по порядку. Эта действие гарантирует, что никакой код не обойдет страницы защиты, используемые Windows для расширения стека. Как правило, обработка выполняется с помощью вспомогательной функции __chkstk, которая использует особое соглашение о вызовах и передает все выделение стека, поделенное на 16, в x15.

Красная зона

16-байтовая область сразу после текущего указателя стека зарезервирована для сценариев анализа и динамического исправления. В эту область можно вставить тщательно выверенный код, который хранит два регистра в [sp, #-16] и временно использует их для произвольных целей. Ядро Windows гарантирует, что эти 16 байтов не будут перезаписаны при возникновении исключения или прерывания как в пользовательском режиме, так и в режиме ядра.

Стек ядра

Стек режима ядра по умолчанию в Windows состоит из шести страниц (24 КБ). Обратите особое внимание на функции, которые имеют большие буферы стека в режиме ядра. Несвоевременное прерывание может возникнуть в тот момент, когда запас стека недостаточен, что вызовет критическую ошибку в виде сбоя стека.

Проверка стека

Код в Windows компилируется с включенными указателями фреймов (/Oy-) для обеспечения быстрой проверки стека. Обычно регистр x29 (fp) указывает на следующее звено в цепочке, которое представляет собой пару {fp, lr}, задающую указатель на предыдущий фрейм в стеке и адрес возврата. В стороннем коде для оптимизации профилирования и трассировки также рекомендуется включать указатели фрейма.

Очистка исключения

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

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

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

Счетчик циклов

Все ЦП с архитектурой ARMv8 должны поддерживать регистр счетчика циклов (64-разрядный регистр, который в Windows доступен для чтения на любом уровне исключения, в том числе и в пользовательском режиме). Доступ к нему может осуществляться посредством специального регистра PMCCNTR_EL0 с использованием кода операции MSR в коде ассемблера или встроенной функции _ReadStatusReg в коде C/C++.

Счетчик циклов подсчитывает реальное количество циклов, а не время по часам. Частота подсчета зависит от тактовой частоты процессора. Если вам необходимо знать частоту счетчика циклов, значит, сам этот счетчик вам не нужен. В таком случае вам требуется измерять время по часам, для чего используется QueryPerformanceCounter.

См. также

Общие вопросы использования Visual C++ ARM
Обработка исключений в ARM64