CPU_flags

**`CPUID`** 指令是一條 x86 架構中的的擴充指令（此處的擴充指相對 i80386），其操作碼輔助記憶碼縮寫於「CPU 辨識」（CPU Identification），其作用是返回特定的 CPU 資訊，使得軟體可以在執行時檢測 CPU 的硬體特性，以便於辨識並決定執行哪些代碼。首批支援 CPUID 指令的處理器是 1993 年發布的奔騰（i586）和 486SL。

通過使用 `CPUID`，軟體可以確定處理器的類型和特性支援（例如 MMX/SSE）。`CPUID` 操作碼為 0Fh、A2h。在呼叫 CPUID 前 EAX 暫存器內的值作為其輸入參數，Intel64 指令集手冊中稱其為「葉」（Leaf)，AMD64 指令集手冊中稱「功能碼」（Function Number），執行 CPUID 後，返回值將被寫入 EAX，EBX，ECX 和 EDX 暫存器內。此外，在使用一些特殊的輸入參數時，必須同時於 ECX 內寫入第二個輸入參數，否則無法得到有意義的返回值。第二個參數也被稱為「子葉」（Sub Leaf）或「子功能碼」（Sub Function）。

## 歷史

### CPUID 前判斷處理器型號的方法

在 `CPUID` 指令被引入前，若要在執行時確定 CPU 的型號，需要編寫晦澀的組合語言代碼以判斷某些指令被執行後處理器的行為。

1990 年以前，對於如何判斷 CPU 的型號沒有官方說明與實現方法，但軟硬體工程師們總結了一套利用 PUSHF/POPF 指令檢測 CPU 行為以判斷其架構的方法。此方法在廣泛應用的同時引起了巨大爭議，反對者認為其並非萬無一失，在某些需要高可靠性的軟體平台上若處理器型號判斷錯誤，則有可能造成巨大損失，但英特爾在 1990 年發布了《i486 微處理器程式設計師參考手冊》（_i486 Microprocessor Programmer's Reference Manual_ Intel Corp. 1990），其中在手冊的第 22.10 節中使用了上述方法判斷 CPU 代數，並且英特爾聲稱「_代碼序列已經過 Intel 驗證，可以檢測 CPUID、數學協處理器功能，並進行相應的初始化。任何其他方法都可能在未來的處理器中產生不可預測的結果_」。此後該方法被應用於大多數作業系統核心中。

以下所介紹之方法為英特爾手冊中的官方演算法。

該演算法的核心是利用 PUSHF/POPF 指令將標誌暫存器中的特定位設為 1 或 0，並觀察處理器隨後的行為，每一代處理器的具體行為均有所不同。

#### 判斷 8086/80286

8086/8088 系列處理器在清除標誌暫存器的 12-15 位後，無論向其中寫入何值，處理器將始終把它們設為 1，其檢測方式如下

對於 80286 系列處理器，在真實模式下這些位始終被清零，在保護模式下，其用於 I/O 特權級別和巢狀任務（NT），其檢測方法如下，代碼接上述代碼執行

#### 判斷 80386/80486

檢測 386/486 處理器的原理與上述代碼相同，但檢測的位有所區別，486 處理器中引入了一個 AC 標誌位，並將把它置 1，其對於 386 而言是無效的，故 386 處理器將其置零，其實現如下

此外，80386 處理器的 EDX 暫存器在處理器被硬體復位後將被初始化為處理器的修訂版本號。

#### 檢測 80486 或奔騰（i586）

奔騰處理器支援使用 CPUID 指令，因此其標誌位中設定了一個被稱為 ID 的特殊標誌，若該標誌存在，則對應的處理器支援 CPUID，檢測實現如下

#### 關於上述方法的注意事項

該演算法無法檢測 80186 處理器（雖然其並未被廣泛使用），80186/88 處理器包 80286 中包含的大部分新指令和異常，包括 _PUSHA/POPA、PUSH、SHL_ 和無效操作碼異常。80186/88 中唯一沒有實現的是專門用於保護模式的指令和異常。未能檢測到此處理器可能會禁止使用某些可以利用這些新指令和異常的軟體。

