Skip to content

Instantly share code, notes, and snippets.

@buzztaiki
Created August 24, 2026 09:02
Show Gist options
  • Select an option

  • Save buzztaiki/a64bf98f3fe6789015fe200847e01f36 to your computer and use it in GitHub Desktop.

Select an option

Save buzztaiki/a64bf98f3fe6789015fe200847e01f36 to your computer and use it in GitHub Desktop.
ACPI の DSDT にパッチを当てて ACPI BIOS Error を解消する話

Claude が全部調べてくれました。Dynabook に ArchLinux をインストール 2026 で書いたラップトップのお話。

DSDT パッチ手順: _Q01CHRG 未定義参照を修正

対象

項目
機種 dynabook (DSDT OEM ID: DYNABO, Table ID: OK0119, 20231019)
OS Arch Linux
カーネル 7.1.4-arch1-1
ACPICA 20251212

修正する問題

_Q01(EC クエリハンドラ)が、名前空間に存在しない CHRG に対して Notify を発行しているため、メソッドが毎回中断される。

ACPI BIOS Error (bug): Could not resolve symbol [\_SB.PC00.LPCB.EC._Q01.CHRG], AE_NOT_FOUND (20251212/psargs-332)
ACPI Error: Aborting method \_SB.PC00.LPCB.EC._Q01 due to previous error (AE_NOT_FOUND) (20251212/psparse-529)

DSDT および SSDT1 から SSDT15 まで全16テーブルを逆アセンブルして確認したところ、Device (CHRG) の実体はどこにも存在しない。External 宣言のみ(逆アセンブラが未解決参照に対して自動生成したもの)。

同じメソッド内で ADP1 には CondRefOf ガードが付いているのに CHRG には付いていないため、BIOS 側の書き漏らしと判断できる。

準備

テーブルの取得

mkdir -p /tmp/acpi && cd /tmp/acpi
sudo cp /sys/firmware/acpi/tables/DSDT /sys/firmware/acpi/tables/SSDT* .
sudo chown $USER: *

逆アセンブル

iasl -d DSDT

外部制御メソッドの未解決に関する警告が出るが、パッチ目的では無視して構わない。

-e SSDT* を付けて参照解決を試みると、SSDT3 (SaSsdt) の _S0W が他テーブルと二重定義になり AE_ALREADY_EXISTS で停止する。実機では条件付きロードのため衝突しないが、iasl に一括で渡すと衝突する。参照解決込みが必要な場合は SSDT3 を除外する。

パッチ内容

cp DSDT.dsl DSDT-patched.dsl
grep -n 'Notify (CHRG, 0x80)' DSDT-patched.dsl

_Q01 内の該当行のみを修正する。

変更前

            Method (_Q01, 0, Serialized)  // _Qxx: EC Query, xx=0x00-0xFF
            {
                DSTR ("_Q01")
                UAAS ()
                If (CondRefOf (\_SB.ADP1))
                {
                    Notify (ADP1, 0x80) // Status Change
                }

                UABS ()
                Notify (BAT1, 0x81) // Information Change
                DSTR ("_Q01 Notify Battery Charger with 0x80")
                Notify (CHRG, 0x80) // Status Change
            }

変更後

            Method (_Q01, 0, Serialized)  // _Qxx: EC Query, xx=0x00-0xFF
            {
                DSTR ("_Q01")
                UAAS ()
                If (CondRefOf (\_SB.ADP1))
                {
                    Notify (ADP1, 0x80) // Status Change
                }

                UABS ()
                Notify (BAT1, 0x81) // Information Change
                DSTR ("_Q01 Notify Battery Charger with 0x80")
                If (CondRefOf (CHRG))
                {
                    Notify (CHRG, 0x80) // Status Change
                }
            }

触ってはいけない箇所

DSDT 内には CHRG への Notify がもう一箇所ある。

32126:  Notify (\_SB.PC00.LPCB.H_EC.CHRG, 0x80) // Status Change

