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


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

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

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

Windows на ARM использует коды очистки для управления очисткой стека во время структурированной обработки исключений (SEH). Коды очистки представляют собой последовательность байт, хранимых в разделе .xdata исполняемого образа. Эти коды описывают операцию пролога функции и эпилога кода абстрактным образом. Обработчик использует их для отмены эффектов пролога функции при очистке кадра стека вызывающего объекта.

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

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

Исполняемые образы для Windows на ARM используют формат переносимого исполняемого файла (PE). Дополнительные сведения см. в разделе Формат переносимого исполняемого файла. Информация об обработке исключений хранится в разделах .pdata и .xdata образа.

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

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

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

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

  • Если условие ставится в эпилоге, оно в равной степени применяется ко всем инструкциям эпилога.

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

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

  • Для очистки кадра стека необходимо выполнить следующие операции.

    • Скорректируйте r13 (SP) с 4-байтными приращениями.

    • Извлеките один или несколько целочисленных регистров.

    • Извлеките один или несколько регистров VFP (виртуальные операции с плавающей запятой).

    • Скопируйте значение произвольного регистра в r13 (SP).

    • Загрузите указатель стека из стека с помощью небольшой постдекрементной операции.

    • Произведите анализ одного из нескольких четко заданных типов кадров.

Записи .pdata

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

Каждая запись .pdata для ARM имеет длину 8 байт. Общий формат записи имеет следующий вид: относительный виртуальный адрес (RVA) в начале функции в первом 32-битном слове, после которого идет второе слово с указателем на блок .xdata переменной длины или упакованным словом, описывающим каноническую последовательность очистки функции, как показано в следующей таблице.

Смещение слов Bits Характер использования
0 0—31 Function Start RVA представляет собой 32-разрядный относительный виртуальный адрес начала функции. Если функция содержит код бегунка, необходимо задать младший бит этого адреса.
1 0–1 Flag представляет собой 2-битовое поле, указывающее, как правильно интерпретировать остальные 30 бит второго слова .pdata. Если Flag имеет значение 0, то оставшиеся биты формируют Exception Information RVA (при этом два младших бита неявно становятся равны 0). Если Flag имеет ненулевое значение, то оставшиеся биты формируют структуру Packed Unwind Data.
1 2—31 Exception Information RVA или Packed Unwind Data.

Exception Information RVA (Относительный виртуальный адрес сведений об исключении) представляет собой адрес структуры сведений об исключении переменной длины, хранимой в разделе .xdata. Эти данные должны быть выровнены по 4-байтовой границе.

Packed Unwind Data (Упакованные данные очистки) представляет собой сжатое описание операций, необходимых для выполнения очистки из функции в канонической форме. В этом случае запись .xdata не требуется.

Упакованные данные очистки

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

В этой таблице показан формат записи .pdata, содержащей упакованные данные очистки.

Смещение слов Bits Характер использования
0 0—31 Function Start RVA представляет собой 32-разрядный относительный виртуальный адрес начала функции. Если функция содержит код бегунка, необходимо задать младший бит этого адреса.
1 0–1 Flag представляет собой 2-битовое поле, имеющее следующие значения.

— 00 = упакованные данные очистки не используются; оставшиеся биты указывают на запись .xdata.
— 01= упакованные данные очистки.
— 10 = упакованные данные очистки, когда для функции предполагается отсутствие пролога. Это удобно для описания фрагментов функции, которые разобщены с началом функции.
— 11 = зарезервировано.
1 2—12 Function Length представляет собой 11-битовое поле, которое предоставляет длину всей функции в байтах, поделенную на 2. Если размер функции больше 4 КБ, следует использовать полную запись .xdata.
1 13—14 Ret представляет собой 2-битовое поле, указывающее способ возврата данных функцией.

