Tim Lewis, Chief BIOS Architect; Cassie Liu, Senior Software Engineer
System Management Interrupts (SMIs) are the highest priority interrupts found on x86 processors. Software SMIs are SMIs generated in response to a software instruction, usually a write to an I/O port such as 0xb2 (the SMI Command Port). The BIOS uses these software SMIs to provide services during late POST and then in runtime.
Software writes a value (the SMI Command Value) to the SMI Command Port. Typically the south bridge detects the write to the SMI Command Port and asserts the SMI# pin or sends an SMI message. Each CPU core detects the SMI after the current instruction has been completed. Then, the CPU saves most of the CPU registers in a buffer, switches to System Management Mode (SMM, a variant of big-real mode) and jumps to a pre-defined entry point.
Once inside of SMM, an SMM driver for the south bridge detects the source of the SMI (in this case, the software SMI), and then calls the routine which was registered for the SMI Command Value. After the routine returns, the SMM driver clears the SMI status and performs a “resume” instruction. This instruction forces the CPU to reload the CPU registers from the buffer and return to the instruction after the one which generated the software SMI.
In fact, while it appears that the SMI services are invoked immediately after writing to the I/O port and before the next CPU instruction, in some systems, the delay in propagation of the interrupt to the CPU cores may allow the cores to execute further instructions before the SMI is actually detected. This happens because the detection of the I/O write is usually the function of another chip in the system (such as a south bridge) and this chip must then signal the CPU using the SMI# signal or using bus messages.
Also, in multi-core systems, although the SMI is propagated to all cores, they enter at slightly different times. This discrepancy occurs because SMIs are serviced in between execution of CPU instructions. Since CPU instructions require a different number of clock cycles, some will enter SMM sooner than others. Phoenix code waits for all cores to enter SMM and then, using a semaphore, allows exactly one core to enter the SMI processing loop.
When writing drivers which use software SMIs, there are three primary tasks:
1. How To Register For Software SMI Notification
2. How To Handle The Software SMI Notification
3. How To Generate Software SMIs
STEP 1: How To Register For Notification
The first task is to write an SMM driver which registers for notification when a certain SMI Command Value is written to the SMI Command Port.
Here is the typical entry point code which registers for a software SMI value.
1. SMM drivers are similar to DXE drivers at the entry point, except they are launched twice. At the first launch, they act as DXE drivers. During the first launch, they call the EFI_SMM_BASE_PROTOCOL’s Register() function to launch the driver a second time in System Management RAM (SMRAM).
2. The SMM library performs all of the steps required by the SMM CIS to re-launch this driver in System Management RAM (SMRAM). On return, the InSmm flag has been set to TRUE if this is the copy of the driver running in SMRAM or FALSE if this is the normal DXE copy.
3. If this driver is running in SMRAM, then perform the following steps…
4. Locate the PHOENIX_SMM_SW_SMI_PROTOCOL. This protocol is a Phoenix service which allocates unique SMI Command Values.
5. The AllocateSwSmi() function returns the unique SMI Command Value associated with the specified GUID. If no SMI Command Value has previously been associated with the specified GUID (and SwContext.SwSmiInputValue is 0xFFFFFFFF), then a unique SMI Command Value is returned. If not 0xFFFFFFFF, then SwContext.SwSmIInputValue will become the SMI Command Value OR an error return if the SMI Command Value has previously been assigned.
6. Locate the EFI_SMM_SW_DISPATCH_PROTOCOL. This protocol is produced by the south bridge driver to callback a function when a specific SMI Command Value was written to the SMI Command Port.
7. The Register() function is used to register a callback function with a particular SMI Command Value. The SwHandle value returned can be used to later UnRegister() the callback.
EFI_SMM_SYSTEM_TABLE *mSmst;
UINT8 mSwSmiCommandValue;
UINT16 mSwSmiCommandPort;
EFI_GUID gSwSmiSampleDriverGuid = SW_SMI_SAMPLE_DRIVER_GUID;
EFI_DRIVER_ENTRY_POINT(SwSmiSampleDriverEntryPoint);
EFI_STATUS
SwSmiSampleDriverEntryPoint(
IN EFI_HANDLE ImageHandle,
IN EFI_SYSTEM_TABLE *SystemTable
)
{
EFI_STATUS Status;
BOOLEAN InSmm;
EFI_SMM_SW_DISPATCH_PROTOCOL *SwDispatch;
PHOENIX_SMM_SW_SMI_PROTOCOL *SwSmiAlloc;
EFI_SMM_SW_DISPATCH_CONTEXT SwContext;
EfiInitializeSmmDriverLib (ImageHandle,SystemTable,&InSmm);
if (InSmm) {
gSMM->GetSmstLocation(gSMM,&mSmst);
Status = gBS->LocateProtocol (
&gEfiPhoenixSmmSwSmiProtocolGuid,
NULL,
(VOID**) &SwSmiAlloc
);
ASSERT_EFI_ERROR (Status);
SwContext.SwSmiInputValue = 0xFFFFFFFF;
Status = SwSmiAlloc->AllocateSwSmi (
&gSwSmiSampleDriverGuid,
&SwContext.SwSmiInputValue
);
ASSERT_EFI_ERROR (Status);
mSwSmiCommandValue = (UINT16) SwContext.SwSmiInputValue;
mSwSmiCommandPort = SwSmiAlloc->SwSmiCommandPort;
Status = gBS->LocateProtocol (
&gEfiSmmSwDispatchProtocolGuid,
NULL,
&SwDispatch
);
ASSERT_EFI_ERROR (Status);
Status = SwDispatch->Register (
SwDispatch,
SwSmiSampleDriverCallback,
&SwContext,
&SwHandle
);
ASSERT_EFI_ERROR (Status);
...other initialization…
}
return EFI_SUCCESS;
}
In two cases, the macro is used with LocateProtocol(). In this case, the risk is very low because the GUIDs for these protocols are listed in the driver’s dependency expression (see SwSmiSampleDriver.dxs), so the driver won’t be launched unless the driver is actually present. In the third case, the macro is used with the Phoenix SMI Allocation protocol, which, in real life, would only fail if the system were out of memory, which is extremely unlikely in the pre-OS environment. Some coding purists may be worried about the use of the ASSERT_EFI_ERROR macro, which detects an error only on debug versions. Why not always check for the error condition? The real reasons are: 1) it makes the code smaller and 2) it makes the code easier to read. But what about the risk?
STEP 2: How To Handle The Software SMI Notification
One the non-SMM code writes the SMI Command Value to the SMI Command Port, eventually control is transferred to the callback function (SwSmiSampleDriverCallback) which was registered during Step 1, above. Typically, Software SMI callbacks need to retrieve and change at the contents of CPU registers, examine the contents of memory, perform their service and then exit.
Reading CPU Registers
Most of the general-purpose registers are saved in a special buffer called the Save State Area when each CPU enters SMM. Other registers are not saved at all. For example, floating-point, XMM and MSRs are not saved.
The callback can read the current values of the general-purpose registers for any of the CPU cores. Since we are interested in the current values of the CPU core which generated the software SMI, we need to figure out which core that is.
Here are the steps:
1. Determine which CPU core actually generated the software SMI, since all we care about are the CPU registers on that CPU.
2. Once we have determined which CPU generated the software SMI, we can then access the CPU registers. In this case, we read EAX using ReadSaveState().
3. Then we can perform our service, perhaps switching services based on the contents of AL.
4. Finally, we put a signature back into the EAX register using WriteSaveState().
VOID
SwSmiSampleDriverCallback (
IN EFI_HANDLE DispatchHandle,
IN EFI_SMM_SW_DISPATCH_CONTEXT *DispatchContext
)
{
EFI_STATUS Status;
UINTN CpuIndex;
EFI_SMM_SAVE_STATE_IO_INFO IoInfo;
//
// Find the CPU that triggered the software SMI
//
for (CpuIndex = 0; CpuIndex < mSmst->NumberOfCpus; CpuIndex++) {
Status = mCpu->ReadSaveState(
mCpu,
sizeof(IoInfo),
EFI_SMM_SAVE_STATE_REGISTER_IO,
(VOID*) &IoInfo
);
if (Status == EFI_SUCCESS &&
IoInfo->IoWidth == EFI_SMM_SAVE_STATE_IO_WIDTH_UINT8 &&
IoInfo->IoPort == (UINT16) mSwSmiCommandPort &&
* (UINT8*) IoInfo->IoData == mSwSmiCommandValue
) {
break;
}
}
if (CpuIndex == mSmst->NumberOfCpus) {
return;
}
UINT32 Eax;
Status = mCpu->ReadSaveState(
mCpu,
sizeof(Eax),
EFI_SMM_SAVE_STATE_REGISTER_RAX,
CpuIndex,
(VOID *) &Eax
);
ASSERT_EFI_ERROR(Status);
...other service code goes here...
Eax = 0xAA55AA55;
Status = mCpu->WriteSaveState(
mCpu,
sizeof(Eax),
EFI_SMM_SAVE_STATE_REGISTER_RAX,
CpuIndex,
(VOID *) &Eax
);
}
Reading System Memory
The software SMI handler may need to read the contents of memory using the same context as the CPU which generated the software SMI. In many cases, the non-SMM code wants to pass a pointer to a buffer which will be used by the software SMI handler. But the non-SMM code may be executing with paging turned on or off and there may not be a 1-to-1 mapping between the linear and physical addresses. The software SMI handler, on the other hand, may be executing with paging turned on, but there is guaranteed to be a 1-to-1 mapping between linear and physical addresses. Adding to this, the buffer pointer may point to data which crosses a page boundary.
Phoenix provides a series of services which translate between physical and linear addresses. Phoenix also provides services which convert select/offset format addresses into linear addresses by looking them up in the GDT. These are encompassed in the PHOENIX_SMM_CPU_PAGE_PROTOCOL.
Let’s assume that the code which generates the software SMI passes a pointer to a 20-byte buffer using DS:ESI (x86) or DS:RSI (X64). We will reverse that buffer and pass it back. Here are the basic steps:
1. Convert the value from DS:RSI to a linear address, using ConvertSegOffsetToLinear().
2. Using this address, the 20 byte buffer is copied from system memory into a temporary buffer on the stack, taking page tables into account, using CopyFromLinear().
3. The contents of the buffer are reversed.
4. The 20 byte buffer is copied from the stack back into system memory, taking page tables into account using CopyToLinear().
EFI_VIRTUAL_ADDRESS Address;
Status = mCpuPage->ConvertSegOffsetRegToLinear(
CpuIndex,
EFI_SMM_SAVE_STATE_REGISTER_DS,
EFI_SMM_SAVE_STATE_REGISTER_RSI,
&Address
);
ASSERT_EFI_ERROR(Status);
UINT8 Buffer[20];
Status = mCpuPage->CopyFromLinear(
Address,
CpuIndex,
(EFI_PHYSICAL_ADDRESS) &Buffer,
sizeof(Buffer)
);
ASSERT_EFI_ERROR(Status);
for (i = 0; i < sizeof(Buffer)/2; i++) {
j = Buffer[i];
Buffer[i] = Buffer[sizeof(Buffer) – i];
Buffer[sizeof(Buffer) – i] = j;
}
Status = mCpuPage->CopyToLinear(
(EFI_PHYSICAL_ADDRESS) &Buffer,
CpuIndex,
Address,
sizeof(Buffer)
);
ASSERT_EFI_ERROR(Status);
STEP 3: How To Generate The Software SMI
For applications which want to use the services created during the previous two steps, there are two important steps: first, find out the SMI Command Value and second, generate the software SMI.
Finding The SMI Command Value
The SMI Command Value that was registered by the software SMI driver can be discovered using the QuerySwSmi() function in the PHOENIX_SMM_SW_SMI_PROTOCOL. Here are the steps.
1. Find the PHOENIX_SMM_SW_SMI_PROTOCOL.
2. Call QuerySwSmi() using the same GUID specified by the driver which allocated the SMI Command Value. The value returned in SwSmiCommandValue is the SMI Command Value.
UINT8 SwSmiCommandValue;
Status = gBS->LocateProtocol (
&gEfiPhoenixSmmSwSmiProtocolGuid,
NULL,
(VOID**) &SwSmiAlloc
);
ASSERT_EFI_ERROR (Status);
SwContext.SwSmiInputValue = 0xFFFFFFFF;
Status = SwSmiAlloc->QuerySwSmi (
&gSwSmiSampleDriverGuid,
&SwSmiCommandValue
);
ASSERT_EFI_ERROR (Status);
Generating The Software SMI
The SMI Command Port can be found in the same protocol and can be used to generate a software SMI with the right value:
_outp(mSwSmiCommandPort, mSwSmiCommandValue);
Conclusion
With this information, you can allocate a unique software SMI value and create new services which rely on either memory or register values. Next time, we will look at how to use the Phoenix SMM Services to “call” protocol functions inside of SMM securely.



Comments