唐凌 发表于 2020-10-13 14:20:44

【UEFI】【SMBIOS】在UEFI中解析SMBIOS获取内存信息

写OS必不可少的就是了解你这个系统的基本信息。其中SMBIOS就是个很有用的东西,本文以解析内存信息为例来了解SMBIOS。本文提到的SMBIOS,特指32位的SMBIOS标准。64位的SMBIOS标准是3.0版本开始才有的。
上回写EFI的时候我自己实现了printf类的函数,但这个轮子根本不完整,并且费力不讨好。其实EDK中早就有了相关的轮子,但是没编译出来,所以当时就临时另造了一个轮子。但今时不同往日,我已经成功以我自己的套路编译出了EDK里的Print函数库,之前造的轮子就可以弃用了。
我的编译方法是非官方的玩法,本文不会赘述EDK库的编译方法,编译脚本已经在GitHub上开源了:https://github.com/MickeyMeowMeowHouse/EDK-II-Library

要解析SMBIOS,首先得找到SMBIOS的地址。根据SMBIOS的标准,在非UEFI环境中,查找SMBIOS的方法是在物理地址[0x000F0000,0x00100000)的区间中,以16字节的对齐粒度暴力搜索字符串"_SM_"。
但本文既然是在说UEFI,那当然得按照UEFI的标准来。在入口函数的SystemTable参数中,结构体里有个成员叫ConfigurationTable,这是个结构体数组,其结构体定义如下:
typedef struct{
EFI_GUID VendorGuid;
VOID *VendorTable;
} EFI_CONFIGURATION_TABLE;
在查找SMBIOS时,我们要找到数组的一个特定元素,这个特定元素的VendorGuid是一个特定的GUID。这个特定的GUID当然代表SMBIOS。根据UEFI文档和SMBIOS标准,这个GUID的定义是:
#define SMBIOS_TABLE_GUID \
{0xeb9d2d31,0x2d88,0x11d3,\
{0x9a,0x16,0x00,0x90,0x27,0x3f,0xc1,0x4d}}
那么通过遍历ConfigurationTable数组,我们就能拿到SMBIOS表的地址。但由于UEFI文档里没说这个数组按GUID排了序,因此不能使用二分搜索,只能线性查找,代码如下:
EFI_STATUS EfiLocateSmBiosTable()
{
        for(UINTN i=0;i<gST->NumberOfTableEntries;i++)
        {
                if(EfiCompareGuid(&gST->ConfigurationTable.VendorGuid,&gEfiSmbiosTableGuid)==0)
                {
                        SmBiosTable=(SMBIOS_TABLE_ENTRY_POINT*)gST->ConfigurationTable.VendorTable;
                        return EFI_SUCCESS;
                }
        }
        return EFI_NOT_FOUND;
}
其中EfiCompareGuid是比较GUID的函数,EDK库中似乎没有造过这个轮子,所以只能自己造了,代码如下:
INTN EfiCompareGuid(EFI_GUID *Guid1,EFI_GUID *Guid2)
{
        if(Guid1->Data1>Guid2->Data1)
                return 1;
        else if(Guid1->Data1<Guid2->Data1)
                return -1;
        if(Guid1->Data2>Guid2->Data2)
                return 1;
        else if(Guid1->Data2<Guid2->Data2)
                return -1;
        if(Guid1->Data3>Guid2->Data3)
                return 1;
        else if(Guid1->Data3<Guid2->Data3)
                return -1;
        for(UINT8 i=0;i<8;i++)
        {
                if(Guid1->Data4>Guid2->Data4)
                        return 1;
                else if(Guid1->Data4<Guid2->Data4)
                        return -1;
        }
        return 0;
}

