Pull to refresh

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

Reading time 6 min
Views 7.2K
Использование технологии 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-битным режимом рассмотрены ниже.

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

;--- Subroutine: EBC/x86 gate for Read MSR ----------------------------------;
; Caller must verify x86 support (IA32 or x64) before call this subroutine,  ;
; but this subroutine differentiate IA32/x64 internally.                     ;
;                                                                            ;
; INPUT:  R1 = Global variables pool base address                            ;
;         R6 = MSR index (same as ECX before RDMSR instruction)              ;
; OUTPUT: R3 = MSR data after Read (same as EDX:EAX after RDMSR instruction) ;
;         R4-R7 can be changed                                               ;
;----------------------------------------------------------------------------;
EBC_Read_MSR:
		XOR64		R7,R7			; R7=0
		PUSH64		R7			; Storage for output
		MOVQ		R7,R0			; Address of storage = stack pointer
		PUSHN		R7			; Parameter#2 = Output address
		PUSHN		R6			; Parameter#1 = MSR address
		MOVINW		R7,1,0
		CMPI32WEQ	R7,4			; R7=4 for 32-bit, R7=8 for 64-bit
		MOVIQW		R7,_IA32_Read_MSR	; This pointer for IA32 (native width=4)
		JMP8CS		Native_Gate
		MOVIQW		R7,_x64_Read_MSR	; This pointer for x64 (native width=8)
Native_Gate:	ADD64		R7,R1			; Add base address = R1
		CALL32EXA	R7
		POPN		R6			; Remove Parameter#1
		POPN		R7			; Remove Parameter#2
		POP64		R3			; Read R3 = Output
		RET

Рис. 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

;--- Read Model-Specific Register, selected by input index --------------;
; INPUT:   Parm#1 = MSR address (ECX before RDMSR), natural width 32/64  ;
;          Parm#2 = Address for write output data, natural width 32/64   ;
; OUTPUT:  R7 = Reserved for UEFI status                                 ;
;          QWORD at Address [Parm#2] = MSR data (EDX:EAX after RDMSR)    ; 
;------------------------------------------------------------------------;
IA32_Read_MSR:				; Entry point for IA32
		push	rbx rcx rdx
		mov	ecx,[rsp+16]	; ECX = Parm#1 = MSR address, assembled same as [esp+16], can use for IA32
		mov	ebx,[rsp+20]	; EBX = Parm#2 = Output address, assembled same as [esp+20], can use for IA32
		jmp	Entry_R_MSR
x64_Read_MSR:				; Entry point for x64
		push	rbx rcx rdx
		mov	rbx,rdx 	; RBX=Output address (p#2), RCX=MSR address (p#1)
Entry_R_MSR:
		rdmsr			; RCX=Input, EDX:EAX=Output
		mov	[rbx+00],eax
		mov	[rbx+04],edx
		pop	rdx rcx rbx
		ret

Рис. 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 приведет к непредсказуемым последствиям из-за различий в системе команд центрального процессора.
Tags:
Hubs:
+23
Comments 13
Comments Comments 13

Articles