同時，由於 PUSHF/POPF 是 Ring0 指令，故該演算法只能在真實模式中執行，保護模式作業系統中執行的應用程式無法使用這個方法，除非處理器以及作業系統支援虛擬模式擴充（VME）。

以上演算法僅供參考與學習，在使用非英特爾處理器，完全軟體虛擬化（Full emulation）的虛擬機器或其他平台上，該演算法很有可能無法正確判斷處理器支援的指令集。考慮到奔騰已經是近 30 年前的處理器，除非有必要在古老的電腦上檢測 CPU，否則不應該再使用這種演算法，而是直接呼叫 CPUID 指令。

對於非 x86 架構的 CPU，仍然需要精心設計的代碼判斷其差別（例如使用 MOVE 的特權要求判斷摩托羅拉 68000 和 68010），但大多數架構都要求具體實現者提供相應的暫存器以提供區分和判斷處理器（例如 ARM64 的 ID\_AA64 暫存器）。

## 呼叫 CPUID

`CPUID` 指令的操作碼是 `0F A2`。`CPUID` 沒有顯式的運算元，其隱式使用 EAX 或 EAX 與 ECX 中的值作為參數，以確定要取得的資訊。在英特爾指令集手冊中，`CPUID` 的輸入參數（EAX），被稱為「葉」（Leaf），AMD 稱其為「功能碼」（Function Number），執行 CPUID 後，返回值將被寫入 EAX，EBX，ECX 和 EDX 暫存器內。此外，在使用一些特殊的輸入參數時，必須同時於 ECX 內寫入第二個輸入參數，否則無法得到有意義的返回值。第二個參數也被稱為「子葉」（Sub Leaf）或「子功能碼」（Sub Function）。

注意，若且唯若 MSR 暫存器 _IA32\_MISC\_ENABLE.BOOT\_NT4_ 的第 22 位為 0 時，EAX 大於 3 的輸入才是有意義的。若且唯若該位被設為 1 時，Windows NT 4.0 SP6 以前的作業系統才能正常啟動。

截止到 2022 年，對於一些常見的處理器架構，最大的有效的基礎輸入是 0x20 (GoldenCove），0x10（Zen4); 最大有效的擴充輸入是 0x80000008（GoldenCove），0x80000028（Zen4）。

### EAX=0: 最高基礎輸入參數與製造商 ID

輸入參數位 0 時，CPUID 返回對當前處理器有意義的（即被當前架構所實現的）最大輸入參數以及 ASCII 編碼的製造商 ID（Manufacturer ID）。

最大輸入參數被寫入 EAX 暫存器中，對於英特爾和 AMD 處理器，已知的返回值如下表所示：

製造商 ID 含有 12 個字元，以 EBX-EDX-ECX 的順序拼接，例如在英特爾處理器上將返回「GenuineIntel」，具體的返回值如下表所示：

已知的製造商以及其 ID 如下：

* `"AMDisbetter!"`– AMD K5 的早期工程樣品
* `"AuthenticAMD"` – AMD
* `"CentaurHauls"`– IDT / 半人馬座（Centaur）/ 兆芯
* `"CyrixInstead"`– Cyrix / 意法半導體 / IBM
* `"GenuineIntel"`– 英特爾
* `"TransmetaCPU"`– 全美達
* `"GenuineTMx86"`– 全美達
* `"Geode by NSC"`– 國家半導體
* `"NexGenDriven"` – NexGen
* `"RiseRiseRise"` – Rise
* `"SiS SiS SiS "`– 矽統
* `"UMC UMC UMC "`– 聯華電子（台聯電）
* `"VIA VIA VIA "`– 威盛
* `"Vortex86 SoC"` – DM&P Vortex86
* `"  Shanghai  "`– 兆芯
* `"HygonGenuine"`– 海光
* `"Genuine  RDC"` – RDC
* `"E2K MACHINE"` – MCST Elbrus

