The use of EFI Byte Code technology (abbreviated to EBC) allows you to create
cross-platform applications and drivers running on a virtual processor implemented as part of the platform firmware. The architecture of this processor is defined in the Unified Extensible Firmware Interface specification. Ideally, an EBC program should interact exclusively with virtual machine resources, UEFI system tables and other objects, abstracted from the hardware implementation of the platform. In practice, the exact adherence to this principle significantly limits the functionality of the software product. You can get out of the situation without losing cross-platformity if you use subroutines in the native code of the central processor, conditionally receiving control in the case of detection of a given hardware platform.
Formulation of the problem
Consider an example of reading a given MSR (Model Specific Register) register from an EBC application. As is well known, the x86 instruction system contains the RDMSR (Read MSR) instruction, which receives the 32-bit MSR address in the ECX register as an input parameter, and returns the 64-bit MSR content in the EDX registers (upper 32 bits) and EAX (lower 32 bits) ). In the command system of the virtual machine EBC, similar functionality is not provided, which is why a subroutine call in native code is required.
Note that in the information-diagnostic utility
UEFImark x64 Edition the RDMSR instruction is used directly, and in the
UEFImark EBC Edition this requires calling the native subroutines from the EBC program.
')
Experimental Conditions
It is assumed that prior to transferring control to the procedure in question, the EBC program detected the x86 platform and found that one of the IA32 or x64 architectures is supported. The method of detection is beyond the scope of this article, it is planned to consider in subsequent publications.
FASM 1.69.50 is used to translate examples. EBC instructions are implemented using macros, x86 code is translated for 64-bit mode, the features of ensuring its compatibility with 32-bit mode are discussed below.
Calling procedure 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
Fig. 1 .
EBC procedure that calls the x86 MSR reading procedureConsider the sequence of operations performed by the calling EBC procedure.
- A reservation on the stack of a 64-bit variable in which the called procedure will write the contents of the specified MSR register.
- The call to the stack of the second input parameter for the called procedure is the address for saving the contents of the MSR. This is the address of the variable created in step 1.
- The stack entry of the first input parameter for the called procedure is the address of the MSR register accepted by the subroutine in register R6.
- Determining the natural bit depth using the EBC instruction MOVINW. If it was previously established that the platform is x86-compatible, then the value of the natural digit capacity of 4 means IA32 (4 bytes = 32 bits), 8 means x64 (8 bytes = 64 bits).
- Selecting the address for the entry point to the called subroutine (in accordance with the results of step 4 and placing it in register R7.
- Call the subroutine at the address obtained in step 5.
- Reading and deleting previously written parameters from the stack, the value of the variable created in step 1 in which the called subroutine wrote the result — the 64-bit content of the MSR — is read into the R3 register.
The bit width of the parameters that are pushed onto the stack by PUSHN instructions (Push Natural) and read from the stack by POPN instructions (Pop Natural) is 32 bits for IA32 UEFI and 64 bits for x64 UEFI.
Called procedures: 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
Fig. 2 Called x86 MSR Read ProcedureConsider the sequence of operations performed by the called x86 procedure.
- Entry point for IA32 (label IA32_Read_MSR). Preservation in a stack of registers EBX, ECX, EDX. From the stack frame created by the calling procedure, the input parameters are read: the address of the MSR and the address of the variable to store the contents of the MSR. Go to step 3.
- Entry point for x64 (x64_Read_MSR tag). Saving to the stack of registers RBX, RCX, RDX. In registers RCX and RDX, the first and second input parameters are accepted respectively.
- Performing the target operation is reading the MSR using the RDMSR instruction.
- Save read MSR content to the address specified by the second input parameter.
- Recovery registers EDX, ECX, EBX (for IA32) or RDX, RCX, RBX (for x64) and return to the calling procedure.
Looking at the procedure, you can find a contradiction: a number of 64-bit mode instructions are used for the 32-bit branch of execution, for example, the PUSH and POP instructions operating with 64-bit registers. How it works? The fact is that the 32-bit and 64-bit forms of these instructions are encoded in the same way, and their interpretation depends on the mode of operation of the processor. Thus, code 53h in the 32-bit mode corresponds to the PUSH EBX instruction, and in the 64-bit mode - the PUSH RBX instruction.
Consider the mechanisms for transferring input and output parameters of subroutines.
For IA32 EFI, the input parameters of the called subroutine are passed through the stack. In the procedure in question, the first parameter is located at [ESP + 16]. The offset 16 is made up of two terms: 4 bytes of the stack are used to store the EIP command counter, which is necessary when returning from the subroutine, 12 bytes for the EBX, ECX, EDX registers written to the stack by the PUSH instruction. 4 + 12 = 16.
For x64 UEFI, the four first input parameters of the called subroutine are passed through the registers RCX, RDX, R8, R9, and the subsequent ones through the stack. In our example, only two parameters are used, transmitted in RCX and RDX.
For IA32 UEFI, the contents of the 32-bit x86-register EAX after returning from the x86-subroutine is in the 32 low bits of the 64-bit EBC-register R7. The contents of the higher 32 bits of R7 are undefined. For x64 UEFI, the contents of the 64-bit x86-register RAX after returning from the x86-subroutine is in the EBC-register R7. This functionality is convenient for the transfer of status codes, in the considered example is not used.
The described technology is used not only to call the procedures included in the application, but also when accessing the UEFI API, the processing of which is implemented in the firmware. For example, when using the
CPU Architectural Protocol and
File I / O Protocol functions.
Summary
The described method should be used only when it is required to provide functionality that is unattainable within the framework of the
EFI Byte Code . Thus, in the information-diagnostic utility
UEFImark EBC Edition , the CPUID instruction in the native code is used to display the processor model and the list of supported technologies.
It is important to note that any direct access to hardware resources makes it difficult to ensure cross-platform. In particular, despite the fact that in the examples above, it is possible to distinguish between the IA86 and x64 x86 platforms, the application must make sure that it works on the x86 platform before transferring control. Running on ARM or Itanium will lead to unpredictable consequences due to differences in the system commands of the CPU.