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


Обработка исключений в ARM64

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

Цели и причины для использования

Соглашения о данных очистки исключений и эти сведения предназначены для реализации следующих задач.

  • Предоставьте достаточно ясное описание, позволяющее производить раскрутку без проверки кода во всех случаях.

    • Для анализа кода требуется загрузка его в память. Он предотвращает размотку в некоторых ситуациях, когда это полезно (трассировка, семплирование, отладка).

    • Анализ кода является сложным процессом, поэтому компилятор должен генерировать только те инструкции, которые может декодировать развёртчик.

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

  • Поддержка очистки в середине пролога и середине эпилога.

    • Очистка в Windows используется не только для обработки исключений. Крайне важно обеспечить правильную очистку кода, даже если он находится в середине пролога или эпилога.
  • Использование минимального объема пространства.

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

    • Так как коды очистки, скорее всего, будут заблокированы в памяти, небольшое пространство обеспечивает минимальные издержки для каждого загруженного двоичного файла.

Предположения

Эти предположения вносятся в описание обработки исключений:

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

  • Как правило, функции в целом имеют относительно небольшой размер. На этом основано несколько оптимизаций для более эффективной упаковки данных.

  • В эпилогах нет условного кода.

  • Выделенный регистр указателя кадра: если sp сохраняется в другом регистре (x29) в прологе, этот регистр остается неизменным на протяжении всей функции. Это означает, что исходный файл sp может быть восстановлен в любое время.

  • Если sp не сохраняется в другом регистре, все манипуляции указателя стека выполняются исключительно в прологу и эпилогу.

  • Структура кадра стека организована так, как описано в следующем разделе.

Схема кадра стека ARM64

Схема, показывающая структуру кадра стека для функций.

Для функций, связанных с кадрами, fplr пара может сохраняться в любой позиции в локальной переменной в зависимости от соображений оптимизации. Цель состоит в том, чтобы максимально увеличить количество локалов, которые можно достичь одной инструкцией на основе указателя кадра (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 область сохранения регистров можно оптимизировать, если ее переместить в нижнюю часть стека. Недостатком является то, что он не согласуется с другими макетами выше. Сохраненные регистры занимают часть диапазона для парных регистров и режима адресации смещения с пред- и пост-индексированием.

  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
    

    * Выделение области сохранения регистров не включено в stp, так как предварительно индексированный рег-лист 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. Односоставный или двусоставный заголовок, описывающий общий размер структуры и предоставляющий ключевые данные о её функциях. Второе слово присутствует только в том случае, если для обоих полей Epilog Count (Число эпилогов) и Code Words (Слова кодов) задано значение 0. Заголовок содержит следующие битовые поля:

    a. Function Length (Длина функции) представляет собой 18-битовое поле. Оно указывает общую длину функции в байтах, поделенную на 4. Если функция превышает 1 мегабайт, для описания функции необходимо использовать несколько .pdata и .xdata записей. Дополнительные сведения см. в разделе Большие функции.

    б. Vers (Версия) представляет собой 2-битовое поле. Она описывает версию оставшейся части .xdata. Сейчас определена только версия 0, поэтому значения 1–3 запрещены.

    с. X представляет собой 1-битовое поле. Оно указывает наличие (1) или отсутствие (0) данных исключения.

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

    д) Epilog Count (Число эпилогов) представляет собой 5-битовое поле, имеющее два значения в зависимости от состояния бита E:

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

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

    е) 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, относительно начала функции.

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

    с. 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 требует, чтобы код мог выполнять очистку из частично выполненного пролога или эпилога. Чтобы выполнить это требование, коды разворачивания были тщательно спроектированы, чтобы однозначно сопоставлять 1:1 с каждым соответствующим кодом операции в прологе и эпилоге. Эта особенность имеет несколько разных применений.

  • Сосчитав число кодов раскрутки, можно вычислить длину пролога и эпилога.

  • Считая число инструкций после начала области эпилога, можно пропустить эквивалентное число кодов очистки. Мы можем выполнить оставшуюся часть последовательности, чтобы завершить частично выполненное эпилогом восстановление.

  • Считая число инструкций до окончания пролога, можно пропустить эквивалентное число кодов распутывания. Можно выполнить оставшуюся часть последовательности, чтобы отменить только те части пролога, которые завершили выполнение.

