Debian 13 工控机 ACPI BIOS Error:从定位到永久修复

背景

一台采用 Intel J1900 平台的工控机主板启动 Debian 时,内核持续报告 ACPI 错误:

ACPI BIOS Error (bug): Could not resolve symbol [\_SB._OSC.CDW1], AE_NOT_FOUND
ACPI Error: Aborting method \_SB._OSC due to previous error (AE_NOT_FOUND)

系统能够继续启动,网络和服务也正常,但这并不代表 BIOS 中的 ACPI 代码没有问题。本文记录如何定位错误、修补 DSDT,并让补丁在系统和内核升级后自动生效。

本文对应的硬件和软件环境:

主板厂商:DS
主板型号:DS FJ04D
处理器平台:Intel J1900
BIOS 厂商:American Megatrends Inc.
BIOS 日期:2020-09-10
BIOS 界面版本:T60K0M11 T2
系统:Debian 13.6
内核:6.12.96+deb13-amd64
启动方式:UEFI
重要:本文生成的 DSDT 只适用于提取它的这块主板和当前 BIOS。不要把文中的 DSDT.aml 用到其他主板,即使处理器和接口看起来相同。

1. 确认错误来源

查看完整的内核日志:

journalctl -k -b --no-pager |
grep -i -A10 -B5 -E 'ACPI BIOS Error|AE_NOT_FOUND|Could not resolve'

关键输出如下:

ACPI BIOS Error (bug): Could not resolve symbol [\_SB._OSC.CDW1], AE_NOT_FOUND
ACPI Error: Aborting method \_SB._OSC due to previous error (AE_NOT_FOUND)

\_SB._OSC 是固件与操作系统协商平台能力的方法。AE_NOT_FOUND 表示该方法运行时引用了尚未定义的 CDW1 对象。这是 BIOS 提供的 AML 代码错误,不是 Debian 安装损坏。

查看硬件信息:

dmidecode -s system-manufacturer
dmidecode -s system-product-name
dmidecode -s baseboard-manufacturer
dmidecode -s baseboard-product-name
dmidecode -s bios-vendor
dmidecode -s bios-version
dmidecode -s bios-release-date

如果当前用户没有 sudo,可以先执行 su - 切换到 root。本文后续命令均以 root 身份执行。

2. 提取并反编译 ACPI 表

安装 ACPICA 工具:

apt update
apt install acpica-tools

提取 ACPI 表:

mkdir -p /root/acpi-fix
cd /root/acpi-fix
acpidump -b

同时加载 SSDT,反编译 DSDT:

iasl -e ssdt*.dat -d dsdt.dat
cp dsdt.dsl dsdt.backup.dsl

查找 _OSCCDW1

grep -n -A80 -B10 'Method (_OSC' dsdt.dsl
grep -n -A10 -B10 'CDW1' dsdt.dsl

3. ACPI 错误的真正原因

主板 DSDT 中存在两个 _OSC 方法。\_SB.PCI0._OSC 的写法正确,出错的是平台级 \_SB._OSC

原始代码的结构如下:

Method (_OSC, 4, NotSerialized)
{
    If ((Arg0 == ToUUID ("0811b06e-4a27-44f9-8d60-3cbbc22e7b48")))
    {
        CreateDWordField (Arg3, Zero, CDW1)
        CreateDWordField (Arg3, 0x04, CDW2)
        // ...
    }
    Else
    {
        CDW1 |= 0x04
        Return (Arg3)
    }
}

CDW1 只在 UUID 匹配的 If 分支里创建,但 UUID 不匹配的 Else 分支同样会访问它。Linux 传入另一 UUID 时直接进入 Else,此时 CDW1 不存在,于是出现 AE_NOT_FOUND

修复方式是把 CDW1 的创建移到条件判断之前:

Method (_OSC, 4, NotSerialized)
{
    CreateDWordField (Arg3, Zero, CDW1)

    If ((Arg0 == ToUUID ("0811b06e-4a27-44f9-8d60-3cbbc22e7b48")))
    {
        CreateDWordField (Arg3, 0x04, CDW2)
        // ...
    }
    Else
    {
        CDW1 |= 0x04
        Return (Arg3)
    }
}

这样无论进入哪个分支,CDW1 都已经存在。

