CPUID
通过使用 历史CPUID前判断处理器型号的方法在 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/8088系列处理器在清除标志寄存器的12-15位后,无论向其中写入何值,处理器将始终把它们设为1,其检测方式如下 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),其检测方法如下,代码接上述代码执行 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
检测386/486处理器的原理与上述代码相同,但检测的位有所区别,486处理器中引入了一个AC标志位,并将把它置1,其对于386而言是无效的,故386处理器将其置零,其实现如下 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寄存器在处理器被硬件复位后将被初始化为处理器的修订版本号。 奔腾处理器支持使用CPUID指令,因此其标志位中设置了一个被称为ID的特殊标志,若该标志存在,则对应的处理器支持CPUID,检测实现如下 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
注意,当且仅当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如下:
已知的软核x86处理器ID:
已知的x86虚拟机ID:
使用GNU格式汇编语言获取制造商ID和最高输入的例子: .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),一般而言,对于使用同一工艺制造的同一架构的处理器,其签名是相同的,故该信息可被用于判断架构和工艺。其包含的内容以及意义如下:
关于版本信息的注意事项:
上述内容用伪代码表示: 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,P,L,S为读取该缓存时返回值的相应字段。 x86外的特定CPU识别信息一些非x86的CPU架构也提供了有关处理器能力的某种形式的结构化信息,通常作为一组特殊寄存器:
参见
参考资料
|
Portal di Ensiklopedia Dunia