Организация вызова x86-процедур из EFI Byte Code

в 11:55, , рубрики: Без рубрики

Использование технологии EFI Byte Code (сокращенно – EBC) позволяет создавать кроссплатформенные приложения и драйверы, выполняемые на виртуальном процессоре, реализованном в составе firmware платформы. Архитектура этого процессора определена в спецификации Unified Extensible Firmware Interface. В идеале, EBC-программа должна взаимодействовать исключительно с ресурсами виртуальной машины, системными таблицами UEFI и другими объектами, абстрагированными от аппаратной реализации платформы. На практике, точное следование данному принципу существенно ограничивает функциональность программного продукта. Выйти из положения не потеряв кроссплатформенности можно, если использовать подпрограммы в нативном коде центрального процессора, условно получающие управление в случае детектирования заданной аппаратной платформы.

Постановка задачи

Рассмотрим пример чтения заданного регистра MSR (Model Specific Register) из EBC-приложения. Как известно, в системе команд x86 предусмотрена инструкция RDMSR (Read MSR), получающая в качестве входного параметра 32-битный адрес MSR в регистре ECX, и возвращающая 64-битное содержимое MSR в регистрах EDX (старшие 32 бита) и EAX (младшие 32 бита). В системе команд виртуальной машины EBC аналогичной функциональности не предусмотрено, именно поэтому требуется вызов подпрограммы в нативном коде.

Отметим, что в информационно-диагностической утилите UEFImark x64 Edition инструкция RDMSR используется непосредственно, а в UEFImark EBC Edition для этого требуется вызов нативных подпрограмм из EBC-программы.

Условия эксперимента

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

Для трансляции примеров используется FASM 1.69.50. Инструкции EBC реализованы с помощью макросов, x86 код транслируется для 64-битного режима, особенности обеспечения его совместимости с 32-битным режимом рассмотрены в пункте 4.

Вызывающая процедура EBC_Read_MSR

Рис. 1. EBC-процедура, вызывающая x86-процедуру чтения MSR

Рассмотрим последовательность операций, выполняемых вызывающей EBC-процедурой.

  1. Резервирование в стеке 64-битной переменной, в которую вызываемая процедура запишет содержимое заданного регистра MSR.
  2. Запись в стек второго входного параметра для вызываемой процедуры – адреса для сохранения содержимого MSR. Это адрес переменной, созданной на шаге 1.
  3. Запись в стек первого входного параметра для вызываемой процедуры – адреса регистра MSR, принятого подпрограммой в регистре R6.
  4. Определение натуральной разрядности с помощью EBC-инструкции MOVINW. Если ранее было установлено, что платформа x86-совместимая, то значение натуральной разрядности 4 означает IA32 (4 байта = 32 бита), 8 означает x64 (8 байт = 64 бита).
  5. Выбор адреса для точки входа в вызываемую подпрограмму (в соответствии с результатами шага 4 и размещение его в регистре R7.
  6. Вызов подпрограммы по адресу, полученному на шаге 5.
  7. Чтение и удаление из стека ранее записанных параметров, в регистр R3 читается значение переменной, созданной на шаге 1, в которую вызываемая подпрограмма записала результат – 64-битное содержимое MSR.

Разрядность параметров, которые записываются в стек инструкциями PUSHN (Push Natural) и считываются из стека инструкциями POPN (Pop Natural) равна 32 бита для IA32 UEFI и 64-бита для x64 UEFI.

Вызываемые процедуры: IA32_Read_MSR, x64_Read_MSR

Рис. 2. Вызываемая x86-процедура чтения MSR

Рассмотрим последовательность операций, выполняемых вызываемой x86-процедурой.

  1. Точка входа для IA32 (метка IA32_Read_MSR). Сохранение в стеке регистров EBX, ECX, EDX. Из стекового фрейма, созданного вызывающей процедурой, читаются входные параметры: адрес MSR и адрес переменной для сохранения содержимого MSR. Переход к пункту 3.
  2. Точка входа для x64 (метка x64_Read_MSR). Сохранение в стеке регистров RBX, RCX, RDX. В регистрах RCX и RDX принимаются соответственно первый и второй входной параметры.
  3. Выполнение целевой операции – чтение MSR с помощью инструкции RDMSR.
  4. Сохранение прочитанного содержимого MSR по адресу, заданному вторым входным параметром.
  5. Восстановление регистров EDX, ECX, EBX (для IA32) или RDX,RCX, RBX (для x64) и возврат в вызвавшую процедуру.

Посмотрев на процедуру, можно найти противоречие: ряд инструкций 64-битного режима используются для 32-битной ветви выполнения, например, инструкции PUSH и POP, оперирующие с 64-битными регистрами. Как это работает? Дело в том, что 32-битные и 64-битные формы этих инструкций кодируются одинаково, а их интерпретация зависит от режима работы процессора. Так, код 53h в 32-битном режиме соответствует инструкции PUSH EBX, а в 64-битном режиме – инструкции PUSH RBX.

Рассмотрим механизмы передачи входных и выходных параметров подпрограмм.

Для IA32 EFI входные параметры вызываемой подпрограммы передаются через стек. В рассматриваемой процедуре первый параметр находится по адресу [ESP+16]. Смещение 16 складывается из двух слагаемых: 4 байта стека используются для хранения счетчика команд EIP, что необходимо при возврате из подпрограммы, 12 байт для регистров EBX, ECX, EDX, записанных в стек инструкцией PUSH. 4+12=16.

Для x64 UEFI четыре первых входных параметра вызываемой подпрограммы передаются через регистры RCX, RDX, R8, R9, последующие – через стек. В нашем примере используются только два параметра, передаваемые в RCX и RDX.

Для IA32 UEFI, содержимое 32-битного x86-регистра EAX после возврата из x86-подпрограммы, находится в 32 младших битах 64-битного EBC-регистра R7. Содержимое старших 32 бит R7 не определено. Для x64 UEFI, содержимое 64-битного x86-регистра RAX после возврата из x86-подпрограммы, находится в EBC-регистре R7. Данная функциональность удобна для передачи статусных кодов, в рассмотренном примере не используется.

Описанная технология применяется не только для вызова процедур, входящих в состав приложения, но и при обращении к UEFI API, обработка которых реализована в firmware. Например, при использовании функций CPU Architectural Protocol и File I/O Protocol.

Резюме

Применять описанный метод следует только тогда, когда требуется обеспечить функциональность, недостижимую в рамках EFI Byte Code. Так, в информационно-диагностической утилитой UEFImark EBC Edition для отображения модели процессора и списка поддерживаемых технологий используется инструкция CPUID в нативном коде.

Важно отметить, что любой непосредственный доступ к аппаратным ресурсам усложняет обеспечение кроссплатформенности. В частности, несмотря на то, что в выше приведенных примерах есть возможность различать x86-платформы IA32 и x64, до передачи управления приложение должно убедиться, что работает именно на платформе x86. Выполнение на ARM или Itanium приведет к непредсказуемым последствиям из-за различий в системе команд центрального процессора.

Автор: icbook

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js