第 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),或從 反查。

  1. FADT 的 與 欄位同時非零時,OS 用哪一個?規格為什麼要保留兩個欄位?

  2. 給你一行 dmesg:。指出 Revision、OEMID、OEM Revision 與編譯器,並回答:這份表的 Integer 是幾 bit?

  3. 修改 DSDT 之後為什麼不需要手動重算 checksum?請設計一個 驗證方法(提示:比較重編前後 offset 9 的位元組)。

  4. 動手做:在自己的 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 的 是零風險練習場。

  1. Override 生效期間,韌體 ROM 裡的 DSDT 變了嗎? 這對「還原」意味著什麼?

  2. 你重開機後 dmesg 出現 ,且 sysfs 出現 與 。 列出至少兩個可能原因與對應修法。

  3. 為什麼 kernel 在 Secure Boot(lockdown)下要擋 ACPI table override?從「AML 由誰執行、以什麼權限執行」的角度說明。

  4. 動手做(QEMU):寫一張在 下新增裝置的 SSDT,用 掛進虛擬機, 再從 guest 的 sysfs 反組譯回來驗證。

  5. 進階實作:在實體機完成整條 override 流水線 (只調高 OEM Revision),用 dmesg 確認生效後, 把系統還原乾淨。