— 00 = возврат посредством извлечения {pc} (в этом случае для флагового разряда L следует установить значение 1).
— 01 = возвращение посредством 16-разрядного ветвления.
— 10 = возвращение посредством 32-разрядного ветвления.
— 11 = полное отсутствие эпилога. Это удобно для описания разобщенного фрагмента функции, который может содержать только пролог, однако его эпилог находится в другом месте.
1 15 H представляет собой 1-битный флаг, который указывает, устанавливает ли функция регистры целочисленных параметров (r0–r3) в начальное расположение посредством их отправки в начале функции, и освобождает 16 байтов стека перед возвратом. (0 = не регистрирует дома, 1 = дома регистры.)
1 16—18 Reg представляет собой 3-битовое поле, указывающее индекс последнего сохраненного неизменяемого регистра. Если бит R равен 0, то сохраняются только целочисленные регистры, которые должны находиться в пределах r4–rN, где значение N равно 4 + Reg. Если бит R равен 1, то сохраняются только регистры операций с плавающей запятой, которые должны находиться в пределах d8–dN, где значение N равно 8 + Reg. Особое сочетание R = 1 и Reg = 7 указывает на то, что никакие регистры не сохраняются.
1 19 R представляет собой 1-битовый флаг, который указывает, предназначены ли сохраненные неизменяемые регистры для целочисленных операций (0) или для операций с плавающей запятой (1). Если R имеет значение 1, а поле Reg имеет значение 7, то никакие неизменяемые регистры не отправлялись.
1 20 L представляет собой 1-битовый флаг, который указывает, выполняет ли функция сохранение или восстановление регистра LR вместе с другими регистрами, указанными в поле Reg. (0 = не сохраняет и не восстанавливает, 1 = сохраняет или восстанавливает.)
1 21 C представляет собой 1-битовый флаг, который указывает, содержит (1) функция дополнительные инструкции по настройке цепочки кадров для ускорения проверки стека или нет (0). Если этот бит задан, r11 неявно добавляется в список сохраняемых неизменяемых регистров для целочисленных операций. (См. описанные ниже ограничения при использовании флага C.)
1 22—31 Stack Adjust представляет собой 10-битовое поле, которое указывает число выделенных для этой функции бит в стеке, поделенное на 4. Однако напрямую возможно кодирование лишь значений из диапазона 0x000—0x3F3. Функции, выделяющие более 4044 байт стека, должны использовать полную запись .xdata. Если поле Stack Adjust имеет размер 0x3F4 и более, то 4 младших бита имеют особое значение.

— Биты 0–1 указывают число слов в подстройке стека (1–4) минус 1.
— Бит 2 имеет значение 1, если пролог объединил эту подстройку со своей операцией отправки.
— Бит 3 имеет значение 1, если эпилог объединил эту подстройку со своей операцией извлечения.

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

  • Если флаг C имеет значение 1.

    • Флаг L также должен иметь значение 1, так как для цепочки кадров требуется r11 и LR.

    • r11 не следует включать в набор регистров, описываемый Reg. Таким образом, при отправке регистров r4–r11 Reg должен описывать только регистры r4–r10, так как флаг C подразумевает r11.

  • Если поле Ret имеет значение 0, для флага L следует установить значение 1.

Нарушение этих ограничений ведет к неподдерживаемой последовательности операций.

В целях дальнейшего обсуждения от Stack Adjust производится наследование двух псевдофлагов:

  • PF или "свертывание пролога" указывает, что Stack Adjust имеет размер 0x3F4 или больше и задан бит 2;

  • EF или "свертывание эпилога" указывает, что Stack Adjust имеет размер 0x3F4 или больше и задан бит 3.

Прологи для канонических функций могут иметь до 5 инструкций (обратите внимание, что 3а и 3б являются взаимоисключающими).

Инструкция Предполагается наличие кода операции, если Размер Код операции Коды очистки
1 H==1 16 push {r0-r3} 04
2 C==1 или L==1 или R==0 или PF==1 16/32 push {registers} 80—BF/D0—DF/EC—ED
C==1 и (R==1 и PF==0) 16 mov r11,sp FB
C==1 и (R==0 или PF==1) 32 add r11,sp,#xx FC
4 R==1 и Reg != 7 32 vpush {d8-dE} E0—E7
5 Stack Adjust != 0 и PF==0 16/32 sub sp,sp,#xx 00—7F/E8—EB

Инструкция 1 всегда присутствует, если для бита H задано значение 1.

Чтобы настроить цепочку кадров, присутствует инструкция 3а или 3б при заданном бите C. Это 16-разрядная mov, если отправляются только регистры r11 и LR; в противном случае это 32-разрядная add.

Если указана подстройка без свертывания, инструкция 5 является явной подстройкой стека.

Инструкции 2 и 4 задаются с учетом того, требуется ли отправка. В этой таблице приведены сводные сведения о том, какие регистры сохраняются в зависимости от состояния полей C, L, R и PF. Во всех случаях N равен Reg + 4, E равен Reg + 8, а S равен (~Stack Adjust) & 3.

C L R ТФ Отправленные целочисленные регистры Отправленные регистры VFP
0 0 0 0 r4 — r*N* ничего
0 0 0 1 r*S* - r*N* ничего
0 0 1 0 ничего d8 - d*E*
0 0 1 1 r*S* - r3 d8 - d*E*
0 1 0 0 r4 — r**N, LR ничего
0 1 0 1 r*S* - r**N, LR ничего
0 1 1 0 LR d8 - d*E*
0 1 1 1 r*S* - r3, LR d8 - d*E*
1 0 0 0 (недопустимая кодировка) (недопустимая кодировка)
1 0 0 1 (недопустимая кодировка) (недопустимая кодировка)
1 0 1 0 (недопустимая кодировка) (недопустимая кодировка)
1 0 1 1 (недопустимая кодировка) (недопустимая кодировка)
1 1 0 0 r4 — r*N*, r11, LR ничего
1 1 0 1 r*S* - r**N, r11, LR ничего
1 1 1 0 r11, LR d8 - d*E*
1 1 1 1 r*S* - r3, r11, LR d8 - d*E*

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

