第 1 章的結尾出現過一條生產線:硬體規格經過 ASL、AML,最後抵達 OS 的 ACPI Interpreter。本章把鏡頭拉近到前兩站,回答一個看似簡單、 卻是每個 BIOS 新人都踩過的問題:ASL 和 AML 到底是不是同一個東西? 答案是否定的——它們的關係就像原始碼與機器碼,中間隔著一個編譯器。
本章你會第一次動手:寫 、用 iasl 編譯、用 xxd 直視 位元組碼、再反組譯回文字——走完這一圈,「ASL \(\ne\) AML」 就是你親眼驗證過的事實。
說明 ASL 與 AML 的差異,以及 iasl 在兩者之間扮演的角色。
逐欄解釋 六個參數的意義與它們在表頭中的位置。
使用 iasl 編譯 檔,並讀懂編譯器的每一行輸出訊息。
用 xxd 檢視 AML,辨認 ScopeOp、DeviceOp、MethodOp 等 opcode。
使用 反組譯 AML,並解釋為什麼結果與原始碼不完全相同。
不看書畫出從 到 ACPI Interpreter 的完整流程圖。
從 C 語言與機器碼談起
如果你寫過 C,下面這張對照表就是理解 ASL 與 AML 最快的捷徑:
| C 的世界 | ACPI 的世界 | |
|---|---|---|
| 原始碼(給人讀寫) | (ASL) | |
| 編譯器 | gcc/clang | iasl |
| 產物(給機器執行) | 機器碼(x86/Arm 指令) | AML 位元組碼 |
| 執行者 | CPU 硬體直接執行 | OS 內的 ACPI Interpreter |
(ACPI Source Language)是給開發者使用的原始語言: 有註解、有縮排、有人類看得懂的關鍵字。 (ACPI Machine Language)則是 ACPI Interpreter 執行的機器語言: 一串緊湊的二進位 opcode,一個位元組都不浪費。兩者之間由 Intel 維護的編譯器 負責轉換。
但這個類比在「執行者」一列破功了:機器碼由 CPU 硬體直接執行, AML 卻是由軟體——住在 OS kernel 裡的直譯器——逐個 opcode 解譯(第 1 章: 與 ACPICA)。更貼切的類比 是 Java: 編出 bytecode 交給 JVM,iasl 編出 AML 交給 ACPI Interpreter;差別是後者內建在每一份 OS kernel 之中。
ASL 是人寫的原始語言;AML 是 interpreter 執行的機器語言; iasl 是橋樑,能編譯也能反組譯。韌體映像與 ACPI Tables 裡 永遠只有 AML,不會有一行 ASL 文字。
第一個 .dsl 檔:DefinitionBlock 逐欄解剖
用任何編輯器建立 (副檔名 也通用, 本教材統一用 )。先看全貌:
DefinitionBlock ("test.aml", "SSDT", 2, "DEMO", "TESTTBL", 0x00000001)
Scope (_SB)
Device (DEV0)
Name (_HID, "DEMO0001")
Method (_STA, 0, NotSerialized) Return (0x0F)
每一份 ASL 原始碼都以 開頭——宣告 「大括號裡是一張完整的 ACPI 表」,六個參數對應表頭欄位:
llcL5.4cm 參數 & 範例值 & 表頭位置 & 說明
AMLFileName & & 不進表頭 & 歷史遺留欄位;現代 iasl 以輸入檔名決定輸出檔名,此欄實際上被忽略
TableSignature & & 0x00(4 B) & 表的簽章,只能是 或
ComplianceRevision & & 0x08(1 B) & 2 以上:Integer 為 64-bit;1:32-bit(見下方補充)
OEMID & & 0x0A(6 B) & 廠商代號,最多 6 字元(實機常見 LENOVO、DELL)
OEM Table ID & & 0x10(8 B) & 這張表自己的名字,最多 8 字元
OEM Revision & & 0x18(4 B) & 表的版本號,每次改表就遞增
表頭剩下的欄位不用你填,iasl 自動算好寫入:Length (0x04,全表長度)、Checksum(0x09,讓全表位元組總和 除以 256 餘 0,OS 載表前會驗算)、Creator ID/Revision (0x1C 起,編譯器簽名 與版本號)。
這個數字決定 AML 世界裡整數的寬度:1 是 32-bit,2 以上是 64-bit,影響算術溢位與位元反轉——同一行 ASL 在兩種模式下可能 算出不同答案。實務上全平台的整數寬度由 DSDT 的 revision 決定,SSDT 跟隨之;revision 1 是古早 32-bit Windows 時代的遺物,現代平台一律寫 2。
大括號裡是表的本體,先快速導覽(細節是第 3 章的主題): 切換到系統匯流排 scope; 宣告一台裝置; 給它 OS 配對驅動程式用的 硬體 ID; 回報狀態, 表示 「存在、啟用、可見、正常」(第 8 章詳談)。
編譯:iasl test.dsl
iasl 是 ACPICA 工具組的一員,各平台都能直接安裝:
\(sudo apt install acpica-tools # Ubuntu / Debian\) brew install acpica # macOS
在 所在目錄執行編譯,完整輸出如下:
\(iasl test.dsl Intel ACPI Component Architecture ASL+ Optimizing Compiler/Disassembler version 20230628 Copyright (c) 2000 - 2023 Intel Corporation ASL Input: test.dsl - 15 lines, 287 bytes, 6 keywords AML Output: test.aml - 75 bytes, 3 named objects, 1 executable opcodes Compilation successful. 0 Errors, 0 Warnings, 0 Remarks\) ls test.aml test.dsl
逐行解讀這份輸出:
ASL Input:來源統計——15 行、287 位元組的文字。
AML Output:只剩 75 位元組,註解與排版全部消失; 「3 named objects」是 、、 ,「1 executable opcodes」是那句 。
Compilation successful:Errors 會讓編譯失敗; Warnings 與 Remarks 是提醒,bring-up 實務上要求三者全零。
目錄裡多出來的 就是 AML——一個不折不扣的二進位檔, 用文字編輯器打開只會看到亂碼。下一節我們用正確的工具直視它。
直擊機器語言:解剖 test.aml
能以十六進位傾印任何二進位檔。75 個位元組一頁看完:
\(xxd test.aml 00000000: 5353 4454 4b00 0000 0246 4445 4d4f 0000 SSDTK....FDEMO.. 00000010: 5445 5354 5442 4c00 0100 0000 494e 544c TESTTBL.....INTL 00000020: 2806 2320 1026 5c5f 5342 5f5b 821e 4445 (.# .&\_SB_[..DE 00000030: 5630 085f 4849 440d 4445 4d4f 3030 3031 V0._HID.DEMO0001 00000040: 0014 095f 5354 4100 a40a 0f ..._STA.... \end{shell} 前 36(0x24)個位元組是表頭,對照上一節的欄位表逐一驗收: 第一列有簽章 \code{SSDT}、little-endian 的總長度 \code{4b 00 00 00}(75)、revision \code{02}、checksum \code{46} 與 OEMID \code{DEMO};第二列是 \code{TESTTBL}、 OEM Revision,以及 iasl 自動填入的 \code{INTL} 與版本號。 從 offset 0x24 開始才是 AML 本體。先認識幾個本例用到的 opcode (完整清單在 ACPI 6.5 規格書的第 20 章「AML Specification」): \begin{center} \small \begin{tabular}{llL{6.6cm}} \toprule \rowcolor{acpiblue!10} \textbf{位元組} & \textbf{名稱} & \textbf{意義} \\ \midrule \code{0x10} & ScopeOp & 開啟一個 Scope \\ \code{0x5B} & ExtOpPrefix & 延伸 opcode 的前導位元組(ASCII 的 \code{[}) \\ \code{0x5B 0x82} & DeviceOp & 定義 Device(雙位元組 opcode) \\ \code{0x14} & MethodOp & 定義 Method \\ \code{0x08} & NameOp & 定義 Name 物件 \\ \code{0xA4} & ReturnOp & 回傳值 \\ \code{0x0A} & BytePrefix & 後面跟著 1 位元組的整數常數 \\ \code{0x0D} & StringPrefix & 後面跟著以 \code{0x00} 結尾的字串 \\ \code{0x5C} & RootChar & 路徑的根前綴(就是 ASCII 的反斜線) \\ \bottomrule \end{tabular} \end{center} 拿著這張表,把 0x24 之後的位元組逐段翻譯: \begin{asciitree}[title={test.aml 位元組逐段解讀(offset 0x24 起)}] 10 26 ← ScopeOp + PkgLength(0x26 = 38) 5C 5F 53 42 5F ← RootChar + NameSeg "_SB_" 5B 82 1E ← DeviceOp + PkgLength(0x1E = 30) 44 45 56 30 ← NameSeg "DEV0" 08 ← NameOp 5F 48 49 44 ← NameSeg "_HID" 0D 44 45 4D 4F 30 30 30 31 00 ← StringPrefix + "DEMO0001" + 結尾 14 09 ← MethodOp + PkgLength(0x09 = 9) 5F 53 54 41 ← NameSeg "_STA" 00 ← MethodFlags(0 個參數、不序列化) A4 0A 0F ← ReturnOp + BytePrefix + 0x0F \end{asciitree} 三個值得停下來咀嚼的觀察: \begin{itemize} \item \textbf{名稱就是 ASCII}:\code{5f 53 42 5f} 直接是 \code{\_SB\_},\code{44 45 56 30} 就是 \code{DEV0}, 所以 xxd 右側的 ASCII 欄看得到它們。名稱固定 4 字元、 原樣內嵌(第 3 章正式介紹這條規則)。 \item \textbf{PkgLength 是包裹的尺寸}:每個會「包住別人」的 opcode(Scope、Device、Method)後面都跟著 \keyterm{PkgLength},標示區塊到哪裡結束,數值含 PkgLength 本身;超過 63 時會追加 1 到 3 個位元組。 \item \textbf{opcode 空間不夠就開第二層}:\code{0x5B} 是 ExtOpPrefix,後面再跟一個位元組決定操作——Device 的 \code{0x5B 0x82} 就是這樣來的。 \end{itemize} 這 75 個位元組就是 interpreter 眼中「機器語言」的全部—— 你已經親眼看過 AML 了。 \section{反組譯:iasl -d 與 roundtrip} \begin{warnbox}[別讓 -d 蓋掉你的原始碼] \code{iasl -d test.aml} 會把結果寫到 \code{test.dsl}——和你的 原始碼\textbf{同名,直接覆蓋、不會詢問}。註解救不回來, 先 \code{cp} 一份再反組譯是韌體工程師的肌肉記憶。 \end{warnbox} \begin{shell}[title={安全的反組譯流程}]\) cp test.aml rt.aml \(iasl -d rt.aml Intel ACPI Component Architecture ASL+ Optimizing Compiler/Disassembler version 20230628 Copyright (c) 2000 - 2023 Intel Corporation Input file rt.aml, Length 0x4B (75) bytes Pass 1 parse of [SSDT] Pass 2 parse of [SSDT] Parsing Deferred Opcodes (Methods/Buffers/Packages/Regions) Parsing completed Disassembly completed ASL Output: rt.dsl - 1058 bytes \end{shell} 打開 \code{rt.dsl},和原始碼比一比: \begin{asl}[title={rt.dsl:反組譯的結果(節錄表頭註解)}] /* * Intel ACPI Component Architecture * AML/ASL+ Disassembler version 20230628 (64-bit version) * * Original Table Header: * Signature "SSDT" * Length 0x0000004B (75) * Revision 0x02 * Checksum 0x46 * OEM ID "DEMO" * ... */ DefinitionBlock ("", "SSDT", 2, "DEMO", "TESTTBL", 0x00000001) { Scope (\_SB) { Device (DEV0) { Name (_HID, "DEMO0001") // _HID: Hardware ID Method (_STA, 0, NotSerialized) // _STA: Status { Return (0x0F) } } } } \end{asl} 語意一模一樣,但文字不是原樣。差異來自四件事: \begin{itemize} \item \textbf{你的註解與空行全數消失}:AML 沒儲存它們, 反組譯器只能按固定風格重新排版。 \item \textbf{AMLFileName 變成空字串}:檔名沒被編進 AML, 反組譯器無從得知。 \item \textbf{多出來的註解是反組譯器加的}: \code{// \_HID: Hardware ID} 來自內建的預定義名稱表。 \item \textbf{寫法被正規化}:數值統一成十六進位;legacy operator(如 \code{Store})預設被改寫成 ASL 2.0 符號寫法, 加 \code{-dl} 可要求 legacy 風格(Module 3 兩種都教)。 \end{itemize} 那語意到底有沒有損失?把反組譯結果重新編譯、與原本的 AML 逐位元組比對: \begin{shell}[title={roundtrip 驗證:語意零損失}]\) iasl rt.dsl
... Compilation successful. 0 Errors, 0 Warnings, 0 Remarks
\(cmp rt.aml test.aml && echo BYTE-IDENTICAL BYTE-IDENTICAL \end{shell} 結論值得背起來:\textbf{ASL 到 AML 丟掉的是外觀(註解、排版、 檔名),留下的是完整語意};所以 AML 到 ASL 再到 AML 可以 位元組級重現,但 ASL 到 AML 再到 ASL 永遠回不到原來的文字。 \section{完整旅程:從 .dsl 到 ACPI Interpreter} 把本章的實驗接回第 1 章的大圖,ASL 的一生如下: \begin{vflow}[4.5mm] \fnode{test.dsl(ASL 原始碼)} \fnodel{iasl 編譯}{test.aml(AML 位元組碼)} \fnodel{整合進 BIOS/UEFI build}{韌體映像(燒進 Flash ROM)} \fnodel{開機時放入記憶體}{ACPI Tables(DSDT 與 SSDT)} \fnodel{OS 逐 opcode 解譯}{ACPI Interpreter(OSPM)} \end{vflow} 為什麼韌體裡只放 AML?因為它緊湊(75 對 287 位元組)、免依賴 (OS 只需 interpreter,不必內建編譯器)、又標準化 (opcode 由規格書定義,任何 OS 讀到的都是同一套語言)。 \begin{warnbox}[常見誤解] \begin{itemize} \item 「AML 是 CPU 執行的機器碼」——錯。CPU 執行的是 kernel 裡 interpreter 的指令;AML 只是 interpreter 的輸入資料。 \item 「\code{.dsl} 檔可以直接塞進韌體」——錯。韌體映像裡 只有編譯後的 AML;沒有任何元件看得懂 ASL 文字。 \item 「ASL 和 AML 只是副檔名不同」——錯。一個是文字原始碼、 一個是二進位位元組碼,中間隔著一次編譯。 \end{itemize} \end{warnbox} \begin{keybox}[本章重點回顧] \begin{itemize} \item ASL 是給人寫的原始語言;AML 是 ACPI Interpreter 執行的 機器語言;iasl 負責編譯與反組譯,韌體裡永遠只有 AML。 \item AML 由 OS 內的 interpreter 以軟體方式解譯執行, 不是 CPU 指令——類比 Java bytecode 與 JVM。 \item \code{DefinitionBlock} 六個參數對應表頭欄位; ComplianceRevision 2 代表 64-bit Integer。 \item AML 等於 36 位元組表頭加 opcode 串流:Device 是 \code{0x5B 0x82}、Method 是 \code{0x14},名稱以 4 字元 ASCII 內嵌,區塊長度靠 PkgLength 界定。 \item 反組譯外觀有損、語意無損:註解與排版永遠消失, 但重新編譯可得到位元組級相同的 AML。 \end{itemize} \end{keybox} \begin{exbox} \begin{enumerate} \item 用自己的話解釋:為什麼說 AML 是「機器語言」, 卻又不是 CPU 執行的?那到底是誰在執行它? \item \code{DefinitionBlock} 的六個參數中,哪一個不會出現在 最終的 AML 表頭?ComplianceRevision 填 1 與 2 差在哪? \item 紙上反組譯:不用電腦,把位元組序列 \code{14 08 4D 59 4D 31 00 A4 01} 翻譯回 ASL (提示:\code{0x01} 是 OneOp,代表常數 1)。 \item 動手做:安裝 iasl 並編譯本章的 \code{test.dsl},用 xxd 找出 OEMID \code{DEMO} 的 offset;再把 OEM Revision 改成 2 重編,用 \code{cmp -l} 找出變動的位元組並解釋原因。 \item 動手做:對 \code{test.aml} 執行安全的反組譯流程, 列出結果與原始碼的三個差異,再重編反組譯結果並用 \code{cmp} 驗證 AML 位元組級相同。 \end{enumerate} \end{exbox} \chapter{ACPI Namespace} \label{ch:namespace} 上一章的結尾,\code{test.aml} 的 75 個位元組抵達了 ACPI Interpreter。但它做的第一件事不是「執行」——大部分 AML 描述的 是結構,不是動作——而是把這串 opcode 展開成一棵活在記憶體裡的 樹:\keyterm{ACPI Namespace}。這棵樹是 OS 認識整台機器的地圖。 本章教你讀懂它:長什麼樣、名字有什麼規則、路徑怎麼解析。 這些是後續所有章節的地基——寫 ASL 的本質就是在對這棵樹做規劃。 \begin{goalbox} \begin{itemize} \item 說明 Namespace 是什麼、由誰在什麼時機建立。 \item 畫出含五個預定義根 scope 的 Namespace 樹並說出各自用途。 \item 說明 NameSeg 的 4 字元規則與補底線行為。 \item 寫出最小可編譯的 Namespace 範例,指出每一行在樹上做了什麼。 \item 分辨絕對路徑、相對路徑、上層前綴與單段名稱的向上搜尋。 \item 說明 Scope 與 Device 的差異、Alias 的用途。 \end{itemize} \end{goalbox} \section{Namespace:OS 眼中的整台機器} \keyterm{ACPI Namespace} 是一棵\textbf{具名物件的樹}: 每個節點有一個名字,節點上掛著物件——Device、Method、Name 這些你在第 2 章見過的東西。重點在於它的生成時機與材料: \begin{keybox}[Namespace 是開機時「蓋」出來的] Namespace 不存在於韌體中。開機時,OS 的 ACPI Interpreter 先載入 DSDT(永遠第一張),逐個 opcode 建立節點;接著依序載入每一張 SSDT,把內容\textbf{掛進同一棵樹}。多張表、一棵樹——韌體給的是 藍圖(AML),樹是 OS 在自己的 kernel 記憶體裡蓋出來的房子。 \end{keybox} \begin{vflow}[4.5mm] \fnode{DSDT 與所有 SSDT(韌體提供的 AML)} \fnodel{開機時依序載入}{ACPI Interpreter 逐 opcode 建節點} \fnodel{全部掛進同一棵樹}{ACPI Namespace(kernel 記憶體中)} \fnodel{走訪 \_SB、讀取 \_HID}{OS 列舉裝置、載入 Driver} \end{vflow} 「多張表合併成一棵樹」有一個直接的推論:\textbf{名字會撞}。 如果 DSDT 和某張 SSDT 都宣告了 \nspath{\_SB.DEV0}, interpreter 載入第二份定義時會回報 \code{AE\_ALREADY\_EXISTS}, 輕則該物件失效、重則整張表被拒載——實機除錯的常客,第 24 章詳談。 \section{一棵樹的全貌與預定義根 Scope} 樹的根是 \keyterm{Root Scope},寫作一個反斜線 \code{\textbackslash}。根底下有幾個規格預先定義的 scope, 各有各的住戶。一台典型筆電的樹長這樣: \begin{asciitree}[title={典型筆電的 Namespace(節錄)}] \ ← Root Scope:一切路徑的起點 ├── _SB_ 系統匯流排:所有裝置物件的家 │ ├── PCI0 PCI Host Bridge │ │ └── LPCB LPC Bridge │ ├── CPU0 處理器(現代寫法放這裡) │ ├── USB0 USB Controller │ └── EC0_ Embedded Controller ├── _PR_ Processor 物件(傳統擺放位置) ├── _TZ_ Thermal Zone(熱管理) ├── _SI_ 系統指示燈 ├── _GPE General Purpose Event 處理方法 └── _OSI 預定義 Method:讓 AML 詢問 OS 身分 \end{asciitree} 五個預定義根 scope 的分工如下: \begin{center} \small \begin{tabular}{llL{6.8cm}} \toprule \rowcolor{acpiblue!10} \textbf{Scope} & \textbf{名稱由來} & \textbf{住著什麼} \\ \midrule \nspath{\_SB} & System Bus & 所有 Device 物件;OS 列舉硬體的起點 \\ \nspath{\_PR} & Processor & 傳統的 Processor 物件 (新平台改用 \nspath{\_SB} 下的 Device) \\ \nspath{\_TZ} & Thermal Zone & ThermalZone 物件與溫控 Method (第 17 章) \\ \nspath{\_SI} & System Indicators & 系統狀態指示,如 \code{\_SST} \\ \nspath{\_GPE} & General Purpose Events & GPE 事件處理 Method (\code{\_L06}、\code{\_E09} 這類命名) \\ \bottomrule \end{tabular} \end{center} \begin{notebox}[這五個 scope 不用你建、也輪不到你建] Interpreter 在載入任何一張表\textbf{之前},就會先把 \nspath{\_SB}、\nspath{\_PR}、\nspath{\_TZ}、\nspath{\_SI}、 \nspath{\_GPE} 連同 \code{\_OSI}、\code{\_REV}、\code{\_GL} 等預定義物件建好,所以 DSDT 可以直接 \code{Scope (\_SB)} 走進去掛東西。 \end{notebox} 如果你熟悉檔案系統,直接套用那個心智模型:根目錄 \code{/} 對應 Root Scope \code{\textbackslash},目錄對應 Device 這類可含子節點 的物件,檔案對應 Name、Method 這類葉節點,「目前工作目錄」對應 目前 scope,而 \code{..} 對應上層前綴 \code{\textasciicircum}。 \section{命名規則:NameSeg 的四字元世界} 樹上每個節點的名字叫 \keyterm{NameSeg},規則硬得像水泥: \begin{itemize} \item \textbf{固定 4 個字元}。在 AML 裡 NameSeg 佔恰好 4 個 位元組,不多不少。 \item \textbf{合法字元}:開頭是大寫字母或底線,其後可用 大寫字母、數字、底線。ASL 名稱不分大小寫,一律視為大寫。 \item \textbf{不足 4 字元,編譯器自動補尾端底線}:你寫 \code{\_SB},AML 裡是 \code{\_SB\_};寫 \code{EC0}, 存的是 \code{EC0\_};\code{CPU0} 剛好 4 字元,原樣保留。 \item \textbf{底線開頭保留給規格}:\code{\_HID}、\code{\_STA}、 \code{\_SB\_} 都是規格定義的名字;自家物件請避開底線 開頭,未來規格新增名稱時才不會撞上。 \end{itemize} 這就是 \code{\_SB\_} 這個怪名字的由來:「\code{\_SB} 補一條 底線」。第 2 章的 xxd 傾印裡你已親眼看過證據——offset 0x27 起 的 \code{5f 53 42 5f} 正是 \code{\_SB\_},補的底線真實存在於 AML 中;Linux 的 \code{/sys} 與 acpidump 顯示的也是這個形式。 為什麼這麼死板?為了 interpreter 的效率:名稱定長 4 位元組, 比對名字只需一次 32-bit 整數比較,路徑編碼也不需要長度欄位。 \section{逐行拆解:最小可編譯的 Namespace} 下面這個最小範例值得倒背如流——10 行原始碼、樹上兩個新節點: \begin{asl}[title={最小 Namespace 範例(DSDT 身分)}] DefinitionBlock ("test.aml", "DSDT", 2, "DEMO", "TEST", 1) { Scope (\_SB) { Device (DEV0) { Name (_HID, "DEMO0001") } } } \end{asl} 逐行拆解: \begin{itemize} \item \textbf{第 1 行 \code{DefinitionBlock}}:這次簽章是 \code{DSDT},模擬平台主表的身分。revision 2 表示 64-bit Integer;OEM Table ID \code{"TEST"} 才 4 字元, 上限是 8,沒問題。 \item \textbf{\code{Scope (\_SB)}}:\code{\textbackslash} 開頭 是絕對路徑——從根走到 \code{\_SB\_}。\code{Scope} \textbf{不建立任何東西},只是把「目前位置」移過去。 \item \textbf{\code{Device (DEV0)}}:在目前位置底下 \textbf{建立}一個 Device 節點。\code{DEV0} 是相對名稱, 完整路徑是 \nspath{\_SB.DEV0}。 \item \textbf{\code{Name (\_HID, "DEMO0001")}}:在 \code{DEV0} 底下建立 Name 物件,值是字串;完整路徑 \nspath{\_SB.DEV0.\_HID}。 \item \textbf{三個右大括號}:依序關閉 Device、Scope、 DefinitionBlock,「目前位置」隨之退回。 \end{itemize} Interpreter 載完這張表之後,樹上多了什麼: \begin{asciitree}[title={這張表建出來的樹}] \ └── _SB_ ← 預先存在;Scope 只是走進去 └── DEV0 ← 新建:Device 物件 └── _HID ← 新建:Name 物件,值為字串 DEMO0001 \end{asciitree} \section{\texorpdfstring{\nspath{\_SB.DEV0}}{\_SB.DEV0} 到底代表什麼?} 這是本章的核心問題,值得用一整節、從三個層次把它回答到見骨。 \textbf{層次一:文法上},它是一條絕對路徑——\code{\textbackslash} 表示從根出發,\code{\_SB}(實為 \code{\_SB\_})與 \code{DEV0} 是兩個 NameSeg,句點是路徑分隔符。 \textbf{層次二:Namespace 上},它指向樹上的\textbf{一個節點}: interpreter 讀到 \code{0x5B 0x82}(DeviceOp)時建立、掛在 \code{\_SB\_} 底下的 Device 物件。路徑只是「住址」, 物件本身才是「住戶」。 \textbf{層次三:OS 眼中},它就是\textbf{一台裝置}。OS 走訪 \nspath{\_SB} 子樹,遇到 Device 節點就讀取 \code{\_HID}、 配對驅動程式。如果這張表真的被載入一台 Linux 機器 (Module 10 會教你怎麼做),你會看到: \begin{shell}[title={Linux 眼中的 DEV0}]\) ls /sys/bus/acpi/devices/ | grep DEMO DEMO0001:00
\(cat /sys/bus/acpi/devices/DEMO0001:00/path \_SB_.DEV0 \end{shell} 注意 sysfs 印出的路徑是 \code{\_SB\_.DEV0}——補滿底線的 四字元真身。 最後一個必須內化的心法:\textbf{它不是硬體本身}。 \nspath{\_SB.DEV0} 只是韌體對 OS 的一句宣告——「這裡有台裝置, ID 是 DEMO0001」。即使主機板上根本沒焊那顆料,宣告依然成立、 OS 照樣列舉出一台裝置(所以才需要 \code{\_STA} 回報實際狀態, 第 8 章詳談)。這正是第 1 章「描述與決策分離」的延伸。 \begin{keybox}[一句話答案] \nspath{\_SB.DEV0} 是 Namespace 樹上的一條絕對路徑,指向一個 名為 \code{DEV0} 的 Device 物件;對 OS 而言,它就是「一台裝置」 的完整住址。 \end{keybox} \section{路徑解析:絕對、相對與向上搜尋} 在 ASL 裡引用一個名字時,interpreter 依你的寫法套用四種規則: \begin{itemize} \item \textbf{絕對路徑}(\code{\textbackslash} 開頭):從根 出發逐段往下走,找不到就報錯,絕不嘗試別處。 \item \textbf{上層前綴}(\code{\textasciicircum} 開頭): 從目前 scope 往上剝一層再解析剩餘路徑;可以疊加, \code{\textasciicircum\textasciicircum} 就是上兩層。 \item \textbf{單一 NameSeg}(無前綴、無句點):先在目前 scope 找,沒有就到上一層找,一路找到根——這叫 \keyterm{search-to-root}(向上搜尋),最先找到者勝, 「就近原則」。 \item \textbf{多段相對路徑}(含句點但不以前綴開頭,如 \code{PCI0.DVAL}):只從目前 scope 出發往下解析, \textbf{不做向上搜尋}。 \end{itemize} 用一張可編譯的表把四種規則一次演完: \begin{asl}[title={路徑解析示範(Store 為「把前者存入後者」,Module 3 詳談)}] DefinitionBlock ("", "SSDT", 2, "DEMO", "PATHS", 1) { Name (ROOT, 0x11) // \ROOT Scope (\_SB) { Name (SVAL, 0x22) // \_SB.SVAL Device (PCI0) { Name (DVAL, 0x33) // \_SB.PCI0.DVAL // Method scope node: \_SB.PCI0.M1 Method (M1, 0, NotSerialized) { Store (DVAL, Local0) // search up: 0x33 Store (SVAL, Local1) // search up: 0x22 Store (ROOT, Local2) // search up: 0x11 Store (\_SB.SVAL, Local3) // absolute Store (^DVAL, Local4) // parent prefix Return (Local0) } } } } \end{asl} 站在 \code{M1} 裡面,每一種寫法的解析過程如下。注意一個 新手最常忽略的事實:\textbf{Method 本身也是一個 namespace 節點},所以目前 scope 是 \nspath{\_SB.PCI0.M1}, 不是 \nspath{\_SB.PCI0}: \begin{center} \small \begin{tabular}{lL{6.2cm}l} \toprule \rowcolor{acpiblue!10} \textbf{寫法} & \textbf{解析過程} & \textbf{結果} \\ \midrule \code{DVAL} & 單段:M1 沒有,上一層 PCI0 命中 & \code{0x33} \\ \code{SVAL} & 單段:M1、PCI0 都沒有,\code{\_SB\_} 命中 & \code{0x22} \\ \code{ROOT} & 單段:一路搜到根才命中 & \code{0x11} \\ \nspath{\_SB.SVAL} & 絕對路徑直達,不搜尋 & \code{0x22} \\ \code{\textasciicircum{}DVAL} & 剝掉 M1 這層、落在 PCI0 & \code{0x33} \\ \code{\textasciicircum\textasciicircum{}SVAL} & 往上兩層到 \code{\_SB\_} & \code{0x22} \\ \code{PCI0.DVAL} & 多段相對:只從 M1 往下找,不上搜 & 找不到 \\ \bottomrule \end{tabular} \end{center} 最後一列是經典陷阱:很多人以為相對路徑也會向上搜尋,於是在 Method 裡寫 \code{PCI0.DVAL},執行期收到 \code{AE\_NOT\_FOUND}。 記住:\textbf{向上搜尋只適用於單一 NameSeg},路徑一旦含有句點 或前綴,就只按字面解析。 \section{Scope 與 Device 的差異} 第 3.4 節說過 \code{Scope} 不建立東西,這一節把話說滿。 兩者的差異可以濃縮成一句:\textbf{Device 蓋新房間, Scope 只是走進既有的房間繼續擺家具}。 \begin{center} \small \begin{tabular}{lL{4.6cm}L{4.6cm}} \toprule \rowcolor{acpiblue!10} & \textbf{Scope} & \textbf{Device} \\ \midrule 建立新物件? & 否——只切換「目前位置」 & 是,建立 Device 物件 \\ 目標必須已存在? & 是,否則編譯錯誤 & 否,名稱本來就是新的 \\ OS 列舉時可見? & Scope 本身不可見 & 可見(搭配 \code{\_HID} 等) \\ 典型用途 & 走進既有位置補充物件(SSDT 最常用) & 宣告一台新裝置 \\ \bottomrule \end{tabular} \end{center} 「目標必須已存在」不是一句空話,iasl 會直接擋下來: \begin{asl}[title={bad.dsl:故意示錯——對不存在的路徑開 Scope}] DefinitionBlock ("", "SSDT", 2, "DEMO", "BAD", 1) { Scope (\_SB.TSCP) // TSCP was never created anywhere { Name (TST0, One) } } \end{asl} \begin{shell}[title={iasl 拒絕編譯}]\) iasl bad.dsl bad.dsl 3: Scope (_SB.TSCP) Error 6084 - Object does not exist ^ (_SB.TSCP)
Compilation failed. 1 Errors, 0 Warnings, 0 Remarks
那 SSDT 想往「別張表定義的裝置」底下掛東西怎麼辦?答案是先用 宣告「這個名字存在於別處」——Module 8 詳談。
Alias:同一個物件的第二個名字
替既有物件取第二個名字——注意,是同一個 物件的另一個門牌,不是複製一份:
DefinitionBlock ("", "SSDT", 2, "DEMO", "ALIAS", 1)
Scope (_SB)
Device (EC0) Name (BATP, One) // battery present flag
// Second doorplate for the same object Alias (_SB.EC0.BATP, BPRE) // creates _SB.BPRE
└── _SB_ ├── EC0_ │ └── BATP ← 原名 └── BPRE ← Alias:與 BATP 是同一個物件
語法是 :來源用任何合法路徑指定, 新名字建在目前 scope 底下;之後從 或 讀寫,碰到的都是同一份資料。常見用途有二: 把樹深處的物件開放到較好引用的淺處;平台改版後保留舊名字, 讓期待舊路徑的 OS 或驅動程式繼續運作。
用 Scope 建新裝置: 在 不存在時直接編譯錯誤;建裝置要用 。
期待多段相對路徑向上搜尋:含句點的路徑只從 目前 scope 出發;向上搜尋只屬於單一 NameSeg。
在 Method 裡用 忘了 Method 也是一層 scope: 先剝掉的是 Method 自己那一層。
兩張表建同名物件:開機載表時 ——第 24 章教你從 dmesg 抓錯。
Namespace 是開機時 interpreter 讀取 DSDT 與所有 SSDT、 合併建出的具名物件樹,活在 OS kernel 的記憶體裡。
五個預定義根 scope——(裝置)、 、、、 ——由 interpreter 預建,直接 Scope 進去用。
NameSeg 固定 4 字元,不足補尾端底線( 實為 );底線開頭的名字保留給規格。
路徑四規則: 絕對、 上層、單段名稱向上搜尋(就近原則)、 多段相對路徑不向上搜尋。
Scope 開門不蓋房、目標必須已存在;Device 蓋房; Alias 給同一物件第二個門牌。
是指向一個 Device 物件的絕對住址, OS 眼中的一台裝置——但它是描述,不是硬體本身。
不看書說出五個預定義根 scope 各自住著什麼, 以及哪一個是 OS 列舉裝置的起點。
下列名稱哪些是合法的 NameSeg?寫出編譯器實際儲存的 4 字元形式:、、、 、。
沿用 3.6 節的路徑範例:若把 直接搬到 底下,表中每種寫法各解析到哪裡? 哪幾行會從此失敗?
解釋:為什麼 會編譯失敗, 卻沒問題?兩者對樹各做了什麼?
動手做:擴充本章的最小範例——在 下新增 ,並用 把 開放成 ;編譯、反組譯,畫出最終的樹。