Коды развёртки кодируются в соответствии с таблицей ниже. Все unwind-коды представляют собой одинарный или двойной байт, за исключением того, который выделяет огромный стек (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 11000xxx'xxxxxxxx: выделить большой стек размером < 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: сохранить рег x(19+#X) на [sp+#Z*8], смещение <= 504
save_reg_x 1101010x'xxxzzzzz: сохранить регистр x(19+#X) по адресу [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: сохранить пару d(8+#X) в [sp-(#Z+1)*8]!, предварительно индексированный сдвиг >= -512
save_freg 1101110x'xxzzzzzz: сохранить регистр d(8+#X) по адресу [sp+#Z*8], смещение <= 504
save_freg_x 11011110'xxxzzzzz: сохранить регистр d(8+#X) по адресу [sp-(#Z+1)*8]!, предварительно индексированное смещение >= -256
alloc_z 11011111'zzzzzzzz: выделить стек размером z * SVE-VL
alloc_l 11100000'xxxxxxxx'xxxxxxxx'xxxxxxxx: выделение большого стека размером < 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: сохранить следующую пару регистров.
save_any_xreg 11100111'0pxrrrrr'00ooooo: save register(s)
  • p: 0/1 => одно X(#r) и пара X(#r) + X(#r+1)
  • x: 0/1 => положительное и отрицательное смещение предварительно индексированного стека
  • o: смещение = o * 16, если x=1 или p=1, иначе o * 8
(Требуется Windows >= 11)
save_any_dreg 11100111'0pxrrrrr'01oooooo: сохранить регистр(ы)
  • p: 0/1 => одно D(#r) и пара D(#r) + D(#r+1)
  • x: 0/1 => положительное и отрицательное смещение предварительно индексированного стека
  • o: смещение = o * 16, если x=1 или p=1, иначе o * 8
(Требуется Windows >= 11)
save_any_qreg 11100111'0pxrrrrr'10oooooo: сохранить регистр(ы)
  • p: 0/1 => одно Q(#r) и пара Q(#r) + Q(#r+1)
  • x: 0/1 => положительное и отрицательное смещение предварительно индексированного стека
  • o: смещение = o * 16
(Требуется Windows >= 11)
save_zreg 11100111'0oo0rrrr'11oooooo: сохранить регистр Z(#r+8) в [sp + #o * VL], (Z8 через Z23)
save_preg 11100111'0oo1rrr'11oooooo: сохраните регистр P(#r) по адресу [sp + #o * (VL / 8)], (отP4 до P15; значения r и [0, 3] зарезервированы)
11100111'1yyyyyyy': зарезервировано
11101xxx: зарезервировано для пользовательских стэков, создаваемых только для ассемблерных подпрограмм.
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'yyyyyyyy'yyyyyyyy: зарезервировано
11111010'yyyyyyyy'yyyyyyyy'yyyyyyyy: зарезервировано
11111011'yyyyyyyy'yyyyyyyy'yyyyyyyy'yyyyyyyy: зарезервировано
pac_sign_lr 11111100: подпишите обратный адрес в lr при помощи pacibsp
11111101: зарезервировано
11111110: зарезервировано
11111111: зарезервировано

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

Использование адресации со смещением после индексирования не допускается в прологе. Все диапазоны смещения (#Z) соответствуют кодировке stp/str адресации, за исключением save_r19r20_x, где 248 достаточно для всех зон сохранения (10 регистров Int + 8 регистров FP + 8 входных регистров).

save_next должно идти после сохранения для пары регистров: save_regp, save_regp_x, save_fregp, save_fregp_x, save_r19r20_xили другого save_next. Его также можно использовать в сочетании с save_any_xreg, save_any_dreg или save_any_qreg, но только если p = 1. Он сохраняет следующую пару регистров в порядке увеличения чисел в следующем пространстве стека. save_next не следует использовать за пределами последнего регистра того же типа.

Поскольку размеры стандартных инструкций возврата и перехода одинаковы, в сценариях хвостового вызова не требуется отдельный 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-битовый флаг, указывающий, помещает ли функция целочисленные параметры регистров (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. Сохраните регистры, сохраняемые вызываемой функцией (callee-saved registers) 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 в эпилоге), мы можем успешно развернуться, пропуская первый код раскрутки. Эту ситуацию можно обобщить и предположить соответствие 1:1 между операционными кодами и кодами сворачивания. Затем, чтобы начать очистку с инструкции 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. В этом регионе кода никогда не будет выполняться частичная очистка.

Еще одним более сложным случаем фрагментов функций является "shrink wrapping". Компилятор может отложить сохранение некоторых регистров, сохраняемых вызываемым, до тех пор, пока не выйдет за пределы входного пролога функции.

  • (регион 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. Индекс начала Epilog указывает на первый код развёртки save_regp 2, 224.

Большие функции

Фрагменты можно использовать для описания функций, превышающих ограничение 1M, введенное битовыми полями в заголовке .xdata. Чтобы описать такую необычно большую функцию, её необходимо разбить на фрагменты меньше 1М. Для каждого фрагмента требуется подстройка, чтобы он не разбивал эпилог на множество частей.

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

Если фрагмент не имеет пролога и эпилога, он по-прежнему требует собственной записи .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