已知的軟核 x86 處理器 ID:

* `"MiSTer AO486"` – ao486
* `"GenuineIntel"` – v586

已知的 x86 虛擬機器 ID:

* `"bhyve bhyve "` – bhyve
* `" KVMKVMKVM  "`  – KVM
* `"TCGTCGTCGTCG"` – QEMU
* `"Microsoft Hv"` – Hyper-V
* `"MicrosoftXTA"` – 微軟 x86 到 ARM 轉譯器
* `" lrpepyh  vr"` – Parallels
* `"VMwareVMware"` – VMware
* `"XenVMMXenVMM"` – Xen HVM
* `"ACRNACRNACRN"` – Project ACRN
* `" QNXQVMBSQG "` – QNX
* `"GenuineIntel"`– 蘋果 羅塞塔 2 x86 到 ARM 轉譯器
* `"VirtualApple"` – 蘋果 羅塞塔 2 x86 到 ARM 轉譯器

使用 GNU 格式組合語言取得製造商 ID 和最高輸入的例子：

### EAX=1: 處理器資訊和特性

該參數在 EAX 中返回處理器的版本資訊，這些資訊也被稱為「簽章」（signature），一般而言，對於使用同一工藝製造的同一架構的處理器，其簽章是相同的，故該資訊可被用於判斷架構和工藝。其包含的內容以及意義如下：

關於版本資訊的注意事項：

* 若系列號部分為 0xF，則實際的系列號為 EAX 中系列號欄位和擴充系列號欄位的和，反之實際系列號與 EAX 中欄位相同。
* 若系列號部分為 0xF 或 6，則實際的型號為 EAX 中擴充型號欄位左移四位後與型號欄位的和，反之實際型號與 EAX 中欄位相同。

上述內容用虛擬碼表示：

步進表示對處理器的修正或使用不同工藝製造，其具體意義應當參考處理器廠商所發布的手冊。

其中，處理器類型（Type）的編碼如下：

EBX 中返回處理器的附加資訊，其內容被分為以下幾個欄位：

ECX 和 EDX 返回處理器實現的功能，一個位元代表一個功能，該位為 1 是表示處理器支援該功能，反之則不支援，各位對應功能或指令如下：

### EAX=2: 快取和 TLB 資訊（英特爾）

對於英特爾處理器，EAX=2 將返回處理器快取，TLB 和預取器資訊，這些資訊被編碼為數個 1 位元組資訊返回於 EAX，EBX，ECX 和 EDX 四個暫存器中，編碼規則如下：

EAX 暫存器返回值的最低位（LSB）一定是 1，它不代表任何資訊，軟體讀取時應當將其忽略。

每個暫存器返回值的最高位（MSB）代表該暫存器中的值是否有效，若 MSB 為 0，則該暫存器包含有意義的資訊，反之則代表該暫存器為保留值且無意義。

若某一暫存器內的返回值有意義，則其應當被分割為 4 個等長部分，每部分長度為 8 為，即一位元組，每部分代表了一個快取 / TLB 或預取器的資訊，具體編碼見英特爾所發行的官方手冊 Intel 64 and IA-32 Architectures Software Developer Manuals 卷 2A 中關於 CPUID 指令的表 3-12。（截至 2023.2，該表位於 Vol2A，3-245 到 3-248 頁）。

#### 關於 EAX=2 的注意事項

代表快取 / TLB / 預取的編碼位元組沒有特定的順序，不存在「某個暫存器的某個位元組對應於哪一級別的快取」。

使用 EAX=2 是取得英特爾處理器快取資訊的傳統方法，但由於快取類型迅速增多，使用該方法已經無法高效的取得資訊，故對於較新的（一般認為是 Core 及以後）的英特爾處理器，返回值的編碼中可能包含類似 0xFF 或 0 的空描述符，對於此種情況，應當使用更新的方法（EAX=4）取得快取資訊。

**對於 AMD 處理器，EAX=2 是保留項。四個暫存器都將返回 0。**