こちらは H_EC スコープの絶対パス指定で _Q01 とは別系統。今回の修正対象ではない。

OEM Revision を上げる(必須)

これを忘れるとオーバーライドが起きない。 カーネルは、initrd に置いたテーブルがプラットフォームのテーブルと Signature、OEMID、OEM Table ID のすべてで一致する場合、より新しい OEM Revision を持つときにのみ置き換える。同じ値のままだと「initrd に見つけた」というログは出るが、適用はされない。

DefinitionBlock の最後の引数が OEM Revision。

grep -n 'DefinitionBlock' DSDT-patched.dsl
DefinitionBlock ("", "DSDT", 2, "DYNABO", "OK0119", 0x20231019)   ← 変更前
DefinitionBlock ("", "DSDT", 2, "DYNABO", "OK0119", 0x20231020)   ← 変更後

値は元より大きければよい。日付形式に見えるが、単なる 32bit 整数として比較される。

コンパイル

iasl -tc DSDT-patched.dsl
ls -l DSDT-patched.aml

未解決 External に起因するエラーが出ても .aml は生成されることが多い。生成されない場合は以下を試す。

iasl -f -tc DSDT-patched.dsl

それでも通らない場合、逆アセンブラが自動生成した External 宣言の型推定が誤っている可能性がある。エラー行の External 宣言を削除するか型を修正する。

initrd への差し込み

Arch のカーネルは CONFIG_ACPI_TABLE_UPGRADE=y なので、initrd 先頭に非圧縮 cpio として置けば上書きされる。

cd /tmp/acpi
mkdir -p kernel/firmware/acpi
cp DSDT-patched.aml kernel/firmware/acpi/dsdt.aml
find kernel | cpio -H newc --create > acpi_override.img
sudo cp acpi_override.img /boot/

ディレクトリ構成は kernel/firmware/acpi/dsdt.aml である必要がある。ファイル名は小文字。

cpio 自体の名前は任意だが、/boot に既にある intel-ucode.imginitramfs-linux.img と揃えて .img を付けている。マイクロコードも同じ「非圧縮 cpio を initrd の先頭に置く」仕組みなので、性質としても同類。

ブートローダ設定 (systemd-boot)

acpi_override.img を通常の initramfs より先に読み込ませる。順序が逆だと適用されない。

既存のエントリは書き換えず、パッチ適用版を別エントリとして追加する。素の DSDT で起動する経路を常に残しておけば、切り分けも復旧も再起動だけで済む。

cd /boot/loader/entries
sudo cp arch.conf arch-acpi-patched.conf   # 既存のファイル名に合わせる

追加したエントリに initrd /acpi_override.img の行を挿入し、title を変更する。

title   Arch Linux (ACPI DSDT patched)
linux   /vmlinuz-linux
initrd  /acpi_override.img
initrd  /intel-ucode.img
initrd  /initramfs-linux.img
options root=... rw

options 行は元のエントリからそのままコピーすること。

acpi_override.imgintel-ucode.img よりも前に置くこと。 実測で、intel-ucode.img を先に置いた構成では DSDT が読み込まれなかった(ACPI: DSDT ACPI table found in initrd が出ず、/sys/firmware/acpi/tables/DSDT もパッチ前のまま)。順序を入れ替えたところ読み込まれるようになった。理由は未特定だが、マイクロコードの cpio が 15MB と大きいことが関係している可能性がある。

この順序でもマイクロコードの早期ロードは正常に動作する(microcode: Updated early from: ... が出る)。両方が読み込まれていることを起動後に確認すること。

デフォルトで選択させるなら /boot/loader/loader.conf に指定する。

default arch-acpi-patched.conf

この環境は /boot がそのまま ESP になっているので、パスは / 起点でそのまま書ける。

エントリは手書きの .conf なので、カーネル更新で書き換えられることはない。

検証

再起動後に以下を実行する。