Инструкция Предполагается наличие кода операции, если Размер Код операции
6 Stack Adjust!=0 и EF==0 16/32 add sp,sp,#xx
7 R==1 и Reg!=7 32 vpop {d8-dE}
8 C==1 или (L==1 и (H===0 или !=0)) или R==0 или Ret EF==1 16/32 pop {registers}
H==1 и (L==0 или Ret!=0) 16 add sp,sp,#0x10
H==1 и ==1 и LRet==0 32 ldr pc,[sp],#0x14
10a Ret==1 16 bx reg
10б Ret==2 32 b address

Инструкция 6 является явной подстройкой стека, если указана подстройка без свертывания. Поскольку PF не зависит от EF, можно получить инструкцию 5 без инструкции 6 или наоборот.

Инструкции 7 и 8 используют ту же логику, что и пролог, чтобы определить, какие регистры восстанавливаются из стека, но с этими тремя изменениями: во-первых, EF используется PFвместо ; второе, если Ret = 0 и = 0, то LR заменяется компьютером в списке регистрации и эпилог заканчивается немедленно; третий, если Ret = 0 и H H = 1, затем LR не указан в списке регистрации и отображается инструкцией 9b.

Если задан H, то присутствует либо инструкция 9а, либо инструкция 9б. Инструкция 9a используется, если Ret ненулевое значение, которое также подразумевает наличие 10a или 10b. Если L=1, то LR был порван в рамках инструкции 8. Инструкция 9b используется, когда L значение равно 1 и Ret равно нулю, чтобы указать ранний конец эпилога, а также для возврата и настройки стека одновременно.

Если эпилог еще не закончился, то в зависимости от значения Retприсутствует инструкция 10a или 10b, чтобы указать 16-разрядную или 32-разрядную ветвь.

Записи .xdata

Когда формата упакованных данных очистки недостаточно для описания очистки функции, необходимо создать запись .xdata переменной длины. Адрес этой записи хранится во втором слове записи .pdata. Формат .xdata представляет собой упакованный набор слов переменной длины, состоящий из четырех разделов.

  1. Заголовок из 1 или 2 слов, описывающий общий размер структуры .xdata и предоставляющий ключевые функциональные данные. Второе слово присутствует только в том случае, если для полей Epilogue Count и Code Words задано значение 0. Эти поля описаны в следующей таблице.

    Word Bits Характер использования
    0 0—17 Function Length представляет собой 18-битовое поле, которое указывает общую длину функции в байтах, поделенную на 2. Если размер функции больше 512 КБ, для ее описания следует создать несколько записей .pdata и .xdata. Дополнительные сведения см. в разделе "Большие функции" настоящего документа.
    0 18—19 Vers представляет собой 2-битовое поле, описывающее версию оставшейся записи .xdata. Сейчас определена только версия 0; значения 1—3 зарезервированы.
    0 20 X представляет собой 1-битовое поле, указывающее наличие (1) или отсутствие (0) данных исключения.
    0 21 E представляет собой 1-битовое поле, указывающее, что информация с описанием отдельного эпилога упакована в заголовок (1) и позднее не потребует дополнительных слов (0).
    0 22 F представляет собой 1-битовое поле, которое указывает, что данная запись описывает фрагмент функции (1) или всю функцию целиком (0). Фрагмент означает, что пролога нет и что все прологи должны игнорироваться.
    0 23—27 Epilogue Count представляет собой 5-битовое поле, имеющее два значения в зависимости от состояния бита E:

    — Если E значение равно 0, это поле является числом общих областей эпилога, описанных в разделе 2. Если в функции присутствует более 31 области, для этого поля и поля Code Words следует установить значение 0, чтобы указать на потребность в слове расширения.
    – Если E равен 1, это поле указывает индекс первого кода очистки, описывающего единственный эпилог.
    0 28—31 Code Words представляет собой 4-битовое поле, которое указывает число 32-разрядных слов, требуемое для размещения всех кодов очистки в разделе 4. Если требуется более 15 слов для более чем 63 байт кодов очистки, для этого поля и поля Epilogue Count следует установить значение 0, чтобы указать на потребность в слове расширения.
    1 0-15 Extended Epilogue Count представляет собой 16-битовое поле, которое предоставляет дополнительное место для кодирования необычайно большого числа эпилогов. Слово расширения, содержащее это поле, присутствует только в том случае, если для полей Epilogue Count и Code Words в первом слове заголовка задано значение 0.
    1 16—23 Extended Code Words представляет собой 8-битовое поле, которое предоставляет дополнительное место для кодирования необычайно большого числа слов кодов очистки. Слово расширения, содержащее это поле, присутствует только в том случае, если для полей Epilogue Count и Code Words в первом слове заголовка задано значение 0.
    1 24—31 Зарезервировано
  2. После данных об исключении — если бит E в заголовке был установлен в значение 0 — находится список информации об областях эпилога, которые по одной упакованы в слова и хранятся в порядке увеличения начального смещения. Каждая область содержит следующие поля.

    Bits Характер использования
    0—17 Смещение начала эпилога представляет собой 18-битовое поле, которое описывает смещение эпилога в байтах, поделенное на 2, относительно начала функции.
    18—19 Res представляет собой 2-битовое поле, зарезервированное для расширения в будущем. Оно должно иметь значение 0.
    20—23 Условие представляет собой 4-битовое поле, содержащее условие для выполнения эпилога. Для безусловных эпилогов в нем следует задать значение 0xE, которое означает "всегда". (Эпилог должен быть полностью условным или полностью безусловным, а в режиме Thumb-2 эпилог начинается с первой инструкции после кода операции IT.)
    24—31 Индекс начала эпилога представляет собой 8-битовое поле, которое указывает индекс байта первого кода очистки, описывающего этот эпилог.
  3. После списка областей эпилога идет массив с кодами очистки, которые подробно описаны в разделе "Коды очистки" данной статьи. Этот массив дополняется в конец ближайшей границы полного слова. Байты хранятся с прямым порядком, чтобы их можно было напрямую получать в соответствующем режиме.

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

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

