SystemIO 冲突并非固件缺陷
摘要
本文解释了ACPI SystemIO冲突警告常被误认为是固件缺陷,详细说明了ACPI操作区域和互斥锁如何用于处理硬件访问冲突。
<p><a href="https://lobste.rs/s/jz8fmz/systemio_conflicts_are_not_firmware_bugs">评论</a></p>
查看缓存全文
缓存时间: 2026/09/10 02:13
# SystemIO 冲突不是固件缺陷
来源:https://codon.org.uk/~mjg59/blog/p/systemio-conflicts-are-not-firmware-bugs/
我原本在研究完全不相关的内容,但偶然发现一些搜索结果让我意识到,许多人仍然认为出现类似`ACPI 警告:SystemIO 范围 0x0000000000001828\-0x000000000000182F 与 OpRegion 0x0000000000001800\-0x000000000000187F 冲突`这样的错误意味着存在固件缺陷。这种看法通常是错误的。我们需要深入了解 ACPI 是什么,才能解释清楚原因。
高级配置与电源接口(ACPI)规范(https://uefi.org/specifications)1 (https://codon.org.uk/~mjg59/blog/p/systemio-conflicts-are-not-firmware-bugs/#fn:1)定义了大量内容,但此处对我们而言重要的是它提供的硬件抽象。虽然 PC 名义上是一个定义明确的平台,但一旦超越一定复杂度,硬件层面其实并非如此。例如,当挂起系统时,你希望以正确的顺序关闭硬件电源,而了解这个顺序需要你掌握特定主板设计的详细信息。嵌入式领域采用的方法是将这些知识以某种形式固化到操作系统中,这就是我们最终有设备树(https://devicetree.org/)的原因。ACPI 采取了另一种方法——它不是将信息作为必须由操作系统驱动程序处理的数据来提供,而是将其作为代码分发。
ACPI 源语言(ASL)是一种简单的语言,被编译成字节码,然后在运行时由操作系统解释执行。该语言的特性之一是能够定义“操作区域”,实际上是描述如何访问底层硬件的结构定义。让我们想象一个简单的设备,有两个暴露的寄存器。第一个是索引寄存器——它描述我们想访问哪个内部寄存器。第二个是数据寄存器,读取它会返回当前索引寄存器地址所指向的内部寄存器的值,写入它则修改该寄存器。一个操作区域声明示例大致如下:
```
OperationRegion(OPR1, SystemIO, 0x400, 0x2)
Field(OPR1, ByteAcc, NoLock, Preserve)
{
INDX, 8
DATA, 8
}
```
这定义了一个名为“OPR1”的操作区域,位于 IO 端口 0x400,长度为 2 字节。其中包含两个 8 位字段:INDX 和 DATA。它们需要逐一访问,ACPI 解释器在访问它们时不需要获取全局锁,并且如果寄存器的子集被修改,则应保留其他值(由于这里的字段只有一位宽,这点无关紧要)。现在,在此作用域内任何对 INDX 或 DATA 的引用都会触发对这些寄存器的访问。因此,读取寄存器 0x03 值的方法大致如下:
```
Method (RD03) {
INDX = 0x3
Return (DATA)
}
```
即,将 INDX 设置为 3,然后读取 DATA 的值并返回。但是!如果另一个 ACPI 方法同时运行呢?假设我们有一个写入寄存器 0x05 的方法:
```
Method (WR05, 1) {
INDX = 0x05
DATA = Arg1
}
```
如果 RD03 在 WR05 执行到一半时运行,会发生什么?INDX 可能被重置为 0x03,现在 WR05 将修改寄存器 0x03 而不是 0x05。糟糕!但我们可以避免这种情况——我们声明一个互斥锁(`Mutex (MUTX, 0x00)`),并将我们的方法更新如下:
```
Method (RD03) {
Acquire (MUTX, 0xFFFF)
INDX = 0x3
Local0 = DATA
Release (MUTX)
Return (Local0)
}
Method (WR05, 1) {
Acquire (MUTX, 0xFFFF)
INDX = 0x05
DATA = Arg1
Release (MUTX)
}
```
每个方法都获取一个锁(等待最多 0xffff 毫秒,如果失败则报错),然后执行访问。现在就不存在竞争条件了。呼!
现在假设有人为这个硬件编写了一个 Linux 驱动程序。它直接访问硬件,完全不知道 ACPI 的存在。有什么能阻止驱动程序与某个 ACPI 访问方法竞争呢?完全没有。哦不!又来了!顺便说一下,这并非假设——这里(https://bugzilla.kernel.org/show_bug.cgi?id=13620)有一个相对无害的例子,但在过去,我们确实遇到过温度监控芯片同时被固件和 Linux 访问的情况,结果你可能以为你在读取温度,实际上却在读取状态标志,导致出现不可能的高温和立即热关机。
在这种情况下,内核通过打印类似`ACPI 警告:SystemIO 范围 0x0000000000000400\-0x000000000000401 与 OpRegion 0x0000000000000400\-0x000000000000401 (OPR1) 冲突`的消息来挽救你(避免潜在的硬件损坏),该消息表明内核检测到某个驱动程序正尝试分配 IO 端口 0x400\-0x401,但存在一个名为 OPR1 的 ACPI 操作区域声称拥有相同的地址。内核无法知道固件在该区域可能执行何种类型的访问,因此假定这可能有危险,并阻止该驱动程序加载。
但并非毫无希望!内核还打印了一些有用的建议,`ACPI:如果此设备有可用的 ACPI 驱动程序,你应该使用它而不是原生驱动程序`。而且 ACPI 表实际上通常包含如下定义:
```
Device (HDW1)
{
Name (_HID, "VEND0001")
OperationRegion(OPR1, SystemIO, 0x400, 0x2)
Field(OPR1, ByteAcc, NoLock, Preserve)
{
INDX, 8
DATA, 8
}
Mutex (MUTX, 0)
Method (RD03) {
Acquire (MUTX, 0xFFFF)
INDX = 0x3
Local0 = DATA
Release (MUTX)
Return (Local0)
}
Method (WR05, 1) {
Acquire (MUTX, 0xFFFF)
INDX = 0x05
DATA = Arg1
Release (MUTX)
}
}
```
它定义了一个 ACPI 设备及其相关方法。`\_HID`字段定义了设备类型,可以编写一个 Linux 驱动程序,当看到类型为`VEND0001`的设备时自动加载。然后该驱动程序可以调用与该设备关联的 ACPI 方法,并以符合固件预期的方式访问资源。
(有兴趣编写这样的驱动程序吗?我早在 2009 年就写过一篇指南(https://lwn.net/Articles/367630/))
固件在这里绝对没有做错任何事2 (https://codon.org.uk/~mjg59/blog/p/systemio-conflicts-are-not-firmware-bugs/#fn:2),但尝试加载原生驱动程序会产生错误,互联网会告诉你 PC 固件开发者无能3 (https://codon.org.uk/~mjg59/blog/p/systemio-conflicts-are-not-firmware-bugs/#fn:3),你应该传递一个内核参数来覆盖这种行为,而它从未对他们造成伤害,可能也不会对你造成伤害,但它*可能*,而你可能永远不知道你的系统为何偶尔会死机或着火。
相似文章
Framework 回应 BIOS 更新导致 Ryzen 7040 笔记本电脑变砖的投诉
Framework 解决关于 BIOS 更新导致部分基于 Ryzen 7040 的笔记本电脑变砖的投诉,提供超出保修期的更换服务,并在即将发布的 BIOS 版本中引入“Crisis Recovery Mode”。
NX位不仅仅关乎安全
一位开发者描述了在postmarketOS的ARM64裸机虚拟机管理程序中调试一个复杂bug的过程,涉及指令缓存一致性问题和与NX位相关的硬件特定行为。
理解在试图绕过规则时规则背后的原理
本文来自微软的《老东西》博客,解释了Windows内核回调函数最佳实践背后的原理,特别是为什么阻塞或等待工作项会违背其目的,并通过一个关于驱动程序导致系统挂起的警示故事来说明。
利用超长中断攻破系统管理模式
一种新的攻击技术通过使用一个极其耗时的单条指令使处理器核心失同步,从而攻破x86 CPU的系统管理模式(SMM)安全,使攻击者能够在另一个核心仍处于受保护环境之外时执行SMM代码。针对Zen 3 Ryzen处理器的概念验证(PoC)已提供。
RISC-V:他们本应更清楚
Dmitry.GR 批评了RISC-V的设计,认为它并非对所有用例都最优,尤其是在微控制器领域,因为代码密度和中断延迟的问题。