4. 处理新版 iasl 检出的旧代码

重新编译时,新版 iasl 还发现原厂 DSDT 中有一处与 _OSC 无关的旧式写法:MCHK 方法动态创建 DCFE 字段,另一个方法却通过 ^^MCHK.DCFE 跨方法访问它。

错误类似:

Object is created temporarily in another method and cannot be accessed (^^MCHK.DCFE)

原始方法:

Method (MCHK, 0, Serialized)
{
    If ((MADR != 0xFFFFFFFF))
    {
        OperationRegion (IGMM, SystemMemory, MADR, 0x3000)
        Field (IGMM, AnyAcc, NoLock, Preserve)
        {
            Offset (0x20C8),
                ,   4,
            DCFE,   4
        }
    }
}

将它改成直接返回字段值:

Method (MCHK, 0, Serialized)
{
    If ((MADR != 0xFFFFFFFF))
    {
        OperationRegion (IGMM, SystemMemory, MADR, 0x3000)
        Field (IGMM, AnyAcc, NoLock, Preserve)
        {
            Offset (0x20C8),
                ,   4,
            DCFE,   4
        }

        Return (DCFE)
    }

    Return (Zero)
}

然后把:

DerefOf (CDCT [^^MCHK.DCFE])

改成:

DerefOf (CDCT [MCHK ()])

这一步只是为了让旧 DSDT 能通过新版编译器检查,并不是最初 ACPI 报错的根因。

5. 提升 OEM Revision

Linux 的 ACPI table upgrade 机制不会用相同版本的表覆盖 BIOS 表。原厂 DSDT 的 OEM Revision 是:

0x01072009

因此需要把 DefinitionBlock 中的版本提高,例如:

DefinitionBlock ("", "DSDT", 2, "ALASKA", "A M I ", 0x0107200A)

可以精确替换第一次出现的版本号:

sed -i '0,/0x01072009/s//0x0107200A/' dsdt.dsl
grep -m1 'DefinitionBlock' dsdt.dsl

重新编译:

iasl -tc dsdt.dsl 2>&1 | tee compile.log
grep -E 'Compilation successful|Compilation failed' compile.log

预期结果:

Compilation successful. 0 Errors

检查编译后 AML 的 OEM Revision:

od -An -tx4 -j24 -N4 dsdt.aml

预期输出:

0107200a

编译器仍可能报告原厂表中的 Warnings 和 Remarks,只要最终是 0 Errors 并生成 dsdt.aml,就不妨碍继续测试。

6. 制作一次性 initramfs override

内核要求 ACPI override 位于 initramfs 最前面的未压缩 newc CPIO 中,路径必须是:

kernel/firmware/acpi/DSDT.aml

制作 CPIO:

cd /root/acpi-fix
mkdir -p initrd-override/kernel/firmware/acpi
cp dsdt.aml initrd-override/kernel/firmware/acpi/DSDT.aml

cd initrd-override
find kernel -print | cpio --quiet -H newc -o > ../acpi_override.cpio
cd ..

保留当前内核的原始 initramfs,然后将 CPIO 放到最前面:

KVER="$(uname -r)"
cp -a "/boot/initrd.img-$KVER" "/boot/initrd.img-$KVER.no-acpi"

cat acpi_override.cpio "/boot/initrd.img-$KVER.no-acpi" \
    > "/boot/initrd.img-$KVER"

确认文件已进入 initramfs:

lsinitramfs "/boot/initrd.img-$KVER" |
grep 'kernel/firmware/acpi/DSDT.aml'

7. 重启并验证

重启后检查:

journalctl -k -b --no-pager |
grep -iE 'Table Upgrade|Physical table override|AE_NOT_FOUND|ACPI BIOS Error'

成功时应看到:

ACPI: Table Upgrade: override [DSDT-ALASKA-  A M I ]
ACPI: DSDT ... Physical table override, new table: ...

并且不再出现:

AE_NOT_FOUND
ACPI BIOS Error

还应检查服务和关键设备:

systemctl --failed
ip link
journalctl -k -b -p err --no-pager

8. 让内核升级后自动应用补丁

普通系统升级不会修改 BIOS,但安装新内核或执行 update-initramfs 会重新生成 initramfs。为了避免补丁丢失,可以安装一个 post-update 钩子。

