Обработка исключений в ARM64
Windows на устройствах ARM64 использует один механизм структурированной обработки исключений для асинхронных аппаратных и синхронных программных исключений. Обработчики исключения для конкретных языков созданы на базе структурированной обработки исключений Windows с помощью вспомогательных функций языка. В этом документе описывается обработка исключений в Windows в ARM64. Он иллюстрирует вспомогательные средства языка, используемые кодом, созданным сборщиком Microsoft ARM и компилятором MSVC.
Цели и причины для использования
Соглашения о данных очистки исключений и эти сведения предназначены для реализации следующих задач.
Предоставление описания, достаточного для выполнения очистки без проверки кода во всех случаях.
Для анализа кода требуется, чтобы код был страничным. Он предотвращает очистку в некоторых случаях, когда полезно (трассировка, выборка, отладка).
Анализ кода является сложным процессом, поэтому компилятор должен генерировать только те инструкции, которые может декодировать средство очистки.
Если очистка не может быть полностью описана с помощью кодов очистки, то в некоторых случаях она должна вернуться к декодированию инструкций. Декодирование инструкций повышает общую сложность и в идеале следует избегать.
Поддержка очистки в середине пролога и середине эпилога.
- Очистка в Windows используется не только для обработки исключений. Крайне важно обеспечить правильную очистку кода, даже если он находится в середине пролога или эпилога.
Использование минимального объема пространства.
Во избежание значительного увеличения размера двоичного файла коды очистки не должны агрегироваться.
Так как коды очистки, скорее всего, будут заблокированы в памяти, небольшое пространство обеспечивает минимальные издержки для каждого загруженного двоичного файла.
Предположения
Эти предположения вносятся в описание обработки исключений:
Прологи и эпилоги стремятся зеркально отображать друг друга. Используя эту общую особенность, можно значительно сократить объем метаданных, необходимых для описания очистки. В теле функции не имеет никакого значения, выполнены ли операции пролога или операции эпилога выполняются с опережением. Оба случая должны давать одинаковые результаты.
Как правило, функции в целом имеют относительно небольшой размер. На этом основано несколько оптимизаций для более эффективной упаковки данных.
В эпилогах нет условного кода.
Выделенный регистр кадра: если
sp
он сохраняется в другом регистре (x29
) в прологе, этот регистр остается незамеченным во всей функции. Это означает, что исходный файлsp
может быть восстановлен в любое время.Если
sp
он не сохраняется в другом регистре, все манипуляции указателя стека выполняются строго в прологе и эпилоге.Схема кадра стека организована так, как описано в следующем разделе.
Схема кадра стека ARM64
Для функций, связанных с кадрами, fp
lr
пара может сохраняться в любой позиции в локальной переменной в зависимости от соображений оптимизации. Цель состоит в том, чтобы максимально увеличить число локальных жителей, которые можно достичь одной инструкцией на основе указателя кадра (x29
) или указателя стека (sp
). Однако для alloca
функций он должен быть цепочки и x29
должен указывать на нижней части стека. Чтобы обеспечить лучшее покрытие с регистрацией в режиме адресации, области сохранения в неизменяемых регистрах располагаются в верхней части локального стека. Ниже приведены примеры, иллюстрирующие несколько наиболее эффективных последовательностей пролога. Чтобы обеспечить простоту и улучшить локальность кэша, сохраняемые вызываемой функцией регистры во всех канонических прологах хранятся в возрастающем порядке. #framesz
ниже представлен размер всего стека (за исключением alloca
области). #localsz
и #outsz
обозначают размер локальной области (включая область сохранения для пары <x29, lr>
) и размер исходящего параметра соответственно.
Со сцеплением, #localsz <= 512
stp x19,x20,[sp,#-96]! // pre-indexed, save in 1st FP/INT pair stp d8,d9,[sp,#16] // save in FP regs (optional) stp x0,x1,[sp,#32] // home params (optional) stp x2,x3,[sp,#48] stp x4,x5,[sp,#64] stp x6,x7,[sp,#82] stp x29,lr,[sp,#-localsz]! // save <x29,lr> at bottom of local area mov x29,sp // x29 points to bottom of local sub sp,sp,#outsz // (optional for #outsz != 0)
Цепочка, #localsz > 512
stp x19,x20,[sp,#-96]! // pre-indexed, save in 1st FP/INT pair stp d8,d9,[sp,#16] // save in FP regs (optional) stp x0,x1,[sp,#32] // home params (optional) stp x2,x3,[sp,#48] stp x4,x5,[sp,#64] stp x6,x7,[sp,#82] sub sp,sp,#(localsz+outsz) // allocate remaining frame stp x29,lr,[sp,#outsz] // save <x29,lr> at bottom of local area add x29,sp,#outsz // setup x29 points to bottom of local area
Незаклинированные, конечные функции (
lr
несохраненные)stp x19,x20,[sp,#-80]! // pre-indexed, save in 1st FP/INT reg-pair stp x21,x22,[sp,#16] str x23,[sp,#32] stp d8,d9,[sp,#40] // save FP regs (optional) stp d10,d11,[sp,#56] sub sp,sp,#(framesz-80) // allocate the remaining local area
Доступ ко всем локальным ресурсам зависит от
sp
.<x29,lr>
указывает на предыдущий кадр. Для размера <кадра = 512 можно оптимизировать,sub sp, ...
если сохраненная область regs перемещается в нижней части стека. Недостатком является то, что он не согласуется с другими макетами выше. А сохраненные regs принимают участие в диапазоне для парных регов и режима адресации смещения после индексирования.Незаклинированные, неконечные функции (сохраняется
lr
в сохраненной области Int)stp x19,x20,[sp,#-80]! // pre-indexed, save in 1st FP/INT reg-pair stp x21,x22,[sp,#16] // ... stp x23,lr,[sp,#32] // save last Int reg and lr stp d8,d9,[sp,#48] // save FP reg-pair (optional) stp d10,d11,[sp,#64] // ... sub sp,sp,#(framesz-80) // allocate the remaining local area
Или с четным числом сохраненных регистров типа Int,
stp x19,x20,[sp,#-80]! // pre-indexed, save in 1st FP/INT reg-pair stp x21,x22,[sp,#16] // ... str lr,[sp,#32] // save lr stp d8,d9,[sp,#40] // save FP reg-pair (optional) stp d10,d11,[sp,#56] // ... sub sp,sp,#(framesz-80) // allocate the remaining local area
Сохранено только
x19
:sub sp,sp,#16 // reg save area allocation* stp x19,lr,[sp] // save x19, lr sub sp,sp,#(framesz-16) // allocate the remaining local area
* Выделение области сохранения reg не сложено в
stp
область, так как предварительно индексированная reg-lrstp
не может быть представлена с помощью кодов очистки.Доступ ко всем локальным ресурсам зависит от
sp
.<x29>
указывает на предыдущий кадр.Со сцеплением, #framesz <= 512, #outsz = 0
stp x29,lr,[sp,#-framesz]! // pre-indexed, save <x29,lr> mov x29,sp // x29 points to bottom of stack stp x19,x20,[sp,#(framesz-32)] // save INT pair stp d8,d9,[sp,#(framesz-16)] // save FP pair
По сравнению с первым примером prolog выше этот пример имеет преимущество: все инструкции по сохранению регистра готовы выполняться только после одной инструкции выделения стека. Это означает, что не существует антизависимостей от
sp
того, что предотвращает параллелизм на уровне инструкций.Цепочка, размер > кадра 512 (необязательно для функций без
alloca
)stp x29,lr,[sp,#-80]! // pre-indexed, save <x29,lr> stp x19,x20,[sp,#16] // save in INT regs stp x21,x22,[sp,#32] // ... stp d8,d9,[sp,#48] // save in FP regs stp d10,d11,[sp,#64] mov x29,sp // x29 points to top of local area sub sp,sp,#(framesz-80) // allocate the remaining local area
Для оптимизации можно поместить в любую позицию в локальной области,
x29
чтобы обеспечить лучшее покрытие для режима адресации смещений в виде reg-pair и до/после индексированного смещения. Локальные указатели кадров ниже можно получить на основеsp
.Цепочка, размер > кадра 4K, с или без alloca(),
stp x29,lr,[sp,#-80]! // pre-indexed, save <x29,lr> stp x19,x20,[sp,#16] // save in INT regs stp x21,x22,[sp,#32] // ... stp d8,d9,[sp,#48] // save in FP regs stp d10,d11,[sp,#64] mov x29,sp // x29 points to top of local area mov x15,#(framesz/16) bl __chkstk sub sp,sp,x15,lsl#4 // allocate remaining frame // end of prolog ... sub sp,sp,#alloca // more alloca() in body ... // beginning of epilog mov sp,x29 // sp points to top of local area ldp d10,d11,[sp,#64] ... ldp x29,lr,[sp],#80 // post-indexed, reload <x29,lr>
Сведения об обработке исключений в ARM64
.pdata
Записи
Записи .pdata
представляют собой упорядоченный массив элементов фиксированной длины, описывающих каждую функцию управления стека в двоичном файле PE. Фраза "управление стеком" имеет важное значение: конечные функции, которые не требуют локального хранилища, и не нужно сохранять и восстанавливать ненезависимые регистры, не требуют .pdata
записи. Для экономии пространства эти записи должны быть опущены явным образом. Очистка от одной из этих функций может получить обратный адрес непосредственно из lr
вызывающего объекта.
Каждая .pdata
запись для ARM64 составляет 8 байтов. Общий формат каждой записи помещает 32-разрядную единицу RVA функции, которая начинается в первом слове, а затем второе слово, содержащее указатель на блок переменной длины .xdata
, или упакованное слово, описывающее каноническую последовательность очистки функции.
Используются следующие поля.
Function Start RVA (Относительный виртуальный адрес начала функции) представляет собой 32-битовый относительный виртуальный адрес начала функции.
Флаг — это 2-разрядное поле, указывающее, как интерпретировать оставшиеся 30 битов второго
.pdata
слова. Если Flag (Флаг) имеет значение 0, то оставшиеся биты формируют Exception Information RVA (Относительный виртуальный адрес сведений об исключении) (при этом два младших бита неявно становятся равны 0). Если Flag (Флаг) имеет ненулевое значение, то оставшиеся биты формируют структуру Packed Unwind Data (Упакованные данные очистки).Exception Information RVA (Относительный виртуальный адрес сведений об исключении) представляет собой адрес структуры сведений об исключении переменной длины, хранимой в разделе
.xdata
. Эти данные должны быть выровнены по 4-байтовой границе.Упакованные данные очистки представляют собой сжатое описание операций, необходимых для выполнения очистки из функции в канонической форме. В этом случае запись
.xdata
не требуется.
.xdata
Записи
Когда формата упакованных данных очистки недостаточно для описания очистки функции, необходимо создать запись .xdata
переменной длины. Адрес этой записи хранится во втором слове записи .pdata
. Формат .xdata
представляет собой упакованный набор слов переменной длины:
Эти данные состоят из четырех разделов.
Заголовок 1-word или 2-word, описывающий общий размер структуры и предоставляющий ключевые данные функции. Второе слово присутствует только в том случае, если для обоих полей Epilog Count (Число эпилогов) и Code Words (Слова кодов) задано значение 0. Заголовок содержит следующие битовые поля:
a. Function Length (Длина функции) представляет собой 18-битовое поле. Оно указывает общую длину функции в байтах, поделенную на 4. Если функция превышает 1 млн, для описания функции необходимо использовать несколько записей и
.xdata
записей.pdata
. Дополнительные сведения см. в разделе Большие функции.b. Vers (Версия) представляет собой 2-битовое поле. В ней описывается версия оставшейся
.xdata
части. Сейчас определена только версия 0, поэтому значения 1–3 запрещены.c. X представляет собой 1-битовое поле. Оно указывает наличие (1) или отсутствие (0) данных исключения.
d. E представляет собой 1-битовое поле. Он указывает, что информация, описывающая один эпилог, упакована в заголовок (1), а не требует больше слов области позже (0).
д) Epilog Count (Число эпилогов) представляет собой 5-битовое поле, имеющее два значения в зависимости от состояния бита E:
Если E равен 0, то он указывает общее число областей эпилога, описанных в разделе 2. Если в функции присутствует более 31 области, для поля Code Words (Слова кодов) следует установить значение 0, чтобы указать на потребность в слове расширения.
Если E равен 1, это поле указывает индекс первого кода очистки, описывающего единственный эпилог.
f. Code Words (Слова кодов) представляет собой 5-битовое поле, которое указывает число 32-разрядных слов, требуемое для размещения всех кодов очистки в разделе 3. Если требуется более 31 слов (т. е. 124 кодов очистки), это поле должно быть равно 0, чтобы указать, что требуется слово расширения.
ж. Extended Epilog Count (Расширенное число эпилогов) и Extended Code Words (Расширенные слова кодов) представляют собой 16- и 8-битовые поля, соответственно. Они предоставляют дополнительное место для кодирования необычайно большого числа эпилогов или слов кода очистки. Слово расширения, содержащее это поле, присутствует только в том случае, если для полей Epilog Count (Число эпилогов) и Code Words (Слова кодов) в первом слове заголовка задано значение 0.
Если количество эпилогов не равно нулю, список сведений о областях эпилога, упакованных в слово, приходит после заголовка и необязательного расширенного заголовка. Они хранятся в порядке увеличения начального смещения. Каждая область содержит следующие биты:
a. Epilog Start Offset (Смещение начала эпилога) представляет собой 18-битовое поле, которое описывает смещение эпилога в байтах, поделенное на 4, относительно начала функции.
b. Res (Зарезервировано) представляет собой 4-битовое поле, зарезервированное для расширения в будущем. Оно должно иметь значение 0.
c. Epilog Start Index (Индекс начала эпилога) представляет собой 10-битовое поле (на 2 бита больше, чем поле Extended Code Words (Расширенные слова кодов)). Оно указывает индекс байта первого кода очистки, описывающего этот эпилог.
После списка областей эпилога идет массив байтов с кодами очистки, которые подробно описаны в одном из следующих разделов. Этот массив дополняется в конец ближайшей границы полного слова. В этот массив записываются коды очистки. Они начинаются с ближайшего к телу функции и переходят к краям функции. Байты для каждого кода очистки хранятся в порядке большого плана, поэтому наиболее значимый байт возвращается первым, который определяет операцию и длину остального кода.
Наконец, если поле X в заголовке равно 1, после байтов кодов очистки располагаются сведения об обработчике исключений. Он состоит из одного относительного виртуального адреса обработчика исключений, содержащего адрес самого обработчика исключений. После него сразу идет объем данных переменной длины, требуемых этому обработчику исключений.
Запись .xdata
разработана таким образом, чтобы получить первые 8 байтов и использовать их для вычисления полного размера записи, минус длина следующих данных исключений переменной размера. Следующий фрагмент кода вычисляет размер записи:
ULONG ComputeXdataSize(PULONG Xdata)
{
ULONG Size;
ULONG EpilogScopes;
ULONG UnwindWords;
if ((Xdata[0] >> 22) != 0) {
Size = 4;
EpilogScopes = (Xdata[0] >> 22) & 0x1f;
UnwindWords = (Xdata[0] >> 27) & 0x1f;
} else {
Size = 8;
EpilogScopes = Xdata[1] & 0xffff;
UnwindWords = (Xdata[1] >> 16) & 0xff;
}
if (!(Xdata[0] & (1 << 21))) {
Size += 4 * EpilogScopes;
}
Size += 4 * UnwindWords;
if (Xdata[0] & (1 << 20)) {
Size += 4; // Exception handler RVA
}
return Size;
}
Хотя пролог и каждый эпилог имеют собственный индекс в кодах очистки, эта таблица используется ими совместно. Довольно распространена ситуация, когда все они используют одинаковые коды очистки. (Пример см. в примере 2 в Примеры раздела.) В частности, средства записи компилятора должны оптимизироваться для этого случая. Это связано с тем, что самый большой индекс, который может быть указан в 255, что ограничивает общее количество кодов очистки для определенной функции.
Коды очистки
Массив кодов очистки — это пул последовательностей, описывающих именно способ отмены эффектов пролога. Они хранятся в том же порядке, что и операции, которые необходимо отменить. Коды очистки представляют собой небольшие наборы инструкций, закодированные в виде строки байтов. После завершения выполнения обратный адрес вызывающей функции находится в регистре lr
. А для всех неизменяемых регистров восстанавливаются значения, актуальные на момент вызова функции.
Если бы существовала уверенность в том, что исключения возникают только в теле функции и никогда не возникают в прологе или эпилоге, потребовалась бы всего одна последовательность. Однако модель очистки Windows требует, чтобы код мог выполнять очистку из частично выполненного пролога или эпилога. Чтобы выполнить это требование, коды очистки были тщательно спроектированы для обеспечения однозначного сопоставления с каждым соответствующим кодом операции в прологе и эпилоге. Эта особенность имеет несколько разных применений.
Сосчитав число кодов очистки, можно вычислить длину пролога и эпилога.
Считая число инструкций после начала области эпилога, можно пропустить эквивалентное число кодов очистки. Мы можем выполнить оставшуюся последовательность, чтобы завершить частично выполненную очистку, выполненную эпилогом.
Считая число инструкций до окончания пролога, можно пропустить эквивалентное число кодов очистки. Можно выполнить оставшуюся часть последовательности, чтобы отменить только те части пролога, которые завершили выполнение.
Коды очистки кодируются в соответствии с таблицей ниже. Все коды очистки — это одинарный или двойной байт, за исключением того, который выделяет огромный стек (alloc_l
). В общей сложности существует 22 кодов очистки. Каждый код очистки соответствует ровно одной инструкции в прологе или эпилоге, что делает возможной очистку частично выполненных прологов и эпилогов.
Код очистки | Биты и интерпретация |
---|---|
alloc_s |
000xxxxx: выделение небольшого стека размером < 512 (2^5 * 16). |
save_r19r20_x |
001zzzzz: сохранение <x19,x20> пары в [sp-#Z*8]! , предварительно индексированного смещения >= -248 |
save_fplr |
01zzzzzz: сохранение пары <x29,lr> в [sp+#Z*8] , смещение <= 504. |
save_fplr_x |
10zzzzzz: сохранение <x29,lr> пары в [sp-(#Z+1)*8]! , предварительно индексированного смещения >= -512 |
alloc_m |
11000x'xxxx: выделяет большой стек размером < 32K (2^11 * 16). |
save_regp |
110010xx'xxzzzz: save x(19+#X) pair at [sp+#Z*8] , offset <= 504 |
save_regp_x |
110011xx'xxzzzzzzzz: сохранение пары x(19+#X) в [sp-(#Z+1)*8]! , предварительно индексированного смещения >= -512 |
save_reg |
110100xx'xxzzzzzz: save reg x(19+#X) at [sp+#Z*8] , offset <= 504 |
save_reg_x |
1101010x'xxxzzzzz: save reg x(19+#X) at [sp-(#Z+1)*8]! , предварительно индексированного смещения >= -256 |
save_lrpair |
1101011x'xxzzzzzz: сохранение пары <x(19+2*#X),lr> в [sp+#Z*8] , смещение <= 504 |
save_fregp |
1101100x'xxzzzz: save pair d(8+#X) at [sp+#Z*8] , offset <= 504 |
save_fregp_x |
1101101x'xxzzzzzzzz: save pair d(8+#X) at [sp-(#Z+1)*8]! , pre-indexed offset >= -512 |
save_freg |
1101110x'xxzzzzzzzz: save reg d(8+#X) at [sp+#Z*8] , offset <= 504 |
save_freg_x |
11011110'xxxzzzzz: save reg d(8+#X) at [sp-(#Z+1)*8]! , предварительно индексированного смещения >= -256 |
alloc_l |
11100000'xxxx'xxxx'xxxx'xxxx: выделение большого стека размером < 256M (2^24 * 16) |
set_fp |
11100001: настройка x29 с помощью mov x29,sp |
add_fp |
11100010'xxxxxxxx: настройка с помощью x29 add x29,sp,#x*8 |
nop |
11100011: операция очистки не требуется. |
end |
11100100: конец кода очистки. Подразумевает ret в эпилоге. |
end_c |
11100101: конец кода очистки в текущей сцепленной области. |
save_next |
11100110: сохранение следующей неизменяемой или пары регистра типа Int или FP. |
11100111: зарезервировано | |
11101xxx: зарезервировано для случаев использования настаиваемого стека ниже, создаваемых только для подпрограмм ASM | |
11101000: настраиваемый стек для MSFT_OP_TRAP_FRAME |
|
11101001: настраиваемый стек для MSFT_OP_MACHINE_FRAME |
|
11101010: настраиваемый стек для MSFT_OP_CONTEXT |
|
11101011: настраиваемый стек для MSFT_OP_EC_CONTEXT |
|
11101100: настраиваемый стек для MSFT_OP_CLEAR_UNWOUND_TO_CALL |
|
11101101: зарезервировано | |
11101110: зарезервировано | |
11101111: зарезервировано | |
11110xx: зарезервировано | |
11111000'гг: зарезервировано | |
11111001'y'y: зарезервировано | |
11111010'y: зарезервировано | |
11111011'y: зарезервировано | |
pac_sign_lr |
11111100: войдите в возвращаемый адрес с lr помощью pacibsp |
11111101: зарезервировано | |
11111110: зарезервировано | |
11111111: зарезервировано |
В инструкциях с большими значениями, охватывающими несколько байт, старшие биты сохраняются первыми. Это позволяет определять общий размер кода очистки в байтах путем поиска только первого байта кода. Поскольку каждый код очистки однозначно сопоставлен с инструкцией в прологе или эпилоге, можно вычислить размер пролога или эпилога. Перейдите от последовательности до конца и используйте таблицу подстановки или аналогичное устройство, чтобы определить длину соответствующего opcode.
В прологе не допускается адресация постындексированного смещения. Все диапазоны смещения (#Z) соответствуют кодировке stp
/str
адресации, за исключением save_r19r20_x
того, что 248 достаточно для всех областей сохранения (10 регистров Int + 8 регистров FP + 8 входных регистров).
save_next
должен следовать за парой изменяемых регистров регистра типа Int или FP: save_regp
, save_regp_x
, save_fregp
, save_fregp_x
, save_r19r20_x
или другим save_next
. Он сохраняет следующую пару регистров в следующем 16-байтовом слоте в возрастающем порядке. save_next
ссылается на первую пару регистров типа FP, если следует за save-next
, обозначающим последнюю пару регистров типа Int.
Так как размеры регулярных возвращаемых и переходных инструкций одинаковы, в сценариях хвостового вызова не требуется разделенный end
код очистки.
end_c
предназначен для управления несмежными фрагментами функций в целях оптимизации. Значение end_c
, указывающее конец кодов очистки в текущей области, должно следовать еще одна серия кодов очистки, заканчивающаяся реальным end
. Коды очистки между end_c
и end
представляют операции пролога в родительском регионе (пролог "фантом". Дополнительные сведения и примеры описаны в разделе ниже.
Упакованные данные очистки
Для функций, прологи и эпилоги которых соответствуют описанной ниже канонической форме, можно использовать упакованные данные очистки. Это устраняет потребность в .xdata
записи полностью и значительно снижает затраты на предоставление данных очистки. Канонические прологи и эпилоги предназначены для удовлетворения распространенных требований простой функции: одна из которых не требует обработчика исключений, и которая выполняет операции установки и удаления в стандартном порядке.
Формат записи с упакованными данными очистки .pdata
выглядит следующим образом:
Используются следующие поля.
- Function Start RVA (Относительный виртуальный адрес начала функции) представляет собой 32-битовый относительный виртуальный адрес начала функции.
- Флаг — это 2-разрядное поле, как описано выше, со следующими значениями:
- 00 = упакованные данные очистки не используются; оставшиеся биты указывают на
.xdata
запись - 01 = упакованные данные очистки, используемые с одним прологом и эпилога в начале и в конце области.
- 10 = упакованные данные очистки, используемые для кода без пролога и эпилога. Полезно для описания отдельных сегментов функций
- 11 = зарезервировано.
- 00 = упакованные данные очистки не используются; оставшиеся биты указывают на
- Function Length (Длина функции) представляет собой 11-битовое поле, которое предоставляет длину всей функции в байтах, поделенную на 4. Если функция превышает 8 кб, вместо нее необходимо использовать полную
.xdata
запись. - Frame Size (Размер кадра) представляет собой 9-битовое поле, которое указывает число выделенных для этой функции бит в стеке, поделенное на 16. Функции, которые выделяют больше 8k-16 байт стека, должны использовать полную
.xdata
запись. Он включает в себя локальную переменную, исходящую область параметров, область сохраненного абонентом int и FP, а также область параметров дома. Он исключает динамическую область выделения. - CR — это 2-разрядный флаг, указывающий, включает ли функция дополнительные инструкции по настройке цепочки кадров и обратной связи:
- 00 = незавязаемая функция,
<x29,lr>
пара не сохраняется в стеке - 01 = функция без сцепления,
<lr>
сохранен в стеке - 10 = цепочка функции с подписанным возвращаемым адресом
pacibsp
- 11 = функция со сцеплением, инструкция пары хранения или загрузки используется в прологе или эпилоге
<x29,lr>
- 00 = незавязаемая функция,
- H — это 1-битовый флаг, указывающий, помещает ли функция регистры параметров типа INT в начальное расположение (x0–x7), сохраняя их в самом начале функции. (0 = не регистрирует дома, 1 = регистры домов).
- RegI представляет собой 4-битовое поле, указывающее число неизменяемых регистров типа INT (x19–x28), сохраненных в каноническом расположении стека.
- RegF представляет собой 3-битовое поле, указывающее число неизменяемых регистров типа FP (d8–d15), сохраненных в каноническом расположении стека. (RegF=0: регистрация FP не сохраняется; RegF 0: сохраняются регистры RegF>+1 FP). Упакованные данные очистки нельзя использовать для функций, которые сохраняют только один регистр типа FP.
Канонические прологи, которые попадают в категории 1, 2 (без области исходящих параметров), 3 и 4 в разделе выше, могут быть представлены в упакованном формате очистки. Эпилоги для канонических функций имеют аналогичную форму, за исключением того, что H не оказывает никакого влияния, инструкция set_fp
опускается, и порядок выполнения шагов и инструкции в каждом шаге в эпилоге является обратным. Алгоритм упакованного .xdata
выполняет следующие действия, подробные сведения в следующей таблице:
Шаг 0. Предварительное вычисление размера каждой области.
Шаг 1. Подпись возвращаемого адреса.
Шаг 2. Сохранение регистров, сохраненных в вызываемом объекте Int.
Шаг 3. Этот шаг предназначен для типа 4 в ранних разделах. lr
сохраняется в конце области Int.
Шаг 4. Сохранение регистров вызываемого абонента FP.
Шаг 5. Сохранение входных аргументов в области параметров дома.
Шаг 6. Выделение оставшегося стека, включая локальную область, <x29,lr>
пару и исходящую область параметров. 6a соответствует каноническому типу 1. 6b и 6c предназначены для канонического типа 2. 6d и 6e предназначены для типа 3 и типа 4.
Номер шага | Значения флагов | Число инструкций | Код операции | Код очистки |
---|---|---|---|---|
0 | #intsz = RegI * 8; if (CR==01) #intsz += 8; // lr #fpsz = RegF * 8; if(RegF) #fpsz += 8; #savsz=((#intsz+#fpsz+8*8*H)+0xf)&~0xf) #locsz = #famsz - #savsz |
|||
1 | CR == 10 | 1 | pacibsp |
pac_sign_lr |
2 | 0 <RegI<= 10 | RegI / 2 + RegI % 2 |
stp x19,x20,[sp,#savsz]! stp x21,x22,[sp,#16] ... |
save_regp_x save_regp ... |
3 | CR == 01* | 1 | str lr,[sp,#(intsz-8)] * |
save_reg |
4 | 0 <RegF<= 7 | (RegF + 1) / 2 + (RegF + 1) % 2) |
stp d8,d9,[sp,#intsz] **stp d10,d11,[sp,#(intsz+16)] ... str d(8+RegF),[sp,#(intsz+fpsz-8)] |
save_fregp ... save_freg |
5 | H == 1 | 4 | stp x0,x1,[sp,#(intsz+fpsz)] stp x2,x3,[sp,#(intsz+fpsz+16)] stp x4,x5,[sp,#(intsz+fpsz+32)] stp x6,x7,[sp,#(intsz+fpsz+48)] |
nop nop nop nop |
6a | (CR == 10 || CR == 11) &>#locsz <= 512 |
2 | stp x29,lr,[sp,#-locsz]! mov x29,sp *** |
save_fplr_x set_fp |
6b | (CR == 10 || CR == 11) &> 512 < #locsz <= 4080 |
3 | sub sp,sp,#locsz stp x29,lr,[sp,0] add x29,sp,0 |
alloc_m save_fplr set_fp |
6c | (CR == 10 || CR == 11) &>#locsz > 4080 |
4 | sub sp,sp,4080 sub sp,sp,#(locsz-4080) stp x29,lr,[sp,0] add x29,sp,0 |
alloc_m alloc_s /alloc_m save_fplr set_fp |
6d | (CR == 00 || CR == 01) &>#locsz <= 4080 |
1 | sub sp,sp,#locsz |
alloc_s /alloc_m |
6e | (CR == 00 || CR == 01) &>#locsz > 4080 |
2 | sub sp,sp,4080 sub sp,sp,#(locsz-4080) |
alloc_m alloc_s /alloc_m |
* Если CR == 01 и RegI является нечетным числом, шаг 3 и последний save_reg
в шаге 2 объединяются в один save_regp
.
** Если regI == CR == 0 и RegF != 0, первый stp
для с плавающей запятой выполняет предопределение.
В эпилоге нет инструкций, соответствующих mov x29,sp
инструкциям. Упакованные данные очистки нельзя использовать, если для функции требуется восстановление sp
из x29
.
Очистка частичных прологов и эпилогов
В наиболее распространенных ситуациях очистки исключение или вызов возникает в теле функции, от пролога и всех эпилогов. В таких ситуациях очистка является простой: очистка просто выполняет коды в массиве очистки. Он начинается с индекса 0 и продолжается до обнаружения опкода end
.
Если исключение возникает во время выполнения пролога или эпилога, правильно выполнить очистку еще сложнее. В таких ситуациях кадр стека создается только частично. Проблема заключается в том, чтобы определить, что именно было сделано, чтобы правильно отменить это.
Например, возьмем следующую последовательность пролога и эпилога.
0000: stp x29,lr,[sp,#-256]! // save_fplr_x 256 (pre-indexed store)
0004: stp d8,d9,[sp,#224] // save_fregp 0, 224
0008: stp x19,x20,[sp,#240] // save_regp 0, 240
000c: mov x29,sp // set_fp
...
0100: mov sp,x29 // set_fp
0104: ldp x19,x20,[sp,#240] // save_regp 0, 240
0108: ldp d8,d9,[sp,224] // save_fregp 0, 224
010c: ldp x29,lr,[sp],#256 // save_fplr_x 256 (post-indexed load)
0110: ret lr // end
Рядом с каждым кодом операции находится подходящий код очистки, описывающий эту операцию. Как видно, последовательность кодов очистки для пролога является зеркальным отображением кодов очистки для эпилога (без учета последней инструкции эпилога). Это распространенная ситуация: поэтому мы всегда предполагаем, что коды очистки для пролога хранятся в обратном порядке из порядка выполнения пролога.
Таким образом, пролог и эпилог имеют общий набор кодов очистки:
set_fp
, , save_regp 0,240
save_fregp,0,224
, save_fplr_x_256
end
Ситуация с эпилогом гораздо проще, так как действия выполняются в обычном порядке. Начиная с смещения 0 в эпилоге (который начинается с смещения 0x100 в функции), мы ожидаем, что полная последовательность очистки еще не выполнена. Если очистка выполняется из первой инструкции (со смещения 2 в эпилоге), пропускается первый код очистки. Эту ситуацию можно обобщить и предположить однозначное сопоставление между кодами операций и кодами очистки. Затем, чтобы начать очистку с инструкции n в эпилоге, следует пропустить первые n кодов очистки и начать выполнение оттуда.
Аналогичная логика работает и в обратном порядке для пролога. Если очистка осуществляется с нулевого смещения в прологе, ничего выполнять не требуется. Если начать очистку со смещения 2, то есть с погружением в одну инструкцию, последовательность очистки должна начаться с предпоследнего кода очистки. (Помните, что коды хранятся в обратном порядке.) И здесь мы также можем обобщить: если мы начинаем очистку из инструкции n в прологе, мы должны начать выполнять n коды очистки с конца списка кодов.
Коды пролога и эпилога не всегда совпадают точно, поэтому массив очистки может содержать несколько последовательностей кодов. Чтобы определить смещение для начала обработки кодов, используйте следующую логику.
При отмене очистки из текста функции начинайте выполнять коды очистки по индексу 0 и продолжать работу до попадания
end
в опкод.Если очистка выполняется из эпилога, используйте в качестве отправной точки специальный начальный индекс эпилога, указанный в области эпилога. Вычислите, на сколько байт PC отстоит от начала эпилога. Затем перейдите по кодам очистки, пропустив такое число кодов очистки, которое соответствует уже выполненным инструкциям. Выполните последовательность очистки, начиная с этой точки.
Если очистка осуществляется из пролога, начните с индекса 0 в кодах очистки. Вычислите длину кода пролога из последовательности, а затем рассчитайте, на сколько байт PC отстоит от конца пролога. Затем перейдите по кодам очистки, пропустив такое число кодов очистки, которое соответствует еще не выполненным инструкциям. Выполните последовательность очистки, начиная с этой точки.
Эти правила означают, что коды очистки для пролога должны всегда стоять в массиве первыми. И именно эти коды используются для очистки в общем случае, когда очистка осуществляется из тела. Сразу после последовательности кода для пролога должны идти специальные последовательности для эпилога.
Фрагменты функции
Для целей оптимизации кода и других причин может потребоваться разделить функцию на отдельные фрагменты (также называемые регионами). При разбинии каждый результирующий фрагмент функции требует отдельной .pdata
записи (и, возможно, )..xdata
Если у разделенного вторичного фрагмента есть собственный пролог, выполнять подстройку стека в этом прологе не следует. Все пространство стека, необходимое вторичному региону, должно быть предварительно выделено его родительским регионом (или главным регионом). Это предварительное размещение позволяет строго манипулировать указателем стека в исходном прологе функции.
Типичным случаем фрагментов функции является "разделение кода", где компилятор может переместить регион кода из его основной функции. Существует три необычных случая, которые могут привести к разделению кода.
Пример
(регион 1: начало)
stp x29,lr,[sp,#-256]! // save_fplr_x 256 (pre-indexed store) stp x19,x20,[sp,#240] // save_regp 0, 240 mov x29,sp // set_fp ...
(регион 1: конец)
(регион 3: начало)
...
(регион 3: конец)
(регион 2: начало)
... mov sp,x29 // set_fp ldp x19,x20,[sp,#240] // save_regp 0, 240 ldp x29,lr,[sp],#256 // save_fplr_x 256 (post-indexed load) ret lr // end
(регион 2: конец)
Только пролог (регион 1: все эпилоги находятся в отдельных регионах):
Необходимо описать только пролог. Этот пролог не может быть представлен в компактном
.pdata
формате. В полном.xdata
случае его можно представить, задав значение Epilog Count = 0. См. регион 1 в примере выше.Коды очистки:
set_fp
,save_regp 0,240
,save_fplr_x_256
,end
.Только эпилоги (регион 2: пролог находится в главном регионе)
Предполагается, что к тому времени контроль времени переходит в этот регион, все коды пролога были выполнены. Частичная очистка может выполняться в эпилогах так же, как в обычной функции. Этот тип региона не может быть представлен компактным
.pdata
. В полной.xdata
записи его можно закодировать с помощью "фантомного" пролога, скобленного паройend_c
кода иend
отмены. Начальныйend_c
указывает, что размер пролога равен нулю. Начальный индекс эпилога для одного эпилога указывает наset_fp
.Код очистки для региона 2:
end_c
,set_fp
,save_regp 0,240
,save_fplr_x_256
,end
.Нет прологов или эпилогов (регион 3: прологи все эпилоги находятся в других фрагментах):
Компактный
.pdata
формат можно применить с помощью параметра Flag = 10. С полной.xdata
записью эпилог счетчик = 1. Код очистки совпадает с кодом для региона 2 выше, но индекс начала эпилога также указывает наend_c
. В этом регионе кода никогда не будет выполняться частичная очистка.
Еще одним более сложным случаем фрагментов функций является "сжатие упаковки". Компилятор может отложить сохранение некоторых сохраненных вызываемого регистра до тех пор, пока не выходит за пределы пролога записи функции.
(регион 1: начало)
stp x29,lr,[sp,#-256]! // save_fplr_x 256 (pre-indexed store) stp x19,x20,[sp,#240] // save_regp 0, 240 mov x29,sp // set_fp ...
(регион 2: начало)
stp x21,x22,[sp,#224] // save_regp 2, 224 ... ldp x21,x22,[sp,#224] // save_regp 2, 224
(регион 2: конец)
... mov sp,x29 // set_fp ldp x19,x20,[sp,#240] // save_regp 0, 240 ldp x29,lr,[sp],#256 // save_fplr_x 256 (post-indexed load) ret lr // end
(регион 1: конец)
В прологе региона 1 пространство стека выделено заранее. Как видно, в регионе 2 будет использоваться тот же код очистки, даже если он перемещается из главной функции размещения.
Регион 1: set_fp
, save_regp 0,240
, save_fplr_x_256
end
. Эпилог начального индекса указывает как set_fp
обычно.
Регион 2: save_regp 2, 224
, end_c
, set_fp
, save_regp 0,240
, save_fplr_x_256
, end
. Индекс начала эпилога указывает на первый код очистки save_regp 2, 224
.
Большие функции
Фрагменты можно использовать для описания функций, превышающих ограничение 1M, введенное битовых полей в заголовке .xdata
. Чтобы описать необычно большую функцию, как это, необходимо разбить на фрагменты меньше 1M. Для каждого фрагмента требуется подстройка, чтобы он не разбивал эпилог на множество частей.
Только первый фрагмент функции будет содержать пролог, все остальные фрагменты помечаются как не содержащие пролог. В зависимости от имеющегося числа эпилогов каждый фрагмент может содержать от нуля до нескольких эпилогов. Помните, что область каждого эпилога во фрагменте указывает начальное смещение относительно начала данного фрагмента, а не начала функции.
Если фрагмент не имеет пролога и эпилога, он по-прежнему требует собственной .pdata
записи (и, возможно .xdata
, ), чтобы описать, как снять данные из тела функции.
Примеры
Пример 1. Обрамленная, компактная форма
|Foo| PROC
|$LN19|
str x19,[sp,#-0x10]! // save_reg_x
sub sp,sp,#0x810 // alloc_m
stp fp,lr,[sp] // save_fplr
mov fp,sp // set_fp
// end of prolog
...
|$pdata$Foo|
DCD imagerel |$LN19|
DCD 0x416101ed
;Flags[SingleProEpi] functionLength[492] RegF[0] RegI[1] H[0] frameChainReturn[Chained] frameSize[2080]
Пример 2. Кадры с полной формой с зеркальным прологом и эпилогом
|Bar| PROC
|$LN19|
stp x19,x20,[sp,#-0x10]! // save_regp_x
stp fp,lr,[sp,#-0x90]! // save_fplr_x
mov fp,sp // set_fp
// end of prolog
...
// begin of epilog, a mirror sequence of Prolog
mov sp,fp
ldp fp,lr,[sp],#0x90
ldp x19,x20,[sp],#0x10
ret lr
|$pdata$Bar|
DCD imagerel |$LN19|
DCD imagerel |$unwind$cse2|
|$unwind$Bar|
DCD 0x1040003d
DCD 0x1000038
DCD 0xe42291e1
DCD 0xe42291e1
;Code Words[2], Epilog Count[1], E[0], X[0], Function Length[6660]
;Epilog Start Index[0], Epilog Start Offset[56]
;set_fp
;save_fplr_x
;save_r19r20_x
;end
Индекс начала эпилога [0] указывает на ту же последовательность кода очистки пролога.
Пример 3. Вариадинская несоединяемая функция
|Delegate| PROC
|$LN4|
sub sp,sp,#0x50
stp x19,lr,[sp]
stp x0,x1,[sp,#0x10] // save incoming register to home area
stp x2,x3,[sp,#0x20] // ...
stp x4,x5,[sp,#0x30]
stp x6,x7,[sp,#0x40] // end of prolog
...
ldp x19,lr,[sp] // beginning of epilog
add sp,sp,#0x50
ret lr
AREA |.pdata|, PDATA
|$pdata$Delegate|
DCD imagerel |$LN4|
DCD imagerel |$unwind$Delegate|
AREA |.xdata|, DATA
|$unwind$Delegate|
DCD 0x18400012
DCD 0x200000f
DCD 0xe3e3e3e3
DCD 0xe40500d6
DCD 0xe40500d6
;Code Words[3], Epilog Count[1], E[0], X[0], Function Length[18]
;Epilog Start Index[4], Epilog Start Offset[15]
;nop // nop for saving in home area
;nop // ditto
;nop // ditto
;nop // ditto
;save_lrpair
;alloc_s
;end
Индекс начала эпилога [4] указывает на середину кода очистки пролога (частичное повторное использование массива очистки).
См. также
Общие сведения о соглашениях ABI ARM64
Обработка исключений в ARM