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


Обработка исключений в 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>) и размер исходящего параметра соответственно.

  1. Со сцеплением, #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)
    
  2. Цепочка, #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
    
  3. Незаклинированные, конечные функции (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 принимают участие в диапазоне для парных регов и режима адресации смещения после индексирования.

  4. Незаклинированные, неконечные функции (сохраняется 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-lr stp не может быть представлена с помощью кодов очистки.

    Доступ ко всем локальным ресурсам зависит от sp. <x29> указывает на предыдущий кадр.

  5. Со сцеплением, #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 того, что предотвращает параллелизм на уровне инструкций.

  6. Цепочка, размер > кадра 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.

  7. Цепочка, размер > кадра 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 , или упакованное слово, описывающее каноническую последовательность очистки функции.

Схема записи .PDATA.

Используются следующие поля.

  • 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 представляет собой упакованный набор слов переменной длины:

Схема записи .XDATA.

Эти данные состоят из четырех разделов.

  1. Заголовок 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:

    1. Если E равен 0, то он указывает общее число областей эпилога, описанных в разделе 2. Если в функции присутствует более 31 области, для поля Code Words (Слова кодов) следует установить значение 0, чтобы указать на потребность в слове расширения.

    2. Если E равен 1, это поле указывает индекс первого кода очистки, описывающего единственный эпилог.

    f. Code Words (Слова кодов) представляет собой 5-битовое поле, которое указывает число 32-разрядных слов, требуемое для размещения всех кодов очистки в разделе 3. Если требуется более 31 слов (т. е. 124 кодов очистки), это поле должно быть равно 0, чтобы указать, что требуется слово расширения.

    ж. Extended Epilog Count (Расширенное число эпилогов) и Extended Code Words (Расширенные слова кодов) представляют собой 16- и 8-битовые поля, соответственно. Они предоставляют дополнительное место для кодирования необычайно большого числа эпилогов или слов кода очистки. Слово расширения, содержащее это поле, присутствует только в том случае, если для полей Epilog Count (Число эпилогов) и Code Words (Слова кодов) в первом слове заголовка задано значение 0.

  2. Если количество эпилогов не равно нулю, список сведений о областях эпилога, упакованных в слово, приходит после заголовка и необязательного расширенного заголовка. Они хранятся в порядке увеличения начального смещения. Каждая область содержит следующие биты:

    a. Epilog Start Offset (Смещение начала эпилога) представляет собой 18-битовое поле, которое описывает смещение эпилога в байтах, поделенное на 4, относительно начала функции.

    b. Res (Зарезервировано) представляет собой 4-битовое поле, зарезервированное для расширения в будущем. Оно должно иметь значение 0.

    c. Epilog Start Index (Индекс начала эпилога) представляет собой 10-битовое поле (на 2 бита больше, чем поле Extended Code Words (Расширенные слова кодов)). Оно указывает индекс байта первого кода очистки, описывающего этот эпилог.

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

  4. Наконец, если поле 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: настройка с помощью x29add 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 выглядит следующим образом:

Запись .PDATA с упакованными данными очистки.

Используются следующие поля.

  • Function Start RVA (Относительный виртуальный адрес начала функции) представляет собой 32-битовый относительный виртуальный адрес начала функции.
  • Флаг — это 2-разрядное поле, как описано выше, со следующими значениями:
    • 00 = упакованные данные очистки не используются; оставшиеся биты указывают на .xdata запись
    • 01 = упакованные данные очистки, используемые с одним прологом и эпилога в начале и в конце области.
    • 10 = упакованные данные очистки, используемые для кода без пролога и эпилога. Полезно для описания отдельных сегментов функций
    • 11 = зарезервировано.
  • 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>
  • 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,240save_fregp,0,224, save_fplr_x_256end

Ситуация с эпилогом гораздо проще, так как действия выполняются в обычном порядке. Начиная с смещения 0 в эпилоге (который начинается с смещения 0x100 в функции), мы ожидаем, что полная последовательность очистки еще не выполнена. Если очистка выполняется из первой инструкции (со смещения 2 в эпилоге), пропускается первый код очистки. Эту ситуацию можно обобщить и предположить однозначное сопоставление между кодами операций и кодами очистки. Затем, чтобы начать очистку с инструкции n в эпилоге, следует пропустить первые n кодов очистки и начать выполнение оттуда.

Аналогичная логика работает и в обратном порядке для пролога. Если очистка осуществляется с нулевого смещения в прологе, ничего выполнять не требуется. Если начать очистку со смещения 2, то есть с погружением в одну инструкцию, последовательность очистки должна начаться с предпоследнего кода очистки. (Помните, что коды хранятся в обратном порядке.) И здесь мы также можем обобщить: если мы начинаем очистку из инструкции n в прологе, мы должны начать выполнять n коды очистки с конца списка кодов.

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

  1. При отмене очистки из текста функции начинайте выполнять коды очистки по индексу 0 и продолжать работу до попадания end в опкод.

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

  3. Если очистка осуществляется из пролога, начните с индекса 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. Только пролог (регион 1: все эпилоги находятся в отдельных регионах):

    Необходимо описать только пролог. Этот пролог не может быть представлен в компактном .pdata формате. В полном .xdata случае его можно представить, задав значение Epilog Count = 0. См. регион 1 в примере выше.

    Коды очистки: set_fp, save_regp 0,240, save_fplr_x_256, end.

  2. Только эпилоги (регион 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. Нет прологов или эпилогов (регион 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_256end. Эпилог начального индекса указывает как 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