ULONG ComputeXdataSize(PULONG Xdata)
{
    ULONG Size;
    ULONG EpilogueScopes;
    ULONG UnwindWords;

    if ((Xdata[0] >> 23) != 0) {
        Size = 4;
        EpilogueScopes = (Xdata[0] >> 23) & 0x1f;
        UnwindWords = (Xdata[0] >> 28) & 0x0f;
    } else {
        Size = 8;
        EpilogueScopes = Xdata[1] & 0xffff;
        UnwindWords = (Xdata[1] >> 16) & 0xff;
    }

    if (!(Xdata[0] & (1 << 21))) {
        Size += 4 * EpilogueScopes;
    }

    Size += 4 * UnwindWords;

    if (Xdata[0] & (1 << 20)) {
        Size += 4;  // Exception handler RVA
    }

    return Size;
}

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

Коды очистки

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

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

  • Можно вычислить длину пролога и эпилога, сосчитав число кодов очистки. Это возможно даже при инструкциях переменной длины в режиме Thumb-2, так как имеются четкие сопоставления для 16- и 32-разрядных кодов операций.

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

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

В приведенной ниже таблице показано сопоставление кодов очистки с кодами операций. Наиболее распространены коды длиной в один байт, реже попадаются коды из двух, трех и даже четырех байт. Каждый код хранится от старшего байта к младшему. Структура кода очистки отличается от кодировки, описанной в стандартном встроенном двоичном интерфейсе приложения ARM, так как эти коды очистки рассчитаны на наличие однозначного сопоставления с кодами операций в прологе и эпилоге, что делает возможной очистку частично выполненных прологов и эпилогов.

Байт 1 Байт 2 Байт 3 Байт 4 Размер операции Описание
00—7F 16 add sp,sp,#X

where X is (Code & 0x7F) * 4
80—BF 00—FF 32 pop {r0-r12, lr}

Где LR извлекается, если "код & 0x2000" и r0–r12 извлекаются, когда соответствующий бит задан в значение "код & 0x1FFF".
C0—CF 16 mov sp,rX

Где X равен "код & 0x0F"
D0—D7 16 pop {r4-rX,lr}

Где X равен (код & 0x03) + 4, а LR извлекается, если "код & 0x04".
D8—DF 32 pop {r4-rX,lr}

Где X равен (код & 0x03) + 8, а LR извлекается, если "код & 0x04".
E0—E7 32 vpop {d8-dX}

Где X равен (код & 0x07) + 8
E8—EB 00—FF 32 addw sp,sp,#X

where X is (Code & 0x03FF) * 4
EC—ED 00—FF 16 pop {r0-r7,lr}

Где LR извлекается, если "код & 0x0100" и r0–r7 извлекаются, когда соответствующий бит задан в значение "код & 0x00FF"
EE 00—0F 16 Только для систем Майкрософт
EE 10—FF 16 На месте
EF 00—0F 32 ldr lr,[sp],#X

where X is (Code &0x000F) * 4
EF 10—FF 32 На месте
F0—F4 - На месте
F5 00—FF 32 vpop {dS-dE}

где S ( Code & 0x00F0) >> 4 и E — Code &0x000F
F6 00—FF 32 vpop {dS-dE}

where S is (Code & 0x00F0) 4) >> + 16 и E is (Code & 0x000F) + 16
F7 00—FF 00—FF 16 add sp,sp,#X

where X is (Code & 0x00FFFF) * 4
F8 00—FF 00—FF 00—FF 16 add sp,sp,#X