保存已经验证过的 CPIO:

mkdir -p /etc/initramfs/post-update.d
mkdir -p /usr/local/lib/acpi

cp /root/acpi-fix/acpi_override.cpio \
   /usr/local/lib/acpi/acpi_override.cpio

创建 /etc/initramfs/post-update.d/acpi-dsdt-override

#!/bin/sh
set -eu

KVER="${1:-}"
INITRD="${2:-/boot/initrd.img-$KVER}"
ARCHIVE="/usr/local/lib/acpi/acpi_override.cpio"

[ -n "$KVER" ] || exit 0
[ -f "$INITRD" ] || exit 0
[ -f "$ARCHIVE" ] || exit 0

if lsinitramfs "$INITRD" 2>/dev/null |
    grep -qx 'kernel/firmware/acpi/DSDT.aml'
then
    exit 0
fi

cp -a "$INITRD" "$INITRD.no-acpi"
cat "$ARCHIVE" "$INITRD.no-acpi" > "$INITRD.acpi-new"
chmod --reference="$INITRD.no-acpi" "$INITRD.acpi-new"
chown --reference="$INITRD.no-acpi" "$INITRD.acpi-new"
mv "$INITRD.acpi-new" "$INITRD"

授权并检查:

chmod 0755 /etc/initramfs/post-update.d/acpi-dsdt-override
sh -n /etc/initramfs/post-update.d/acpi-dsdt-override

测试:

update-initramfs -u -k "$(uname -r)"

lsinitramfs "/boot/initrd.img-$(uname -r)" |
grep 'kernel/firmware/acpi/DSDT.aml'

update-initramfs 在 J1900 上可能需要几分钟,不要中途按 Ctrl+C

9. 本次排障中的两个关键坑

只找到表,不代表已经覆盖

第一次启动时日志只有:

ACPI: DSDT ACPI table found in initrd

但错误仍然存在。这表示内核发现了新表,却没有采用。原因是修补表与原厂表的 OEM Revision 相同。提高 OEM Revision 后,日志才出现 Table Upgrade: override

Secure Boot 并不是本次原因

可以通过以下命令检查相关条件:

grep CONFIG_ACPI_TABLE_UPGRADE "/boot/config-$(uname -r)"
cat /sys/kernel/security/lockdown 2>/dev/null || true
mokutil --sb-state

本机结果是:

CONFIG_ACPI_TABLE_UPGRADE=y
[none] integrity confidentiality
SecureBoot disabled

因此内核支持 override,且没有被 Secure Boot 或 lockdown 阻止。

10. 回退和维护

每次钩子运行都会保留未注入补丁的文件:

/boot/initrd.img-<内核版本>.no-acpi

同时应保留:

/root/acpi-fix/
/usr/local/lib/acpi/acpi_override.cpio
/etc/initramfs/post-update.d/acpi-dsdt-override

如果新内核启动异常,可以在 GRUB 的 Advanced options for Debian 中选择旧内核。

如果以后升级主板 BIOS,应先停用钩子,因为新版 BIOS 的 DSDT 可能已经变化或已修复问题:

chmod 0644 /etc/initramfs/post-update.d/acpi-dsdt-override
update-initramfs -u -k all

更换主板后也必须移除该补丁。DSDT 与具体主板和 BIOS 强绑定,不能跨硬件复用。

总结

这次故障的根因不是 Linux ACPI 实现,而是 BIOS 的 \_SB._OSC 方法存在变量作用域错误:Else 分支引用了只在 If 分支中创建的 CDW1

最终方案没有使用 acpi=offpci=noacpi 等可能破坏电源管理和中断路由的启动参数,也没有隐藏日志,而是:

  1. 提取原厂 ACPI 表;
  2. 修复 DSDT 中的 AML 逻辑;
  3. 提高 OEM Revision;
  4. 通过 Linux ACPI table upgrade 覆盖原厂 DSDT;
  5. 使用 initramfs post-update 钩子保证内核升级后自动生效。

最终内核明确确认:

ACPI: Table Upgrade: override
ACPI: Physical table override

原来的 \_SB._OSC.CDW1 / AE_NOT_FOUND 错误随之消失。