发布于 ,更新于 

用 CPUID 指令获取处理器信息(书摘)

CPUID 程序

CPUID 是一条获取 CPU 信息的指令,以寄存器 EAX 作为输入信息的参数,执行该指令之后,会输出具体结果到 EBX、ECX、EDX 寄存器上面,具体会输出什么信息,由输入参数寄存器 EAX 决定。

CPUID 指令具体使用的寄存器 EAX 的不同的输入选项如下所示:

EAX值 CPUID 输出
0 厂商 ID字符串(Vendor ID)与支持的最大的 CPUID 选项值
1 处理器类型、系列、型号和步进信息
2 处理器缓存配置
3 处理器的序列号
4 缓存配置(线程数量、核心数量和物理属性)
5 监视信息
80000000h 扩展的厂商 ID 字符串和支持的级别
80000001h 扩展的处理器类型、系列、型号和步进信息
80000002h ~ 80000004h 扩展的处理器名称字符串

如果寄存器 EAX 输入 0, 则 EBX、ECX、EDX 寄存器输出如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
EBX 寄存器:
31 23 15 7 0
┌─────────┬─────────┬─────────┬─────────┐
byte4 byte3 byte2 byte1
└─────────┴─────────┴─────────┴─────────┘
EDX 寄存器:
31 23 15 7 0
┌─────────┬─────────┬─────────┬─────────┐
byte8 byte7 byte6 byte5
└─────────┴─────────┴─────────┴─────────┘
ECX 寄存器:
31 23 15 7 0
┌─────────┬─────────┬─────────┬─────────┐
byte12 byte11 byte10 byte9
└─────────┴─────────┴─────────┴─────────┘

把上面寄存器的值组成字符串即可。范例程序 (Linux下):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# cpuid.s 
.section .data
output:
.ascii "The processor Vendor ID is 'xxxxxxxxxxxx' \n"

.section .text
.globl _start
_start:
nop
movl $0, %eax
cpuid
movl $output, %edi
movl %ebx, 28(%edi)
movl %edx, 32(%edi)
movl %ecx, 36(%edi)
movl $4, %eax
movl $1, %ebx
movl $output, %ecx
movl $42, %edx
int $0x80
movl $1, %eax
movl $0, %ebx
int $0x80

上面代码中,int $0x80 是调用 Linux 系统调用。_start 标签所定义的地址是 GNU 链接器默认的程序运行起始地址。

用下面命令进行编译、链接并运行 (要求在 32 位 x86 的 Linux 系统下,如果是 64 位系统的话,可以在虚拟机下面装一个 32 位的):

1
2
3
4
5
6
7
8
as -o cpuid.o cpuid.s
ld -o cpuid cpuid.o

# 运行:
./cpuid

结果:
The processor Vendor ID is 'GenuineIntel'

调试程序

如果需要调试程序,则需要在编译的时候,加上 -gstabs 选项,会把调试信息直接附加进可执行二进制文件中:

1
2
as -gstabs -o cpuid.o cpuid.s
ld -o cpuid cpuid.o

GDB 单步运行程序

运行 gdb cpuid

1
2
3
4
5
6
7
8
9
gdb cpuid
GNU gdb Red Hat Linux (5.3post-0.20021129.18rh)
Copyright 2003 Free Software Foundation, Inc.
GDB is free software, covered by the GNU General Public License, and you are
welcome to change it and/or distribute copies of it under certain conditions.
Type "show copying" to see the conditions.
There is absolutely no warranty for GDB. Type "show warranty" for details.
This GDB was configured as "i386-redhat-linux-gnu"...
(gdb)

使用 run 命令直接运行 cpuid 程序:

1
2
3
4
5
(gdb) run
Starting program: /root/Test/cpuid
The processor Vendor ID is 'GenuineIntel'
Program exited normally.
(gdb)

可以使用 break 命令设置断点,汇编语言设定断点,必须指定某个标签的相对位置。格式如下:

1
break * label+offset

gdb 会直接忽略 _start 这个标签的断点:

1
2
3
4
5
6
7
(gdb) break * _start
Breakpoint 1 at 0x8048074: file cpuid.s, line 9.
(gdb) run
Starting program: /root/Test/cpuid
The processor Vendor ID is 'GenuineIntel'
Program exited normally.
(gdb)

所以可以在 _start 标签之后直接加上一个 nop 指令,以这个nop指定作为断点,表示为 _start + 1,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
(gdb) break * _start+1
Breakpoint 2 at 0x8048075: file cpuid.s, line 10.
(gdb) run
Starting program: /root/Test/cpuid

