CPUID

**<code>CPUID</code>**指令是一条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，并观察处理器随后的行为，每一代处理器的具体行为均有所不同。

#### 判断[[Intel 8086|8086]]/[[Intel 80286|80286]]
8086/8088系列处理器在清除标志寄存器的12-15位后，无论向其中写入何值，处理器将始终把它们设为1，其检测方式如下
```asm
pushf                   ; 将标志寄存器内容压栈
        pop     ax              ; 将标志寄存器内容送入AX
        mov     cx, ax          ; 将标志寄存器内容送入CX备份
        and     ax, 0fffh       ; 将AX的12-15位置零
        push    ax              ; 将标志寄存器的新值压栈
        popf                    ; 送入FLAG
        pushf                   ; 获取弹栈后标志寄存器的内容
        pop     ax              ; 存入AX
        and     ax, 0f000h      ; 如果12-15位被处理器置1则为8086/8088
        cmp     ax, 0f000h     
        je      end_cpu_type_8688    ; 跳转至对应代码
```
对于80286系列处理器，在实模式下这些位始终被清零，在保护模式下，其用于I/O特权级别和嵌套任务（NT），其检测方法如下，代码接上述代码执行
```asm
or      cx, 0f000h      ; 将12-15位置1
        push    cx              ; 压栈
        popf                    ; 将栈内容弹入标志寄存器
        pushf                   ; 获取标志寄存器并存入AX
        pop     ax             
        and     ax, 0f000h      ; 若12-15位位0则为286
        jz      end_cpu_type_286
```


#### 判断[[Intel 80386|80386]]/[[Intel 80486|80486]]
检测386/486处理器的原理与上述代码相同，但检测的位有所区别，486处理器中引入了一个AC标志位，并将把它置1，其对于386而言是无效的，故386处理器将其置零，其实现如下
```asm
pushfd；EFLAGS压栈
        pop eax ；获取原始 EFLAGS 
        mov ecx, eax ；保存原来的 EFLAGS 
        xor eax, 40000h ; 翻转 EFLAGS 中的 AC 位
        push eax ；将新的 EFLAGS 压栈
        popfd 中；替换当前的 EFLAGS 值
        pushfd；获取新的 EFLAGS 
        pop eax ；将新的 EFLAGS 存储在 EAX 
        xor eax, ecx 中；无法反转AC则为386
        jz end_cpu_type_386 ；如果 80386 处理器
```
此外，80386处理器的EDX寄存器在处理器被硬件复位后将被初始化为处理器的修订版本号。

#### 检测[[Intel 80486|80486]]或[[奔腾 (初代)|奔腾]]（i586）
奔腾处理器支持使用CPUID指令，因此其标志位中设置了一个被称为ID的特殊标志，若该标志存在，则对应的处理器支持CPUID，检测实现如下
```asm
mov eax, ecx ; 获取原始 EFLAGS 
        xor eax, 200000h ；翻转 EFLAGS 中的 ID 位
        push eax ；将新的 EFLAGS 值保存在堆栈
        popfd 中；替换当前的 EFLAGS 值
        pushfd ；获取新的 EFLAGS 
        pop eax ；将新的 EFLAGS 存储在 EAX 
        xor eax, ecx 中；可切换则为奔腾或486SL
        jne end_cpu_type_586
```


#### 关于上述方法的注意事项
该算法无法检测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
* `"&nbsp; Shanghai &nbsp;"`兆芯
* `"HygonGenuine"` 海光
* `"Genuine &nbsp;RDC"` RDC
* `"E2K MACHINE"` MCST Elbrus

已知的软核x86处理器ID:

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

已知的x86虚拟机ID:

* `"bhyve bhyve "` bhyve
* `" KVMKVMKVM&nbsp; "` KVM
* `"TCGTCGTCGTCG"` QEMU
* `"Microsoft Hv"` Hyper-V
* `"MicrosoftXTA"` –  微软x86到ARM转译器
* `" lrpepyh&nbsp; vr"` Parallels
* `"VMwareVMware"` VMware
* `"XenVMMXenVMM"` Xen HVM
* `"ACRNACRNACRN"` Project ACRN
* `" QNXQVMBSQG "` QNX
* `"GenuineIntel"` 苹果 罗塞塔2 x86到ARM转译器
* `"VirtualApple"` –  苹果 罗塞塔2 x86到ARM转译器

使用GNU格式汇编语言获取制造商ID和最高输入的例子：
```asm
.data

s0:	.asciz	"CPUID: %x\n"
s1:	.asciz	"Largest basic function number implemented: %i\n"
s2:	.asciz	"Vendor ID: %.12s\n"

	.text

	.align	32
	.globl	main

main:
	pushq	%rbp
	movq	%rsp,%rbp
	subq	$16,%rsp

	movl	$1,%eax
	cpuid

	leaq	s0(%rip),%rdi
	movl	%eax,%esi
	xorl	%eax,%eax
	call	printf

	pushq	%rbx

	xorl	%eax,%eax
	cpuid

	movl	%ebx,8(%rsp)
	movl	%edx,12(%rsp)
	movl	%ecx,16(%rsp)

	popq	%rbx

	leaq	s1(%rip),%rdi
	movl	%eax,%esi
	xorl	%eax,%eax
	call	printf

	leaq	s2(%rip),%rdi
	movq	%rsp,%rsi
	xorl	%eax,%eax
	call	printf

	movq	%rbp,%rsp
	popq	%rbp
//	ret
	movl	$1,%eax
	int	$0x80
```


### EAX=1: 处理器信息和特性
该参数在EAX中返回处理器的版本信息,这些信息也被称为“签名”（signature），一般而言，对于使用同一工艺制造的同一架构的处理器，其签名是相同的，故该信息可被用于判断架构和工艺。其包含的内容以及意义如下：

关于版本信息的注意事项：

*若系列号部分为0xF，则实际的系列号为EAX中系列号字段和扩展系列号字段的和，反之实际系列号与EAX中字段相同。

*若系列号部分为0xF或6，则实际的型号为EAX中扩展型号字段左移四位后与型号字段的和，反之实际型号与EAX中字段相同。

上述内容用伪代码表示：
```basic
IF Family_ID ≠ 0FH
THEN DisplayFamily = Family_ID;
ELSE DisplayFamily = Extended_Family_ID + Family_ID;
END;
IF (Family_ID = 06H or Family_ID = 0FH)
THEN DisplayModel = (Extended_Model_ID « 4) + Model_ID;
ELSE DisplayModel = Model_ID;
END;
```
步进表示对处理器的修正或使用不同工艺制造，其具体意义应当参考处理器厂商所发布的手册。

其中，处理器类型（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`协处理器寄存器。
* 大型机处理器自1983年的起支持“Store CPU ID”（`STIDP`）指令，用于查询处理器ID。
*  MIPS32架构定义了一个强制性的*Processor Identification*（`PrId`）和一系列菊花链“配置寄存器”。
*  PowerPC处理器有32位只读的`PVR`寄存器来识别使用的处理器型号。.

## 参见
* CPU-Z，一个使用`CPUID`等信息识别系统配置的Windows实用工具。
* CPU-X，一个使用`CPUID`等信息识别系统配置的Linux实用工具。
* `cpuid`，類Unix作業系統下的讀取CPUID的實用工具。

## 参考资料