拿到SMBIOS表之后我们就要开始解析SMBIOS了。首先先看看SMBIOS表的结构体定义:
typedef struct {
UINT8   AnchorString;
UINT8   EntryPointStructureChecksum;
UINT8   EntryPointLength;
UINT8   MajorVersion;
UINT8   MinorVersion;
UINT16MaxStructureSize;
UINT8   EntryPointRevision;
UINT8   FormattedArea;
UINT8   IntermediateAnchorString;
UINT8   IntermediateChecksum;
UINT16TableLength;
UINT32TableAddress;
UINT16NumberOfSmbiosStructures;
UINT8   SmbiosBcdRevision;
} SMBIOS_TABLE_ENTRY_POINT;
我们需要关注的几点:AnchorString, TableLength, TableAddress, NumberOfSmbiosStructures。
AnchorString: 这是个签名,构成一个ANSI字符串"_SM_"。
TableLength: SMBIOS所有表项的字节数。
TableAddress: SMBIOS表项的起始地址(物理地址)。
NumberOfSmbiosStructures: SMBIOS表项总数。
由于是32位SMBIOS,那TableAddress项当然是32位的咯。(笑)

SMBIOS的表项的组织方式是连续排列,即每个表项在地址上是连续的。每个表项的结构均一分为三:表头,定长值表项,字符串数组。
表头是一个简单的结构体,每个类型的表项表头的定义都一样,其定义如下:
typedef struct {
SMBIOS_TYPE    Type;
UINT8          Length;
SMBIOS_HANDLEHandle;
} SMBIOS_STRUCTURE;
其中Type是表项类型,Length是表头加定长值表项但不包括字符串数组的字节数,Handle是可用于区分表项的“句柄”。
不同类型的表项,定长值表项的定义也不同。本文讲的是查询内存信息,那么相关定义如下:
#define SMBIOS_TYPE_MEMORY_DEVICE                        17