# initrd から DSDT が読まれたか
sudo dmesg | grep -iE 'found in initrd|Physical table override|ACPI OVERRIDE'

# マイクロコードの早期ロードも生きているか
sudo dmesg | grep -i microcode

# バイナリ一致の確認(最も確実)
sudo cmp /sys/firmware/acpi/tables/DSDT ~/acpi-patch/DSDT-patched.aml && echo "パッチ適用済み"

# エラーが消えたか
journalctl -k -b | grep -iE 'CHRG|_Q01|AE_NOT_FOUND'

# EC の GPE 頻度(適用前は 60 秒あたり約 997)
grep -H . /sys/firmware/acpi/interrupts/gpe6E
sleep 60
grep -H . /sys/firmware/acpi/interrupts/gpe6E

成功時の出力例。

[    0.010009] ACPI: DSDT ACPI table found in initrd [kernel/firmware/acpi/dsdt.aml][0xb77fc]
[    0.0100xx] ACPI: Override [DSDT-DYNABO], this is unsafe: tainting kernel
[    0.0100xx] ACPI: Table Upgrade: override [DSDT-DYNABO-OK0119]
[    1.188335] microcode: Current revision: 0x0000043b
[    1.188336] microcode: Updated early from: 0x00000430

0xb77fc は 751612 で、パッチ版 AML のサイズと一致する。

判定の要点は2段階ある。

  1. found in initrd が出るか。出なければ initrd の順序を疑う(acpi_override.imgintel-ucode.img より前に)
  2. tainting kernel / Table Upgrade が出るか。found in initrd だけで止まる場合は OEM Revision を上げ忘れている

cmp が一致すれば確実に適用されている。差分が byte 5(Length フィールド)から出る場合は未適用。

_Q01 は EC のイベントで呼ばれるため、起動直後に AE_NOT_FOUND が出ていないだけでは判断できない場合がある。この環境では 20 秒から 30 秒おきに規則的に出ていた。確実を期すなら AC アダプタを抜き差しして _Q01 を発火させてから確認する。

echo 'file drivers/acpi/ec.c +p' | sudo tee /sys/kernel/debug/dynamic_debug/control
# ここで AC を抜き差し
sleep 20
echo 'file drivers/acpi/ec.c -p' | sudo tee /sys/kernel/debug/dynamic_debug/control
journalctl -k --since '1 minute ago' | grep -iE 'Query|AE_NOT_FOUND'

Query(0x01) startedstopped の間にエラーが挟まらなければ成功。

バッテリーと AC の状態が正常に取れることも確認する。

cat /sys/class/power_supply/*/status /sys/class/power_supply/*/capacity 2>/dev/null

保存しておくファイル

/tmp は再起動で消えるため、照合用に永続的な場所へ退避しておく。

mkdir -p ~/acpi-patch
cp /tmp/acpi/{DSDT,DSDT.dsl,DSDT-patched.dsl,DSDT-patched.aml,acpi_override.img} ~/acpi-patch/

素の DSDT バイナリも残しておくと、BIOS 更新後の比較対象になる。

メンテナンス

カーネル更新では作り直し不要。 acpi_override.img は独立した cpio で、AML バイナリのみを含み、カーネルとリンクしていない。CONFIG_ACPI_TABLE_UPGRADE=y である限り、どのカーネルバージョンでも同じように読まれる。

カーネル更新後に念のため確認するなら、上記の cmp を流せばよい。

BIOS を更新する場合

古いパッチを当てたまま BIOS を更新してはいけない。 override は無条件に採用されるため、新しい DSDT が古いもので置き換えられる。「上書きされるだけ」では済まない。

想定されるリスクは2つある。

SSDT との不整合。 SSDT は新しい BIOS から供給されるが、DSDT だけが古いまま残る。DSDT は SSDT 側の名前を大量に参照しているため、名前空間の構成が変われば AE_NOT_FOUNDAE_ALREADY_EXISTS が続出する。今回直そうとしているのと同じ種類の問題を自分で作ることになる。

