第 3 章告訴你 Namespace 是一棵掛滿物件的樹;本章回答下一個問題:這些物件到底 「是什麼」?ASL 沒有 、 這類型別宣告關鍵字——你宣告的是 物件,型別由初始值決定,而且執行期還會換。這種動態型別的世界觀,讓你在 C 語言養成的直覺一半有用、一半會害你。
更麻煩的是,型別錯誤常常要到執行期才爆炸:iasl 願意讓你把 String 存進 Integer,因為規格書定義了一整套「隱式轉換」規則,直譯器會默默照做。 規則沒背熟,Method 就會回傳「看起來像對、其實差很遠」的值—— 本章是整本書最值得反覆翻閱的基本功。
列出 ASL 的主要物件型別,寫出各型別的字面值與典型用途。
說明 Integer 寬度由 DefinitionBlock 的 ComplianceRevision 決定, 並解釋 32/64 位元差異造成的經典坑。
區分 String 與 Buffer,寫出 初始化與巢狀 。
使用 、、 操作 Object Reference,說明 Field Unit 的「代理」角色。
背出 implicit conversion 的方向與規則,需要時改用 等 explicit conversion 運算子。
用 與 在執行期檢查物件。
一切都是物件:型別總覽
在 ASL 裡,(object)是唯一的建材:整數是物件、字串是物件、 一個 Device 是物件、一段可執行的 Method 也是物件。物件可以匿名存在 (例如運算式中間的暫時值),也可以用 掛上名字、放進 Namespace。 型別大致分成三個家族:
ASL 的物件型別 ├── 資料物件(computational data) │ ├── Integer ← 64-bit 無號整數 │ ├── String ← ASCII 字串,null 結尾 │ ├── Buffer ← 原始位元組陣列 │ └── Package ← 異質容器,可巢狀 ├── 物件參考(object reference) │ └── 由 RefOf、Index、CondRefOf 產生 └── 命名空間物件(named objects) ├── Field Unit ← 硬體暫存器的代理人 ├── Device、Method、Mutex、Event └── OperationRegion、ThermalZone、PowerResource 等
資料物件是 Method 裡搬來搬去的「值」;物件參考是指向其他物件的「箭頭」; 命名空間物件則是掛在樹上的「結構」——它們描述硬體、提供行為,通常不會被 當成值來運算。各型別的寫法與用途整理如下:
L2.6cmL6.0cmL5.0cm 型別 & 建立/字面值寫法 & 典型用途
Integer & 、、、、 & 暫存器值、旗標、計數器
String & & 識別字串、除錯訊息
Buffer & & 原始位元組、GUID、資源資料
Package & & 結構化資料表(、)
Object Reference & 、 & 間接存取、走訪容器
Field Unit & 在 中宣告 & 硬體暫存器的代理(第 9 章)
Device & & 裝置節點(第 8 章)
Method & & 可執行的控制邏輯
Mutex、Event & & 同步物件(第 12 章)
OperationRegion & & 位址空間視窗(第 9 章)
ASL 是(dynamically typed)語言:名字只是掛勾, 勾著什麼型別的物件、就是什麼型別;同一個 這行裝 Integer、下行可改裝 String。唯一的重要例外在 Store 的目標端—— 存進「已存在的具名資料物件」時,來源會被轉成目標的既有型別(4.6 節)。
Integer:64 位元的無號整數
ASL 的 Integer 沒有 / 之分,也沒有正負號與浮點數: 一律是無號整數,運算溢位時默默繞回(wrap around),不會有任何警告。 它的寬度由 的第三個參數 決定——就是本書每個範例開頭 裡的那個 :
ComplianceRevision :Integer 是 64 位元, 等於 。
ComplianceRevision :Integer 是 32 位元, 等於 ,超過 32 位元的常數與 運算結果一律被截斷。
很多老 DSDT 為了相容早期 Windows 而寫 1,於是 這類判斷在新舊表之間行為不同, 移植程式碼時務必先看這個參數。新專案一律寫 2。
由於沒有負數字面值,想表達「\(-1\)」時韌體工程師寫的是 ; 想做減法防下溢,得自己先比大小(第 6 章實戰)。
String 與 Buffer
String 與 Buffer 都是「一串位元組」,初學者常混用,其實個性完全不同。 String 是給人看的:內容限可列印 ASCII 字元,結尾隱含一個 null(); Buffer 是給機器看的:任意位元組都能放,長度明確宣告或由初始值推得。
Name (RAW0, Buffer () 0x10, 0x00, 0xFF) // 3 bytes, as listed Name (ZBF0, Buffer (8) ) // 8 bytes, all zero Name (MIX0, Buffer (4) 0x01, 0x02) // 01 02 00 00
的括號內是「宣告長度」:初始值不足時自動補零 (),完全省略初始值就是全零緩衝區(); 省略長度時由初始值個數決定()。兩者的差異整理如下:
L3.0cmL5.2cmL5.2cm 比較項目 & String & Buffer
內容 & 可列印 ASCII,null 結尾 & 任意位元組值
中途可含 & 不可(null 即結尾) & 可以
回傳 & 字元數(不含結尾 null) & 位元組數
典型用途 & 識別字串、訊息 & 暫存器資料、GUID、資源
一個好記的判斷法:資料若要跟硬體或二進位協定打交道,用 Buffer; 若要給人(或 OS 的字串比對)讀,用 String。
Package:異質、可巢狀的容器
是 ASL 唯一的複合容器:元素可以是任何資料物件, 甚至是另一個 Package,型別不必一致。它是 ACPI「用資料表描述硬體」 哲學的主要載體:中斷繞線表 、處理器效能狀態表 、 裝置屬性表 ,回傳的全是層層巢狀的 Package。
Name (PST0, Package () // one CPU P-state entry 2400, // core frequency (MHz) 35000, // power (mW) Package () 0x10, 0x00 // nested: control/status pair )
Package 可以具名(掛在 底下常駐)、也可以匿名 (直接寫在 裡,用完即丟)。單一 Package 上限 255 個 元素;超過時 iasl 自動改用 VarPackage 位元組碼,語法不變。
讀取 Package 元素拿到的不是值本身,而是一個 Object Reference, 必須用 解參考才能取得值(下一節詳談)。 這是 ASL 與 C 陣列手感差最多的地方: 不等於 「第 1 號元素的值」,而是「指向第 1 號元素的箭頭」。
Object Reference 與 Field Unit
是 ASL 的指標。三個運算子會產生它: 指向具名物件;——ASL 2.0 糖衣寫法 ——指向 Package、Buffer 或 String 的第 個元素; 在物件可能不存在時安全試探。 反向操作只有一個: 把箭頭變回值。
Local0 = PKG0 [1] // reference to element 1 Local1 = DerefOf (Local0) // now the actual value Local2 = DerefOf (BUF0 [0]) // one-liner: first byte of BUF0 Local3 = RefOf (NUM0) // reference to a named object PKG0 [0] = 0x5678 // store THROUGH the reference
最後一行示範了 reference 當「寫入目標」的行為:透過 參考寫入,改的是容器裡的那個元素。這也是修改 Package 內容的標準做法。
則是 ASL 最獨特的型別:它看起來像變數,背後卻是 硬體暫存器的代理人(proxy)。它在 區塊中 宣告(第 9 章詳解);例如把 EC 上的電池溫度暫存器宣告成 8 位元的 之後,Method 裡一句 ,直譯器就會 真的去 EC 讀回即時內容——語法上與讀一般變數毫無差別, 這正是 ASL 好寫、也好踩雷的原因。
執行期想知道手上物件是什麼型別,用 ;它對 reference 會自動解參考、回傳目標物件的型別代碼:
| 代碼 | 型別 | 代碼 | 型別 | 代碼 | 型別 |
|---|---|---|---|---|---|
| 0 | Uninitialized | 6 | Device | 12 | Processor(棄用) |
| 1 | Integer | 7 | Event | 13 | Thermal Zone |
| 2 | String | 8 | Method | 14 | Buffer Field |
| 3 | Buffer | 9 | Mutex | 15 | DDB Handle |
| 4 | Package | 10 | Operation Region | 16 | Debug Object |
| 5 | Field Unit | 11 | Power Resource |
搭配的 回傳「大小」:String 的字元數(不含 null)、 Buffer 的位元組數、Package 的元素個數——第 7 章走訪 Package 就靠它決定終點。
Implicit Conversion:直譯器的自動轉換
當運算子拿到「型別不對」的運算元,直譯器不會報錯,而是依規格書的 轉換表現場變出一個對的型別,這叫 (implicit conversion)。它發生在三種場合:
運算元轉換:運算子要 Integer 你給了 String,先轉 再算;雙運算元運算子(、比較)以 第一個運算元的型別為準,把第二個轉過去。
Store 目標轉換:目標是已存在的具名資料物件時, 來源被轉成目標的既有型別。
Field 寫入轉換:寫 Field Unit/Buffer Field 時 轉成 Integer 或 Buffer,超出欄寬的位元直接捨棄。
Store 的目標不同,行為差很大,這張小表值得背下來:
L4.4cmL8.6cm Store 目標 & 行為
具名 Integer、String、Buffer & 來源隱式轉換成目標的既有型別,目標型別不變
Local0–Local7、ArgX & 不轉換:整個物件直接取代,型別跟著來源走
Field Unit、Buffer Field & 轉成 Integer 或 Buffer 寫入,超寬位元捨棄
& 任何型別都收,轉成文字送到除錯輸出
三種資料型別之間的轉換規則如下(以 64 位元 Integer 為例):
lL6.0cmL4.4cm 轉換方向 & 規則 & 範例
Integer \(\to\) String & 轉成 16 字元十六進位 ASCII (含前導零、無 ) & \(\to\)
Integer \(\to\) Buffer & little-endian 逐位元組填入, 長度等於 Integer 寬度 & \(\to\)
String \(\to\) Integer & 一律視為十六進位, 遇非十六進位字元停止 & \(\to\)
String \(\to\) Buffer & 逐字元複製,含結尾 null & \(\to\)
Buffer \(\to\) Integer & 取前 8 bytes(little-endian), 不足補零 & \(\to\)
Buffer \(\to\) String & 逐位元組轉 開頭十六進位文字, 空白分隔 & \(\to\)
隱式的 String \(\to\) Integer 轉換一律當十六進位: 會變成 (十進位 16)。但顯式的 看前綴: 是十六進位 16、 是十進位 10。同一個字串走兩條路,結果差一截—— 處理 EC 或韌體吐出的數字字串時,永遠用 把意圖寫清楚,不要賭隱式轉換。
Explicit Conversion:把轉換寫清楚
隱式轉換是「直譯器幫你猜」;好的 ASL 應該用顯式運算子把意圖寫死。 常用的一組如下:
L4.0cmlL7.0cm 運算子 & 結果 & 行為要點
& Integer & 字串 開頭當十六進位、 否則十進位;Buffer 取前 8 bytes
& String & Buffer 位元組當 ASCII 字元, 取到 null 或 ( 表示全部)
& Buffer & Integer 給 8 bytes;String 連 結尾 null 一起複製
& String & Buffer 逐位元組轉 , 逗號分隔
& String & Buffer 逐位元組轉十進位文字, 逗號分隔
& 同 & 先轉成 的型別再串接;兩個 Integer 串成 16-byte Buffer
& Buffer & 編譯期巨集:GUID 字串轉 16-byte Buffer( 常客)
是隱式與顯式的交會點:它自己是顯式串接, 第二個運算元卻走隱式轉換——把 Integer 接在提示字串後面, 會自動變成 16 字元的十六進位文字(上一節表格的規則), 這是韌體工程師組除錯訊息的日常招式。
完整範例:型別實驗室
把本章所有觀念裝進一個可編譯、可執行的 SSDT。先看程式:
DefinitionBlock ("types.aml", "SSDT", 2, "ACME", "TYPES", 0x00000001)
Name (NUM0, 0x2A) // Integer (decimal 42) Name (STR0, "ACPI") // String Name (BUF0, Buffer () 0x41, 0x43, 0x50, 0x49) // "ACPI" Name (PKG0, Package () 0x1234, // Integer element "battery", // String element Buffer () 0xAA, 0xBB, // Buffer element Package () 1, 2 // nested Package )
Method (DEMO, 0, NotSerialized)
// explicit conversions Local0 = ToInteger ("0x2A") // hex prefix -> 0x2A Debug = Local0 Local1 = ToInteger ("42") // no prefix -> 42 = 0x2A Debug = Local1 Local2 = ToHexString (BUF0) // "0x41,0x43,0x50,0x49" Debug = Local2 Local3 = ToString (BUF0, Ones) // bytes as ASCII: "ACPI" Debug = Local3 Local4 = ToBuffer (NUM0) // 8-byte buffer, LSB first Debug = SizeOf (Local4) // 8
// implicit conversions Local5 = Concatenate ("NUM0 = ", NUM0) Debug = Local5 // integer became hex text NUM0 = "1F" // store String -> Integer Debug = NUM0 // 0x1F
// runtime type inspection Debug = ObjectType (PKG0) // 4 = Package Return (SizeOf (PKG0)) // 4 elements
逐行拆解重點:
開頭四個 宣告四種資料物件; 故意選 四個字母的 ASCII 碼,讓 有戲唱。
與 殊途同歸都是 42——一個走十六進位、一個走十進位, 印證顯式轉換「看前綴」的規則。
與 對同一個 給出完全不同的字串:前者是位元組的十六進位報表, 後者把位元組當字元讀。
:目標是具名 Integer,String 走隱式 十六進位轉換, 變成 ——型別不變、 值變了;對照 把 Integer 隱式轉成 16 字元十六進位文字, 以及 Store 進 Local 完全不轉換。
用 iasl 編譯後,以使用者空間直譯器 acpiexec 執行(第 21 章詳解), 每一步轉換的實際結果一覽無遺:
是特殊的內建物件:任何型別存進去都會轉成文字,送到 直譯器的除錯輸出——acpiexec 的主控台,或 Linux 的 kernel log (Module 10 教你打開)。它是 ASL 最接近 的東西。
ASL 動態型別:型別跟著值走;但 Store 進具名資料物件時, 來源會被轉成目標的既有型別,Local 與 Arg 則直接取代。
Integer 一律無號;寬度由 ComplianceRevision 決定: 是 64 位元、 是 32 位元, 的值跟著變。
String 是 null 結尾的 ASCII;Buffer 裝任意位元組; Package 是可巢狀的異質容器,元素以 reference 存取。
/ 造箭頭、 取值; Field Unit 是硬體暫存器的代理型別。
隱式轉換的 String \(\to\) Integer 一律十六進位; 才看 前綴。轉換規則表要背。
回傳型別代碼, 回傳 字元數/位元組數/元素數。
對 String、Buffer、Package 各代表什麼? 是多少?為什麼不是 5?
ComplianceRevision 為 1 與 2 時, 分別是什麼值? 寫出一個在兩種設定下結果不同的 判斷。
經過隱式轉換與 各得到多少? 呢?把四個答案列成表。
用 Package 描述一顆電池的三筆資料:設計容量(Integer)、 電壓(Integer)、序號(String),並分別用 legacy 的 + 與 ASL 2.0 的 寫法取出電壓。
動手做:輸入本章 types.dsl,用 iasl 編譯、acpiexec 執行; 然後把 改成 , 重新執行並解釋輸出為什麼變成 0。
Variables & Constants
型別解決了「資料長什麼樣」,本章解決「資料放在哪裡」。ASL 給你三種容器: 用 建立、常駐 Namespace 的具名物件;隨 Method 呼叫而生滅的 到 ;以及呼叫者塞進來的 到 。它們大致對應 C 語言的全域變數、區域變數與函式參數, 但生命週期與重入行為藏著幾個 ASL 特有的地雷。
本章同時把「資料怎麼進出容器」講完: 與 ASL 2.0 的 完全等價, 把值送回呼叫者,再加上 、 、 這批內建常數。學完本章,你就具備讀寫完整 Method 的最小裝備。
說明 Name、Local、Arg 三者的作用域、生命週期與適用場景。
寫出 與 兩種等價寫法, 讀懂任一風格的既有程式碼。
使用 、、、, 正確書寫十進位、十六進位與八進位字面值。
說明在 Method 內用 建立物件為何在重入時 觸發 。
畫出一次 Method 呼叫中變數的生命週期,逐行解釋課綱範例 的執行過程。
資料的三個家:Name、Local、Arg
先看全貌,再逐一深入:
L2.2cmL3.7cmL3.7cmL3.7cm 項目 & Name & Local0–Local7 & Arg0–Arg6
建立者 & 你,用 宣告 & 直譯器自動準備 & 呼叫者傳入
作用域 & Namespace(依宣告位置) & 單次 Method 執行 & 單次 Method 執行
生命週期 & 表載入起常駐(Method 內宣告者除外) & 進入 Method 而生,Return 即滅 & 同 Local
數量 & 不限 & 固定 8 個 & 最多 7 個
初始值 & 宣告時給定 & 未初始化,讀取即錯 & 呼叫者給定
典型用途 & 組態、常數、跨呼叫狀態 & 暫存計算 & 輸入參數
選擇的原則很單純:值要活過一次呼叫、或要給別的 Method 看,用 Name; 只在這次呼叫裡打轉的中間值,用 Local;呼叫者決定的輸入,自然是 Arg。
Name:常駐 Namespace 的具名物件
做兩件事:建立一個值為 100 的 Integer 物件, 並在「目前 scope」掛上 這個名字。寫在 或 底下時,物件在表載入時就建立, 之後任何 Method 都能依第 3 章的名稱搜尋規則找到它——它就是 ASL 的 全域變數,開機後一直活著,直到表被卸載。
真正的地雷在「把 寫進 Method 裡」。此時物件改為 執行到那一行才建立、Method 結束時移除,看起來像區域變數, 其實名字仍然掛在共享的 Namespace 上:
Method (BAD0, 0, NotSerialized) // do NOT copy this pattern Name (TMP0, 0) // created at run time TMP0 = 5 Return (TMP0) // TMP0 is removed when BAD0 exits ... usually
兩條執行緒同時跑 (或 Method 還沒結束又被事件處理重入)時, 第二次執行的 會發現名字已存在,直譯器丟出 ,整個 Method 中止——這是 Linux dmesg 裡 出鏡率極高的 ACPI 錯誤。iasl 也會提醒你:編譯上面的程式會得到 Remark「Control Method should be made Serialized due to creation of named objects within」。解法有二:把 Method 宣告成 (第 12 章詳講),或者根本別在 Method 裡用 ,改用 Local。
Local0–Local7:方法內的暫存空間
每次 Method 被呼叫,直譯器就發給這次執行八個全新的暫存槽 到 。它們是每次呼叫獨立的一組, 所以天生執行緒安全、遞迴安全,不會有上面 的重入問題。
兩條規則要記牢。第一,Local 沒有宣告型別,第一次 Store 什麼型別 就是什麼型別,之後還能換——它是純粹的「物件掛勾」,Store 進去 不做任何隱式轉換(對照 4.6 節)。第二,Local 出生時是 Uninitialized 狀態,初始化前就拿去讀是致命錯誤:直譯器 回報 ,Method 直接中止。 C 語言讀未初始化變數頂多拿到垃圾值,ASL 直接翻桌,這反而是好事—— 錯誤藏不住。
Arg0–Arg6:呼叫者給的輸入
的第二個參數宣告「我收三個引數」, Method 本體用 、、 取用,上限七個。 呼叫端寫 ,個數對不上時 iasl 在編譯期就會抓到 表內的呼叫錯誤;OS 驅動程式從外部呼叫時給錯個數,則是執行期錯誤。
Integer、String 這類單純資料是傳值:在 Method 裡對 重新 Store 只改到自己的複本。但 Buffer 與 Package 傳進來的是 物件參考——用 這種 Index 寫法穿透去改, 改到的是呼叫者的原始資料。第 7 章走訪 Package 的範例 會利用這一點;不想被改到就先 一份。
Store、指定運算子與 Return
資料進容器只有一個動詞。ACPI 6.0 世代之前寫 ; ASL 2.0 引入 C 風格的 。兩者編譯出一模一樣 的 AML 位元組碼(StoreOp),純粹是原始碼層的糖衣:
Store (0x1F, Local0) // legacy style Local0 = 0x1F // ASL 2.0 style, identical AML
Store (Local0, VAL0) // legacy: source first! VAL0 = Local0 // ASL 2.0: target first
注意方向: 是「來源在前」, 是「目標在前」。 反組譯舊機器的 DSDT、翻 BIOS 原始碼樹時滿眼都是 , 新程式碼則建議一律用 ——兩種都要能秒讀(第 6 章會把這個 對照擴大到所有運算子)。
結束 Method 並把 複製一份給呼叫者; 沒有值可回就寫 或讓 Method 自然結束。 規格書說沒執行到 的 Method 回傳值未定義;ACPICA 為了相容老 Windows AML 有「隱式回傳最後運算結果」的怪癖, 但那是相容性特技,永遠明確寫 Return。
常數與數字字面值
ASL 內建四個常數物件,各自有專屬的 AML opcode(比一般整數字面值 省空間,編譯器會自動幫你選):
、:就是 0 與 1,常見於舊程式碼與 反組譯輸出。
:所有位元為 1。64 位元下是 ,32 位元下是 (回想第 4 章的 ComplianceRevision 坑)。慣用作 「錯誤」「無效」「全選遮罩」的記號。
:直譯器實作的 AML 版本號,極少用到。
數字字面值支援三種進位。十進位直接寫()、十六進位加 ()——以及一個容易忘記的老朋友:
Name (CNT0, 100) // decimal -> 100 Name (CNT1, 0x64) // hexadecimal -> 100 Name (CNT2, 0144) // OCTAL! leading 0 -> also 100 Name (EFLG, Ones) // idiomatic error / all-bits marker
ASL 文法繼承了 C 的八進位規則: 不是 144,是 \(1\times64+4\times8+4=100\)。手滑在十進位數字前面補零對齊排版, 值就悄悄變了——用 iasl 反組譯回來會看到它變成 。 想排版對齊,請用十六進位加前導零(),那才是安全的。
常數物件是唯讀的:把 或字面值放在 Store 的目標端 不會有意義,編譯器會直接指出錯誤。
生命週期:一次呼叫的一生
把前面幾節串成時間軸——以下一節的課綱範例 為例, 一次呼叫從生到死長這樣:
對照之下,掛在 Method 之外的 物件完全不參與這個循環: 它們在表載入時出生,一直活到表卸載,是跨呼叫、跨 Method 共享的狀態。
課綱範例逐行拆解
把課綱指定的範例包進完整的 DefinitionBlock,先編譯起來再說:
DefinitionBlock ("vars.aml", "SSDT", 2, "ACME", "VARS", 0x00000001)
Name (VAL0, 100) // named object, lives in namespace
Method (TEST, 0) Local0 = 10 // first store initializes Local0 Local1 = 20 Local2 = Local0 + Local1 Return (Local2) // hand 30 (0x1E) back to caller
逐行拆解:
:表載入時建立具名 Integer,值 100 (十進位字面值,等於 )。它掛在根 scope, 與 無關,但任何 Method 都能讀寫它—— 故意放在這裡對照兩種生命週期。
:宣告零參數 Method。沒寫 Serialized/NotSerialized 時預設 NotSerialized; 因為本體只用 Local,不碰 ,重入也安全。
與 :第一次 Store 同時完成「初始化」與「定型別」,兩個 Local 現在都是 Integer。
:右邊的加法產生匿名的 30,Store 進 。三個 Local 都只活在這次呼叫。
:把 30 複製給呼叫者;隨後框架銷毀, 到 蒸發, 依然是 100。
同一個 Method 的 legacy 寫法如下——AML 完全相同,只是外衣不同, 拿去反組譯老 DSDT 時你會天天看到這種臉:
Method (TSTL, 0) Store (10, Local0) Store (20, Local1) Add (Local0, Local1, Local2) Return (Local2)
Name 常駐 Namespace(全域);Local 是每次呼叫獨立的八個 暫存槽;Arg 是呼叫者綁進來的最多七個輸入。
與 等價、產生相同 AML; 注意兩者來源/目標的方向相反。
Local 未初始化就讀取是致命錯誤 ;Store 進 Local 不做隱式轉換。
Method 內用 動態建物件,重入時觸發 ——用 Serialized(第 12 章) 或改用 Local 避開。
、、 有專屬 opcode; 的實際值取決於 Integer 寬度。
字面值三種進位:十進位、 十六進位、 前導零八進位——第三種是陷阱。
Name、Local、Arg 三者中,哪些在 Method 結束後仍然存在? 用「表載入」「呼叫框架」兩個詞各造一句解釋。
、、、 四個 字面值,哪些相等?各是十進位多少?
為什麼 Local 永遠不會觸發 , 而 Method 內的 會?從「名字掛在哪裡」回答。
把 反向改寫回 ASL 2.0 風格;再把 改成接受兩個引數、回傳兩者之和的 (提示:)。
動手做:編譯 vars.dsl,用 執行, 確認回傳 ;再故意在 之前 插入 ,觀察未初始化錯誤長什麼樣子。
Operators
名詞(型別)與容器(變數)都有了,本章補上動詞。ASL 的運算子家族 歷史上有兩套外衣:從 ACPI 1.0 用到今天的 legacy 函式式寫法 (),以及 ACPI 6.0 世代(2015 年前後)隨 ACPICA 引入的 (又稱 ASL+)符號式寫法()。 兩套語法編譯成完全相同的 AML,純粹是給人看的差別。
為什麼兩種都要學?因為你的日常工作是「讀舊碼、寫新碼」: OEM 與 BIOS 原始碼樹(AMI、Insyde、coreboot)裡的 DSDT 大量是 legacy 風格,舊版 iasl 反組譯的輸出也是;而新版 iasl 反組譯預設 輸出 ASL 2.0 風格,新專案也該這樣寫。只會一種,等於半文盲。
對照 legacy 與 ASL 2.0 寫法,任一風格都能讀、能互相改寫。
解釋 legacy 運算子的 target 參數,以及 「餘數在前、商在後」的雙 target 慣例。
使用算術、位元、邏輯與比較運算子,說明無號 64 位元運算 與 / 回傳慣例。
區分位元運算 與邏輯運算 ,避開 最常見的條件式 bug。
用「讀取—遮罩—位移—組合」四步驟處理暫存器欄位。
一種位元組碼,兩種外衣
先建立一個關鍵事實:兩種寫法餵給 iasl,產出的 AML 一個位元組都不差。 以加法為例, 的編碼是 ,後面跟著兩個運算元 與一個目標的 NameString:
Add (LOCA, LOCB, RSLT) ← legacy 寫法 RSLT = LOCA + LOCB ← ASL 2.0 寫法 │ │ iasl(兩種寫法產出完全相同的位元組) ↓ 72 4C 4F 43 41 4C 4F 43 42 52 53 4C 54 │ │ │ └─ Target:RSLT │ │ └─ Operand 2:LOCB │ └─ Operand 1:LOCA └─ AddOp
所以「該用哪種」不是技術問題,是溝通問題:跟著你所在程式碼庫的 既有風格走,反組譯輸出拿到什麼就要能讀什麼。
運算子速查總表
這張表是本章的地圖,之後各節逐一展開。建議影印貼在螢幕旁, 直到兩種寫法都變成反射動作:
lllL4.0cm 分類 & Legacy 寫法 & ASL 2.0 寫法 & 說明
算術 & & & 加法
& & & 減法
& & & 乘法
& & & 除法; 收餘數
& & & 取餘數
& & & 遞增 1
& & & 遞減 1
位元 & & & 位元 AND
& & & 位元 OR
& & & 位元 XOR
& & & 位元反相
& & & 左移 n 位
& & & 右移 n 位
邏輯 & & & 邏輯 AND
& & & 邏輯 OR
& & & 邏輯 NOT
比較 & & & 等於
& & & 不等於
& & & 大於
& & & 大於等於
& & & 小於
& & & 小於等於
指定 & & & 指定(方向相反!)
& 等 & 等 & 複合指定(6.6 節)
補充兩個沒有進表的實用位元運算子: 與 分別回傳最高、最低設定位元的位置 (1 起算,全零回傳 0),掃描狀態暫存器時偶爾派得上用場。
算術運算子與 target 參數
legacy 運算子的最後一個參數叫 :運算結果除了作為 運算式的值傳出去,還會順手存一份到 target。target 可以省略, 所以老程式碼有兩種等價的臉:
Add (Arg0, Arg1, Local0) // result into Local0 Store (Add (Arg0, Arg1), Local0) // nested: same effect
Local0 = Arg0 + Arg1 // ASL 2.0, same AML idea
第二種「巢狀」寫法在反組譯輸出很常見:運算子本身是運算式, 可以層層套疊。ASL 2.0 的中置寫法把這些巢狀攤平,可讀性高得多。
是唯一有兩個 target 的運算子,也是經典 陷阱製造機:
Divide (17, 5, Local0, Local1) // Local0 = 2 <- REMAINDER comes 3rd // Local1 = 3 <- quotient comes 4th
Local1 = 17 / 5 // ASL 2.0: quotient only Local0 = 17
十個人裡有九個直覺以為第三個參數是商。規格書的順序是 ——餘數在前。 拿到反組譯碼看到 時先默唸一次這句口訣再讀下去。 另外,除數為零會讓直譯器以 中止 Method,除法前先檢查除數是韌體的基本禮貌。
所有算術都是無號 64 位元(或 32 位元,看 ComplianceRevision) 的模運算: 會繞回 ,乘法溢位高位默默丟掉, 沒有任何旗標或例外。需要「偵測下溢」時,先比較再相減。
位元運算子
韌體工程師跟位元的關係,比跟十進位親密得多——暫存器裡每個位元 都有自己的名字與脾氣。ASL 的位元運算子與 C 一一對應: 、、(XOR)、 (反相)、、。
四個日常招式,先用片段熱身,下一節的完整範例會組合技:
Local0 = STAT & 0x01 // test: isolate bit 0 Local1 = CTRL | 0x80 // set: force bit 7 on Local2 = CTRL & 0x08 // clear: force bit 3 off Local3 = (STAT >> 4) & 0x0F // field: extract bits [7:4]
第四招是重點:想取出暫存器中間的欄位,先右移到最低位、 再用遮罩切掉高位。反過來組欄位就是先遮罩、再左移、最後 OR 起來。 這一套「移位+遮罩」的手感,練到不用想才算過關。
邏輯與比較運算子
邏輯家族的名字都以 L 開頭(Logical):、、 、……ASL 2.0 對應 、、 、 等符號。它們的回傳值不是 1 與 0,而是 與 :
Local0 = (5 > 3) // Local0 = Ones (all bits set), NOT 1 Local1 = (5 < 3) // Local1 = Zero
AML 規格把「真」定義成全位元為一,好處是它同時是萬用遮罩。 因此不要拿邏輯結果去比 1: 永遠為假, 正確寫法是直接 。另一個與 C 的重大差異是 ASL 的 與 沒有短路求值: 兩側運算元一定都會被求值,所以「左邊先擋住、右邊才安全」的 C 慣用法在 ASL 行不通,得拆成巢狀 。
比較運算子不只吃 Integer:兩個 String 或兩個 Buffer 也能比 (逐位元組),而且第一運算元的型別決定第二運算元要被隱式轉換成 什麼——又回到第 4 章的規則表。
是位元反相、 才是邏輯否定。想寫 「旗標為 0 時」而誤用 :當 , 是 ——非零,條件照樣成立, bug 就此誕生且極難用肉眼看出。口訣:條件式裡只用 L 家族 (或符號 ),/ 只拿來翻位元。
複合指定、遞增與遞減
ASL 2.0 把 C 的複合指定也搬了過來:、、、 、、、、、、 ,以及 與 。 它們全是糖衣: 展開成 , 就是 :
CNT0 += 4 // Add (CNT0, 4, CNT0) CTRL |= 0x01 // Or (CTRL, 0x01, CTRL) MASK <<= 2 // ShiftLeft (MASK, 2, MASK) Local0++ // Increment (Local0) Local1– // Decrement (Local1)
在迴圈計數器與「讀—改—寫」暫存器操作裡,這些糖衣能把三行縮成一行, 下一節與第 7 章都會大量使用。
實戰:暫存器欄位的讀改寫
情境:某裝置有一個狀態暫存器 STAT 與一個控制暫存器 CTRL (本章先用 模擬,第 9 章換成真正的 Field Unit, 程式邏輯一行都不用改)。STAT 的位元配置如下:
STAT = 0xB5 = 二進位 1011 0101 bit 7 PWR 電源良好 = 1 bit 6..4 FANS 風扇檔位 = 011(十進位 3) bit 3 ERR 錯誤旗標 = 0 bit 2..1 RSVD 保留位元 = 10 bit 0 READY 資料就緒 = 1
完整程式:
DefinitionBlock ("bitops.aml", "SSDT", 2, "ACME", "BITOPS", 0x00000001)
Name (STAT, 0xB5) // simulated status register Name (CTRL, 0x00) // simulated control register
// Extract FANS field, bits [6:4] of STAT Method (GFAN, 0, NotSerialized) Local0 = (STAT >> 4) & 0x07 // shift, then mask Return (Local0) // 0xB5 -> 0x03
// Read-modify-write: set ENABLE (bit 0), keep the rest Method (SETE, 0, NotSerialized) Local0 = CTRL // read Local0 |= 0x01 // modify: set bit 0 CTRL = Local0 // write back
// Same logic, legacy style (decompiled-DSDT flavor) Method (SETL, 0, NotSerialized) Store (CTRL, Local0) Or (Local0, 0x01, Local0) Store (Local0, CTRL)
// Clear ERR (bit 3) without touching other bits Method (CLRE, 0, NotSerialized) CTRL &= 0x08 // AND with 1111 0111b
// Combine two bytes into one 16-bit word Method (MK16, 2, NotSerialized) // Arg0 = low, Arg1 = high Return (((Arg1 & 0xFF) << 8) | (Arg0 & 0xFF))
逐行拆解:
: 把 FANS 欄位移到最低三位 (),再 切掉上面殘留的 PWR 位元, 得到 。「先移位、再遮罩」順序對調也行 (),選一種風格貫徹到底。
:教科書級的 (read-modify-write)。直接 是錯的—— 會把其他位元全部歸零;先讀出、只改目標位元、再寫回, 才不會誤傷鄰居。
:與 逐行對應的 legacy 版。 拿反組譯輸出對照著讀,兩種外衣的對應關係就內化了。
:清位元的慣用式「AND 上反相遮罩」。 是編譯期常數摺疊, iasl 會直接算好塞進 AML。
:低位元組 OR 上左移八位的高位元組, 組出 16 位元值——讀感測器常見的「兩個 8-bit 暫存器 拼一個讀數」場景。防禦性的 確保呼叫者 傳超過一個位元組寬的值時不會污染結果。
legacy 與 ASL 2.0 編譯成相同 AML;讀舊碼要認得 ,寫新碼建議 。
legacy 的 target 是「順手存一份」; 雙 target 口訣:餘數在前、商在後。
所有算術都是無號模運算:下溢繞回 、 溢位默默截斷、除零直接中止 Method。
邏輯/比較回傳 /;條件式只用 L 家族;ASL 的 、 沒有短路求值。
翻位元、 翻真假,用錯的 bug 肉眼幾乎抓不到。
暫存器欄位操作四招:測試、設定、清除、取欄位—— 以及永遠的讀—改—寫紀律。
把 改寫成 ASL 2.0;再把 改寫成 legacy。
之後, 與 各是多少?
想表達「旗標為 0 時成立」, 錯在哪裡?寫出兩種正確寫法(legacy 與 ASL 2.0 各一)。
寫一個 :回傳 從第 位起、寬 位的欄位值 (提示: 是現成的遮罩)。
動手做:編譯 bitops.dsl,先手算 與 的結果,再用 執行驗證。
Control Flow
資料、容器、運算子都備齊了,最後一塊拼圖是「句型」。ASL 的流程控制 刻意極簡:條件分支只有 //, 迴圈只有 ,加上 、、 三個跳躍——沒有 、沒有 、 沒有 。這不是偷懶,而是 AML 位元組碼本來就只定義了 這幾個控制 opcode(第 2 章的精神:ASL 的每個句型都對得上 AML)。
工具少不代表能力弱:本章示範用 While 搭出 for 慣用式、走訪 Package、拆穿 Switch 語法糖的真面目,比較 Sleep 與 Stall 兩種等待, 最後用一個輪詢範例把整個 Module 3 的武器全部用上。
寫出 // 與 // 的完整結構。
說明條件式的真假值規則:非零即真、比較回傳 /。
用 While +計數器實作 for 慣用式,走訪 Package 與 Buffer。
解釋 Switch 展開成 的原理, 以及它為何要求 Serialized。
區分 與 的單位、行為與適用場景。
綜合條件、迴圈、位元運算寫出帶逾時的輪詢 Method。
If、ElseIf 與 Else
直接看課綱指定的範例——一個門檻判斷器:
Method (TEST, 1) If (Arg0 > 100) Return (1) Else Return (0)
逐行拆解:
:收一個引數。條件式 是 ASL 2.0 寫法,legacy 等價於 ——反組譯舊表時常見。
:條件為真走這塊。注意 ASL 的 If 條件一定要括號,大括號就算只有一行也不能省。
兩個分支都以 收尾,Method 不會「掉出去」; 拿掉 的話, 時走完沒有 Return,回傳值未定義。
多路分支用 串接。它其實是 的糖衣(AML 只有 IfOp 與 ElseOp 兩個 opcode),語意與 C 相同—— 由上而下,第一個成立的分支勝出:
Method (GRAD, 1) // Arg0: temperature in Celsius If (Arg0 >= 90) Return (3) // critical ElseIf (Arg0 >= 60) Return (2) // hot ElseIf (Arg0 >= 45) Return (1) // warm Else Return (0) // normal
條件式的真假值
規則只有一條:條件運算式最後會被當成 Integer 看, 非零即真、零即假。配合第 6 章的知識,就能解讀所有慣用形:
:比較運算子回傳 (真) 或 (假),直接可用。
:位元測試慣用式——遮罩後非零 代表該位元為 1,不必再寫 。
千萬別寫 ——回去複習第 6 章的 warnbox,這裡再踩一次就說不過去了。
While、Break 與 Continue
是 ASL 唯一的迴圈。三個角色的關係 一張圖講完:
While(條件) ├── 條件為真 → 執行迴圈本體 │ ├── 遇到 Break → 立刻離開迴圈 │ ├── 遇到 Continue → 跳回條件重新判斷 │ └── 本體跑完 → 跳回條件重新判斷 └── 條件為假 → 離開迴圈,執行 While 後面的敘述
沒有 for,但「初始化—測試—更新」三件套用 While 一樣寫得出來, 這是 ASL 最重要的迴圈慣用式:
// C: for (i = 0; i < 10; i++) body Local0 = 0 // init While (Local0 < 10) // test // ... body ... Local0++ // update – easy to forget!
只跳出最內層迴圈; 跳回條件 判斷。用 Continue 時要特別小心計數器的位置:
// Sum only the non-zero elements of PKG0 Local0 = 0 // index Local1 = 0 // running sum While (Local0 < SizeOf (PKG0)) Local2 = DerefOf (PKG0 [Local0]) Local0++ // increment BEFORE Continue If (Local2 == 0) Continue // skip zero entries Local1 += Local2
把 放在迴圈尾端是 C 工程師的肌肉記憶——但只要 中間有 ,遞增就被跳過,同一個元素永遠判斷不完。 上面的範例把遞增提前到取值之後,就是為了讓每條路徑都會前進。 寫 While 前先自問:「每一條路徑都會讓條件逼近假嗎?」
真的寫出死迴圈也不會讓整台機器永遠卡死:ACPICA 內建 While 迴圈逾時(預設約 30 秒,版本可能不同),超過就以 中止 Method 並在 kernel log 留案底。 但被保險絲救回來不值得驕傲——輪詢迴圈永遠要自帶重試上限, 章末範例示範標準寫法。
實戰:走訪 Package
第 4 章說過 Package 元素要用 reference 取值,第 5 章給了 SizeOf, 現在全部串起來。情境:風扇控制表 存放遞增的溫度 觸發點,給定溫度,找出對應的風扇檔位:
DefinitionBlock ("fanctl.aml", "SSDT", 2, "ACME", "FANCTL", 0x00000001)
// Fan trip points in Celsius, ascending order Name (TRIP, Package () 30, 45, 60, 75, 90)
// Map a temperature (Arg0) to a fan level 0..5 Method (FLVL, 1, NotSerialized) Local0 = 0 // index = fan level While (Local0 < SizeOf (TRIP)) // stop at the first trip point above Arg0 If (Arg0 < DerefOf (TRIP [Local0])) Break Local0++ Return (Local0)
逐行拆解:
回傳元素個數 5,迴圈條件因此不必 寫死長度——之後往 加觸發點, 一行都不用改。
:Index 產生 reference、 DerefOf 取值,一氣呵成。忘了 DerefOf 的話,比較運算子 拿到的是 reference,行為不是你要的。
在第一個「高於目前溫度」的觸發點停下, 此時 恰好是跨過的觸發點數,直接當檔位用: 回傳 0、 回傳 2、 掃完全表回傳 5。
邊界行為:溫度剛好等於觸發點時( 不成立) 視為升檔;想改成「等於不升檔」,換 即可。
Switch、Case 與 Default
多路等值分支寫成 ElseIf 階梯很囉唆,ASL 從 ACPI 2.0 起提供 。它常見於 這類「依功能編號分派」的 Method:
Method (DSPT, 1, Serialized) // Serialized: see below! Switch (ToInteger (Arg0)) Case (0) Return (Buffer () 0x3F) Case (Package () 1, 2) Return (One) // multi-match Default Return (Zero)
三個語法重點: 運算元慣例上包一層 確保與 值型別一致; 一次匹配多個值;沒有 C 的 fall-through,每個 Case 執行完自動結束, 不需要(也沒有) 敘述。
但 Switch 其實是:AML 根本沒有 SwitchOp。 iasl 把它展開成「只跑一圈的 While」加上一個隱藏的暫存物件:
While (One) // conceptual expansion Name (_T_0, 0) // hidden temp, created by iasl _T_0 = ToInteger (Arg0) If (_T_0 == 0) Return (Buffer () 0x3F) ElseIf ((_T_0 == 1) || (_T_0 == 2)) Return (One) Else Return (Zero) Break // never loops twice
看到 想起什麼了嗎?第 5 章的地雷—— 在 Method 內動態建立具名物件。這就是含 Switch 的 Method 必須 的原因:寫成 NotSerialized 時,iasl 會給 Remark「Control Method marked Serialized」並自作主張幫你改掉。
AML 裡沒有 Switch,反組譯器看到的是 While (One) 展開形。新版 iasl 認得這個模式(連同 )會還原成 Switch;模式 稍有走樣或用舊版工具,你就會直接面對展開形——認得它, 反組譯輸出才不會讀成天書。
Sleep 與 Stall:兩種等待
硬體常常要人等:EC 需要幾毫秒回應、電源軌拉起要時間。 ASL 提供兩個等待原語,個性南轅北轍:
L2.6cmL5.4cmL5.4cm 比較項目 & &
單位 & 毫秒 & 微秒
行為 & 讓出 CPU,執行緒可被排程 & busy-wait,整顆 CPU 空轉等你
時間保證 & 至少 ms 毫秒(可能更久) & 約 us 微秒(實作相依)
規範限制 & 無上限 & 單次不得超過 100 微秒
適用場景 & 等 EC、裝置上電(毫秒級) & 極短硬體時序(微秒級)
有兩宗罪:一是違反規格的 100 微秒上限; 二是 AML 常在不可排程的環境執行,busy-wait 五毫秒等於讓 整顆 CPU 抱著暫存器發呆,Windows 的驅動驗證器會直接抓包。 要等毫秒級,用 ;要等更久,用「 + 重試上限」的輪詢迴圈——正是下一節的主角。
順帶認識 :回傳單調遞增的 100 奈秒刻度計數, 量測實際等待時間時可當碼表用(Module 7 的電源時序除錯會再遇到)。
綜合範例:輪詢一個裝置
期末考:把 Module 3 的所有武器裝進一個 Method。情境是韌體日常—— 向裝置要資料前,得先等它的 READY 位元亮起,而且不能無限等:
DefinitionBlock ("poll.aml", "SSDT", 2, "ACME", "POLL", 0x00000001)
// Simulated registers (chapter 9 swaps in real Field Units) Name (DSTA, 0x81) // status: bit7 = power, bit0 = ready Name (DDAT, 0x5A) // data register
// Poll until READY (bit 0) or timeout. // Return: masked data on success, Ones on timeout. Method (RDAT, 0, NotSerialized) Local0 = 20 // retry budget: 20 tries While (Local0 > 0) If (DSTA & 0x01) // ready bit set? Local1 = DDAT & 0x7F // strip parity bit Return (Local1) Sleep (5) // yield CPU for 5 ms Local0– Return (Ones) // timeout marker
逐行拆解:
是重試預算:搭配每輪 ,總逾時約 100 毫秒。輪詢迴圈的三要素 ——條件、讓出 CPU、遞減預算——一個都不能少, 少一個就是潛在死迴圈。
:位元測試慣用式當迴圈出口; 就緒後 剝掉同位位元,只留有效資料。 成功路徑從迴圈中間直接 —— ASL 允許隨處返回,比設旗標再 Break 乾淨。
:用第 5 章的慣用常數當逾時記號, 呼叫者拿到 就知道別把它當資料用。
自己驗證兩條路徑:本例 ,acpiexec 執行 立即回傳 ;把 改成 (READY 熄滅)再跑,約 100 毫秒後 回傳 。
讀完 Module 3,你已經握有 ASL 的全部「語言核心」:型別、變數、 運算子、流程控制。從 Module 4 起,戰場轉向真正的硬體—— 用 Device 描述裝置、用 OperationRegion 與 Field 接管暫存器, 到時你會發現本模組的每一個慣用式都在現場等你。
分支://; ElseIf 是糖衣,AML 只有 IfOp 與 ElseOp。
真假值一條規則:條件轉成 Integer,非零即真; 位元測試 是慣用式。
While 是唯一迴圈;for 用「初始化—測試—更新」慣用式; Break 只出最內層,Continue 前記得遞增計數器。
走訪 Package: 定終點、 取值。
Switch 是 +隱藏 的語法糖,因此 Method 必須 Serialized。
讓出 CPU(毫秒); 忙等 (微秒、單次上限 100);長等待用 Sleep +重試預算輪詢。
ASL 為什麼不需要 ?寫出 的 While 對應片段。
把本章開頭的 改寫成全 legacy 風格 (、),並確認兩版對 都回傳 0——邊界值算哪一邊?
寫 :用 While 走訪 Buffer 的每個 位元組加總,回傳總和的低 8 位元 (提示: 對 Buffer 也有效)。
犯了哪兩條戒律?用 Sleep 與 While 改寫成等待 5 毫秒的合規寫法。
動手做:編譯 fanctl.dsl,先在紙上追蹤 、、 每輪迴圈的 值,再用 等指令驗證你的答案。