- UID
- 1043
- 精华
- 积分
- 11692
- 威望
- 点
- 宅币
- 个
- 贡献
- 次
- 宅之契约
- 份
- 最后登录
- 1970-1-1
- 在线时间
- 小时
|
写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[i].VendorGuid,&gEfiSmbiosTableGuid)==0)
- {
- SmBiosTable=(SMBIOS_TABLE_ENTRY_POINT*)gST->ConfigurationTable[i].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[i]>Guid2->Data4[i])
- return 1;
- else if(Guid1->Data4[i]<Guid2->Data4[i])
- return -1;
- }
- return 0;
- }
复制代码
拿到SMBIOS表之后我们就要开始解析SMBIOS了。首先先看看SMBIOS表的结构体定义:
- typedef struct {
- UINT8 AnchorString[4];
- UINT8 EntryPointStructureChecksum;
- UINT8 EntryPointLength;
- UINT8 MajorVersion;
- UINT8 MinorVersion;
- UINT16 MaxStructureSize;
- UINT8 EntryPointRevision;
- UINT8 FormattedArea[5];
- UINT8 IntermediateAnchorString[5];
- UINT8 IntermediateChecksum;
- UINT16 TableLength;
- UINT32 TableAddress;
- UINT16 NumberOfSmbiosStructures;
- 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_HANDLE Handle;
- } 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[i]!='\0' || StringPool[i+1]!='\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[0x10];
- 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[i],Len-Entry->Length-i)+1;
- if(Strings[i]=='\0')break;
- StringPool[j++]=&Strings[i];
- }
- // Memory Module Information in Strings
- CHAR8* DeviceLocator=StringPool[MemModInfo->DeviceLocator];
- CHAR8* BankLocator=StringPool[MemModInfo->BankLocator];
- CHAR8* Manufacturer=StringPool[MemModInfo->Manufacturer];
- CHAR8* SerialNumber=StringPool[MemModInfo->SerialNumber];
- CHAR8* AssetTag=StringPool[MemModInfo->AssetTag];
- CHAR8* PartNumber=StringPool[MemModInfo->PartNumber];
- CHAR8* MemoryType=MemTypeList[MemModInfo->MemoryType];
- CHAR8* FormFactor=MemFormList[MemModInfo->FormFactor];
- 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[i]);
- 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 |
|