### EAX=3 處理器序列號（英特爾）

以 3 作為輸入參數時，CPUID 將在 ECX 和 EDX 暫存器中返回處理器序列號。

#### 關於 EAX=3 的注意事項

該參數僅在使用核心代號為 Katmai 和 Coppermine 的奔騰 III 處理器上可用，處理器序列號是一個獨一無二的 96 位長的二進位數（實際只使用了 64 位元），每個處理器僅對應一個序列號，通過序列號可用追蹤該處理器從生產到銷售使用的全部流程，英特爾自稱引入該功能的原因是「為了提高電子商務安全性，例如只能在某台電腦上使用某張銀行卡」，但其在中美歐等多個國家和地區引起了強烈的隱私和國家安全問題爭議，民間認為該功能侵犯了使用者隱私權，居心不良者可用該功能追蹤他人的電腦，且一旦英特爾的序列號資料庫泄露，將帶來難以估量的損失，而多國的國家安全機關認為，敵對國家的情報部門可以使用該功能輕而易舉的記錄並分析電腦活動，迫於市場壓力，英特爾從核心代號 Tulatin 的奔騰 III 處理器開始禁用了這一功能，且在奔騰 4 以及 Core 處理器等後續產品上再未使用過該功能。

**對於 AMD 處理器，EAX=3 是保留項。四個暫存器都將返回 0。**

### EAX=4 快取參數（英特爾）

以 4 作為 EAX 輸入參數時，CPUID 依據 ECX 暫存器中輸入的第二個參數（也稱為子葉）返回處理器快取資訊。

EAX 返回快取類型資訊，值分為以下幾個欄位

EBX，ECX 返回快取尺寸資訊，返回值各個欄位如下：

ECX 返回組相聯快取的的組數（Sets，S）。

#### 關於 EAX=4 的注意事項

##### 關於子葉的意義以及最大支援的子葉

英特爾並未在其發布的官方手冊中定義子葉與快取的絕對對應關係，也未說明最大支援的子葉，上述關係表格只是軟硬體工程師的總結，但截止到 2023 年的 SunnyCove 架構，所有的英特爾處理器都遵守這一對應關係。

在面對一個全新的英特爾架構時，應當假定上述對應關係與最大子葉數不存在，並通過該指令判斷，其中對應關係由上述 EAX 中的返回值相應欄位確定，最大子葉值由以下方法判斷：在 EAX 輸入 4，ECX 中的初始值為 0 並執行 CPUID 指令，每次執行後將 ECX 內值加 1，EAX 仍為 4，直到四個暫存器的返回值全部為 0，則使返回值不為 0 的最大 ECX 輸入就是最大支援的子葉。

**對於 AMD 處理器，EAX=3 是保留項。四個暫存器都將返回 0。**

##### 關於快取尺寸的計算

英特爾 SDM 手冊中未直接給出讀取快取尺寸的方法，但可以通過如下公式計算：

$$
C=(W+1)*(P+1)*(L+1)*(S+1)
$$

其中 C 為以位元組為單位的快取大小，W，P，L，S 為讀取該快取時返回值的相應欄位。

## x86 外的特定 CPU 辨識資訊

一些非 x86 的 CPU 架構也提供了有關處理器能力的某種形式的結構化資訊，通常作為一組特殊暫存器：

* ARM 架構有一個 `CPUID` 協處理器暫存器。
* IBM System z 大型電腦處理器自 1983 年的 IBM 4381 起支援「Store CPU ID」（`STIDP`）指令，用於查詢處理器 ID。
* MIPS32 架構定義了一個強制性的 _Processor Identification_（`PrId`）和一系列菊花鏈「組態暫存器」。
* PowerPC 處理器有 32 位元唯讀的 `PVR` 暫存器來辨識使用的處理器型號。.

## 參見

* CPU-Z，一個使用 `CPUID` 等資訊辨識系統組態的 Windows 實用工具。
* `cpuid`，類 Unix 作業系統下的讀取 CPUID 的實用工具。

## 參考資料