ハードウェアとの不整合。 DSDT の OperationRegion は EC のレジスタオフセットや MMIO アドレスを直接指している。BIOS 更新で EC ファームウェアのレイアウトが変われば、古い AML が新しい EC の別の意味を持つオフセットに書き込む。バッテリー充電制御やサーマル制御が絡む領域なので、単なるエラーでは済まない可能性がある。

カーネル側が OEM ID や Table ID を照合して古いテーブルを拒否するかは未確認。BIOS 更新で Table ID が変わらないケースもあるため、防護としては当てにしない。

手順

  1. パッチなしのエントリで起動する(別エントリにしてあるので選ぶだけ)
  2. BIOS を更新する
  3. 素の状態で journalctl -k -b | grep AE_NOT_FOUND を確認する
  4. _Q01 の問題が解消していればパッチ不要。残っていればテーブルを取り直してパッチを作り直す

3 を先にやること。ベンダーが修正している可能性があるので、無条件にパッチを再適用しない。

なお、この機種の BIOS 更新は Windows 専用の exe でしか実行できず、Linux 単体機では Windows To Go 等の準備が要る。うっかり更新されることはないので、上記を更新手順の一部として覚えておけば足りる。

復旧

パッチが原因で起動しなくなった場合、systemd-boot のメニューでパッチなしのエントリを選べば元の DSDT で起動する。

エントリを分けていない場合は、メニューで e を押して一時編集し、initrd /acpi_override.img の行を削除する。物理的にファイルを消す必要はない。

ACPI Large Reference Count flood 調査メモ

dynabook + Arch Linux で発生する ACPI Warning: Large Reference Count の大量出力について、原因を追った記録。

環境

項目
機種 dynabook (DSDT OEM: DYNABO / OK0119 / 20231019)
OS Arch Linux
カーネル 7.1.4-arch1-1
ACPICA 20251212
ホスト名 colts

症状

観測されたログ

ACPI Warning: Large Reference Count (0x8A12) in object 000000002a2bceae, Type=0x02 Operation=Decrement (20251212/utdelete-454)
ACPI Warning: Large Reference Count (0x7B40) in object 00000000739dd9bc, Type=0x01 Operation=Increment (20251212/utdelete-454)
  • Type=0x02ACPI_TYPE_STRINGType=0x01ACPI_TYPE_INTEGER
  • 閾値は ACPI_MAX_REFERENCE_COUNT = 0x1000 (4096)
  • 一度閾値を超えたオブジェクトは、以降参照カウントを触るたびに毎回警告を出す

実害

単なる dmesg ノイズではない。

  • printk storm によりコンソールが実質的に使用不能になる
  • journald が大量のメッセージを取りこぼす(Missed 10429 kernel messages など)
  • ジャーナルがローテーションで押し流され、発生時点より前のログが失われる
  • シャットダウンが完了しなくなる

last -x reboot の履歴では、複数回にわたって crash(クリーンシャットダウン失敗)が記録されていた。

reboot   system boot  7.1.4-arch1-1    Sun Aug  9 21:09 - 13:50 (12+16:41)
reboot   system boot  7.1.4-arch1-1    Sun Jul 26 12:22 - crash (14+08:46)
reboot   system boot  7.1.3-arch1-3    Thu Jul 16 09:33 - crash (6+00:12)

発症条件

  • 連続稼働 6 日から 2 週間程度で発生
  • monotonic タイムスタンプで確認したところ、8/9 起動の boot では稼働 524927 秒(約6日1時間48分)の時点ですでに flood 中
  • 起動直後は蓄積ゼロ。約1時間後の時点で参照カウント 64 を超えるオブジェクトは存在しなかった

数値の読み解き

Missed メッセージ数との整合

