3. RSDP 与 RSDT 解析:签名校验、校验和计算与表遍历
RSDP(Root System Description Pointer)是 ACPI 子系统的入口点,操作系统通过它找到 RSDT(Root System Description Table)或 XSDT(Extended System Description Table),进而遍历所有 ACPI 表。本章将深入解析 RSDP 的签名校验、校验和计算,以及 RSDT/XSDT 的地址解析与表遍历方法。
核心要点:RSDP 是 ACPI 表的“根指针”,其校验和机制保证了引导时数据的完整性;RSDT 与 XSDT 分别提供 32 位和 64 位的表地址数组,内核通过它们完成 ACPI 表的注册与遍历。
3.1 RSDP 结构解析与签名校验
RSDP 位于系统内存的低 1MB 区域(通常由 BIOS/UEFI 在引导时填充),其结构在 ACPI 1.0 中定义,ACPI 2.0 后扩展为 36 字节。内核通过扫描特定内存区域并验证签名来定位 RSDP。
| 偏移 | 长度(字节) | 字段 | 说明 |
|---|---|---|---|
| 0 | 8 | Signature | 固定为 "RSD PTR "(含尾随空格) |
| 8 | 1 | Checksum | 前 20 字节的校验和,必须为 0 |
| 9 | 6 | OEMID | OEM 厂商标识 |
| 15 | 1 | Revision | 0 表示 ACPI 1.0,2 表示 ACPI 2.0+ |
| 16 | 4 | RsdtAddress | RSDT 的 32 位物理地址 |
| 20 | 4 | Length | RSDP 总长度(ACPI 2.0+ 扩展字段) |
| 24 | 8 | XsdtAddress | XSDT 的 64 位物理地址 |
| 32 | 1 | Extended Checksum | 整个 RSDP 的校验和,必须为 0 |
| 33 | 3 | Reserved | 保留字段 |
签名校验流程:内核在物理内存 0x000E0000 ~ 0x000FFFFF(EBDA 区域)以及 0x000F0000 ~ 0x000FFFFF(系统 BIOS 区域)中,以 16 字节对齐扫描 "RSD PTR " 签名。找到后,立即对前 20 字节进行校验和验证。
实践提示:签名校验时务必检查尾随空格字符(0x20),部分固件可能填充其他空白字符,但标准要求严格匹配 8 字节 "RSD PTR "。
3.2 校验和计算与验证
ACPI 规范要求 RSDP 包含两个校验和字段:
- Checksum(偏移 8):覆盖 RSDP 前 20 字节(Signature + Checksum + OEMID + Revision + RsdtAddress)。计算方式为:所有字节相加后取低 8 位,结果必须为 0。
- Extended Checksum(偏移 32):覆盖整个 RSDP(Length 字段指示的实际长度,通常为 36 字节)。同样要求求和结果为 0。
常见陷阱:部分固件在 ACPI 2.0+ 模式下,RsdtAddress 可能为 0,此时必须使用 XsdtAddress。校验和验证时,如果 Revision < 2,则只验证前 20 字节的 Checksum。
校验和计算示例代码:
// RSDP 校验和验证函数
int acpi_verify_rsdp(struct acpi_table_rsdp *rsdp) {
u8 *bytes = (u8 *)rsdp;
u8 sum = 0;
u32 length;
// 验证前 20 字节的校验和
for (int i = 0; i < 20; i++)
sum += bytes[i];
if (sum != 0) {
pr_err("RSDP: 前 20 字节校验和无效\n");
return -EINVAL;
}
// 如果 Revision >= 2,验证扩展校验和
if (rsdp->revision >= 2) {
length = rsdp->length;
if (length < 36 || length > 4096) {
pr_err("RSDP: 无效的长度字段 %u\n", length);
return -EINVAL;
}
sum = 0;
for (int i = 0; i < length; i++)
sum += bytes[i];
if (sum != 0) {
pr_err("RSDP: 扩展校验和无效\n");
return -EINVAL;
}
}
return 0;
}
3.3 RSDT 与 XSDT 结构解析
RSDT 和 XSDT 是 ACPI 表的“目录”,它们包含指向其他 ACPI 表(如 DSDT、FACP、APIC 等)的物理地址数组。两者的区别在于地址宽度:
| 特性 | RSDT | XSDT |
|---|---|---|
| 引入版本 | ACPI 1.0 | ACPI 2.0 |
| 地址宽度 | 32 位 | 64 位 |
| 表头结构 | 标准 ACPI 表头(36 字节) | 标准 ACPI 表头(36 字节) |
| 地址数组 | u32 数组 | u64 数组 |
| 适用系统 | 32 位系统 | 64 位系统 |
| 表数量 | (Length - 36) / 4 | (Length - 36) / 8 |
标准 ACPI 表头结构(36 字节):
| 偏移 | 长度(字节) | 字段 | 说明 |
|---|---|---|---|
| 0 | 4 | Signature | 表签名(如 "RSDT"、"XSDT"、"FACP") |
| 4 | 4 | Length | 表总长度(含表头) |
| 8 | 1 | Revision | 表版本号 |
| 9 | 1 | Checksum | 整个表的校验和 |
| 10 | 6 | OEMID | OEM 标识 |
| 16 | 8 | OEM Table ID | OEM 表标识 |
| 24 | 4 | OEM Revision | OEM 版本 |
| 28 | 4 | Creator ID | 创建者 ID |
| 32 | 4 | Creator Revision | 创建者版本 |
3.4 表遍历方法
内核通过 RSDP 获取 RSDT/XSDT 地址后,遍历其地址数组,对每个表地址进行签名匹配和校验和验证,然后注册到 ACPI 表链表中。
遍历步骤:
- 获取根表:从 RSDP 的 RsdtAddress(32 位)或 XsdtAddress(64 位)读取 RSDT/XSDT 的物理地址。
- 映射表头:将物理地址映射到内核虚拟地址空间,读取表头(前 36 字节)。
- 验证签名:检查 Signature 是否为 "RSDT" 或 "XSDT"。
- 计算表数量:根据 Length 和地址宽度计算表项数量。
- 遍历地址数组:对每个表项,映射并验证其表头,然后注册到 ACPI 子系统。
3.5 内核实现要点
Linux 内核中,ACPI 表遍历的核心代码位于 drivers/acpi/tables.c。关键函数包括:
acpi_find_root_pointer():扫描内存定位 RSDP。acpi_tb_parse_root_table():解析 RSDT/XSDT 并注册所有子表。acpi_tb_install_table():将单个 ACPI 表安装到全局表列表中。
性能调优关联:电源性能调优需要访问 DSDT(Differentiated System Description Table)和 SSDT(Secondary System Description Table),这些表都通过 RSDT/XSDT 遍历获得。正确的表遍历顺序和校验和验证是后续调优的基础。
表遍历核心代码片段:
// 遍历 RSDT 并注册所有 ACPI 表
void acpi_tb_parse_root_table(acpi_physical_address address, u8 revision) {
struct acpi_table_header *table;
u32 table_count, i;
acpi_physical_address *entry;
// 映射 RSDT/XSDT 表头
table = acpi_os_map_memory(address, sizeof(struct acpi_table_header));
if (!table)
return;
// 计算表项数量
if (revision < 2) {
// RSDT: 32 位地址
table_count = (table->length - sizeof(struct acpi_table_header)) / 4;
entry = (acpi_physical_address *)(table + 1);
} else {
// XSDT: 64 位地址
table_count = (table->length - sizeof(struct acpi_table_header)) / 8;
entry = (acpi_physical_address *)(table + 1);
}
// 遍历并注册每个表
for (i = 0; i < table_count; i++) {
acpi_tb_install_table(entry[i]);
}
acpi_os_unmap_memory(table, sizeof(struct acpi_table_header));
}
调试技巧:在引导时添加
acpi=debug 内核参数,可以打印 ACPI 表遍历的详细信息,包括每个表的签名、地址和校验和状态。这对于排查表解析失败问题非常有效。
3.6 常见问题与避坑指南
| 问题 | 原因 | 解决方案 |
|---|---|---|
| RSDP 签名未找到 | 扫描区域被覆盖或固件损坏 | 检查内存映射,尝试扩展扫描范围 |
| 校验和失败 | 内存损坏或固件 bug | 使用 acpi=rsdp 手动指定 RSDP 地址 |
| RSDT/XSDT 表头无效 | 地址错误或表被覆盖 | 验证 RSDP 中的地址字段是否正确 |
| 表遍历顺序异常 | 固件未按规范排列表项 | 内核应支持乱序表项,但需校验签名 |