技术宅的结界

 找回密码
 立即注册→加入我们

QQ登录

只需一步,快速开始

搜索
热搜: 下载 VB C 实现 编写
查看: 108|回复: 1
收起左侧

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

[复制链接]

39

主题

111

帖子

5282

积分

用户组: 管理员

UID
1043
精华
20
威望
254 点
宅币
4072 个
贡献
491 次
宅之契约
0 份
在线时间
882 小时
注册时间
2015-8-15
发表于 2020-10-13 14:20:44 | 显示全部楼层 |阅读模式

欢迎访问技术宅的结界,请注册或者登录吧。

您需要 登录 才可以下载或查看,没有帐号?立即注册→加入我们

x
写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,0x0010000)的区间中,以16字节的对齐粒度暴力搜索字符串"_SM_"。
但本文既然是在说UEFI,那当然得按照UEFI的标准来。在入口函数的SystemTable参数中,结构体里有个成员叫ConfigurationTable,这是个结构体数组,其结构体定义如下:
[C] 纯文本查看 复制代码
typedef struct{
EFI_GUID VendorGuid;
VOID *VendorTable;
} EFI_CONFIGURATION_TABLE;

在查找SMBIOS时,我们要找到数组的一个特定元素,这个特定元素的VendorGuid是一个特定的GUID。这个特定的GUID当然代表SMBIOS。根据UEFI文档和SMBIOS标准,这个GUID的定义是:
[C] 纯文本查看 复制代码
#define SMBIOS_TABLE_GUID \
{0xeb9d2d31,0x2d88,0x11d3,\
{0x9a,0x16,0x00,0x90,0x27,0x3f,0xc1,0x4d}}

那么通过遍历ConfigurationTable数组,我们就能拿到SMBIOS表的地址。代码如下:
[C] 纯文本查看 复制代码
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库中似乎没有造过这个轮子,所以只能自己造了,代码如下:
[C] 纯文本查看 复制代码
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表的结构体定义:
[C] 纯文本查看 复制代码
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的表项的组织方式是连续排列,即每个表项在地址上是连续的。每个表项的结构均一分为三:表头,定长值表项,字符串数组。
表头是一个简单的结构体,每个类型的表项表头的定义都一样,其定义如下:
[C] 纯文本查看 复制代码
typedef struct {
  SMBIOS_TYPE    Type;
  UINT8          Length;
  SMBIOS_HANDLE  Handle;
} SMBIOS_STRUCTURE;

其中Type是表项类型,Length是表头加定长值表项但不包括字符串数组的字节数,Handle是可用于区分表项的“句柄”。
不同类型的表项,定长值表项的定义也不同。本文讲的是查询内存信息,那么相关定义如下:
[C] 纯文本查看 复制代码
#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字符串,而下一个字符串就紧跟在零结尾之后。最后一个字符串以两个零字节结尾,标识了这个表项的结束。
那么综上所述,计算表项长度的函数代码如下:
[C] 纯文本查看 复制代码
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获取内存信息的代码如下:
[C] 纯文本查看 复制代码
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虚拟机里跑跑看,如图所示:
General UEFI Development-2020-10-13-03-55-40.png
然后再丢进钓鱼派里跑跑看,由于钓鱼派有一个UART接口,那么可以直接用PuTTY来玩控制台,而不需要接采集卡,如图所示:
MinnowBoard PuTTY 20201013 040615.PNG
丢进巫毒派里跑跑看,可以接到采集卡上,然后用OBS截图,如图所示:
Udoo Bolt 20201013 135320.PNG


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

评分

参与人数 2威望 +20 宅币 +60 贡献 +20 收起 理由
watermelon + 10 + 30 + 10 赞!
0xAA55 + 10 + 30 + 10 弹幕流届不到的爱

查看全部评分

flowers for Broken spirits - a woman turned into stake will hold the world in the basin of fire.
回复

使用道具 举报

36

主题

168

帖子

7420

积分

用户组: 管理员

UID
77
精华
11
威望
126 点
宅币
6802 个
贡献
143 次
宅之契约
0 份
在线时间
124 小时
注册时间
2014-2-22
发表于 2020-10-14 06:53:34 | 显示全部楼层
经典提问:这玩意可以用来过保护吗?

本版积分规则

QQ|申请友链||Archiver|手机版|小黑屋|技术宅的结界 ( 滇ICP备16008837号 )|网站地图  

GMT+8, 2020-10-28 10:03 , Processed in 0.100589 second(s), 36 queries , Gzip On.

Powered by Discuz! X3.4

Copyright © 2001-2020, Tencent Cloud.

快速回复 返回顶部 返回列表