2. SMBIOS 数据结构基础
SMBIOS 的数据组织遵循一套严格的分层结构:系统固件在内存中首先放置一个 Entry Point 结构,用于定位和管理后续的 结构表;结构表由多个 结构表头 开头的数据块组成,每个数据块尾部跟随 字符串区域,并且整个结构表在内存中按 双字节对齐 规则排列。理解这三层基础是开发 SMBIOS 解析器、模拟器或调试工具的前提。
2.1 SMBIOS Entry Point 结构
Entry Point 是操作系统或调试工具查找 SMBIOS 信息的入口。根据 SMBIOS 规范版本,存在两种 Entry Point 格式:SMBIOS 2.1~2.7 使用的 32 位 Entry Point(位于物理内存 0xF0000~0xFFFFF 之间,以 "_SM_" 签名开头),以及 SMBIOS 3.0+ 使用的 64 位 Entry Point(以 "_SM3_" 签名开头,可位于 4GB 以上地址空间)。
| 字段 | 偏移 | 长度 | 说明 |
|---|---|---|---|
| Anchor String | 0x00 | 4 字节 | "_SM_" (0x5F534D5F) |
| Entry Point Structure Checksum | 0x04 | 1 字节 | 整个 Entry Point 结构的校验和 |
| Entry Point Length | 0x05 | 1 字节 | Entry Point 结构总长度,通常为 0x1F |
| Major Version | 0x06 | 1 字节 | SMBIOS 主版本号,如 3 |
| Minor Version | 0x07 | 1 字节 | SMBIOS 次版本号,如 5 |
| Maximum Structure Size | 0x08 | 2 字节 | 最大结构体大小,用于分配缓冲区 |
| Entry Point Revision | 0x0A | 1 字节 | 实现修订版本 |
| Formatted Area | 0x0B | 5 字节 | 保留或特定实现使用,通常填 0 |
| Intermediate Anchor String | 0x10 | 5 字节 | "_DMI_" (0x5F444D495F) |
| Intermediate Checksum | 0x15 | 1 字节 | 从 0x10 开始的中间区域校验和 |
| Structure Table Length | 0x16 | 2 字节 | 整个结构表的总字节数 |
| Structure Table Address | 0x18 | 4 字节 | 结构表在物理内存中的 32 位基地址 |
| Number of SMBIOS Structures | 0x1C | 2 字节 | 结构表中包含的结构总数 |
| SMBIOS BCD Revision | 0x1E | 1 字节 | BCD 格式的版本号,如 0x35 表示 3.5 |
SMBIOS_TABLE_GUID 或 SMBIOS3_TABLE_GUID 获取,不再需要扫描 0xF0000 区域。但在 Legacy BIOS 调试时,扫描内存并验证 "_SM_" 签名和校验和是第一步。
2.2 结构表头格式
每个 SMBIOS 结构都以一个 4 字节的固定表头 开始,紧接的是结构体特有的格式化数据区域。表头定义了结构的类型、长度和句柄,是解析所有结构体的统一入口。
| 字段 | 偏移 | 长度 | 说明 |
|---|---|---|---|
| Type | 0x00 | 1 字节 | 结构类型编号,如 Type 0=BIOS Information |
| Length | 0x01 | 1 字节 | 格式化区域长度(含表头),不包括字符串 |
| Handle | 0x02 | 2 字节 | 唯一标识符,用于交叉引用 |
表头之后是 格式化区域,其长度由 Length 字段指定。格式化区域中可能包含指向字符串区域的索引值(通常为 1 字节或 2 字节的编号),这些编号用于引用结构体末尾的字符串。
\0\0)来确定字符串区域的结束位置,从而定位下一个结构体的起始地址。
2.3 字符串区域与索引规则
字符串区域紧跟在每个结构的格式化数据之后,用于存储结构体中引用的所有字符串(如厂商名称、版本号、序列号等)。字符串区域采用 连续存储、双空终止 的布局方式。
- 存储顺序: 字符串按结构体格式化区域中出现的索引顺序依次存放,每个字符串以
\0结尾。 - 索引规则: 格式化区域中引用字符串的字段值为 1-based 索引。索引值 1 指向字符串区域中的第一个字符串,索引值 2 指向第二个,以此类推。索引值 0 表示该字段未使用或为空字符串。
- 区域终止: 字符串区域以 两个连续的
\0字节 结束,表示该结构体的字符串列表结束。 - 空字符串: 如果某个字符串字段的索引值为 0,解析器应返回空字符串或 NULL,而不是尝试从字符串区域读取。
/* 字符串区域解析示例:假设结构体格式化区域中引用了索引 1 和 2 */
/* 内存布局: "ACME Corp.\0" "Rev 1.0\0" "\0\0" */
/* 索引 1 -> "ACME Corp." */
/* 索引 2 -> "Rev 1.0" */
/* 索引 3 -> 无效,返回空字符串 */
\0 分隔符,而不是假设字符串按固定偏移排列。另外,字符串区域中的字符串 不包含 引号或额外转义字符,直接以原始 ASCII/UTF-8 编码存储。
2.4 双字节对齐规则
SMBIOS 规范要求整个结构表在内存中按 双字节(2 字节)对齐。这意味着每个结构体的结束位置(即下一个结构体的起始位置)必须落在偶数地址上。如果字符串区域结束后地址为奇数,则填充一个 \0 字节使其对齐。
| 场景 | 字符串区域结束地址 | 对齐填充 | 下一个结构体起始地址 |
|---|---|---|---|
| 偶数地址 | 0x1000 | 无填充 | 0x1000 |
| 奇数地址 | 0x1001 | 1 字节 \0 |
0x1002 |
对齐填充字节 不计入 前一个结构体的 Length 字段,也不属于字符串区域的一部分。解析器在计算下一个结构体偏移时,必须显式进行对齐处理。
/* 计算下一个结构体偏移的伪代码 */
uint8_t* next_structure(uint8_t* current_struct) {
uint8_t* formatted_end = current_struct + current_struct->Length;
uint8_t* string_end = formatted_end;
/* 扫描字符串区域直到双空终止符 */
while (!(string_end[0] == 0 && string_end[1] == 0)) {
string_end++;
}
string_end += 2; /* 跳过双空终止符 */
/* 双字节对齐 */
if ((uintptr_t)string_end % 2 != 0) {
string_end++;
}
return string_end;
}
2.5 整体数据结构关系图
下图展示了 SMBIOS 数据结构从 Entry Point 到结构表、再到单个结构体内部布局的完整层次关系:
2.6 结构表遍历流程
基于上述规则,解析 SMBIOS 结构表的完整流程如下:
- 定位 Entry Point: 在 Legacy BIOS 下扫描 0xF0000~0xFFFFF 区域查找 "_SM_" 签名;在 UEFI 下通过
gEfiSmbiosTableGuid或gEfiSmbios3TableGuid获取入口地址。 - 验证 Entry Point: 计算校验和,确认版本号,读取结构表基地址和总长度。
- 初始化遍历指针: 将指针指向结构表基地址,设置剩余长度计数器。
- 解析当前结构体: 读取 Type、Length、Handle,然后跳过 Length 指定的格式化区域。
- 扫描字符串区域: 从格式化区域末尾开始,查找双空终止符
\0\0,确定字符串区域结束位置。 - 应用双字节对齐: 如果字符串区域结束地址为奇数,则跳过 1 字节填充。
- 移动到下一个结构体: 更新指针到对齐后的地址,重复步骤 4~6,直到遍历完所有结构体或超出结构表总长度。
- 终止检测: 遇到 Type=127(End-of-Table)结构体时,停止遍历。