where X is (Code & 0x00FFFFFF) * 4
F9 00—FF 00—FF 32 add sp,sp,#X

where X is (Code & 0x00FFFF) * 4
Fa 00—FF 00—FF 00—FF 32 add sp,sp,#X

where X is (Code & 0x00FFFFFF) * 4
FB 16 nop (16-разрядный)
FC 32 nop (32-разрядный)
FD 16 end + 16-разрядный nop в эпилоге
FE 32 end + 32-разрядный nop в эпилоге
FF - end

Здесь показан диапазон шестнадцатеричных значений для каждого байта в коде очистки Code, а также размер кода операции Opsize и соответствующая интерпретация исходных инструкций. Пустые ячейки обозначают более короткие коды очистки. В инструкциях с большими значениями, охватывающими несколько байт, старшие биты сохраняются первыми. Поле Opsize показывает неявный размер кода операции, связанный с каждой операцией Thumb-2. Видимые повторяющиеся записи в таблице с разными кодировками помогают различить разные размеры кодов операций.

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

Коды очистки 0xFD и 0xFE эквивалентны обычному коду end 0xFF, однако имеют один дополнительный код операции nop в случае эпилога — 16- или 32-разрядный. Для прологов коды 0xFD, 0xFE и 0xFF полностью эквивалентны друг другу. Это дает нам общие окончания эпилога bx lr или b <tailcall-target>, для чего эквивалентная инструкция пролога отсутствует. Это повышает вероятность того, что последовательности очистки можно будет совместно использовать в прологе и эпилогах.

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

Очистка частичных прологов и эпилогов

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

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

Например, рассмотрим данную последовательность пролога и эпилога:

0000:   push  {r0-r3}         ; 0x04
0002:   push  {r4-r9, lr}     ; 0xdd
0006:   mov   r7, sp          ; 0xc7
...
0140:   mov   sp, r7          ; 0xc7
0142:   pop   {r4-r9, lr}     ; 0xdd
0146:   add   sp, sp, #16     ; 0x04
0148:   bx    lr

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

0xc7, 0xdd, 0x04, 0xfd

Код 0xFD предназначен специально для конца последовательности, то есть эпилог на одну 16-разрядную инструкцию длиннее пролога. Это открывает широкие возможности для совместного использования кодов очистки.

Если в данном примере исключение возникает, пока выполняется тело функции между прологом и эпилогом, очистка начинается с эпилога с нулевым смещением в коде эпилога. В примере это соответствует смещению 0x140. Средство очистки выполняет полную последовательность освобождения, так как очистка не выполнялась. Если же исключение возникает на одну инструкцию после начала кода эпилога, средство очистки может успешно выполнить очистку, пропустив первый код очистки. Учитывая однозначное сопоставление между кодами операций и кодами очистки, при выполнении очистки из инструкции n эпилога будут пропущены первые n кодов очистки.

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

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

  1. Если очистка выполняется из тела функции, начинайте выполнять коды очистки с индекса 0 и продолжайте до достижения конечного кода операции.

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

  3. Если очистка осуществляется из пролога, начните с индекса 0 в кодах очистки. Вычислите длину кода пролога из последовательности, а затем рассчитайте, на сколько байт PC отстоит от конца пролога. Пропустите такое число кодов очистки, которое соответствует невыполненным инструкциям. Выполните последовательность очистки, начиная с этой точки.

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

Фрагменты функции

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

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

  • Только пролог; все эпилоги в других фрагментах.

  • Пролог и один или несколько эпилогов; более эпилоги в других фрагментах.

  • Без пролога и эпилогов; пролог и один или несколько эпилогов в других фрагментах.

  • Только эпилоги; пролог и, возможно, более эпилоги в других фрагментах.

В первом сценарии описать требуется только пролог. Это можно сделать в краткой форме .pdata, описав пролог обычным образом и задав для Ret значение 3, указывающее на отсутствие эпилога. В полной форме .xdata это можно сделать, предоставив коды очистки пролога как обычно с индексом 0 и указав число эпилогов равным 0.

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

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

В третьем и четвертом сценарии наличие псевдопролога указывается посредством задания значения 2 для Flag краткой записи .pdata либо значения 1 для флага F в заголовке .xdata. В любом случае проверка потребности в частичной очистке пролога игнорируется, и все очистки, не относящиеся к эпилогу, считаются полными.

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

Фрагменты можно использовать для описания функций, размер которых превышает предел в 512 КБ, накладываемый битовыми полями в заголовке .xdata. Чтобы описать большую функцию, просто разорвать ее на фрагменты меньше 512 КБ. Каждый фрагмент должен быть настроен так, чтобы он не разделял эпилог на несколько частей.

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

Если фрагмент не содержит ни пролог, ни эпилог, ему все равно требуется отдельная запись .pdata и, возможно, запись .xdata, чтобы описать очистку из тела функции.