0x8A12 (35346) → 0x6154 (24916)  差 10430、間に "Missed 10429 kernel messages"
0x6154 → 0x614C                  差 8、    間に "Missed 7"
0x614C → 0x6147                  差 5、    間に "Missed 4"

1 回の参照カウント更新につき 1 警告。つまり flood の正体は「3万回の異常事象」ではなく、1個のオブジェクトが閾値の上に居座り、以降の全アクセスがログ化されている状態。

バースト速度

Increment 側のログ 20 行から計算。

  • 経過時間: 311 マイクロ秒
  • カウント増加: 89
  • 毎秒約 28 万回

printk 自体が律速している可能性が高く、インタプリタの実速度はさらに速い。

判明した原因

EC のクエリループ

/sys/firmware/acpi/interrupts/gpe6E: 60 秒で 997 増加(毎秒約 17 回)

他の GPE はほぼ静止。システムの ACPI 活動はすべて EC 由来。

_Q01 の未定義参照

drivers/acpi/ec.c の dynamic_debug を有効化して判明。

ACPI: EC: Query(0x01) started
ACPI BIOS Error (bug): Could not resolve symbol [\_SB.PC00.LPCB.EC._Q01.CHRG], AE_NOT_FOUND (20251212/psargs-332)
ACPI Error: Aborting method \_SB.PC00.LPCB.EC._Q01 due to previous error (AE_NOT_FOUND) (20251212/psparse-529)
ACPI: EC: Query(0x01) stopped

DSDT の _Q01 本体(149133 行目付近):

Method (_Q01, 0, Serialized)
{
    DSTR ("_Q01")
    UAAS ()
    If (CondRefOf (\_SB.ADP1))       // ← ガードあり
    {
        Notify (ADP1, 0x80)
    }

    UABS ()
    Notify (BAT1, 0x81)
    DSTR ("_Q01 Notify Battery Charger with 0x80")
    Notify (CHRG, 0x80)              // ← ガードなし。ここで AE_NOT_FOUND
}

DSDT + SSDT1〜SSDT15 の全16テーブルを個別に逆アセンブルして確認した結果、Device (CHRG) の実体はどこにも存在しないExternal 宣言のみ(逆アセンブラが未解決参照に対して自動生成したもの)。

直前の ADP1 には CondRefOf が付いているのに CHRG には付いていない。BIOS 側の書き漏らしと判断。

推定される因果

  1. EC がイベントを上げる
  2. _Q01 が呼ばれる
  3. UAAS()UABS()Notify(BAT1) までは正常実行
  4. 最後の Notify(CHRG)AE_NOT_FOUND、メソッド中断
  5. 後始末が実行されず、EC 側のステータスが残る
  6. 再発火して 1 に戻る

_Q04_Q01() を呼んでいるため同じ経路を通る。

メソッドが正常終了すれば acpi_ds_terminate_control_method でローカルとオペランドが解放されるが、エラーパスでは取りこぼしが起きうる。これが数日かけて参照カウントを 35000 まで積み上げた、というのが現時点の仮説。

ただしこの因果関係は未証明。 エラー中断と refcount リークの結び付きは推測にとどまる。

外れた仮説(記録用)

PCIe root port の L2/L3 遷移

DSDT の While ループ 403 個を集計したところ、無条件待ちループが目立った。

88  While ((LASX == Zero))
60  While (L23R)
60  While (L23E)

L23R = L2/L3 Ready、L23E = L2/L3 Exit、LASX = Link Active Status。PCIe ルートポートの PowerResource _ON/_OFF 内のスピン待ち。runtime PM の頻度と蓄積速度が噛み合うと考えたが、実測では EC 以外の GPE が静止していたため否定された。

ACPI fan ドライバの通知ループ

スタックトレースの上位に acpi_fan_notify_handleracpi_fan_get_fstacpi_fan_dsm_set_trip_points が大量に出ていた。ファンは5台とも cur=0(停止)かつ max=1(オンオフのみ)であり、細粒度制御に対応していないファンにトリップ点設定を投げ続けている構図に見えた。

