寫了九個模組的 ASL,你可能還沒有真正「摸」過一台機器裡活著的 ACPI。 Linux 是觀察 ACPI 最友善的平台:核心把 ACPI 的一舉一動攤在 與 kernel log 裡,工具鏈又與第 21 章的 iASL 系出同門。本章建立一條 你之後會反覆走的分析路線:從 sysfs 找到裝置、反組譯 DSDT、讀懂 Method、對應到硬體——第 23、24 章的 Debug 與錯誤排除都蓋在這條 路線上。
說明 與 之下各目錄與檔案的意義。
使用 acpidump、acpixtract 與 ,把韌體中的表 反組譯回可讀的 ASL。
解讀 dmesg 中開機階段的 ACPI 表載入訊息。
依「Device \(\to\) Method \(\to\) OperationRegion \(\to\) Hardware」 的方法論閱讀 DSDT,並以筆電電池完整走一遍。
使用 acpiexec 在使用者空間安全地執行韌體的 AML。
Linux 上的 ACPI 觀測點
Linux 核心解譯完 AML、建好 Namespace 之後,把大量內部狀態匯出到 。你最常拜訪的是三個地方:原始表、列舉出的裝置、 以及事件計數器。先看地圖:
/sys/firmware/acpi/ ├── tables/ ← 原始 ACPI Tables(二進位) │ ├── DSDT ← 主要硬體描述(AML) │ ├── FACP ← 即 FADT(指向 DSDT) │ └── SSDT1 SSDT2 ← 每張 SSDT 一個檔案 └── interrupts/ ← SCI 與各 GPE 的觸發計數 ├── gpe17 ← 單一 GPE:計數 狀態 遮罩 ├── gpe_all ← 所有 GPE 的總計數 └── sci ← SCI 中斷總數
/sys/bus/acpi/devices/ └── PNP0C0A:00/ ← 每個 acpi_device 一個目錄 └── path hid uid status ... ← 對應 ASL 物件(見下表)
tables/:原始表的家
之下的每個檔案,就是一張完整的 ACPI Table,內容與韌體放進記憶體的二進位一模一樣:
\(ls /sys/firmware/acpi/tables/ APIC BGRT DSDT FACP FACS HPET MCFG SSDT1 SSDT2 data\) sudo xxd -l 36 /sys/firmware/acpi/tables/DSDT 00000000: 4453 4454 a5f2 0100 028d 4c45 4e4f 564f DSDT......LENOVO 00000010: 5450 2d4e 3332 2020 0010 0000 494e 544c TP-N32 ....INTL
前 36 個 byte 正是標準表頭:signature、length、OEM ID,以及編譯器 簽名 (iASL)——這些檔案就是韌體的原貨。 第 22.3 節的工具鏈,本質上就是把它們打包、還原成可讀的 ASL。
devices/:列舉出來的裝置
核心每從 Namespace 解譯出一個 ,就建立一個 ,並在 產生一個 「:序號」目錄:
\(ls /sys/bus/acpi/devices/ ACPI0003:00 LNXPWRBN:00 PNP0A08:00 PNP0C0C:00 PNP0C0F:00 LNXCPU:00 LNXSYSTM:00 PNP0C02:00 PNP0C0A:00 PNP0C0D:00 ...\) cd /sys/bus/acpi/devices/PNP0C0A:00 \(cat path hid uid status \_SB_.PCI0.LPCB.EC0_.BAT0 PNP0C0A 1 31 \end{shell} 每個目錄裡的檔案,都對應到你在前面模組寫過的 ASL 物件: \begin{center} \small \begin{tabular}{llL{7.0cm}} \toprule \rowcolor{acpiblue!10} \textbf{檔案} & \textbf{對應物件} & \textbf{內容} \\ \midrule \code{path} & 節點位置 & Namespace 完整路徑(含底線補滿的 4 字元名) \\ \code{hid} & \code{\_HID} & Hardware ID 字串,driver 比對的依據 \\ \code{uid} & \code{\_UID} & 同型多實例時的區分編號 \\ \code{status} & \code{\_STA} & 裝置狀態(十進位),開機時評估的結果 \\ \code{physical\_node} & --- & symlink,指向綁定的 platform/i2c 等實體裝置 \\ \bottomrule \end{tabular} \end{center} 上例的 \code{status} 是 31,即 0x1F:bit 0 存在、bit 1 已啟用、 bit 2 顯示於 UI、bit 3 功能正常、bit 4 電池存在——正是第 8 章 \code{\_STA} 各 bit 的意義。\code{LNX} 開頭(\code{LNXSYSTM}、 \code{LNXCPU})是核心合成的節點,不是 DSDT 寫的。 \subsection{interrupts/:抓事件的計數器} 韌體要主動通知 OS 事件時,發出 \keyterm{SCI}(System Control Interrupt);SCI 的主要來源是 \keyterm{GPE}(General Purpose Event),每個 GPE bit 觸發後核心執行 \code{\_GPE} scope 下對應的 \code{\_Lxx}/\code{\_Exx} handler。這個目錄把每一路的觸發次數 攤開給你看: \begin{shell}[title={GPE 與 SCI 計數}]\) cat /sys/firmware/acpi/interrupts/sci 8241
\(grep -H . /sys/firmware/acpi/interrupts/gpe1? | sort -t: -k2 -rn /sys/firmware/acpi/interrupts/gpe17: 8117 enabled unmasked /sys/firmware/acpi/interrupts/gpe10: 121 enabled unmasked /sys/firmware/acpi/interrupts/gpe13: 3 enabled unmasked /sys/firmware/acpi/interrupts/gpe11: 0 invalid unmasked \end{shell} 檔名與 \code{\_Lxx} 的編號都是\textbf{十六進位}:\code{gpe17} 對應 \code{\_L17} 或 \code{\_E17}。除了計數,檔案還顯示該 GPE 是否啟用 (\code{enabled}/\code{invalid},後者表示沒有對應 handler)與是否被 遮罩。這個目錄是抓 \keyterm{GPE storm}(某路 GPE 瘋狂觸發、吃光 CPU) 的神器,第 24 章的 Case Study 會實際用它辦案。 \begin{notebox}[固定事件也有計數] 同目錄 \code{ff\_} 開頭(如 \code{ff\_pwr\_btn} 電源鍵)是 Fixed Event 計數,\code{error} 記錄 GPE 處理出錯次數。\code{gpeXX} 檔案還可寫入 \code{echo disable} 暫時關閉——第 24 章的急救手段。 \end{notebox} \section{dmesg:開機時的 ACPI 足跡} 核心開機的前幾毫秒,就把它找到的每一張表逐一列在 kernel log 裡。 這是你確認「韌體到底交了什麼給 OS」最快的方法: \begin{shell}[title={開機時的表載入訊息}]\) sudo dmesg | grep "ACPI:" | head -8 ACPI: RSDP 0x00000000BFEFE014 000024 (v02 LENOVO) ACPI: XSDT 0x00000000BFEFD0E8 0000B4 (v01 LENOVO TP-N32 00001000) ACPI: FACP 0x00000000BFEF9000 0000F4 (v05 LENOVO TP-N32 00001000) ACPI: DSDT 0x00000000BFEC7000 01F2A5 (v02 LENOVO TP-N32 00001000 INTL 20230628) ACPI: SSDT 0x00000000BFEF8000 0003A2 (v02 LENOVO TP-SSDT 00001000 INTL 20230628) ACPI: Interpreter enabled ACPI: PM: (supports S0 S3 S4 S5) ACPI: EC: interrupt mode used
以 DSDT 那一行為例,欄位由左到右:signature 之後是表的實體記憶體 位址與長度(,十六進位 byte 數,約 127 KB); 是表格式 revision(DSDT v2 代表 64 位元整數語意); 是 OEM ID 與 OEM Table ID——這張表來自 哪家、哪個平台;結尾 是編譯器 ID 與版本, 就是 iASL,日期即 ACPICA 釋出版本——看得出 OEM 用了多舊的工具鏈。
接著的 表示 ACPICA 已完成 Namespace 建立; 是從 DSDT 掃出的 睡眠狀態。 開完機先跑一次 是好習慣:沒輸出最好,有輸出也別慌——第 24 章教你逐字讀懂。
acpidump 工具鏈:把韌體變回原始碼
要讀 DSDT 的內容,得先把它從韌體手上拿回來、再反組譯成 ASL。 標準流程三步,全部來自 (Debian/Ubuntu 套件名; Fedora 為 ,Arch 為 ):
\(sudo acpidump > acpi.out\) acpixtract -a acpi.out DSDT - 127653 bytes written to dsdt.dat SSDT - 930 bytes written to ssdt1.dat ...
\(iasl -d dsdt.dat Disassembly completed ASL Output: dsdt.dsl - 733725 bytes \end{shell} \code{acpidump} 把記憶體中所有表以十六進位傾印成一個文字檔; \code{acpixtract -a} 把每張表還原成二進位 \code{.dat}; \code{iasl -d} 把 AML 反組譯回 \code{.dsl}。這份兩萬多行的 \code{dsdt.dsl} 就是這台筆電的完整硬體描述。 \begin{notebox}[反組譯的兩個實用技巧] \begin{itemize} \item DSDT 與 SSDT 常互相引用,單獨反組譯會缺外部符號、 導致方法參數個數判讀錯誤:用 \code{iasl -e ssdt*.dat -d dsdt.dat} 讓 SSDT 提供符號。 \item 只要 DSDT 一張表時可直接 \code{sudo cp /sys/firmware/acpi/tables/DSDT dsdt.dat}; acpidump 的優點是打包全部、適合附在 bug report。 \end{itemize} \end{notebox} \section{閱讀 DSDT 的方法論:以筆電電池為例} 兩萬行的 dsl 檔不能從第一行讀起。韌體工程師讀 DSDT 永遠帶著問題來, 沿著固定路徑走:\textbf{Device $\to$ Method $\to$ OperationRegion $\to$ Hardware}——先用 \code{\_HID} 找到 Device,再看 OS 會呼叫的 Method,Method 讀寫的 Field 屬於哪個 OperationRegion,最後由 region 的空間型別與位址對應到真正的硬體。每一步都把答案縮小一圈。 以最生活化的「筆電電池」完整走一遍(標準 \code{\_HID} 是 \code{PNP0C0A},Control Method Battery,第 8 章)。 \textbf{第一步:從 sysfs 找到 Device。}22.1 節已查出電池的路徑是 \nspath{\_SB\_.PCI0.LPCB.EC0\_.BAT0}——光看路徑就知道它是 Embedded Controller 的下游裝置。到 dsl 裡找到它: \begin{shell}[title={Step 1: locate the device}]\) grep -n "PNP0C0A" dsdt.dsl 9482: Name (_HID, EisaId ("PNP0C0A"))
第二步:找到 Method。跳到 dsl 第 9482 行附近, 看 的內容(節錄並簡化):
// dsdt.dsl excerpt (simplified for teaching) Scope (_SB.PCI0.LPCB.EC0)
Device (BAT0)
Name (_HID, EisaId ("PNP0C0A")) // Control Method Battery Name (_UID, One)
Name (PBST, Package (0x04) Zero, // [0] state: bit0 discharging, bit1 charging Ones, // [1] present rate (mA or mW) Ones, // [2] remaining capacity Ones // [3] present voltage (mV) )
Method (_BST, 0, Serialized) PBST [Zero] = BST0 // EC field: state PBST [One] = BRTE // EC field: rate PBST [0x02] = BRCP // EC field: capacity PBST [0x03] = BVLT // EC field: voltage Return (PBST)
逐行拆解: 是規格定義的 4 元素 Package(狀態、瞬時 電流或功率、剩餘電量、電壓),OS 每次查電量都呼叫 (Battery Status),把四個 Field 名字(、 ……)的值填進 Package 後回傳—— 本身不碰 硬體位址,真正的存取藏在 Field 宣告裡。靜態資訊(設計容量、 序號等)在兄弟方法 /,讀法完全相同。
第三步:找到 OperationRegion。在 dsl 裡搜尋 的宣告處:
// dsdt.dsl excerpt (simplified for teaching) Scope (_SB.PCI0.LPCB.EC0) OperationRegion (ERAM, EmbeddedControl, Zero, 0xFF) Field (ERAM, ByteAcc, Lock, Preserve) Offset (0xA0), BST0, 8, // battery state BRTE, 16, // present rate BRCP, 16, // remaining capacity BVLT, 16 // present voltage
第四步:對應 Hardware。region 空間是 :EC 內部 256 byte 的暫存器窗口(第 9–11 章)。「讀電池電壓」的完整真相:OS 呼叫 \(\to\) interpreter 讀 Field \(\to\) EC 協定讀暫存器 \(\to\) EC 韌體透過 SMBus 問電池的 gas gauge 晶片。ASL 的角色,就是把 EC 暫存器配置翻譯成 OS 聽得懂的標準介面。
最後用 Linux 的電池驅動驗證這條鏈真的在動:
\(cat /sys/class/power_supply/BAT0/status Discharging\) cat /sys/class/power_supply/BAT0/voltage_now 11460000
的單位是 \(\mu\)V:battery driver 呼叫 拿到 mV 值 11460,乘以 1000 匯出;state、rate、 capacity 欄位則對應 、(或 )與 /。 「EC 暫存器 \(\to\) Field \(\to\) \(\to\) sysfs」這條 翻譯鏈至此完整對上。
安裝工具:。
反組譯:、 、 。
用 找出 電池的 Namespace 路徑。
在 找到 ,畫出「Method \(\to\) Field \(\to\) EC offset」對照表。
對照 驗證單位換算; 桌機改追 (Lid)或 (電源鍵)。
acpiexec:在使用者空間執行 AML
讀懂了 ASL,下一個願望自然是「跑跑看」,但改一行 DSDT 就重開機 太昂貴。 把 ACPICA interpreter 包成使用者程式: 載入你給的表、建好 Namespace,讓你像用 gdb 一樣呼叫任何 Method。
\(acpiexec dsdt.dat ssdt1.dat ssdt2.dat Table [DSDT] - 2143 Objects with 87 Devices, 612 Methods - find BAT0 \_SB_.PCI0.LPCB.EC0_.BAT0 Device - evaluate \_SB.PCI0.LPCB.EC0.BAT0._STA Evaluation returned object 0x55e3c2a4, buffer length 18 [Integer] = 000000000000000F - execute \_SB.PCI0.LPCB.EC0.BAT0._BST Execution returned object 0x55e3c4f0, buffer length 78 [Package] Contains 4 Elements: [Integer] = 0000000000000000 ...(all four elements are zero) \end{shell} 除了示範中的 \code{find}(用 4 字元名反查完整路徑)與 \code{evaluate}(執行 Method,\code{execute} 是同義字),常用的 還有 \code{namespace}(傾印整棵或某子樹的 Namespace)、 \code{methods}(列出所有 Method)與 \code{help}。帶參數執行時以 空白分隔、數字用 \code{0x} 前綴、字串加引號——第 23 章會實際用到。 注意上面的執行結果:機器明明插著電池,\code{\_STA} 卻回 0x0F (bit 4 電池存在沒亮)、\code{\_BST} 全是 0。這不是 bug: \begin{warnbox}[acpiexec 跑的是模擬硬體] acpiexec 在使用者空間執行,\textbf{不能}碰真正的 EC、MMIO 或 I/O Port:所有 OperationRegion 讀寫都由模擬 handler 接住,讀取 通常回 0。它適合驗證\textbf{控制流程、型別、Namespace 解析} (第 24 章會用它重現錯誤);要看真實硬體值,回到真機的 \code{/sys/class/power\_supply/}。 \end{warnbox} 即使讀值是假的,acpiexec 仍讓「改 ASL\)\(編譯\)\(執行」的 迴圈從一次重開機縮短到一秒鐘——第 23 章的 Debug 實驗會先在這裡 跑通,再放上真機。 \begin{keybox}[本章重點回顧] \begin{itemize} \item \code{/sys} 三大觀測點:\code{tables/}(韌體原貨)、 \code{devices/}(列舉結果:path/hid/uid/status)、 \code{interrupts/}(GPE/SCI 計數器)。 \item dmesg 開機訊息列出每張表的位址、大小、OEM 與編譯器版本, 是確認「韌體交了什麼」的第一手資料。 \item 標準工具鏈:\code{sudo acpidump > acpi.out}\)\(\code{acpixtract -a}\)\(\code{iasl -d}(記得 \code{-e} 帶上 SSDT)。 \item 讀 DSDT 的固定路徑:Device\)\(Method\)\(OperationRegion\)\(Hardware。 \item acpiexec 讓你在使用者空間執行 AML;region 讀寫是模擬的, 驗流程不驗真值。 \end{itemize} \end{keybox} \begin{exbox} \begin{enumerate} \item \code{tables/}、\code{devices/}、\code{interrupts/} 三個目錄各自回答什麼問題?除錯時分別在哪一步派上用場? \item 某裝置的 \code{status} 檔案顯示 11(十進位)。換算成 \code{\_STA} 的 bit,這個裝置處於什麼狀態?OS 會載入它的 driver 嗎? \item 為什麼反組譯 DSDT 時建議加 \code{-e ssdt*.dat}? 少了它會出什麼類型的問題? \item 動手:完成 22.4 節的 Lab,並找出上蓋開關(\code{PNP0C0D}) 的 \code{\_LID} 讀哪個 Field、屬於哪個 OperationRegion。 \item 動手:用 acpiexec 載入反組譯出的表,\code{find} 電池 (或 Lid),\code{evaluate} 其 \code{\_STA} 與 \code{\_BST},解釋你看到的回傳值。 \end{enumerate} \end{exbox} \chapter{ACPI Debug} \label{ch:acpidebug} C 程式不會動了,你會加 printf;AML 在核心深處執行,既沒有終端機 也沒有除錯器,怎麼辦?ASL 其實內建了自己的 printf——\keyterm{Debug 物件}。往它寫任何東西,interpreter 就替你印出來。問題是:印去哪裡? 要開哪些開關才看得到?這正是本章要打通的環節。 本章先講 Debug 物件的語法與原理,接著沿著「ASL\)\(AML\)\(Interpreter\)\(kernel log」把輸出的旅程走一遍,最後動手做一個 完整的 Lab:寫一張會說話的 SSDT,讓它在你的 dmesg 裡留言。 \begin{goalbox} \begin{itemize} \item 使用 \code{Store (..., Debug)} 與 \code{Debug = ...} 兩種寫法輸出除錯訊息。 \item 畫出 Debug 輸出從 ASL 原始碼到 kernel log 的完整路徑。 \item 說明 \code{CONFIG\_ACPI\_DEBUG}、\code{acpi.debug\_layer} 與 \code{acpi.debug\_level} 三個開關的分工,並寫出常用組合。 \item 於執行期透過 \code{/sys/module/acpi/parameters/} 調整輸出。 \item 用 SSDT 加 initrd(或 acpiexec)實際看到自己的 Debug 輸出。 \end{itemize} \end{goalbox} \section{Debug 物件:一個只能寫的虛擬物件} \code{Debug} 是 ASL 預先定義的特殊物件:它不存在於 Namespace、 沒有型別、\textbf{只能當 Store 的目標},不能讀。每次有值寫入, interpreter 就把該值連同型別印到除錯輸出。兩種寫法完全等價: \begin{asl}[title={片段:Debug 物件的兩種寫法}] Store ("Hello ACPI", Debug) // legacy operator style Debug = "Hello ACPI" // ASL 2.0 symbolic style Store (Local0, Debug) // print a local variable Debug = Arg0 // print a method argument Debug = CNT0 // print a named object \end{asl} 舊韌體與反組譯輸出多為 \code{Store} 寫法,新專案建議用 \code{Debug =}——這也是本書一貫「兩種語法都要認得」的原則。 各型別的輸出樣式如下(以 kernel log 為例): \begin{center} \small \begin{tabular}{lL{8.8cm}} \toprule \rowcolor{acpiblue!10} \textbf{寫入型別} & \textbf{輸出樣式(示意)} \\ \midrule String & \code{ACPI Debug: String [0x0A] "Hello ACPI"} \\ Integer & \code{ACPI Debug: Integer 0x000000000000002A} \\ Buffer & \code{ACPI Debug: Buffer [0x04] = 0xDE 0xAD 0xBE 0xEF} \\ Package & \code{ACPI Debug: Package [0x02]},並逐一遞迴列出元素 \\ \bottomrule \end{tabular} \end{center} 方括號內是長度;Integer 一律以 64 位元十六進位列印。把這些樣式記個 大概,之後在幾千行 dmesg 中一眼就能撈出自己的訊息。 下面是本章的主角:一張完整、可編譯的 SSDT,開機時會自我介紹。 (\code{\_INI} 會在核心列舉到該裝置、且 \code{\_STA} 表示存在時 自動執行一次——這讓我們不需要任何 driver 就能觸發 Debug 輸出。) \begin{asl}[title={dbgdemo.dsl:會在開機時說話的 SSDT}] DefinitionBlock ("dbgdemo.aml", "SSDT", 2, "DEMO", "DBGDEMO", 0x00000001) { Scope (\_SB) { Device (DBG0) { Name (_HID, "DEMO0002") Name (MSG0, "Hello ACPI from DBG0") Method (_STA, 0, NotSerialized) { Return (0x0F) // present, enabled, functioning } Method (_INI, 0, Serialized) { Debug = "DBG0._INI: SSDT is alive" Debug = MSG0 Local0 = 0x2A Debug = Local0 Debug = Buffer (0x04) { 0xDE, 0xAD, 0xBE, 0xEF } Store ("legacy style still works", Debug) } Method (DBLE, 1, Serialized) { Debug = Arg0 // echo the argument Return (Arg0 * 2) } } } } \end{asl} 逐段拆解: \begin{itemize} \item \code{DefinitionBlock} 的簽章是 \code{SSDT}:我們是去 \textbf{補充} DSDT,不是取代它(第 18--20 章的做法)。 \item \code{\_STA} 回 0x0F,確保核心認為裝置存在,才會呼叫 \code{\_INI}。 \item \code{\_INI} 依序印出 String、Named object、Local、Buffer, 以及一行 legacy 寫法——正好把上表的樣式各示範一次。 \item \code{DBLE} 帶一個參數並回傳兩倍,留給 acpiexec 練習 參數傳遞。 \end{itemize} \section{一行 Debug 的旅程} 「寫入 Debug 就會印出來」的背後是一條完整的管線。理解它, 之後任何一站沒動靜你都知道去哪查: \begin{vflow}[4.2mm] \fnode{ASL:Debug = 訊息或值} \fnodel{iasl 編譯}{AML:DebugOp(0x5B 0x31)為 Store 目標} \fnode{ACPI Interpreter(ACPICA)攔下對 Debug 的寫入} \fnodel{兩道閘門過濾}{acpi\_ex\_do\_debug\_object()} \fnode{printk 寫入 kernel ring buffer} \fnode[flownodealt]{dmesg / journalctl -k 顯示} \end{vflow} 編譯期,\code{Debug} 變成 AML 的 \code{DebugOp}(extended opcode \code{0x5B 0x31})。執行期,interpreter 發現 Store 的目標是 Debug, 就呼叫 ACPICA 的 \code{acpi\_ex\_do\_debug\_object()} 把值格式化後 丟給 OS 的列印服務——Linux 上就是 \code{printk}。但訊息要真的出現 在 dmesg,必須通過兩道閘門,這是下一節的主題。 \section{打開輸出:兩道閘門} \textbf{閘門一:編譯期選項 \code{CONFIG\_ACPI\_DEBUG}。} ACPICA 的所有除錯列印(包括 Debug 物件)都包在條件編譯裡; 核心沒開這個選項,程式碼根本不存在。先確認你的核心: \begin{shell}[title={檢查閘門一}]\) grep CONFIG_ACPI_DEBUG /boot/config-\((uname -r) CONFIG_ACPI_DEBUG=y \end{shell} 多數發行版的預設核心\textbf{有}開此選項(例如 Ubuntu),但若顯示 \code{is not set},就得改用發行版的 debug 核心或自行編譯。 \textbf{閘門二:執行期的 \code{debug\_layer} 與 \code{debug\_level}。} 兩個 32 位元的 bitmask,共同決定哪些訊息放行: \begin{itemize} \item \code{debug\_layer}——\textbf{誰}可以說話:ACPICA 依元件劃分 (interpreter、namespace、事件……),每個元件一個 bit。 \item \code{debug\_level}——說\textbf{哪一類}話:訊息依類型劃分 (初始化、資訊、Debug 物件輸出……),每類一個 bit。 \end{itemize} 兩者都過才會印。常用的名稱與值(節錄自 \code{include/acpi/acoutput.h}): \begin{center} \small \begin{tabular}{llL{5.4cm}} \toprule \rowcolor{acpiblue!10} \textbf{名稱} & \textbf{值} & \textbf{意義} \\ \midrule ACPI\_ALL\_COMPONENTS & 0xFFFFFFFF & layer:全部元件 \\ ACPI\_EVENTS & 0x00000004 & layer:GPE 與 Notify 路徑 \\ ACPI\_NAMESPACE & 0x00000010 & layer:Namespace 操作 \\ ACPI\_EXECUTER & 0x00000080 & layer:AML 執行核心 \\ \midrule ACPI\_LV\_INIT & 0x00000001 & level:初始化訊息 \\ ACPI\_LV\_DEBUG\_OBJECT & 0x00000002 & level:\textbf{Debug 物件輸出} \\ ACPI\_LV\_INFO & 0x00000004 & level:一般資訊 \\ \bottomrule \end{tabular} \end{center} 實務上你只需要背一個組合——「只看 Debug 物件輸出」: layer 全開、level 只開 \code{ACPI\_LV\_DEBUG\_OBJECT}: \begin{center} \small \begin{tabular}{L{6.2cm}ll} \toprule \rowcolor{acpiblue!10} \textbf{目的} & \textbf{debug\_layer} & \textbf{debug\_level} \\ \midrule 只看 Debug 物件輸出(標準組合) & 0xFFFFFFFF & 0x00000002 \\ Debug 物件+初始化訊息 & 0xFFFFFFFF & 0x00000003 \\ 一般資訊全開 & 0xFFFFFFFF & 0x00000007 \\ 全部訊息(洪水,慎用) & 0xFFFFFFFF & 0xFFFFFFFF \\ \bottomrule \end{tabular} \end{center} 設定方式有二。開機參數(對 \code{\_INI} 這類開機期執行的方法是 唯一選擇): \begin{shell}[title={開機參數(編輯 GRUB)}]\) sudo nano /etc/default/grub GRUB_CMDLINE_LINUX="acpi.debug_layer=0xFFFFFFFF acpi.debug_level=0x2" \(sudo update-grub && sudo reboot \end{shell} 或執行期動態調整(對之後才執行的方法生效,例如 EC query、 driver 呼叫的方法): \begin{shell}[title={執行期調整}] # echo 0xFFFFFFFF > /sys/module/acpi/parameters/debug_layer # echo 0x00000002 > /sys/module/acpi/parameters/debug_level \end{shell} \begin{warnbox}[別把 log 開成洪水] \code{debug\_level=0xFFFFFFFF} 會讓每個 AML opcode、每次 namespace 查找都印一行,每秒數千行,瞬間淹掉 ring buffer,機器也會明顯變慢。 需要全開時搭配 \code{log\_buf\_len=8M} 加大緩衝區,抓完立刻關掉。 日常除錯永遠從 \code{0x2}(只看 Debug 物件)開始,不夠再加。 \end{warnbox} \begin{notebox}[新版核心的預設值] 較新的 ACPICA 已把 \code{ACPI\_LV\_DEBUG\_OBJECT} 納入預設 \code{debug\_level},因此有些機器只開 \code{CONFIG\_ACPI\_DEBUG} 就能看到 Debug 輸出。但預設值隨版本變動,\textbf{顯式設定}才不吃 版本運氣——寫進 SOP 的永遠是上面那組參數。 \end{notebox} \section{Lab:讓核心印出你的 Debug 訊息} 把 23.1 的 \code{dbgdemo.dsl} 放上真機。Linux 提供 \keyterm{Table Upgrade} 機制:把 \code{.aml} 打包成 cpio 檔接在 initrd 前面,核心開機時會安裝這張 SSDT(同第 20 章載入 SSDT 的 思路,這裡走 initrd 版本)。 \begin{labbox}[SSDT + initrd + dmesg 完整流程] \begin{enumerate} \item 編譯:\code{iasl dbgdemo.dsl} 產生 \code{dbgdemo.aml}。 \item 打包並串接:aml 放進 cpio 的 \code{kernel/firmware/acpi/} 路徑,接在現有 initrd \textbf{前面}(見下方指令)。 \item 開機:GRUB 選單按 \code{e},\code{initrd} 行改指向新檔、 \code{linux} 行尾加上 \code{acpi.debug\_layer=0xFFFFFFFF acpi.debug\_level=0x2},\code{Ctrl-x} 開機(只影響本次)。 \item 驗證:\code{sudo dmesg | grep -E "Table Upgrade|ACPI Debug"}。 \end{enumerate} \end{labbox} \begin{shell}[title={步驟 1--3 的實際指令}]\) iasl dbgdemo.dsl \(mkdir -p kernel/firmware/acpi\) cp dbgdemo.aml kernel/firmware/acpi/ \(find kernel | cpio -H newc --create > ssdt.cpio\) cat ssdt.cpio /boot/initrd.img-\((uname -r) > initrd-dbg.img\) sudo cp initrd-dbg.img /boot/
\(sudo dmesg | grep -E "Table Upgrade|ACPI Debug" [ 0.008112] ACPI: Table Upgrade: install [SSDT-DEMO -DBGDEMO] [ 2.310547] ACPI Debug: String [0x18] "DBG0._INI: SSDT is alive" [ 2.310551] ACPI Debug: String [0x14] "Hello ACPI from DBG0" [ 2.310554] ACPI Debug: Integer 0x000000000000002A [ 2.310558] ACPI Debug: Buffer [0x04] = 0xDE 0xAD 0xBE 0xEF [ 2.310561] ACPI Debug: String [0x18] "legacy style still works" \end{shell} 第一行是核心宣告安裝了我們的 SSDT;接著五行正是 \code{\_INI} 的 五個 Debug 寫入,型別與長度都對得上 23.1 的樣式表。若核心開了 \code{CONFIG\_ACPI\_CONFIGFS},還可免重開機:\code{modprobe acpi\_configfs} 後在 \code{/sys/kernel/config/acpi/table/} 下 \code{mkdir} 一個目錄、把 aml \code{cat} 進其中的 \code{aml} 檔 即可(多數發行版預設沒開,故本 Lab 以 initrd 為主)。 \section{更快的迴圈:acpiexec 直接印} 上真機前,先讓 SSDT 在 acpiexec 裡跑通。acpiexec 不需要任何開關, Debug 輸出直接印在終端機: \begin{shell}[title={秒級開發迴圈}]\) iasl dbgdemo.dsl && acpiexec dbgdemo.aml
- execute _SB.DBG0._INI Executing _SB.DBG0._INI [ACPI Debug] String [0x18] "DBG0._INI: SSDT is alive" [ACPI Debug] String [0x14] "Hello ACPI from DBG0" [ACPI Debug] Integer 0x000000000000002A [ACPI Debug] Buffer [0x04] = 0xDE 0xAD 0xBE 0xEF [ACPI Debug] String [0x18] "legacy style still works"
- execute _SB.DBG0.DBLE 0x15 Executing _SB.DBG0.DBLE [ACPI Debug] Integer 0x0000000000000015 Execution returned object 0x5601a8c2, buffer length 18 [Integer] = 000000000000002A
第二段示範參數傳遞:(21)傳入 ,Debug 印出 它,方法回傳 0x2A(42)。建議的工作節奏:acpiexec 迭代到 邏輯正確,再用 initrd 上真機驗證與硬體互動的部分。
kernel 前綴為 ,acpiexec 為 , 長度欄位與縮排在不同 ACPICA 版本間略有出入。寫解析腳本時抓語意 (型別關鍵字與引號內容),不要寫死整行格式。
進階:Method Tracing 簡介
Debug 物件要你事先在原始碼裡佈點;如果拿到的是別人的韌體, 想看某個 Method 的完整執行軌跡呢?ACPICA 提供 method tracing: 指定方法名,interpreter 在進入該方法時自動打開你指定的 layer/level,離開時關閉——等於「只對這個方法開洪水」。
# cd /sys/module/acpi/parameters # echo "_SB.DBG0.DBLE" > trace_method_name # echo method > trace_state
可為 (每次都追)、 (只追第一次)、(關閉); 搭配 與 指定追蹤期間放行的訊息。同名的開機參數 ( 等)可以從第一次執行就開追。 完整說明見核心文件 。
Debug 是只能寫的虛擬物件:寫入即列印,型別(String/ Integer/Buffer/Package)自動標註。
路徑:ASL \(\to\) AML DebugOp(0x5B 0x31)\(\to\) interpreter \(\to\) printk \(\to\) dmesg。
兩道閘門:編譯期 ;執行期 (誰說話)與 (說哪類話)。
標準組合:;執行期可透過 調整。
開發節奏:acpiexec 秒級迭代邏輯,initrd Table Upgrade 上真機驗證。
method tracing 能只對單一方法開啟完整追蹤,適合分析 別人的韌體。
為什麼 Debug 物件被設計成「只能寫」?如果允許讀取, 會對 interpreter 與韌體行為造成什麼問題?
會印出幾行、 各是什麼樣式?對照 23.1 的樣式表回答。
想同時看到 Debug 物件輸出與初始化訊息, 應設多少?寫出計算過程。
動手:修改 ,在 中用 迴圈印出 1 到 5,用 acpiexec 驗證輸出。
動手(真機):完成 23.4 的 initrd Lab,把你的名字放進 ,截圖 dmesg 的輸出。
ACPI Error Debugging
打開任何一台量產筆電的 dmesg,十之八九能撈到幾行 。 有些是無害噪音,有些正是「休眠喚不醒」「風扇狂轉」「電量顯示卡死」 的元兇。分辨兩者、並把有害的那種一路追到 ASL 原始碼,是韌體工程師 最值錢的技能之一。
本章先解剖錯誤訊息的每個欄位,建立十個常見錯誤碼的直覺, 再用三個擬真 Case Study 完整演練「Kernel Log \(\to\) ACPI Error \(\to\) Method \(\to\) ASL Source \(\to\) Namespace \(\to\) Hardware」這條定位鏈。
拆解一行 ACPI Error:方法路徑、錯誤碼、ACPICA 版本與 原始碼位置。
說出十個常見 錯誤碼的意義、典型成因與修法。
依定位鏈從 kernel log 一路追到 ASL 與硬體。
診斷並修復三類實戰問題:、 與 GPE storm。
使用 fwts 對韌體做自動化健檢。
解剖一行 ACPI Error
先看一組典型的錯誤(實際各為單行,此處為排版換行;下同):
\(sudo dmesg | grep -B1 "Aborting method" [ 312.480211] ACPI BIOS Error (bug): Could not resolve symbol [\_SB.PCI0.LPCB.EC0.XDAT], AE_NOT_FOUND (20230628/psargs-330) [ 312.480230] ACPI Error: Aborting method \_SB.PCI0.LPCB.EC0._Q66 due to previous error (AE_NOT_FOUND) (20230628/psparse-529) \end{shell} 逐段解讀第二行: \begin{center} \small \begin{tabular}{lL{8.4cm}} \toprule \rowcolor{acpiblue!10} \textbf{片段} & \textbf{意義} \\ \midrule \code{ACPI Error:} & 訊息來自 ACPICA(核心的 ACPI 元件) \\ \code{Aborting method ...\_Q66} & 被中止的 Method 的完整 Namespace 路徑——這台機器的 EC query 66 號 \\ \code{(AE\_NOT\_FOUND)} & ACPICA 錯誤碼:找不到引用的名稱 \\ \code{20230628} & ACPICA 版本(釋出日期),回報 bug 必附 \\ \code{psparse-529} & 拋錯位置:ACPICA 原始碼 \code{psparse.c} 第 529 行——拿去搜尋核心原始碼可直達現場 \\ \bottomrule \end{tabular} \end{center} 兩個關鍵閱讀習慣: \begin{itemize} \item \textbf{找第一行}。錯誤常成串出現:第一行(本例的 \code{Could not resolve symbol [...XDAT]})才是根因, 後面的 \code{Aborting method} 是連鎖反應。從最早的那行 開始讀。 \item \textbf{看前綴}。\code{ACPI BIOS Error (bug)} 是 ACPICA 明確判定「錯在韌體交來的表,不在核心」;純 \code{ACPI Error} 則兩種可能都有。看到 BIOS Error, 方向直接轉向 DSDT/SSDT。 \end{itemize} \begin{notebox}[錯誤碼定義在哪裡] 所有 \code{AE\_} 開頭的碼定義於 ACPICA 的 \code{acexcep.h} (核心原始碼 \code{include/acpi/acexcep.h}),\code{AE} 即 ACPI Exception。其中 \code{AE\_AML\_} 子類代表「執行 AML 期間」的錯誤 ——十之八九可以在 ASL 原始碼裡找到對應的病灶。 \end{notebox} \section{常見錯誤碼速查表} \begin{center} \footnotesize \begin{tabular}{L{4.0cm}L{2.6cm}L{4.0cm}L{3.4cm}} \toprule \rowcolor{acpiblue!10} \textbf{錯誤碼} & \textbf{意義} & \textbf{典型成因} & \textbf{修法} \\ \midrule \code{AE\_ALREADY\_EXISTS} & 要建立的名稱已存在 & \code{NotSerialized} 方法內用 \code{Name()} 且被併發重入; 同一張 SSDT 載入兩次 & 方法改 \code{Serialized};檢查重複載表 \\ \code{AE\_NOT\_FOUND} & 找不到引用的名稱 & 韌體版本差異;\code{External} 騙過編譯器但執行期不存在; 路徑拼錯 & \code{CondRefOf} 防禦;用 acpiexec 核對 Namespace \\ \code{AE\_AML\_OPERAND\_TYPE} & 運算元型別不符 & Method 某路徑沒回傳值卻被當值用;把 Package 當 Integer 運算 & 檢查所有 Return 路徑與 \code{ObjectType} \\ \code{AE\_AML\_UNINITIALIZED\_ARG} & 使用了未傳入的 ArgX & 呼叫端參數個數不足(\code{\_DSM} 轉呼叫是慣犯) & 對齊宣告與呼叫的參數個數 \\ \code{AE\_AML\_UNINITIALIZED\_\allowbreak LOCAL} & 使用未初始化的 LocalX & 某條 If 分支忘了先賦值就使用 & 進方法先初始化再分支 \\ \code{AE\_AML\_PACKAGE\_LIMIT} & Package 索引越界 & \code{Index} 超出元素個數;迴圈上限寫錯 & 以 \code{SizeOf} 界定索引範圍 \\ \code{AE\_AML\_BUFFER\_LIMIT} & Buffer 索引越界 & \code{CreateField} 系列的位移或長度算錯 & 核對位移、長度與 Buffer 大小 \\ \code{AE\_AML\_INVALID\_\allowbreak RESOURCE\_TYPE} & 資源描述子型別錯誤 & \code{\_CRS}/\code{\_SRS} 回傳的 Buffer 不是合法的 resource template & 用 ResourceTemplate 重寫並以 iasl 驗證 \\ \code{AE\_TIME} & 等待逾時 & \code{Acquire}/\code{Wait} 逾時;EC 交易沒回應 & 查 Mutex 死鎖順序與 EC 狀態 \\ \code{AE\_AML\_LOOP\_TIMEOUT} & While 迴圈超過時限 & 輪詢的硬體 bit 永遠不變(預設保險絲約 30 秒) & 迴圈加計數上限;查硬體為何不回應 \\ \bottomrule \end{tabular} \end{center} \begin{warnbox}[不是每行 ACPI Error 都要修] 量產機器的 log 常有「無害噪音」:例如 \code{\_DSM} 查詢不支援的 功能、\code{\_OSI} 分支導致某平台本來就沒有的物件被引用一次。 判斷準則是\textbf{有沒有伴隨功能異常}(睡眠、電池、熱管理、 熱鍵……)。先確認症狀,再決定投入;別看到紅字就急著改 DSDT 或刷 BIOS。 \end{warnbox} \section{Case Study 1:AE\_ALREADY\_EXISTS——重入的代價} \textbf{症狀}:使用者回報「連按亮度熱鍵,螢幕亮度偶爾卡住」。 dmesg 出現: \begin{shell}[title={Case 1 的錯誤訊息}] [ 201.118402] ACPI Error: [TBUF] Namespace lookup failure, AE_ALREADY_EXISTS (20230628/dswload2-326) [ 201.118420] ACPI Error: Aborting method \_SB.PCI0.LPCB.EC0._Q11 due to previous error (AE_ALREADY_EXISTS) (20230628/psparse-529) \end{shell} \textbf{定位}:根因行說「建立 \code{TBUF} 時發現名稱已存在」; 被中止的是 EC query \code{\_Q11}(亮度熱鍵的事件 handler, 第 12--14 章)。反組譯 DSDT,找到: \begin{asl}[title={dsdt.dsl 節錄:有 bug 的 \_Q11}] // dsdt.dsl excerpt: brightness hotkey EC query (buggy) Method (_Q11, 0, NotSerialized) // BUG: NotSerialized { Name (TBUF, Package (0x02) { Zero, Zero }) TBUF [Zero] = BRTL // EC field: current level TBUF [One] = BRTM // EC field: max level \_SB.PCI0.GFX0.UPBL (TBUF) // ask GFX to update backlight } \end{asl} \textbf{機轉}:方法內的 \code{Name()} 在\textbf{執行期}建立節點、 方法結束時拆除。使用者連按熱鍵,EC 連續送出兩個 query,第二個 \code{\_Q11} 在第一個還沒退出時進場——第二次 \code{Name (TBUF, ...)} 發現節點還在,拋出 \code{AE\_ALREADY\_EXISTS},方法中止, 亮度就「卡住」了。 \textbf{修正}:把宣告改成 \code{Method (\_Q11, 0, Serialized)} ——一字之差,interpreter 會替方法加上隱式 Mutex,重入的執行緒 排隊等前一個完成,\code{Name()} 不再撞名。這正是第 12 章的鐵律 「方法內有 \code{Name()} 就宣告 \code{Serialized}」的實戰版。 現代 iasl 對 NotSerialized 方法內的 \code{Name()} 會直接給出 remark 警告——編譯器的嘮叨要當真。 \section{Case Study 2:AE\_NOT\_FOUND——防禦性引用} \textbf{症狀}:某批機器升級 EC 韌體後,dmesg 在熱事件時報錯: \begin{shell}[title={Case 2 的錯誤訊息}] [ 88.130077] ACPI BIOS Error (bug): Could not resolve symbol [\_SB.PCI0.LPCB.EC0.SEN3], AE_NOT_FOUND (20230628/psargs-330) [ 88.130095] ACPI Error: Aborting method \_SB.PCI0.LPCB.EC0._QD5 due to previous error (AE_NOT_FOUND) (20230628/psparse-529) \end{shell} \textbf{定位}:\code{\_QD5} 引用了 EC field \code{SEN3},但這台 機器的 DSDT 沒宣告它——這張 SSDT 是兩個機種共用,另一機種的 EC 多一顆感測器。編譯時的 \code{External} 宣告(第 19 章)只是 「我保證執行期會有」的承諾,兌現不了就是 \code{AE\_NOT\_FOUND}: \begin{asl}[title={ssdt 節錄:有 bug 的 \_QD5}] // ssdt excerpt: thermal sensor query (buggy) External (\_SB.PCI0.LPCB.EC0.SEN3, FieldUnitObj) Method (_QD5, 0, Serialized) { Local0 = \_SB.PCI0.LPCB.EC0.SEN3 // may not exist! If (Local0 > 0x5F) { Notify (\_TZ.THM0, 0x80) // re-evaluate temperature } } \end{asl} \textbf{修正}:用 \code{CondRefOf} 先確認物件存在。它回傳真假而 \textbf{不會}因為找不到而拋錯,是跨韌體版本的標準防禦寫法: \begin{asl}[title={修正後的 \_QD5:CondRefOf 防禦}] Method (_QD5, 0, Serialized) { If (CondRefOf (\_SB.PCI0.LPCB.EC0.SEN3, Local0)) { If (DerefOf (Local0) > 0x5F) { Notify (\_TZ.THM0, 0x80) } } Else { Debug = "_QD5: SEN3 absent on this EC firmware" } } \end{asl} \code{CondRefOf} 成功時把\textbf{參照}放進 \code{Local0}, 用 \code{DerefOf} 取值才會真正去讀 EC。Else 分支順手放一行 Debug(第 23 章),日後遇到同型問題 dmesg 直接告訴你原因。 在 acpiexec 裡把兩個版本各跑一次,就能離線驗證修正。 \section{Case Study 3:GPE Storm——用計數器抓兇手} \textbf{症狀}:機器待機也風扇狂轉,\code{top} 顯示 kworker 執行緒 吃掉一顆核心。這是 GPE storm 的經典長相:某路 GPE 不停觸發, 核心疲於執行它的 handler。用第 22 章的計數器辦案: \begin{shell}[title={Step 1:確認 SCI 暴增,鎖定兇手 GPE}]\) cat /sys/firmware/acpi/interrupts/sci; sleep 5; cat /sys/firmware/acpi/interrupts/sci 418772 601233
\(grep -H . /sys/firmware/acpi/interrupts/gpe?? | sort -t: -k2 -rn \ | head -3 /sys/firmware/acpi/interrupts/gpe17: 600911 enabled unmasked /sys/firmware/acpi/interrupts/gpe09: 3120 enabled unmasked /sys/firmware/acpi/interrupts/gpe02: 411 enabled unmasked \end{shell} 五秒漲了 18 萬次,幾乎全記在 \code{gpe17} 頭上。GPE 編號是十六進位, 對應 handler 就是 \code{\_GPE} scope 下的 \code{\_L17}(level)或 \code{\_E17}(edge)。到 dsl 裡看: \begin{asl}[title={dsdt.dsl 節錄:有 bug 的 \_L17}] // dsdt.dsl excerpt: thermal GPE handler (buggy) Scope (\_GPE) { Method (_L17, 0, NotSerialized) { If (\_SB.PCI0.LPCB.EC0.TSTS) // thermal status bit { Notify (\_TZ.THM0, 0x80) // re-evaluate temperature } // BUG: TSTS is never cleared. The level-triggered // GPE line stays asserted and fires again immediately. } } \end{asl} \textbf{機轉}:\code{\_L17} 是 level-triggered handler:只要事件源 沒被清掉,GPE 線持續有效,處理完馬上又觸發。這個 handler 讀了狀態 bit \code{TSTS} 卻忘了清除(該 bit 規格為 write-1-to-clear), 於是無限循環。 \textbf{修正}:在 handler 內\textbf{先清事件源再做事}——於 \code{If} 區塊開頭、\code{Notify} 之前加上一行 \code{\_SB.PCI0.LPCB.EC0.TSTS = One}(write-1-to-clear), GPE 線放掉,storm 即止。 \textbf{救急 workaround}(改不了韌體時):執行期把 \code{disable} 寫進 \code{interrupts/\allowbreak gpe17}(\code{echo disable >} 該檔案),或用開機參數 \code{acpi\_mask\_gpe=0x17} 永久遮罩—— 代價是犧牲該 GPE 承載的功能(本例為熱事件通知),只能當止血帶。 \begin{labbox}[體檢你的機器有沒有隱性 storm] \begin{enumerate} \item 記下 \code{gpe\_all} 的值,等 60 秒再記一次,相減得出 每分鐘 GPE 次數(安靜的機器通常是個位數到數十)。 \item 若異常,用本節的 \code{grep + sort} 找出最活躍的 GPE。 \item 在你反組譯的 \code{dsdt.dsl} 中找到對應的 \code{\_Lxx}/ \code{\_Exx},讀懂它服務的裝置,判斷觸發頻率是否合理。 \end{enumerate} \end{labbox} \section{通用定位流程與檢查清單} 三個 Case 走的其實是同一條鏈——課綱那條你必須內化的定位流程: \begin{vflow}[4.2mm] \fnode{Kernel Log:dmesg 擷取完整錯誤串} \fnodel{找第一行根因}{ACPI Error:錯誤碼 + Method 路徑} \fnodel{acpidump 工具鏈(第 22 章)}{Method:反組譯出 dsdt.dsl} \fnode{ASL Source:閱讀方法邏輯與引用} \fnodel{acpiexec/sysfs 驗證假設}{Namespace:物件存在?型別對?} \fnode[flownodealt]{Hardware:EC/暫存器行為對不對} \end{vflow} \begin{keybox}[ACPI 錯誤除錯 checklist] \begin{enumerate} \item 收證據:完整 dmesg(含錯誤前後各十行)、BIOS 版本、 ACPICA 版本(錯誤行裡那個日期)。 \item 看前綴:\code{ACPI BIOS Error (bug)} 直接朝韌體方向查。 \item 找第一行根因,記下 Method 路徑與 \code{AE\_} 碼, 對照 24.2 速查表。 \item 反組譯 DSDT 與所有 SSDT(記得 \code{iasl -e}), 讀該 Method 與它引用的每個名稱。 \item 用 \code{/sys/bus/acpi/devices} 與 acpiexec 驗證 Namespace 假設;GPE 問題加看 \code{interrupts/} 計數。 \item 修 ASL(或先上 workaround),用 initrd Table Upgrade 驗證,確認 log 乾淨、功能恢復。 \end{enumerate} \end{keybox} \section{fwts:自動化健檢} 手動找完個案,出貨前還需要地毯式掃描。Canonical 維護的 \keyterm{fwts}(FirmWare Test Suite)內建數百項韌體檢查, 其中 ACPI 相關測項會靜態驗證每張表的欄位、並實際執行 Method 檢查回傳型別是否符合規格: \begin{shell}[title={用 fwts 掃一遍 ACPI}]\) sudo apt install fwts \(sudo fwts acpi klog method\) grep -A3 "Test Failure Summary" results.log Test Failure Summary ==================== Critical failures: NONE High failures: 1 method: BAT0._BIF returned package of 12 elements, expected 13
測項驗表、 掃 kernel log、 逐一執行 Method 驗回傳型別;結果依嚴重度寫進 。 bring-up 階段每改一版韌體跑一輪 fwts,能在使用者看到之前攔下 大多數本章等級的問題。
讀錯誤先找第一行根因; 前綴 代表 ACPICA 判定錯在韌體。錯誤行給四樣線索:Method 路徑、 碼、ACPICA 版本、原始碼位置。
:方法內 被併發 重入——用 修。
:引用不存在的名稱(韌體版本差異) ——用 防禦。
GPE storm 用 計數器抓兇手,根修是在 內清事件源; 只是止血帶。
定位鏈:Kernel Log \(\to\) ACPI Error \(\to\) Method \(\to\) ASL Source \(\to\) Namespace \(\to\) Hardware;出貨前用 fwts 地毯式掃描。
逐段解讀: 。 你會先檢查 ASL 裡的什麼?
為什麼能修 ? 它的代價是什麼?在什麼情況下你反而要小心它造成 ?
與 都能處理「別的表定義的 物件」,兩者的職責差在哪?為什麼只用前者不安全?
動手:寫一張引用不存在物件的 SSDT,在 acpiexec 中執行, 觀察 的完整輸出;再加上 驗證錯誤消失。
動手:在你的機器上做 24.5 的 GPE 體檢 Lab,找出最活躍的 GPE,並在 dsdt.dsl 中讀懂它的 handler 在服務誰。