Упаковка со сжатием

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

ShrinkWrappedFunction
    push   {r4, lr}          ; A: save minimal non-volatiles
    sub    sp, sp, #0x100    ; A: allocate all stack space up front
    ...                      ; A:
    add    r0, sp, #0xE4     ; A: prepare to do the inner save
    stm    r0, {r5-r11}      ; A: save remaining non-volatiles
    ...                      ; B:
    add    r0, sp, #0xE4     ; B: prepare to do the inner restore
    ldm    r0, {r5-r11}      ; B: restore remaining non-volatiles
    ...                      ; C:
    pop    {r4, pc}          ; C:

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

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

Средний B регион получает собственную .pdata или .xdata запись, описывающую фрагмент, который не имеет пролога и без эпилога. Однако коды очистки для этого региона все равно должны присутствовать, так как он считается телом функции. Коды должны описать составной пролог, представляющий как исходные регистры, сохраненные в прологе региона A , так и дополнительные регистры, сохраненные перед вводом региона B, как если бы они были созданы одной последовательностью операций.

Регистр сохраняется для региона не может рассматриваться как "внутренний пролог", так как составной пролог, описанный для региона B , должен описать как пролог региона B A , так и дополнительные регистры, сохраненные. Если фрагмент B имел пролог, коды очистки также подразумевают размер этого пролога, и нет способа описать составной пролог таким образом, чтобы сопоставить один к одному с opcodes, которые сохраняют только дополнительные регистры.

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

Последний C регион получает свой собственный .pdata или .xdata запись, описывая фрагмент, который не имеет пролога, но имеет эпилог.

Альтернативный подход также может работать, если обработка стека выполняется перед вводом региона B , может быть сокращена до одной инструкции:

ShrinkWrappedFunction
    push   {r4, lr}          ; A: save minimal non-volatile registers
    sub    sp, sp, #0xE0     ; A: allocate minimal stack space up front
    ...                      ; A:
    push   {r4-r9}           ; A: save remaining non-volatiles
    ...                      ; B:
    pop    {r4-r9}           ; B: restore remaining non-volatiles
    ...                      ; C:
    pop    {r4, pc}          ; C: restore non-volatile registers

Ключевое представление заключается в том, что на каждой границе инструкции стек полностью согласуется с кодами очистки для региона. Если перед внутренней отправкой в этом примере происходит очистка, это считается частью региона A. Только пролог региона A распрогружается. Если после внутренней отжимки происходит очистка, она считается частью региона B, которая не имеет пролога. Однако в нем есть коды очистки, описывающие как внутреннюю отправку, так и исходный пролог из региона A. Аналогичная логика содержит внутреннее всплывающее окно.

Оптимизации кодирования

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

Самая важная идея оптимизации: не путайте границы пролога и эпилога для очистки целей с логическим прологом и эпилогом границ с точки зрения компилятора. Для повышения эффективности границы очистки можно сжать, сделав их более тесными. Например, пролог может содержать код после установки стека для проверки проверки. Но после завершения всех операций стека нет необходимости кодировать дальнейшие операции, и все, что может быть удалено из пролога очистки.

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

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

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

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

Следует активно использовать коды очистки повторно. Индекс каждой области эпилога указывает точки произвольной начальной точки в массиве кодов очистки. Он не должен указывать на начало предыдущей последовательности; он может указать в середине. Лучший подход — создать последовательность кода очистки. Затем проверьте точное совпадение байтов в уже закодированном пуле последовательностей. Используйте любое найденное совпадение в качестве начальной точки для повторного использования.

Если после пропуска эпилогов с одной инструкцией эпилогов не остается, рекомендуется использовать краткую форму .pdata; она становится еще актуальнее при полном отсутствии эпилогов.

Примеры

В этих примерах база образа находится в 0x00400000.

Пример 1: конечная функция, без локальных элементов

Prologue:
  004535F8: B430      push        {r4-r5}
Epilogue:
  00453656: BC30      pop         {r4-r5}
  00453658: 4770      bx          lr

.pdata (исправлено, 2 слова):

  • Слово 0

    • Function Start RVA = 0x000535F8 (= 0x004535F8–0x00400000).
  • Слово 1

    • Flag = 1, указывает канонические форматы пролога и эпилога.

    • Function Length = 0x31 (= 0x62/2).

    • Ret = 1, указывает на возвращение посредством 16-разрядного ветвления.

    • H = 0, указывает, что параметры не помещены в начальное состояние.

    • R = 0 и Reg = 1, указывающее push/pop r4-r5

    • L = 0, указывает на отсутствие сохранения / восстановления LR.

    • C = 0, указывает на отсутствие цепочки кадров.

    • Stack Adjust = 0, указывает на отсутствие подстройки стека.

Пример 2: вложенная функция с локальным выделением

Prologue:
  004533AC: B5F0      push        {r4-r7, lr}
  004533AE: B083      sub         sp, sp, #0xC