PNP0C0B:00 を unbind して検証したところ、gpe6E の 60 秒あたり増分は 997 から 996 とまったく変化しなかった

ファンドライバは Notify を受けた側であり、原因ではなかった。スタックの読み違い。

使用したコマンド

ログの分析

# monotonic タイムスタンプで実稼働時間を確認
journalctl -k -b -1 -o short-monotonic | tail -1

# 起動履歴とクリーンシャットダウンの有無
last -x reboot | head -5

# オブジェクトごとの初出・最終・件数を集計
journalctl -k -b -1 -o short-iso --grep 'Large Reference Count' \
| sed -nE 's/^([^ ]+) .*object ([0-9a-f]+), Type=0x([0-9A-F]+) Operation=([A-Za-z]+).*/\1 \2 \3 \4/p' \
| awk '{k=$2"\t0x"$3"\t"$4; if(!(k in f))f[k]=$1; l[k]=$1; n[k]++}
       END{for(k in n) printf "%8d  %s  %s .. %s\n", n[k], k, f[k], l[k]}' \
| sort -rn

journalctl --list-boots の FIRST ENTRY は「その boot 最初のエントリ」ではなく「現存する最古のエントリ」なので、稼働時間の推定には使えない。ローテーションで前半が消えていると誤読する。

参照カウントの追跡

acpi_ut_update_ref_countstatic でインライン化されており kprobe を張れない。以下は張れる。

sudo grep -E ' (acpi_ut_update_object_reference|acpi_ut_add_reference|acpi_ut_remove_reference|acpi_warning)$' /proc/kallsyms

オブジェクトヘッダのオフセット(64bit):

オフセット フィールド
0 next_object (8 bytes)
8 descriptor_type (u8)
9 type (u8)
10 reference_count (u16)
# 高 refcount オブジェクトの推移
sudo bpftrace -e '
kprobe:acpi_ut_update_object_reference {
  $t  = *(uint8  *)(arg0 + 9);
  $rc = *(uint16 *)(arg0 + 10);
  if ($rc > 64) { @rc[arg0, $t] = max($rc); }
}
interval:s:60 { time("%H:%M:%S\n"); print(@rc); }'

# refcount 分布
sudo bpftrace -e '
kprobe:acpi_ut_update_object_reference { @ = hist(*(uint16 *)(arg0 + 10)); }
interval:s:30 { print(@); clear(@); }'

# 呼び出し元スタック(上位3件)
sudo bpftrace -e '
kprobe:acpi_ut_update_object_reference { @[kstack(12)] = count(); }
interval:s:20 { print(@, 3); exit(); }'

健全時の分布は [1][2,4) に集中し [4,8) で裾が切れる。

注意: acpi_ut_add_referenceacpi_ut_remove_reference の呼び出し回数の差は漏れの指標にならない。オブジェクトは refcount 1 で生成されるため、対応する add なしに remove されるものが構造的に存在する(実測で remove が add の約2倍)。見るべきは refcount の推移。

EC クエリの特定

echo 'file drivers/acpi/ec.c +p' | sudo tee /sys/kernel/debug/dynamic_debug/control
sleep 10
echo 'file drivers/acpi/ec.c -p' | sudo tee /sys/kernel/debug/dynamic_debug/control
journalctl -k --since '1 minute ago' | grep -iE 'query|_Q[0-9A-F]'

GPE カウンタ

grep -H . /sys/firmware/acpi/interrupts/* | grep -vE ':[[:space:]]+0[[:space:]]'

対処

実施予定

_Q01Notify (CHRG, 0x80)CondRefOf ガードを追加する DSDT パッチ。。

BIOS 更新による解決は期待できない。dynabook の BIOS 更新モジュールは Windows 専用の exe のみで配布されており、Linux 単体機では Windows To Go 等の準備が必要。かつ更新履歴を見る限りこの種の修正が入る見込みは薄い。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment