From 4036b4e57ccef4e0fa48d8389acf6390826c2bed Mon Sep 17 00:00:00 2001 From: Paolo Bonzini Date: Mon, 30 Nov 2015 18:46:37 +0000 Subject: [PATCH] OvmfPkg: SmmCpuFeaturesLib: implement SMRAM state save map access This implementation copies SMRAM state save map access from the PiSmmCpuDxeSmm module. The most notable change is: - dropping support for EFI_SMM_SAVE_STATE_REGISTER_IO - changing the implementation of EFI_SMM_SAVE_STATE_REGISTER_LMA to use the SMM revision id instead of a local variable (which UefiCpuPkg/PiSmmCpuDxeSmm/PiSmmCpuDxeSmm.c initializes from CPUID's LM bit). This accounts for QEMU's implementation of x86_64, which always uses revision 0x20064 even if the LM bit is zero. Contributed-under: TianoCore Contribution Agreement 1.0 Signed-off-by: Paolo Bonzini [lersek@redhat.com: reflow commit message & fix typo, convert patch to CRLF] Cc: Paolo Bonzini Contributed-under: TianoCore Contribution Agreement 1.0 Signed-off-by: Laszlo Ersek Reviewed-by: Michael Kinney git-svn-id: https://svn.code.sf.net/p/edk2/code/trunk/edk2@19056 6f19259b-4bc3-4df7-8a09-765794883524 --- .../SmmCpuFeaturesLib/SmmCpuFeaturesLib.c | 377 +++++++++++++++++- .../SmmCpuFeaturesLib/SmmCpuFeaturesLib.inf | 2 + 2 files changed, 376 insertions(+), 3 deletions(-) diff --git a/OvmfPkg/Library/SmmCpuFeaturesLib/SmmCpuFeaturesLib.c b/OvmfPkg/Library/SmmCpuFeaturesLib/SmmCpuFeaturesLib.c index 3b6f186ee8..b3d0e3a8fb 100644 --- a/OvmfPkg/Library/SmmCpuFeaturesLib/SmmCpuFeaturesLib.c +++ b/OvmfPkg/Library/SmmCpuFeaturesLib/SmmCpuFeaturesLib.c @@ -15,11 +15,18 @@ WITHOUT WARRANTIES OR REPRESENTATIONS OF ANY KIND, EITHER EXPRESS OR IMPLIED. #include #include #include +#include #include #include +#include #include #include +// +// EFER register LMA bit +// +#define LMA BIT10 + /** The constructor function @@ -128,7 +135,35 @@ SmmCpuFeaturesHookReturnFromSmm ( IN UINT64 NewInstructionPointer ) { - return 0; + UINT64 OriginalInstructionPointer; + SMRAM_SAVE_STATE_MAP *CpuSaveState = (SMRAM_SAVE_STATE_MAP *)CpuState; + + if ((CpuSaveState->x86.SMMRevId & 0xFFFF) == 0) { + OriginalInstructionPointer = (UINT64)CpuSaveState->x86._EIP; + CpuSaveState->x86._EIP = (UINT32)NewInstructionPointer; + // + // Clear the auto HALT restart flag so the RSM instruction returns + // program control to the instruction following the HLT instruction. + // + if ((CpuSaveState->x86.AutoHALTRestart & BIT0) != 0) { + CpuSaveState->x86.AutoHALTRestart &= ~BIT0; + } + } else { + OriginalInstructionPointer = CpuSaveState->x64._RIP; + if ((CpuSaveState->x64.IA32_EFER & LMA) == 0) { + CpuSaveState->x64._RIP = (UINT32)NewInstructionPointer32; + } else { + CpuSaveState->x64._RIP = (UINT32)NewInstructionPointer; + } + // + // Clear the auto HALT restart flag so the RSM instruction returns + // program control to the instruction following the HLT instruction. + // + if ((CpuSaveState->x64.AutoHALTRestart & BIT0) != 0) { + CpuSaveState->x64.AutoHALTRestart &= ~BIT0; + } + } + return OriginalInstructionPointer; } /** @@ -359,6 +394,213 @@ SmmCpuFeaturesSetSmmRegister ( ASSERT (FALSE); } +/// +/// Macro used to simplify the lookup table entries of type CPU_SMM_SAVE_STATE_LOOKUP_ENTRY +/// +#define SMM_CPU_OFFSET(Field) OFFSET_OF (SMRAM_SAVE_STATE_MAP, Field) + +/// +/// Macro used to simplify the lookup table entries of type CPU_SMM_SAVE_STATE_REGISTER_RANGE +/// +#define SMM_REGISTER_RANGE(Start, End) { Start, End, End - Start + 1 } + +/// +/// Structure used to describe a range of registers +/// +typedef struct { + EFI_SMM_SAVE_STATE_REGISTER Start; + EFI_SMM_SAVE_STATE_REGISTER End; + UINTN Length; +} CPU_SMM_SAVE_STATE_REGISTER_RANGE; + +/// +/// Structure used to build a lookup table to retrieve the widths and offsets +/// associated with each supported EFI_SMM_SAVE_STATE_REGISTER value +/// + +#define SMM_SAVE_STATE_REGISTER_FIRST_INDEX 1 + +typedef struct { + UINT8 Width32; + UINT8 Width64; + UINT16 Offset32; + UINT16 Offset64Lo; + UINT16 Offset64Hi; + BOOLEAN Writeable; +} CPU_SMM_SAVE_STATE_LOOKUP_ENTRY; + +/// +/// Table used by GetRegisterIndex() to convert an EFI_SMM_SAVE_STATE_REGISTER +/// value to an index into a table of type CPU_SMM_SAVE_STATE_LOOKUP_ENTRY +/// +static CONST CPU_SMM_SAVE_STATE_REGISTER_RANGE mSmmCpuRegisterRanges[] = { + SMM_REGISTER_RANGE (EFI_SMM_SAVE_STATE_REGISTER_GDTBASE, EFI_SMM_SAVE_STATE_REGISTER_LDTINFO), + SMM_REGISTER_RANGE (EFI_SMM_SAVE_STATE_REGISTER_ES, EFI_SMM_SAVE_STATE_REGISTER_RIP), + SMM_REGISTER_RANGE (EFI_SMM_SAVE_STATE_REGISTER_RFLAGS, EFI_SMM_SAVE_STATE_REGISTER_CR4), + { (EFI_SMM_SAVE_STATE_REGISTER)0, (EFI_SMM_SAVE_STATE_REGISTER)0, 0 } +}; + +/// +/// Lookup table used to retrieve the widths and offsets associated with each +/// supported EFI_SMM_SAVE_STATE_REGISTER value +/// +static CONST CPU_SMM_SAVE_STATE_LOOKUP_ENTRY mSmmCpuWidthOffset[] = { + {0, 0, 0, 0, 0, FALSE}, // Reserved + + // + // CPU Save State registers defined in PI SMM CPU Protocol. + // + {0, 8, 0 , SMM_CPU_OFFSET (x64.GdtBaseLoDword) , SMM_CPU_OFFSET (x64.GdtBaseHiDword), FALSE}, // EFI_SMM_SAVE_STATE_REGISTER_GDTBASE = 4 + {0, 8, 0 , SMM_CPU_OFFSET (x64.IdtBaseLoDword) , SMM_CPU_OFFSET (x64.IdtBaseHiDword), FALSE}, // EFI_SMM_SAVE_STATE_REGISTER_IDTBASE = 5 + {0, 8, 0 , SMM_CPU_OFFSET (x64.LdtBaseLoDword) , SMM_CPU_OFFSET (x64.LdtBaseHiDword), FALSE}, // EFI_SMM_SAVE_STATE_REGISTER_LDTBASE = 6 + {0, 0, 0 , 0 , 0 , FALSE}, // EFI_SMM_SAVE_STATE_REGISTER_GDTLIMIT = 7 + {0, 0, 0 , 0 , 0 , FALSE}, // EFI_SMM_SAVE_STATE_REGISTER_IDTLIMIT = 8 + {0, 0, 0 , 0 , 0 , FALSE}, // EFI_SMM_SAVE_STATE_REGISTER_LDTLIMIT = 9 + {0, 0, 0 , 0 , 0 , FALSE}, // EFI_SMM_SAVE_STATE_REGISTER_LDTINFO = 10 + + {4, 4, SMM_CPU_OFFSET (x86._ES) , SMM_CPU_OFFSET (x64._ES) , 0 , FALSE}, // EFI_SMM_SAVE_STATE_REGISTER_ES = 20 + {4, 4, SMM_CPU_OFFSET (x86._CS) , SMM_CPU_OFFSET (x64._CS) , 0 , FALSE}, // EFI_SMM_SAVE_STATE_REGISTER_CS = 21 + {4, 4, SMM_CPU_OFFSET (x86._SS) , SMM_CPU_OFFSET (x64._SS) , 0 , FALSE}, // EFI_SMM_SAVE_STATE_REGISTER_SS = 22 + {4, 4, SMM_CPU_OFFSET (x86._DS) , SMM_CPU_OFFSET (x64._DS) , 0 , FALSE}, // EFI_SMM_SAVE_STATE_REGISTER_DS = 23 + {4, 4, SMM_CPU_OFFSET (x86._FS) , SMM_CPU_OFFSET (x64._FS) , 0 , FALSE}, // EFI_SMM_SAVE_STATE_REGISTER_FS = 24 + {4, 4, SMM_CPU_OFFSET (x86._GS) , SMM_CPU_OFFSET (x64._GS) , 0 , FALSE}, // EFI_SMM_SAVE_STATE_REGISTER_GS = 25 + {0, 4, 0 , SMM_CPU_OFFSET (x64._LDTR) , 0 , FALSE}, // EFI_SMM_SAVE_STATE_REGISTER_LDTR_SEL = 26 + {4, 4, SMM_CPU_OFFSET (x86._TR) , SMM_CPU_OFFSET (x64._TR) , 0 , FALSE}, // EFI_SMM_SAVE_STATE_REGISTER_TR_SEL = 27 + {4, 8, SMM_CPU_OFFSET (x86._DR7) , SMM_CPU_OFFSET (x64._DR7) , SMM_CPU_OFFSET (x64._DR7) + 4, FALSE}, // EFI_SMM_SAVE_STATE_REGISTER_DR7 = 28 + {4, 8, SMM_CPU_OFFSET (x86._DR6) , SMM_CPU_OFFSET (x64._DR6) , SMM_CPU_OFFSET (x64._DR6) + 4, FALSE}, // EFI_SMM_SAVE_STATE_REGISTER_DR6 = 29 + {0, 8, 0 , SMM_CPU_OFFSET (x64._R8) , SMM_CPU_OFFSET (x64._R8) + 4, TRUE }, // EFI_SMM_SAVE_STATE_REGISTER_R8 = 30 + {0, 8, 0 , SMM_CPU_OFFSET (x64._R9) , SMM_CPU_OFFSET (x64._R9) + 4, TRUE }, // EFI_SMM_SAVE_STATE_REGISTER_R9 = 31 + {0, 8, 0 , SMM_CPU_OFFSET (x64._R10) , SMM_CPU_OFFSET (x64._R10) + 4, TRUE }, // EFI_SMM_SAVE_STATE_REGISTER_R10 = 32 + {0, 8, 0 , SMM_CPU_OFFSET (x64._R11) , SMM_CPU_OFFSET (x64._R11) + 4, TRUE }, // EFI_SMM_SAVE_STATE_REGISTER_R11 = 33 + {0, 8, 0 , SMM_CPU_OFFSET (x64._R12) , SMM_CPU_OFFSET (x64._R12) + 4, TRUE }, // EFI_SMM_SAVE_STATE_REGISTER_R12 = 34 + {0, 8, 0 , SMM_CPU_OFFSET (x64._R13) , SMM_CPU_OFFSET (x64._R13) + 4, TRUE }, // EFI_SMM_SAVE_STATE_REGISTER_R13 = 35 + {0, 8, 0 , SMM_CPU_OFFSET (x64._R14) , SMM_CPU_OFFSET (x64._R14) + 4, TRUE }, // EFI_SMM_SAVE_STATE_REGISTER_R14 = 36 + {0, 8, 0 , SMM_CPU_OFFSET (x64._R15) , SMM_CPU_OFFSET (x64._R15) + 4, TRUE }, // EFI_SMM_SAVE_STATE_REGISTER_R15 = 37 + {4, 8, SMM_CPU_OFFSET (x86._EAX) , SMM_CPU_OFFSET (x64._RAX) , SMM_CPU_OFFSET (x64._RAX) + 4, TRUE }, // EFI_SMM_SAVE_STATE_REGISTER_RAX = 38 + {4, 8, SMM_CPU_OFFSET (x86._EBX) , SMM_CPU_OFFSET (x64._RBX) , SMM_CPU_OFFSET (x64._RBX) + 4, TRUE }, // EFI_SMM_SAVE_STATE_REGISTER_RBX = 39 + {4, 8, SMM_CPU_OFFSET (x86._ECX) , SMM_CPU_OFFSET (x64._RCX) , SMM_CPU_OFFSET (x64._RCX) + 4, TRUE }, // EFI_SMM_SAVE_STATE_REGISTER_RCX = 40 + {4, 8, SMM_CPU_OFFSET (x86._EDX) , SMM_CPU_OFFSET (x64._RDX) , SMM_CPU_OFFSET (x64._RDX) + 4, TRUE }, // EFI_SMM_SAVE_STATE_REGISTER_RDX = 41 + {4, 8, SMM_CPU_OFFSET (x86._ESP) , SMM_CPU_OFFSET (x64._RSP) , SMM_CPU_OFFSET (x64._RSP) + 4, TRUE }, // EFI_SMM_SAVE_STATE_REGISTER_RSP = 42 + {4, 8, SMM_CPU_OFFSET (x86._EBP) , SMM_CPU_OFFSET (x64._RBP) , SMM_CPU_OFFSET (x64._RBP) + 4, TRUE }, // EFI_SMM_SAVE_STATE_REGISTER_RBP = 43 + {4, 8, SMM_CPU_OFFSET (x86._ESI) , SMM_CPU_OFFSET (x64._RSI) , SMM_CPU_OFFSET (x64._RSI) + 4, TRUE }, // EFI_SMM_SAVE_STATE_REGISTER_RSI = 44 + {4, 8, SMM_CPU_OFFSET (x86._EDI) , SMM_CPU_OFFSET (x64._RDI) , SMM_CPU_OFFSET (x64._RDI) + 4, TRUE }, // EFI_SMM_SAVE_STATE_REGISTER_RDI = 45 + {4, 8, SMM_CPU_OFFSET (x86._EIP) , SMM_CPU_OFFSET (x64._RIP) , SMM_CPU_OFFSET (x64._RIP) + 4, TRUE }, // EFI_SMM_SAVE_STATE_REGISTER_RIP = 46 + + {4, 8, SMM_CPU_OFFSET (x86._EFLAGS) , SMM_CPU_OFFSET (x64._RFLAGS) , SMM_CPU_OFFSET (x64._RFLAGS) + 4, TRUE }, // EFI_SMM_SAVE_STATE_REGISTER_RFLAGS = 51 + {4, 8, SMM_CPU_OFFSET (x86._CR0) , SMM_CPU_OFFSET (x64._CR0) , SMM_CPU_OFFSET (x64._CR0) + 4, FALSE}, // EFI_SMM_SAVE_STATE_REGISTER_CR0 = 52 + {4, 8, SMM_CPU_OFFSET (x86._CR3) , SMM_CPU_OFFSET (x64._CR3) , SMM_CPU_OFFSET (x64._CR3) + 4, FALSE}, // EFI_SMM_SAVE_STATE_REGISTER_CR3 = 53 + {0, 4, 0 , SMM_CPU_OFFSET (x64._CR4) , 0 , FALSE}, // EFI_SMM_SAVE_STATE_REGISTER_CR4 = 54 +}; + +// +// No support for I/O restart +// + +/** + Read information from the CPU save state. + + @param Register Specifies the CPU register to read form the save state. + + @retval 0 Register is not valid + @retval >0 Index into mSmmCpuWidthOffset[] associated with Register + +**/ +static UINTN +GetRegisterIndex ( + IN EFI_SMM_SAVE_STATE_REGISTER Register + ) +{ + UINTN Index; + UINTN Offset; + + for (Index = 0, Offset = SMM_SAVE_STATE_REGISTER_FIRST_INDEX; mSmmCpuRegisterRanges[Index].Length != 0; Index++) { + if (Register >= mSmmCpuRegisterRanges[Index].Start && Register <= mSmmCpuRegisterRanges[Index].End) { + return Register - mSmmCpuRegisterRanges[Index].Start + Offset; + } + Offset += mSmmCpuRegisterRanges[Index].Length; + } + return 0; +} + +/** + Read a CPU Save State register on the target processor. + + This function abstracts the differences that whether the CPU Save State register is in the + IA32 CPU Save State Map or X64 CPU Save State Map. + + This function supports reading a CPU Save State register in SMBase relocation handler. + + @param[in] CpuIndex Specifies the zero-based index of the CPU save state. + @param[in] RegisterIndex Index into mSmmCpuWidthOffset[] look up table. + @param[in] Width The number of bytes to read from the CPU save state. + @param[out] Buffer Upon return, this holds the CPU register value read from the save state. + + @retval EFI_SUCCESS The register was read from Save State. + @retval EFI_NOT_FOUND The register is not defined for the Save State of Processor. + @retval EFI_INVALID_PARAMTER This or Buffer is NULL. + +**/ +static EFI_STATUS +ReadSaveStateRegisterByIndex ( + IN UINTN CpuIndex, + IN UINTN RegisterIndex, + IN UINTN Width, + OUT VOID *Buffer + ) +{ + SMRAM_SAVE_STATE_MAP *CpuSaveState; + + CpuSaveState = gSmst->CpuSaveState[CpuIndex]; + + if ((CpuSaveState->x86.SMMRevId & 0xFFFF) == 0) { + // + // If 32-bit mode width is zero, then the specified register can not be accessed + // + if (mSmmCpuWidthOffset[RegisterIndex].Width32 == 0) { + return EFI_NOT_FOUND; + } + + // + // If Width is bigger than the 32-bit mode width, then the specified register can not be accessed + // + if (Width > mSmmCpuWidthOffset[RegisterIndex].Width32) { + return EFI_INVALID_PARAMETER; + } + + // + // Write return buffer + // + ASSERT(CpuSaveState != NULL); + CopyMem(Buffer, (UINT8 *)CpuSaveState + mSmmCpuWidthOffset[RegisterIndex].Offset32, Width); + } else { + // + // If 64-bit mode width is zero, then the specified register can not be accessed + // + if (mSmmCpuWidthOffset[RegisterIndex].Width64 == 0) { + return EFI_NOT_FOUND; + } + + // + // If Width is bigger than the 64-bit mode width, then the specified register can not be accessed + // + if (Width > mSmmCpuWidthOffset[RegisterIndex].Width64) { + return EFI_INVALID_PARAMETER; + } + + // + // Write lower 32-bits of return buffer + // + CopyMem(Buffer, (UINT8 *)CpuSaveState + mSmmCpuWidthOffset[RegisterIndex].Offset64Lo, MIN(4, Width)); + if (Width >= 4) { + // + // Write upper 32-bits of return buffer + // + CopyMem((UINT8 *)Buffer + 4, (UINT8 *)CpuSaveState + mSmmCpuWidthOffset[RegisterIndex].Offset64Hi, Width - 4); + } + } + return EFI_SUCCESS; +} + /** Read an SMM Save State register on the target processor. If this function returns EFI_UNSUPPORTED, then the caller is responsible for reading the @@ -386,7 +628,52 @@ SmmCpuFeaturesReadSaveStateRegister ( OUT VOID *Buffer ) { - return EFI_UNSUPPORTED; + UINTN RegisterIndex; + SMRAM_SAVE_STATE_MAP *CpuSaveState; + + // + // Check for special EFI_SMM_SAVE_STATE_REGISTER_LMA + // + if (Register == EFI_SMM_SAVE_STATE_REGISTER_LMA) { + // + // Only byte access is supported for this register + // + if (Width != 1) { + return EFI_INVALID_PARAMETER; + } + + CpuSaveState = gSmst->CpuSaveState[CpuIndex]; + + // + // Check CPU mode + // + if ((CpuSaveState->x86.SMMRevId & 0xFFFF) == 0) { + *(UINT8 *)Buffer = 32; + } else { + *(UINT8 *)Buffer = 64; + } + + return EFI_SUCCESS; + } + + // + // Check for special EFI_SMM_SAVE_STATE_REGISTER_IO + // + if (Register == EFI_SMM_SAVE_STATE_REGISTER_IO) { + return EFI_NOT_FOUND; + } + + // + // Convert Register to a register lookup table index. Let + // PiSmmCpuDxeSmm implement other special registers (currently + // there is only EFI_SMM_SAVE_STATE_REGISTER_PROCESSOR_ID). + // + RegisterIndex = GetRegisterIndex (Register); + if (RegisterIndex == 0) { + return Register < EFI_SMM_SAVE_STATE_REGISTER_IO ? EFI_NOT_FOUND : EFI_UNSUPPORTED; + } + + return ReadSaveStateRegisterByIndex (CpuIndex, RegisterIndex, Width, Buffer); } /** @@ -414,7 +701,91 @@ SmmCpuFeaturesWriteSaveStateRegister ( IN CONST VOID *Buffer ) { - return EFI_UNSUPPORTED; + UINTN RegisterIndex; + SMRAM_SAVE_STATE_MAP *CpuSaveState; + + // + // Writes to EFI_SMM_SAVE_STATE_REGISTER_LMA are ignored + // + if (Register == EFI_SMM_SAVE_STATE_REGISTER_LMA) { + return EFI_SUCCESS; + } + + // + // Writes to EFI_SMM_SAVE_STATE_REGISTER_IO are not supported + // + if (Register == EFI_SMM_SAVE_STATE_REGISTER_IO) { + return EFI_NOT_FOUND; + } + + // + // Convert Register to a register lookup table index. Let + // PiSmmCpuDxeSmm implement other special registers (currently + // there is only EFI_SMM_SAVE_STATE_REGISTER_PROCESSOR_ID). + // + RegisterIndex = GetRegisterIndex (Register); + if (RegisterIndex == 0) { + return Register < EFI_SMM_SAVE_STATE_REGISTER_IO ? EFI_NOT_FOUND : EFI_UNSUPPORTED; + } + + CpuSaveState = gSmst->CpuSaveState[CpuIndex]; + + // + // Do not write non-writable SaveState, because it will cause exception. + // + if (!mSmmCpuWidthOffset[RegisterIndex].Writeable) { + return EFI_UNSUPPORTED; + } + + // + // Check CPU mode + // + if ((CpuSaveState->x86.SMMRevId & 0xFFFF) == 0) { + // + // If 32-bit mode width is zero, then the specified register can not be accessed + // + if (mSmmCpuWidthOffset[RegisterIndex].Width32 == 0) { + return EFI_NOT_FOUND; + } + + // + // If Width is bigger than the 32-bit mode width, then the specified register can not be accessed + // + if (Width > mSmmCpuWidthOffset[RegisterIndex].Width32) { + return EFI_INVALID_PARAMETER; + } + // + // Write SMM State register + // + ASSERT (CpuSaveState != NULL); + CopyMem((UINT8 *)CpuSaveState + mSmmCpuWidthOffset[RegisterIndex].Offset32, Buffer, Width); + } else { + // + // If 64-bit mode width is zero, then the specified register can not be accessed + // + if (mSmmCpuWidthOffset[RegisterIndex].Width64 == 0) { + return EFI_NOT_FOUND; + } + + // + // If Width is bigger than the 64-bit mode width, then the specified register can not be accessed + // + if (Width > mSmmCpuWidthOffset[RegisterIndex].Width64) { + return EFI_INVALID_PARAMETER; + } + + // + // Write lower 32-bits of SMM State register + // + CopyMem((UINT8 *)CpuSaveState + mSmmCpuWidthOffset[RegisterIndex].Offset64Lo, Buffer, MIN (4, Width)); + if (Width >= 4) { + // + // Write upper 32-bits of SMM State register + // + CopyMem((UINT8 *)CpuSaveState + mSmmCpuWidthOffset[RegisterIndex].Offset64Hi, (UINT8 *)Buffer + 4, Width - 4); + } + } + return EFI_SUCCESS; } /** diff --git a/OvmfPkg/Library/SmmCpuFeaturesLib/SmmCpuFeaturesLib.inf b/OvmfPkg/Library/SmmCpuFeaturesLib/SmmCpuFeaturesLib.inf index aa4792c5f1..4b86deb589 100644 --- a/OvmfPkg/Library/SmmCpuFeaturesLib/SmmCpuFeaturesLib.inf +++ b/OvmfPkg/Library/SmmCpuFeaturesLib/SmmCpuFeaturesLib.inf @@ -31,5 +31,7 @@ [LibraryClasses] BaseLib + BaseMemoryLib PcdLib DebugLib + SmmServicesTableLib -- 2.39.2