typedef struct {
SMBIOS_STRUCTURE                        Hdr;
UINT16                                    MemoryArrayHandle;
UINT16                                    MemoryErrorInformationHandle;
UINT16                                    TotalWidth;
UINT16                                    DataWidth;
UINT16                                    Size;
UINT8                                     FormFactor;         ///< The enumeration value from MEMORY_FORM_FACTOR.
UINT8                                     DeviceSet;
SMBIOS_TABLE_STRING                     DeviceLocator;
SMBIOS_TABLE_STRING                     BankLocator;
UINT8                                     MemoryType;         ///< The enumeration value from MEMORY_DEVICE_TYPE.
MEMORY_DEVICE_TYPE_DETAIL               TypeDetail;
UINT16                                    Speed;
SMBIOS_TABLE_STRING                     Manufacturer;
SMBIOS_TABLE_STRING                     SerialNumber;
SMBIOS_TABLE_STRING                     AssetTag;
SMBIOS_TABLE_STRING                     PartNumber;
//
// Add for smbios 2.6
//
UINT8                                     Attributes;
//
// Add for smbios 2.7
//
UINT32                                    ExtendedSize;
//
// Keep using name "ConfiguredMemoryClockSpeed" for compatibility
// although this field is renamed from "Configured Memory Clock Speed"
// to "Configured Memory Speed" in smbios 3.2.0.
//
UINT16                                    ConfiguredMemoryClockSpeed;
//
// Add for smbios 2.8.0
//
UINT16                                    MinimumVoltage;
UINT16                                    MaximumVoltage;
UINT16                                    ConfiguredVoltage;
//
// Add for smbios 3.2.0
//
UINT8                                     MemoryTechnology;   ///< The enumeration value from MEMORY_DEVICE_TECHNOLOGY
MEMORY_DEVICE_OPERATING_MODE_CAPABILITY   MemoryOperatingModeCapability;
SMBIOS_TABLE_STRING                     FirwareVersion;
UINT16                                    ModuleManufacturerID;
UINT16                                    ModuleProductID;
UINT16                                    MemorySubsystemControllerManufacturerID;
UINT16                                    MemorySubsystemControllerProductID;
UINT64                                    NonVolatileSize;
UINT64                                    VolatileSize;
UINT64                                    CacheSize;
UINT64                                    LogicalSize;
//
// Add for smbios 3.3.0
//
UINT32                                    ExtendedSpeed;
UINT32                                    ExtendedConfiguredMemorySpeed;
} SMBIOS_TABLE_TYPE17;
每个SMBIOS_TABLE_TYPE17表项均表示一个内存槽,随着SMBIOS标准的更新,这个结构体也会不断增大。由于市面上多数的SMBIOS至少支持到2.7版,也就是说,常见的SMBIOS结构体至少包含了ExtendedSize成员。
成员太多不一一介绍,只列举几个我们关心的:
Hdr成员即表项的表头。
Size成员表示这个内存槽上插的内存的大小。这个16位的值,最高位表示内存大小的单位。若复位则为MiB,若置位则为KiB。注意1KiB=1024Byte,1MiB=1024KiB;而1KB=1000Byte,1MB=1000KB。若Size成员等于0x7FFF,这个槽上插的内存大于等于32GiB,此时用32位的ExtendedSize成员表示具体的内存大小。如果你的机器不支持SMBIOS 2.7,那么你的机器肯定也不支持单条32GiB的内存。
FormFactor表示内存的构造尺寸,如DIMM,SODIMM等等。
DeviceLocator BankLocator可用于识别内存条所在槽的位置。其类型是SMBIOS_TABLE_STRING,类型相当于BYTE,数值的意义是作为索引在表项的第三部分字符串中引用字符串。
MemoryType表示内存类型,如DDR3 DDR4等等。
TypeDetail表示内存条具备的性质,如Synchronous, Registered等等。
Speed表示内存频率,单位为MT/s,也就是常说的MHz。
Manufacturer, SerialNumber, PartNumber, AssetTag的含义,顾名思义,不言而喻。
ExtendedSize表示内存的大小,最高位为保留位,目测可能还是用于特殊单位的。由于ExtendedSize是32位的,那么我推测当最高位置位的时候,单位是比MiB高三级的单位,也就是PiB。
我们提到过表项还有第三部分,即字符串数组。字符串数组的排列方式也是连续排列。每个字符串均为以零结尾的ANSI字符串,而下一个字符串就紧跟在零结尾之后。最后一个字符串以两个零字节结尾,标识了这个表项的结束。
那么综上所述,计算表项长度的函数代码如下:
UINTN GetSmBiosEntryLength(IN SMBIOS_STRUCTURE *Entry)
{
        CHAR8* StringPool=(CHAR8*)((UINTN)Entry+Entry->Length);
        UINTN i=0;
        while(StringPool!='\0' || StringPool!='\0')i++;
        return Entry->Length+i+2;
}
那么当我们解析SMBIOS获取内存信息的时候,大致的步骤是:
遍历SMBIOS表,用GetSmBiosEntryLength函数获取当前表项的长度,从而计算出下一条表项的地址。然后判断表项类型,如果不是内存槽的,就直接跳过,看下一个。如果内存槽上的内存大小为零,也就是没有内存,那也直接跳过。当内存槽上有内存的时候,就分析这个表项。
分析表项的过程中,先解析字符串数组,以零为分界,把每个字符串的地址丢进数组里(或者其他的你喜欢用的数据结构),但由于表项记录的特性,使用数组可能会更好点。值得注意的是,第零条字符串必须留空,因为SMBIOS_TABLE_STRING类型的成员为零的时候,表示这个成员没有信息。
那么综上所述,解析SMBIOS获取内存信息的代码如下:
EFI_STATUS EfiQueryMemoryModuleInformation(OUT UINT64 *Size)
{
        EFI_STATUS st=EFI_NOT_FOUND;
        UINTN Len=0,Index=0;
        *Size=0;
        for(UINTN CurEntry=(UINTN)SmBiosTable->TableAddress;CurEntry<(UINTN)(SmBiosTable->TableAddress+SmBiosTable->TableLength);CurEntry+=Len)
        {
                SMBIOS_STRUCTURE *Entry=(SMBIOS_STRUCTURE*)CurEntry;
                Len=GetSmBiosEntryLength(Entry);
                if(Entry->Type==SMBIOS_TYPE_MEMORY_DEVICE)
                {
                        SMBIOS_TABLE_TYPE17 *MemModInfo=(SMBIOS_TABLE_TYPE17*)Entry;
                        if(MemModInfo->Size)
                        {
                                CHAR8* Strings=(CHAR8*)((UINTN)Entry+Entry->Length);
                                // 15 strings should be enough for Memory Module Information.
                                CHAR8* StringPool;
                                UINT8 j=1;                // Leave the zeroth as empty.
                                UINTN StringLength=0;
                                // Initialize the String Pool.
                                __stosp((UINTN*)StringPool,(UINTN)"No Info",0x10);
                                for(UINTN i=0;i<Len-Entry->Length;i+=StringLength)
                                {
                                        StringLength=AsciiStrnLenS(&Strings,Len-Entry->Length-i)+1;
                                        if(Strings=='\0')break;
                                        StringPool=&Strings;
                                }
                                // Memory Module Information in Strings
                                CHAR8* DeviceLocator=StringPool;
                                CHAR8* BankLocator=StringPool;
                                CHAR8* Manufacturer=StringPool;
                                CHAR8* SerialNumber=StringPool;
                                CHAR8* AssetTag=StringPool;
                                CHAR8* PartNumber=StringPool;
                                CHAR8* MemoryType=MemTypeList;
                                CHAR8* FormFactor=MemFormList;
                                Print(L"---------------- Memory Module Information for %a ----------------\n",BankLocator);
                                Print(L"Device Slot: %a | Manufacturer: %a | Serial Number: %a | Asset Tag: %a\n",DeviceLocator,Manufacturer,SerialNumber,AssetTag);
                                Print(L"Part Number: %a | Size=",PartNumber);
                                if(_bittest(&MemModInfo->Size,15))                // Unit is KiB
                                {
                                        *Size+=(MemModInfo->Size&0x7FFF);
                                        Print(L"%d KiB",MemModInfo->Size&0x7FFF);
                                }
                                else                        // Unit is MiB
                                {
                                        // This RAM module is greater than or equal to 32 GiB.
                                        if(MemModInfo->Size==0x7FFF)
                                        {
                                                UINT64 MemSize=MemModInfo->ExtendedSize&0x7FFFFFFF;
                                                *Size+=(MemSize<<10);
                                                Print(L"%d MiB",MemModInfo->ExtendedSize&0x7FFFFFFF);
                                        }
                                        else
                                        {
                                                *Size+=(MemModInfo->Size<<10);
                                                Print(L"%d MiB",MemModInfo->Size);
                                        }
                                }
                                Print(L" | Type: %a | Form Factor: %a\n",MemoryType,FormFactor);
                                StdOut->OutputString(StdOut,L"Memory Type Detail:");
                                for(UINT8 i=0;i<16;i++)
                                        if(_bittest(&MemModInfo->TypeDetail,i))
                                                StdOut->OutputString(StdOut,MemTypeDetailList);
                                Print(L"\nMemory Data Width: %d | Memory Total Width: %d | Transfer Rate: %d MT/s\n",MemModInfo->DataWidth,MemModInfo->TotalWidth,MemModInfo->Speed);
                                st=EFI_SUCCESS;
                        }
                }
                if(++Index==SmBiosTable->NumberOfSmbiosStructures)break;
        }
        if(st==EFI_SUCCESS)StdOut->OutputString(StdOut,L"-------------------- Memory Module Information Listing Ended --------------------\r\n");
        return st;
}

我们丢进VMware虚拟机里跑跑看,如图所示:

然后再丢进钓鱼派里跑跑看,由于钓鱼派有一个UART接口,那么可以直接用PuTTY来玩控制台,而不需要接采集卡,如图所示:

丢进巫毒派里跑跑看,可以接到采集卡上,然后用OBS截图,如图所示:


UEFI的文档可以去UEFI官网下载:https://uefi.org/specifications
SMBIOS的文档可以去DMTF官网下载:https://www.dmtf.org/standards/smbios
本文完整代码已在GitHub上开源:https://github.com/MickeyMeowMeowHouse/UefiAccessSmBios
EFI二进制文件已上传至GitHub:https://github.com/MickeyMeowMeowHouse/UefiAccessSmBios/releases

Golden Blonde 发表于 2020-10-14 06:53:34

经典提问:这玩意可以用来过保护吗?
页: [1]
查看完整版本: 【UEFI】【SMBIOS】在UEFI中解析SMBIOS获取内存信息