Breakpoint 2, <function called from gdb>
Current language: auto; currently asm
(gdb) next
11 cpuid
(gdb) next
12 movl $output, %edi
(gdb) step
13 movl %ebx, 28(%edi)
(gdb) step
14 movl %edx, 32(%edi)
(gdb) cont
Continuing.
The processor Vendor ID is 'GenuineIntel'
Program exited normally.
(gdb)

如上操作,设置断点之后,直接 run 命令可以运行程序,到达断点就暂停。这时,使用 next 或者 step 命令可以单步执行每条指令;使用 cont 命令可以继续以正常的方式继续执行。
next 可以简化为 nstep 可以简化为 scont 可以简化为 c

查看数据

数据命令 说明
info registers 显示所有寄存器的值
print 显示特定寄存器或者来自程序的变量的值
print/d 同上,显示为十进制的值
print/t 同上,显示为二进制的值
print/x 同上,显示为十六进制的值
x 显示特定内存位置的值

查看寄存器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
(gdb) run
Starting program: /root/Test/cpuid

Breakpoint 2, <function called from gdb>
(gdb) s
11 cpuid
(gdb) info registers
eax 0x0 0
ecx 0x0 0
edx 0x0 0
ebx 0x0 0
esp 0xbfffeab0 0xbfffeab0
ebp 0x0 0x0
esi 0x0 0
edi 0x0 0
eip 0x804807a 0x804807a
eflags 0x200306 2097926
cs 0x23 35
ss 0x2b 43
ds 0x2b 43
es 0x2b 43
fs 0x0 0
gs 0x0 0
(gdb) s
12 movl $output, %edi
(gdb) info registers
eax 0x16 22
ecx 0x6c65746e 1818588270
edx 0x49656e69 1231384169
ebx 0x756e6547 1970169159
esp 0xbfffeab0 0xbfffeab0
ebp 0x0 0x0
esi 0x0 0
edi 0x0 0
eip 0x804807c 0x804807c
eflags 0x200306 2097926
cs 0x23 35
ss 0x2b 43
ds 0x2b 43
es 0x2b 43
fs 0x0 0
gs 0x0 0
(gdb) print/x $ebx
$1 = 0x756e6547
(gdb) print/x $edx
$2 = 0x49656e69
(gdb) print/x $ecx
$3 = 0x6c65746e
(gdb)

x 命令用于查看内存位置的值。x 命令的格式为:

1
x/nyz

n 为需要显示的字段数。y 是输出格式,y 的值有:

  1. c 用于字符
  2. d 用于十进制
  3. x 用于十六进制

z 是要显示的字段的长度:

  1. b 用于字节
  2. h 用于16位大小
  3. w 用于32位大小

以下是显示 output 位置的内存的数据:

1
2
3
4
5
6
7
8
(gdb) x/42cb &output
0x80490ac <output>: 84 'T' 104 'h' 101 'e' 32 ' ' 112 'p' 114 'r' 111 'o' 99 'c'
0x80490b4 <output+8>: 101 'e' 115 's' 115 's' 111 'o' 114 'r' 32 ' ' 86 'V' 101 'e'
0x80490bc <output+16>: 110 'n' 100 'd' 111 'o' 114 'r' 32 ' ' 73 'I' 68 'D' 32 ' '
0x80490c4 <output+24>: 105 'i' 115 's' 32 ' ' 39 '\'' 120 'x' 120 'x' 120 'x' 120 'x'
0x80490cc <output+32>: 120 'x' 120 'x' 120 'x' 120 'x' 120 'x' 120 'x' 120 'x' 120 'x'
0x80490d4 <output+40>: 39 '\'' 32 ' '
(gdb)

与 C 语言库函数进行链接

以上的系统调用方式改用为调用 C 语言函数库的方式,程序如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# cpuid2.s 
.section .data
output:
.ascii "The processor Vendor ID is '%s' \n"

.section .bss
.lcomm buffer, 16 # 在 .bss 中开辟一个16字节的未初始化数据缓冲区,标签为buffer

.section .text
.globl _start
_start:
nop
movl $0, %eax
cpuid
movl $buffer, %edi
movl %ebx, (%edi)
movl %edx, 4(%edi)
movl %ecx, 8(%edi)
pushl $buffer
pushl $output
call printf # 调用 C 语言的 printf
addl $8, %esp
pushl $0
call exit # 调用 C 语言的 exit

编译方法:

1
2
3
4
5
6
as -o cpuid2.o cpuid2.s
ld -dynamic-linker /lib/ld-linux.so.2 -o cpuid2 -lc cpuid2.o

# 运行:
./cpuid2
The processor Vendor ID is 'GenuineIntel'

汇编语言程序设计