Epilogue:
  00453412: B003      add         sp, sp, #0xC
  00453414: BDF0      pop         {r4-r7, pc}

.pdata (исправлено, 2 слова):

  • Слово 0

    • Function Start RVA = 0x000533AC (= 0x004533AC–0x00400000).
  • Слово 1

    • Flag = 1, указывает канонические форматы пролога и эпилога.

    • Function Length = 0x35 (= 0x6A/2).

    • Ret = 0, указывает на возвращение посредством извлечения {pc}.

    • H = 0, указывает, что параметры не помещены в начальное состояние.

    • R = 0 и Reg = 3, указывающее push/pop r4-r7

    • L = 1, указывает на выполнение сохранения / восстановления LR.

    • C = 0, указывает на отсутствие цепочки кадров.

    • Stack Adjust = 3 (= 0x0C/4).

Пример 3: вложенная функция с переменным числом аргументов

Prologue:
  00453988: B40F      push        {r0-r3}
  0045398A: B570      push        {r4-r6, lr}
Epilogue:
  004539D4: E8BD 4070 pop         {r4-r6}
  004539D8: F85D FB14 ldr         pc, [sp], #0x14

.pdata (исправлено, 2 слова):

  • Слово 0

    • Function Start RVA = 0x00053988 (= 0x00453988–0x00400000).
  • Слово 1

    • Flag = 1, указывает канонические форматы пролога и эпилога.

    • Function Length = 0x2A (= 0x54/2).

    • Ret = 0, указывает на возвращение посредством извлечения {pc} (в данном случае это возвращение ldr pc,[sp],#0x14).

    • H = 1, указывает, что параметры были помещены в начальное состояние.

    • R = 0 и Reg = 2, указывающее push/pop r4-r6

    • L = 1, указывает на выполнение сохранения / восстановления LR.

    • C = 0, указывает на отсутствие цепочки кадров.

    • Stack Adjust = 0, указывает на отсутствие подстройки стека.

Пример 4: функция с несколькими эпилогами

Prologue:
  004592F4: E92D 47F0 stmdb       sp!, {r4-r10, lr}
  004592F8: B086      sub         sp, sp, #0x18
Epilogues:
  00459316: B006      add         sp, sp, #0x18
  00459318: E8BD 87F0 ldm         sp!, {r4-r10, pc}
  ...
  0045943E: B006      add         sp, sp, #0x18
  00459440: E8BD 87F0 ldm         sp!, {r4-r10, pc}
  ...
  004595D4: B006      add         sp, sp, #0x18
  004595D6: E8BD 87F0 ldm         sp!, {r4-r10, pc}
  ...
  00459606: B006      add         sp, sp, #0x18
  00459608: E8BD 87F0 ldm         sp!, {r4-r10, pc}
  ...
  00459636: F028 FF0F bl          KeBugCheckEx     ; end of function

.pdata (исправлено, 2 слова):

  • Слово 0

    • Function Start RVA = 0x000592F4 (= 0x004592F4–0x00400000).
  • Слово 1

    • Flag = 0, указывает на наличие записи .xdata (которая нужна для нескольких эпилогов).

    • Адрес .xdata — 0x00400000

.xdata (переменная, 6 слов):

  • Слово 0

    • Function Length = 0x0001A3 (= 0x000346/2).

    • Vers = 0, указывающее первую версию.xdata

    • X = 0, указывающее отсутствие данных об исключении

    • E = 0, указывает на список областей эпилогов.

    • F = 0, указывающее полное описание функции, включая пролог

    • Epilogue Count = 0x04, указывая 4 общих эпилога областей

    • Code Words = 0x01, указывающее одно 32-разрядное слово кодов очистки

  • Слова 1—4, описывающие 4 области эпилогов в 4 расположениях. Каждая область имеет общий набор кодов очистки, который используется совместно с прологом, имеет смещение 0x00 и является безусловным — задано условие 0x0E ("всегда").

  • Коды очистки, начиная со слова 5: (используются совместно прологом и эпилогом).

    • Код очистки 0 = 0x06: sp += (6 << 2)

    • Код очистки 1 = 0xDE: pop {r4-r10, lr}.

    • Код очистки 2 = 0xFF: end.

Пример 5: функция с динамическим стеком и внутренним эпилогом

Prologue:
  00485A20: B40F      push        {r0-r3}
  00485A22: E92D 41F0 stmdb       sp!, {r4-r8, lr}
  00485A26: 466E      mov         r6, sp
  00485A28: 0934      lsrs        r4, r6, #4
  00485A2A: 0124      lsls        r4, r4, #4
  00485A2C: 46A5      mov         sp, r4
  00485A2E: F2AD 2D90 subw        sp, sp, #0x290
Epilogue:
  00485BAC: 46B5      mov         sp, r6
  00485BAE: E8BD 41F0 ldm         sp!, {r4-r8, lr}
  00485BB2: B004      add         sp, sp, #0x10
  00485BB4: 4770      bx          lr
  ...
  00485E2A: F7FF BE7D b           #0x485B28    ; end of function

.pdata (исправлено, 2 слова):

  • Слово 0

    • Function Start RVA = 0x00085A20 (= 0x00485A20–0x00400000).
  • Слово 1

    • Flag = 0, указывает на наличие записи .xdata (которая нужна для нескольких эпилогов).

    • Адрес .xdata — 0x00400000

.xdata (переменная, 3 слова):

  • Слово 0

    • Function Length = 0x0001A3 (= 0x000346/2).

    • Vers = 0, указывающее первую версию.xdata

    • X = 0, указывающее отсутствие данных об исключении

    • E = 0, указывает на список областей эпилогов.

    • F = 0, указывающее полное описание функции, включая пролог

    • Epilogue Count = 0x001, указывая 1 общую область эпилога

    • Code Words = 0x01, указывающее одно 32-разрядное слово кодов очистки

  • Слово 1: область эпилога со смещением 0xC6 (= 0x18C/2), начальным индексом кода очистки 0x00 и условием 0x0E ("всегда")

  • Коды очистки, начиная со слова 2: (используются совместно прологом и эпилогом).

    • Код очистки 0 = 0xC6: sp = r6.

    • Код очистки 1 = 0xDC: pop {r4-r8, lr}.

    • Код очистки 2 = 0x04: sp += (4 << 2)

    • Код очистки 3 = 0xFD: end, засчитывается как 16-разрядная инструкция для эпилога.

Пример 6: функция с обработчиком исключений

Prologue:
  00488C1C: 0059 A7ED dc.w  0x0059A7ED
  00488C20: 005A 8ED0 dc.w  0x005A8ED0
FunctionStart:
  00488C24: B590      push        {r4, r7, lr}
  00488C26: B085      sub         sp, sp, #0x14
  00488C28: 466F      mov         r7, sp
Epilogue:
  00488C6C: 46BD      mov         sp, r7
  00488C6E: B005      add         sp, sp, #0x14
  00488C70: BD90      pop         {r4, r7, pc}

.pdata (исправлено, 2 слова):

  • Слово 0

    • Function Start RVA = 0x00088C24 (= 0x00488C24–0x00400000).
  • Слово 1

    • Flag = 0, указывает на наличие записи .xdata (которая нужна для нескольких эпилогов).

    • Адрес .xdata — 0x00400000

.xdata (переменная, 5 слов):

  • Слово 0

    • Function Length =0x000027 (= 0x00004E/2).

    • Vers = 0, указывающее первую версию.xdata

    • X = 1, указывающее наличие данных исключений

    • E = 1, указывает на отдельный эпилог.

    • F = 0, указывающее полное описание функции, включая пролог

    • Epilogue Count = 0x00, указывающее, что коды очистки эпилога начинаются с смещения 0x00

    • Code Words = 0x02, указывая два 32-разрядных слова кодов очистки

  • Коды очистки, начиная со слова 1.

    • Код очистки 0 = 0xC7: sp = r7.

    • Код очистки 1 = 0x05: sp += (5 << 2)

    • Код очистки 2 = 0xED/0x90: pop {r4, r7, lr}.

    • Код очистки 4 = 0xFF: end.

  • Слово 3 указывает обработчик исключений = 0x0019A7ED (= 0x0059A7ED – 0x00400000)

  • Слова с 4 и выше являются встроенными данными об исключении.

Пример 7: Funclet

Function:
  00488C72: B500      push        {lr}
  00488C74: B081      sub         sp, sp, #4
  00488C76: 3F20      subs        r7, #0x20
  00488C78: F117 0308 adds        r3, r7, #8
  00488C7C: 1D3A      adds        r2, r7, #4
  00488C7E: 1C39      adds        r1, r7, #0
  00488C80: F7FF FFAC bl          target
  00488C84: B001      add         sp, sp, #4
  00488C86: BD00      pop         {pc}

.pdata (исправлено, 2 слова):

  • Слово 0

    • Function Start RVA = 0x00088C72 (= 0x00488C72–0x00400000).
  • Слово 1

    • Flag = 1, указывает канонические форматы пролога и эпилога.

    • Function Length = 0x0B (= 0x16/2).

    • Ret = 0, указывает на возвращение посредством извлечения {pc}.

    • H = 0, указывает, что параметры не помещены в начальное состояние.

    • R = 0 и Reg = 7, указывая, что регистры не были сохранены или восстановлены

    • L = 1, указывает на выполнение сохранения / восстановления LR.

    • C = 0, указывает на отсутствие цепочки кадров.

    • Stack Adjust = 1, указывает на подстройку стека 1 × 4 байта.

См. также

Обзор соглашений ABI ARM
Общие вопросы использования Visual C++ ARM