第 2、3 章說過:OS 開機時解譯 AML、建立 ACPI Namespace。那 AML 究竟住在 哪裡?答案的主體就是 (Differentiated System Description Table)——每台機器都有、也只有一份的「硬體描述主檔」。你在前七個模組寫的 每一個 Device、OperationRegion、Method,在真實產品中幾乎都躺在 DSDT (或它的延伸 SSDT)裡。
本章把 DSDT 當成一份文件來讀:它從哪裡來(FADT)、表頭說了什麼、內文如何 組織、OS 什麼時候讀它。與其背規格,不如把你自己筆電的 DSDT 倒出來對照著 讀——本章每個 shell 範例都能直接在 Linux 機器上重現。
說明 OS 如何經由 FADT 的 與 欄位找到 DSDT,以及「每系統唯一、開機必載、不可卸載」的意義。
逐欄位解讀 36 位元組的 ACPI 表頭,並說明 Revision 欄位如何決定 Integer 寬度。
畫出一份典型筆電 DSDT 的頂層佈局(、LPCB、 EC0、……)並說明 OEM 常見的組織方式。
說明 OS 建立 Namespace 時 DSDT 與 SSDT 的載入順序。
用 關鍵字策略,在幾千行的 DSDT 裡快速定位電池、EC 與 LID。
DSDT 的定位:每系統唯一、開機必載
ACPI 把「系統描述」分成兩塊。固定的一塊由 FADT 直接用欄位描述:PM timer 在哪個 I/O port、電源按鈕暫存器長什麼樣——這些是死的資料,不需要程式碼。 可變的一塊則交給 AML:主機板上有哪些裝置、怎麼開關電源、EC 暫存器如何存取。 DSDT 就是這塊可變描述的主體,規格把它歸類為 system fixed description 的一部分——意思是:它不是可選配件,而是與 FADT 綁定、 開機時「必定存在、必定載入」的表。
OS 找到 DSDT 的路徑很短:RSDP 指向 XSDT,XSDT 列出 FADT,而 FADT 裡有兩個欄位直接指向 DSDT 的實體位址:
lllL6.2cm FADT 欄位 & 位移 & 寬度 & 說明
& 40 & 4 bytes & DSDT 的 32-bit 實體位址(傳統欄位)
& 140 & 8 bytes & DSDT 的 64-bit 實體位址; 非零時以此為準、忽略 欄位
整條尋找路徑與後續動作如下:
一定只有一份:一個系統只有一張 DSDT,簽章固定為 ;一定載入:OS 初始化 ACPI 時第一張解譯的 AML 表就是它,沒有它就沒有 Namespace;一定不卸載:DSDT 沒有卸載介面,從開機活到關機。相對地,SSDT(第 19 章)可以有多張、 可以動態載入——它們是 DSDT 的延伸,而不是替代。
「不可卸載」有個實務推論:想「換掉」DSDT,唯一的機會是在 OS 讀它 之前動手——這正是第 20 章 override 實戰的原理。
36 位元組表頭:逐欄位解讀
所有標準 ACPI 表共用同一個 36 位元組表頭,DSDT 也不例外。把表頭讀熟, 之後除錯、override、看 dmesg 都會反覆用到:
cllL6.6cm 位移 & 欄位 & 長度 & 說明
& Signature & 4 & 固定為
4 & Length & 4 & 整張表的長度(含表頭),單位 byte
8 & Revision & 1 & 決定 Integer 寬度:小於 2 為 32-bit,2 以上為 64-bit
9 & Checksum & 1 & 使全表位元組總和 mod 256 為 0; iasl 重編時自動計算
10 & OEMID & 6 & OEM 名稱,如
16 & OEM Table ID & 8 & 平台/專案代號,如
24 & OEM Revision & 4 & OEM 自訂版本號——第 20 章 override 成敗的關鍵欄位
28 & Creator ID & 4 & 產生工具的 ID,iasl 填
32 & Creator Revision & 4 & 產生工具版本,如
直接看一份真實表頭。Linux 把每張表原封不動地曝露在 底下:
\(sudo xxd -l 36 /sys/firmware/acpi/tables/DSDT 00000000: 4453 4454 3fcd 0200 02f4 4c45 4e4f 564f DSDT?.....LENOVO 00000010: 4342 2d30 3120 2020 0200 0000 494e 544c CB-01 ....INTL 00000020: 2709 2420 '.\)
逐段對照:開頭的 是簽章 。接著 是 little-endian 的長度 (約 183 KB); 是 Revision 2(64-bit Integer); 是 checksum;再來依序是 OEMID、OEM Table ID、OEM Revision 、Creator ID 與 Creator Revision (iasl 版本以日期命名)。
開機時 kernel 也會把同一份表頭印成一行 dmesg,欄位一一對得上:
括號內從左到右是 Revision()、OEMID、OEM Table ID、 OEM Revision、Creator ID、Creator Revision。之後只要一行 dmesg, 你就能讀出整份表頭。
DSDT 的 Revision 小於 2 時,直譯器把所有 Integer 當成 32-bit;2 以上才是 64-bit。這是為了相容 ACPI 1.0 時代的表。
ACPICA 以 DSDT 的 Revision 決定整個 Namespace 的整數寬度——SSDT 自己標多少都不會改變這個全域設定。
反組譯舊機器的 v1 DSDT 再重編時,不要順手把 Revision 改成 2:OEM 的算式可能依賴 32-bit 溢位截斷行為,改寬度等於改語意。
真實筆電的 DSDT 長什麼樣子
反組譯一台市售筆電的 DSDT,常會得到三萬行以上的 ASL。先別急著逐行讀—— 所有 OEM 的頂層佈局都大同小異,掌握骨架後你就知道每類東西住在哪一區:
(root) ├── _SB_ ← 所有實體裝置的家 │ ├── PCI0 ← PCIe root complex(_HID PNP0A08) │ │ ├── GFX0 ← 內顯(_ADR 0x00020000) │ │ ├── I2C0 ── TPD0 ← I2C 控制器 → 觸控板 │ │ ├── GPP0 ── NVME ← PCIe 橋 → SSD │ │ └── LPCB ← LPC/eSPI 橋(_ADR 0x001F0000) │ │ ├── EC0_ ← Embedded Controller(PNP0C09) │ │ │ ├── BAT0 ← 電池(PNP0C0A) │ │ │ └── AC0_ ← 變壓器(ACPI0003) │ │ ├── HPET ← 高精度計時器 │ │ └── RTC_ ← 即時時鐘 │ ├── LID0 ← 上蓋開關(PNP0C0D) │ └── PWRB ← 電源鍵(PNP0C0C) ├── _TZ_ ── THRM ← 熱區(_TMP、_PSV、_CRT) ├── _GPE ← GPE 事件處理(_L17、_E02……) └── _PR_ ← 舊式 CPU 物件(新平台多搬進 SSDT)
幾個觀察重點:可列舉的 PCI 裝置(GFX0、NVME)只放 當「掛勾」,讓 ACPI 物件與 PCI 功能對上;不可列舉的裝置(EC、電池、 LID)則必須給 ,OS 才知道它們存在。電池與變壓器慣例上掛在 EC 底下,因為它們的資料實際是透過 EC 暫存器讀的(第 9–11 章的 OperationRegion 就派上用場)。LID 有的 OEM 掛 、有的掛 EC 底下,兩者都合法。
把這個骨架縮成一份可編譯的迷你 DSDT,就是下面這個範例——它去掉了 、EC Field 等細節(前幾個模組都教過),只留結構:
DefinitionBlock ("dsdt.aml", "DSDT", 2, "ACME", "NB100", 0x00000001)
Scope (_SB)
Device (PCI0) // PCIe root complex
Name (_HID, EisaId ("PNP0A08")) Name (_CID, EisaId ("PNP0A03")) Name (_UID, Zero)
Device (LPCB) // LPC/eSPI bridge
Name (_ADR, 0x001F0000) // dev 31, func 0
Device (EC0) // Embedded Controller Name (_HID, EisaId ("PNP0C09")) Name (_GPE, 0x17) // EC uses GPE 0x17
Device (LID0) // lid switch Name (_HID, EisaId ("PNP0C0D"))
Device (PWRB) // power button Name (_HID, EisaId ("PNP0C0C"))
Scope (_TZ) ThermalZone (THRM) Method (_TMP, 0, Serialized) Return (3232) // 323.2 K = 50.05 degC
逐行拆解:
的第二個參數 決定簽章;第三個參數 就是表頭的 Revision(64-bit Integer);最後的 是 OEM Revision。
是「不可列舉世界」與「可列舉世界」的交界:root bridge 本身用 宣告,底下的 PCI 裝置改用 。
的 是 ,高 16 位是 device 31、低 16 位是 function 0——Intel 平台 LPC/eSPI 橋的慣用位置。
的 告訴 OS:這顆 EC 的事件走 GPE ,也就是 (Module 7 講過 GPE 與喚醒的關係)。
回傳十分之一 Kelvin: 代表 323.2 K,約攝氏 50 度。
沒有人把三萬行寫在同一個檔案裡。OEM 的 BIOS 原始碼中,DSDT 是幾十個 檔(、、 ……)經由 include 與前置處理器拼出來的, build 時才組成單一 DefinitionBlock。你反組譯得到的 是「攤平」的結果,行號與 OEM 原始檔完全無關——這就是為什麼溝通時 要引用物件路徑(如 ), 而不是行號。
OS 如何用 DSDT 建立 Namespace
把第 2 章的 Namespace 知識與本章的表串起來,OS 端的載入順序是固定的:
三個值得記住的細節:
DSDT 永遠第一。SSDT 可以引用 DSDT 定義的物件 (靠 External,第 19 章),反過來 DSDT 引用 SSDT 的物件就得小心載入時序。
解譯 DSDT 是「載入」而非「執行」:直譯器走過整份 AML, 把 Device、Name、Method 掛上樹,但 Method 的本體此時不執行——要等 OS 或驅動呼叫才跑(第 12–14 章)。
載入完成後才開始裝置列舉:對每個 Device 呼叫 問存在與否、呼叫 做初始化,再用 找 Driver(第 1 章的資料流圖)。
幾千行裡找到你要的裝置:搜尋策略
拿到一份三萬行的 ,工程師的第一個技能不是讀,而是 搜。最有效的進入點是 :標準裝置的 ID 是固定的, 背下常用幾個,等於拿到 DSDT 的目錄頁:
llL5.6cm & 裝置 & 常見物件名
/ & PCIe/PCI root bridge & PCI0
& Embedded Controller & EC0、EC、H_EC
& 電池(Control Method Battery) & BAT0、BAT1
& AC 變壓器 & AC、AC0、ADP1
& 上蓋開關(Lid) & LID、LID0
& 電源鍵 & PWRB、PBTN
& 睡眠鍵 & SLPB
& HPET 計時器 & HPET
& RTC 即時時鐘 & RTC
& TPM 2.0 & TPM
配合 拿行號、 看前後文,三分鐘就能定位目標:
反方向也行得通:如果你是從 Linux 這頭出發(例如 回報電池異常),先問 kernel 這顆裝置的 Namespace 路徑,再回頭搜 :
\(ls /sys/bus/acpi/devices/ | head -4 ACPI0003:00 LNXPOWER:00 PNP0C09:00 PNP0C0A:00\) cat /sys/bus/acpi/devices/PNP0C0A:00/path _SB_.PCI0.LPCB.EC0_.BAT0
其他好用的搜尋錨點:搜 找 EC 的 OperationRegion 與 Field 定義;搜 這類 GPE method 名找事件處理;搜 找誰在發事件。
DSDT 是 system fixed description 的一部分:每系統唯一、 開機必載、不可卸載;OS 經由 FADT 的 / 欄位找到它( 非零時優先)。
36 位元組表頭中,Revision 決定 Integer 是 32 還是 64 bit(以 DSDT 為準、全域生效);OEM Revision 是第 20 章 override 的關鍵;checksum 由 iasl 自動計算。
典型佈局: 底下用 掛可列舉裝置,LPCB 底下住 EC,電池與變壓器掛在 EC 下, 放熱區。
OS 建 Namespace 的順序固定:預定義物件、DSDT、再來才是 SSDT;載入只掛物件,Method 等被呼叫才執行。
找東西先搜 ( EC、 電池、 LID),或從 反查。
FADT 的 與 欄位同時非零時,OS 用哪一個?規格為什麼要保留兩個欄位?
給你一行 dmesg:。指出 Revision、OEMID、OEM Revision 與編譯器,並回答:這份表的 Integer 是幾 bit?
修改 DSDT 之後為什麼不需要手動重算 checksum?請設計一個 驗證方法(提示:比較重編前後 offset 9 的位元組)。
動手做:在自己的 Linux 機器上倒出 DSDT 並反組譯(作法見第 20 章前兩節),回答四個問題——總共幾行?有幾個 ?電池物件的完整路徑是什麼?EC 的 值是多少?
SSDT
上一章說 DSDT 每系統唯一,但你在自己機器上執行 ,多半會看到七、八張 (Secondary System Description Table)。同一份硬體描述 為什麼要拆成這麼多張表?答案與軟體工程的模組化一模一樣:DSDT 是穩定的主幹,SSDT 是可拆卸的擴充模組——不同 SKU、不同 CPU、 有無獨顯,都靠換裝不同的 SSDT 解決,主幹不必重驗證。
本章先講「為什麼拆」,再講最重要的合併語意——多張表載入後仍是 同一棵 Namespace——然後動手寫一張引用 DSDT 物件的 SSDT, 最後認識動態載入的 Load 家族。
說明平台把描述拆成 DSDT 加多張 SSDT 的工程理由, 並舉出動態產生的實例。
描述多張表合併成單一 Namespace 的規則、載入順序與同名 衝突的後果。
寫出一張用 External 引用 DSDT 既有裝置、可通過 iasl 編譯的擴充 SSDT。
說明 Load 與 LoadTable 的語意、使用情境,以及 Unload 被廢止的原因。
在 Linux 上列出系統的所有 SSDT 並反組譯其中一張。
為什麼要拆表:SSDT 存在的理由
想像你是 OEM 的 BIOS 工程師,同一代筆電有三個 SKU:內顯版、獨顯版、 面板不同的教育版。如果所有描述都塞在 DSDT,你就得維護三份幾萬行的 DSDT,改一行熱區參數要重新驗證三份表。實務的做法是:
主機板共通描述進 DSDT:EC、LPC、電池、LID 這些每台都有的東西。
變體差異進 SSDT:獨顯版多載一張 dGPU 的 SSDT, 內顯版根本不放;不同面板的背光表各自一張。
CPU 世代差異進 SSDT:C-state、P-state 隨 CPU 不同而異,Intel 平台的 (、 ……)系列 SSDT 甚至是 BIOS 在 POST 階段依實際插的 CPU 動態產生的,不是編譯期寫死的。
執行期才知道的東西動態載入:例如接上 dock 之後才需要的裝置描述,可由 AML 用 Load 追加(見 19.4 節)。
在一台真實筆電上看 dmesg,就能數出這種拆法的痕跡:
\(sudo dmesg | grep 'ACPI: SSDT' ACPI: SSDT 0xAA3A5000 0003A5 (v02 LENOVO Tpm2Tabl 00001000 ...) ACPI: SSDT 0xAA3A0000 001B1C (v02 PmRef Cpu0Ist 00003000 ...) ACPI: SSDT 0xAA39E000 000394 (v02 PmRef Cpu0Cst 00003001 ...) ACPI: SSDT 0xAA390000 077B69 (v02 LENOVO SaSsdt 00003000 ...) ACPI: SSDT 0xAA38C000 003657 (v02 LENOVO GppSsdt 00001000 ...) \end{shell} OEM Table ID 一眼就能猜出分工:TPM 一張、CPU 電源兩張、 北橋(System Agent)一張、PCIe 橋一張。 \section{合併語意:多張表、同一棵樹} SSDT 的表頭格式與 DSDT 完全相同(簽章換成 \code{SSDT}),內容同樣是 AML。關鍵觀念是:\textbf{表只是「來源檔」,Namespace 才是「成品」}。 直譯器載入每張表時,把表裡宣告的物件一一掛到同一棵樹上;載入完成後, 樹上根本不記得哪個物件來自哪張表。 \begin{vflow}[4.5mm] \fnode{FADT 指到的 DSDT 先載入} \fnodel{依 XSDT 出現順序}{SSDT 1 到 n 逐張載入} \fnodel{開機後(可選)}{Load / LoadTable 動態追加} \fnode[flownodealt]{同一棵 ACPI Namespace} \end{vflow} 載入順序由規格保證:DSDT 最先,其餘 SSDT 依它們在 XSDT 裡出現的順序(ACPI 6.4 起規格特別澄清了這一點)。順序之所以重要, 是因為\textbf{後載入的表可以引用先載入的物件,反之不行}; 而如果兩張表宣告了同一個完整路徑的物件,就會撞名。 用第 19.3 節將完成的例子預先看合併的效果: \begin{asciitree}[title={載入前後:SSDT 分支長到 DSDT 的樹上}] 載入前 —— DSDT 建好的樹: \ └── _SB_ └── PCI0 └── LPCB └── EC0_ ├── BAT0 └── LID0 SSDT 想追加的分支(用 External 宣告掛載點): Scope (\_SB.PCI0.LPCB.EC0) └── SEN0 ← 新裝置 載入後 —— 仍是同一棵樹: \ └── _SB_ └── PCI0 └── LPCB └── EC0_ ├── BAT0 ├── LID0 └── SEN0 ← 來自 SSDT,與 BAT0 平起平坐 \end{asciitree} \begin{warnbox}[同名衝突:AE\_ALREADY\_EXISTS] 兩張表若在同一路徑宣告同名物件(例如都定義 \nspath{\_SB.PCI0.I2C1.TPL1}),先載入者建立成功,後載入者觸發 \code{AE\_ALREADY\_EXISTS}。kernel log 會出現 \code{ACPI BIOS Error (bug)} 開頭的錯誤,該物件建立失敗, 依 ACPICA 版本與錯誤位置,整張表的載入可能就此中止。OEM 拆表時必須保證各表的路徑互斥;你在第 20 章做 override 或加自訂 SSDT 時,同樣要先確認路徑沒被用走。另外,內容完全相同的重複表會被 ACPICA 偵測並跳過,不會載入兩次。 \end{warnbox} \section{External:跨表引用的橋樑} SSDT 要把 SEN0 掛到 \nspath{\_SB.PCI0.LPCB.EC0} 底下,就得在 \code{Scope} 裡寫出這條路徑。問題來了:編譯這張 SSDT 時,iasl 只看得到這一個檔案,根本不知道 EC0 是誰——直接編會報 「Object does not exist」錯誤。\keyterm{External} 就是為此而生:它向編譯器\textbf{承諾}「這個名字存在於別張表, 放行吧」,本身不建立任何物件。 完整範例——在既有 EC 底下新增一顆示範感測器裝置: \begin{asl}[title={ssdt-sen.dsl:在 DSDT 的 EC0 下新增裝置}] DefinitionBlock ("ssdt-sen.aml", "SSDT", 2, "ACME", "ECSENSOR", 0x00000001) { // Promise to the compiler: this device exists in the DSDT External (\_SB.PCI0.LPCB.EC0, DeviceObj) Scope (\_SB.PCI0.LPCB.EC0) { Device (SEN0) { Name (_HID, "ACME0001") // vendor-specific HID Name (_UID, Zero) Method (_STA, 0, NotSerialized) { Return (0x0F) // present and enabled } } } } \end{asl} 逐行拆解: \begin{itemize} \item \code{External (\textbackslash\_SB.PCI0.LPCB.EC0, DeviceObj)}: 第一個參數是完整路徑,第二個參數宣告物件型別。編譯期它只是 承諾;產出的 AML 中會留下 External 記錄,但\textbf{不會} 建立 EC0。 \item \code{Scope (...)}:切到既有裝置的範圍。Scope 本身也不建立物件,只是把後續宣告掛到那個節點下。 \item \code{Device (SEN0)}:真正的新物件。載入這張 SSDT 時, 直譯器在樹上找到 EC0,把 SEN0 掛上去。 \item 用 \code{iasl ssdt-sen.dsl} 即可編譯,產出的 \code{ssdt-sen.aml} 可用第 20 章的 initrd 或 QEMU 掛載驗證。 \end{itemize} External 的常見型別:\code{DeviceObj}、\code{MethodObj}、 \code{IntObj}、\code{FieldUnitObj}、\code{MutexObj}。其中 \code{MethodObj} 最需要小心——編譯器得知道該 Method 收幾個參數才能正確解析呼叫點,宣告時可一併給出參數型別清單,例如 \code{External (\textbackslash{}ECRD, MethodObj, IntObj, \{IntObj\})}。 三個典型的 External 陷阱: \begin{itemize} \item \textbf{路徑打錯照樣編譯過}。承諾不會被驗證,錯誤要到 \textbf{載入期}才爆:直譯器回報 \code{AE\_NOT\_FOUND}, 整段 Scope 掛載失敗。 \item \textbf{型別或參數個數宣告錯},AML 可能照樣產出, 但呼叫端行為錯亂,或反組譯器解讀成錯誤的語法樹 (第 20 章 \code{-e} 旗標的由來)。 \item \textbf{把 External 當成「建立物件」}。它從不建立東西; 如果目標表根本沒載入,引用它的程式碼全數失效。 \end{itemize} \section{動態載入:Load、LoadTable 與 Unload} 到目前為止的表都是韌體開機前擺好、OS 開機時一次載入的。ASL 還提供執行期追加定義塊的能力,典型情境:筆電接上 dock 之後,AML 從 dock 的記憶體區把一張描述 dock 裝置的 SSDT 讀進來掛上樹。 現行規格(ACPI 6.5)的語法:\code{Load (Object, Result)}—— \code{Object} 可以是 OperationRegion、Field 或 Buffer, 內容必須是一張完整的表;載入成功時 \code{Result} 收到真值。 \begin{asl}[title={ssdt-load.dsl:執行期載入另一張表(示意)}] DefinitionBlock ("ssdt-load.aml", "SSDT", 2, "ACME", "DYNLOAD", 0x00000001) { Scope (\_SB) { // Example address only: table image exposed by firmware OperationRegion (TMEM, SystemMemory, 0xDF000000, 0x400) Field (TMEM, AnyAcc, NoLock, Preserve) { TBLD, 8192 // 1024-byte table image } Name (LDST, Zero) // load-state flag Method (LDTB, 0, Serialized) { If (LDST == Zero) { Load (TBLD, Local0) // Local0: success flag If (Local0) { LDST = One } } } } } \end{asl} 拆解重點:\code{TBLD} 這個 Field 蓋住一張放在記憶體裡的完整表 影像;\code{LDTB} 用旗標避免重複載入——載入兩次同一張表 就會撞上一節說的 \code{AE\_ALREADY\_EXISTS}。 \code{LoadTable} 則不從記憶體讀,而是去 XSDT 找一張「已存在但 尚未載入」的表,依簽章、OEMID、OEM Table ID 三個字串比對: \begin{asl}[title={LoadTable 片段(節錄自 Method 內)}] // Load a staged table: signature, OEM ID, OEM table ID Local0 = LoadTable ("OEM1", "ACME", "DOCKTBL") If (Local0) { Store ("dock table loaded", Debug) } \end{asl} \begin{notebox}[Unload 已廢止] 早年的 Load 第二個參數是 \code{DDBHandle},搭配 \code{Unload (DDBHandle)} 可把表拆下來。實務上幾乎沒有韌體使用、 OS 端也難以安全實作(拆表等於從活著的 Namespace 上鋸樹枝),因此 \textbf{ACPI 6.3 正式把 Unload 列為 deprecated},新版 ACPICA 直譯器已不支援;Load 的第二個參數也改成單純回報成功與否的結果。 讀舊 DSDT 看到 Unload 知道它是什麼即可,新程式碼一律不要用。 \end{notebox} \section{在 Linux 上數 SSDT} 驗收本章觀念最快的方式,是打開自己的機器數一數: \begin{shell}[title={觀察系統的 SSDT(Linux)}]\) ls /sys/firmware/acpi/tables/ | grep SSDT SSDT1 SSDT2 SSDT3 SSDT4 SSDT5
\(ls /sys/firmware/acpi/tables/dynamic/ 2>/dev/null SSDT6 # tables added by Load at runtime\) sudo cp /sys/firmware/acpi/tables/SSDT2 /tmp/ssdt2.dat \(cd /tmp && iasl -d ssdt2.dat && head -3 ssdt2.dsl Intel ACPI Component Architecture AML/ASL+ Disassembler version 20240927 ... \end{shell} 注意兩件事:韌體給的 SSDT 直接以 \code{SSDT1}、\code{SSDT2}…… 命名;執行期由 Load 動態載入的表則收在 \code{dynamic/} 子目錄。 把每張 SSDT 都反組譯開頭幾行、對照 OEM Table ID, 就能拼出這台機器的描述拼圖——這正是本章練習的最後一題。 \begin{keybox}[本章重點回顧] \begin{itemize} \item SSDT 是 DSDT 的延伸:表頭格式相同、內容同為 AML, 可以有多張;拆表動機是 SKU 變體、CPU 世代差異、 選配硬體與動態產生(Intel PmRef 系列就是 POST 時生成的)。 \item 合併語意:所有表載入同一棵 Namespace,DSDT 先、SSDT 依 XSDT 順序後;後載者可引用先載者。 \item 同一路徑宣告兩次觸發 \code{AE\_ALREADY\_EXISTS}, 該物件建立失敗、表可能整張載入中止。 \item External 只是給編譯器的承諾,不建立物件;路徑錯誤要到 載入期才以 \code{AE\_NOT\_FOUND} 爆出來。 \item Load 從 OpRegion/Field/Buffer 載入表、LoadTable 從 XSDT 挑表載入;Unload 自 ACPI 6.3 起廢止。動態載入的表出現在 \code{/sys/firmware/acpi/tables/dynamic/}。 \end{itemize} \end{keybox} \begin{exbox} \begin{enumerate} \item 為什麼 CPU 的 P-state/C-state 描述特別適合做成 POST 階段動態產生的 SSDT,而不是寫死在 DSDT 裡? \item 一張 SSDT 想在 \nspath{\_SB.PCI0} 底下加裝置,卻忘了寫 External 就直接 \code{Scope}。請問問題會在編譯期 還是載入期爆出來?錯誤訊息大概長什麼樣? \item 兩張 SSDT 都定義了 \nspath{\_SB.FOO},載入結果由什麼決定? 「贏家」是誰?輸家那張表會發生什麼事? \item 承第 19.3 節範例:如果把 External 的型別誤寫成 \code{MethodObj},iasl 還會編譯通過嗎?試著解釋 為什麼 External 的錯誤這麼難在編譯期攔下來。 \item 動手做:數一數你機器上有幾張 SSDT(含 \code{dynamic/}), 全部反組譯後列出各自的 OEM Table ID 與用途猜測,並找出 其中一張 External 引用了哪些 DSDT 物件。 \end{enumerate} \end{exbox} \chapter{DSDT/SSDT Override} \label{ch:override} 前兩章把表讀懂了,本章來真的:\textbf{改}。場景很常見——筆電閤蓋不會 睡、電池閾值方法有 bug、或者你想驗證「如果 \code{\_LID} 這樣寫就好了」 的假說。BIOS 是 OEM 的地盤,你沒有原始碼也沒有簽章金鑰;但 Linux 提供了一條正規的實驗路徑:把修改過的 DSDT 放進 initrd,kernel 開機時用它\textbf{取代}韌體給的那份。韌體本身一個位元組都沒動, 實驗失敗把 initrd 還原即可。 這條流水線是 BIOS/驅動工程師的日常除錯手段,也是本書第一個端到端 實戰。請找一台可重開機的 Linux 機器跟著做;想零風險練習, 直接用 20.8 節的 QEMU 方案。 \begin{goalbox} \begin{itemize} \item 完整操作 extract、disassemble、modify、recompile、 override、boot、test 七步流程。 \item 說明反組譯多表系統時 \code{-e} 旗標的作用與缺它的後果。 \item 說明 initrd override 的比對規則,以及為什麼必須調高 OEM Revision。 \item 在 dmesg 中辨認 override 成功與失敗(install)的訊息。 \item 用 QEMU 的 \code{-acpitable} 安全地掛載自訂表。 \item 說明 Secure Boot/lockdown 的限制與改壞後的還原方法。 \end{itemize} \end{goalbox} \section{全景:七步流水線} 先看全貌,再逐步拆解: \begin{vflow}[4mm] \fnode{acpidump + acpixtract\\{\scriptsize 取出二進位表}} \fnodel{iasl -e -d}{反組譯出 dsdt.dsl} \fnode[flownodealt]{修改:調高 OEM Revision + 小修} \fnodel{iasl}{重編出 dsdt.aml} \fnodel{cpio}{打包 kernel/firmware/acpi} \fnodel{GRUB}{initrd 前置掛載} \fnodel{重開機}{dmesg 驗證 override} \end{vflow} \begin{labbox}[DSDT Override 一頁清單] 以下是完整流程速查,細節見後續各節。全程需要 root 權限, 建議在可實驗的機器(或 QEMU 虛擬機)上進行。 \begin{enumerate} \item 取表:\code{sudo acpidump > acpi.out},再 \code{acpixtract -a acpi.out}。 \item 反組譯:\code{iasl -e ssdt*.dat -d dsdt.dat},得到 \code{dsdt.dsl}。 \item 修改:調高 DefinitionBlock 的 OEM Revision, 再加入你要的變更。 \item 重編:\code{iasl dsdt.dsl},把錯誤修到 0 Errors。 \item 打包:把 \code{dsdt.aml} 放進 \code{kernel/firmware/acpi/},cpio 打包。 \item 掛載:設定 GRUB,讓它把這個 cpio 放在原本 initrd 之前。 \item 驗證:dmesg 出現 Table Upgrade: override 且 OEM Revision 已更新。 \end{enumerate} \end{labbox} \section{Step 1:取出系統的表} \code{acpidump} 把所有 ACPI 表連同位址倒成一個文字檔, \code{acpixtract} 再從中抽出二進位(兩個工具的完整用法見第 21 章): \begin{shell}[title={取出並抽出所有表}]\) mkdir /acpi-lab && cd /acpi-lab \(sudo acpidump > acpi.out\) acpixtract -a acpi.out
Acpi table [DSDT] - 183615 bytes written to dsdt.dat Acpi table [SSDT] - 933 bytes written to ssdt1.dat Acpi table [SSDT] - 6940 bytes written to ssdt2.dat Acpi table [FACP] - 276 bytes written to facp.dat ...
表示抽出全部的表;不加的話預設只抽 DSDT 與 SSDT。 把整組 留著當備份——之後不管改壞什麼, 都能從這裡重來。
Step 2:反組譯——為什麼需要 -e
DSDT 常會呼叫定義在 SSDT 裡的 Method(反之亦然)。反組譯器遇到 一個不認識的名字時,最大的困擾是:它不知道那是幾個參數的 Method。AML 的呼叫點沒有標參數個數,反組譯器只能靠已知的宣告去切 後面的位元組流;猜錯參數個數,整段程式碼會被切歪,輕則產生錯誤的 引用、重則反組譯出根本不能重編的垃圾。
旗標把「其他表」餵給反組譯器當符號參考, 讓它能正確解析跨表引用:
反組譯完成後, 開頭會自動帶出一排 宣告,正是第 19 章教的機制。若你少帶了某張 SSDT, 輕則重編時冒出一堆「Object does not exist」,重則反組譯結果 本身就是歪的——遇到怪異的反組譯輸出,第一反應就是檢查 有沒有帶齊。
Step 3:安全的小修改
第一次 override,不要急著修真正的 bug。先做兩個「安全改動」, 把流程本身驗證通過:
調高 OEM Revision 不是儀式,是硬規則:20.7 節會看到,只有 initrd 表的 OEM Revision 嚴格大於 韌體表才會被採用。忘了調,整個 override 靜悄悄失效。
是無害的記號物件,足以證明「跑的是我這份表」。 想留執行期腳印,可再往常被呼叫的 Method 裡 字串到 Debug 物件——但那要開 kernel 除錯輸出才看得到 (Module 10 詳述),本章先用 OEM Revision 驗證。
Step 4:重編——面對 vendor DSDT 的現實
OEM 的 DSDT 往往是多年前的舊版 iasl 甚至微軟 asl.exe 編的, 語法寬鬆;新版 iasl 檢查嚴格,直接重編幾乎必有訊息:
\(iasl dsdt.dsl Intel ACPI Component Architecture ASL+ Optimizing Compiler/Disassembler version 20240927 dsdt.dsl 1482: If (CondRefOf (\_OSI, Local0)) Warning 3144 - Method Local is set but never used ^ (Local0) dsdt.dsl 9021: Return (Local3) Error 6066 - Method local variable is not initialized ^ (Local3) \end{shell} 常見訊息的分類與處置: \begin{center} \small \begin{tabular}{L{5.9cm}lL{5.2cm}} \toprule \rowcolor{acpiblue!10} \textbf{訊息(節錄)} & \textbf{等級} & \textbf{典型處置} \\ \midrule Method Local is set but never used(3144) & Warning & 多為 \code{CondRefOf} 舊寫法,可忽略; 想消音就刪掉多餘的第二參數 \\ Method local variable is not initialized(6066) & Error & 舊編譯器放水的真 bug;補上初始化,或比照原 AML 邏輯改寫 \\ Called method returns no value(6080) & Error & 呼叫點期待回傳值;對照上下文補 \code{Return} \\ Called method may not always return a value(3122) & Warning & 某些路徑沒 \code{Return};補齊每條路徑 \\ Not a parameter, used as local only(2087) & Remark & 風格提醒,可忽略 \\ Object does not exist & Error & 多半是反組譯時 \code{-e} 沒帶齊;回 Step 2 重來 \\ \bottomrule \end{tabular} \end{center} 原則:\textbf{Error 必修(不出 AML)、Warning 逐條看懂再決定、 Remark 與 Optimization 可放行}。訊息編號可配合 \code{-vw} 選擇性關閉、\code{-f} 可無視錯誤硬出檔——兩者的細節與風險第 21 章統一整理;在 override 情境裡,\code{-f} 等於把沒修好的炸彈 裝進 initrd,強烈不建議。 \section{Step 5:打包 initrd 與 GRUB 設定} kernel 在極早期會掃描 initrd 開頭是否有一個未壓縮的 cpio, 裡面路徑固定為 \code{kernel/firmware/acpi/*.aml}。做法照抄即可: \begin{shell}[title={打包 override 用的 cpio}]\) cd /acpi-lab \(mkdir -p kernel/firmware/acpi\) cp dsdt.aml kernel/firmware/acpi/ \(find kernel | cpio -H newc --create > acpi-override.img 360 blocks\) sudo cp acpi-override.img /boot/
接著讓 GRUB 把它放在既有 initrd 之前。Debian/Ubuntu 系有現成掛鉤;其他發行版用通用的串接法:
# Way 1 (Debian/Ubuntu): early-initrd hook # add this line to /etc/default/grub: # GRUB_EARLY_INITRD_LINUX_CUSTOM="acpi-override.img" \(sudo update-grub # Way 2 (universal): prepend to the existing initrd\) cat acpi-override.img /boot/initrd.img-\((uname -r) \ > initrd-acpi.img\) sudo cp initrd-acpi.img /boot/ # then point one GRUB menu entry’s initrd at initrd-acpi.img
這套機制叫 ACPI table upgrade,需要 (主流發行版預設開啟)。 initrd 裡最多放 64 張表;除了 RSDP 與 FACS,幾乎所有表都能換, 換 SSDT、換 FADT 都是同一套玩法。載入非原廠表後 kernel 會標記 taint——這是提醒「除錯報告請註明表被改過」,不是故障。
Step 6:重開機驗證——override 還是 install?
重開機後,第一件事看 dmesg:
\(sudo dmesg | grep -iE 'table upgrade|ACPI: DSDT' ACPI: DSDT ACPI table found in initrd [kernel/firmware/acpi/dsdt.aml] ACPI: Table Upgrade: override [DSDT-LENOVO-CB-01 ] ACPI: DSDT 0xAA372000 Physical table override, new table: 0xA9F1C000 ACPI: DSDT 0xA9F1C000 02CD41 (v02 LENOVO CB-01 00000003 ...) \end{shell} 重點有三:出現 \code{Table Upgrade: override} 字樣;最後一行的 OEM Revision 變成 \code{00000003}(我們調高後的值);長度也從 \code{02CD3F} 變成 \code{02CD41}——多了記號物件的位元組級差異。 不放心的話,再用第 18 章的 xxd 技巧讀 \code{/sys/firmware/acpi/tables/DSDT} 的 offset 24 起 4 個位元組, 應該看到 \code{03 00 00 00}。 如果 dmesg 出現的是 \code{Table Upgrade: install} 而不是 override,代表你的表被當成\textbf{新表附加}(sysfs 裡出現 \code{DSDT1}、\code{DSDT2} 兩份),對 DSDT 而言等於沒生效。 原因幾乎都是比對規則沒滿足: \begin{keybox}[initrd override 的比對規則] kernel 逐張比對 initrd 表與韌體表,\textbf{全部}滿足才 override: \begin{itemize} \item Signature 相同(例如都是 \code{DSDT}); \item OEMID 相同、OEM Table ID 相同(所以反組譯後 \textbf{不要}改動 DefinitionBlock 裡的這兩個字串); \item initrd 表的 OEM Revision \textbf{嚴格大於}韌體表 ——相等都不行。 \end{itemize} 不滿足時,簽章唯一的表會被 install 成附加表,其餘直接忽略。 每張 initrd 表最多被使用一次;檔名不影響比對, 放在 \code{kernel/firmware/acpi/} 目錄下即可。 \end{keybox} \section{QEMU:零風險的練習場} 不想拿實體機冒險?QEMU 的 \code{-acpitable} 可以直接把一張自訂表附加給虛擬機。先寫一張迷你 SSDT: \begin{asl}[title={qemu-lab.dsl:掛給 QEMU 的自訂表}] DefinitionBlock ("qemu-lab.aml", "SSDT", 2, "CLASS", "LAB20", 0x00000001) { Scope (\_SB) { Device (DEMO) { Name (_HID, "ACME0002") Name (_UID, Zero) Name (_STA, 0x0F) Name (MSG, "Hello from lab SSDT") } } } \end{asl} 編譯後開機、進 guest 驗證: \begin{shell}[title={掛表、開機、驗證}]\) iasl qemu-lab.dsl \(qemu-system-x86_64 -m 2G -enable-kvm \ -drive file=lab.qcow2,if=virtio \ -acpitable file=qemu-lab.aml # inside the guest:\) ls /sys/firmware/acpi/tables/ | grep SSDT SSDT1 SSDT2 \(sudo cp /sys/firmware/acpi/tables/SSDT2 /tmp && cd /tmp\) iasl -d SSDT2 && grep -n ’DEMO’ SSDT2.dsl 10: Device (DEMO)
注意 是附加一張表(適合練 SSDT), 不會替換 guest 的 DSDT;想練完整的 DSDT override,就在 guest 裡照 20.2 到 20.6 節原樣操作——虛擬機玩壞了,砍掉重建就好。
擋路的東西與退路
AML 以 kernel 權限執行,換表等於注入核心程式碼。因此開啟 Secure Boot 時,kernel 的 lockdown 機制會直接忽略 initrd 裡的 ACPI 表,dmesg 可看到 Lockdown 相關提示。 實驗前請在韌體設定中關閉 Secure Boot,或改用 QEMU。
改壞了怎麼辦?韌體那份 DSDT 從頭到尾沒被動過, 所以不存在「變磚」問題。開機選單選一個沒掛 override 的 舊項目,或用救援模式把 裡的 移除、還原 GRUB 設定即可。
給自己留退路:永遠保留一個乾淨的 GRUB 開機項, 不要把唯一的開機項指向掛了 override 的 initrd。
Windows 也有對應機制:用微軟工具把改過的表寫進 registry 供開機 載入(需測試簽章模式)——原理同樣是「OS 端替換、韌體不動」, 本書不展開。
七步流水線:acpidump 取表、 反組譯、 修改、重編、cpio 打包、GRUB 前置 initrd、開機 dmesg 驗證。
反組譯多表系統必帶 ,否則跨表 Method 參數個數猜錯,輸出不可信。
override 生效條件:Signature、OEMID、OEM Table ID 全同,且 OEM Revision 嚴格較大——所以每次改表都要調高它。
dmesg 見 才是成功;見 代表被當新表附加,DSDT 沒被換掉。
Secure Boot/lockdown 會使 initrd 表被忽略;還原只需 移除 initrd 裡的表,韌體原表永遠完好。
QEMU 的 是零風險練習場。
Override 生效期間,韌體 ROM 裡的 DSDT 變了嗎? 這對「還原」意味著什麼?
你重開機後 dmesg 出現 ,且 sysfs 出現 與 。 列出至少兩個可能原因與對應修法。
為什麼 kernel 在 Secure Boot(lockdown)下要擋 ACPI table override?從「AML 由誰執行、以什麼權限執行」的角度說明。
動手做(QEMU):寫一張在 下新增裝置的 SSDT,用 掛進虛擬機, 再從 guest 的 sysfs 反組譯回來驗證。
進階實作:在實體機完成整條 override 流水線 (只調高 OEM Revision),用 dmesg 確認生效後, 把系統還原乾淨。