Intel x86_64 CPUID指令介绍

本文阅读 9 分钟
首页 Linux,系统 正文

1.1 CPUID 功能简介

从名字就可以知道该指令是用来读取处理器的各种标识和特性信息(比如:CPU型号和支持的功能),并将指令执行完后返回的信息保存在 EAX, EBX, ECX,和 EDX 寄存器中。

CPUID指令有两组功能:一组返回的是基本信息,另一组返回的是扩展信息。

该指令有一个输入参数(可能会有两个),该参数会传递给EAX(ECX)寄存器,一般情况下只输入一个参数,根据输入参数的不同,返回给EAX, EBX, ECX, and EDX寄存器的信息也不一样。简介如下: img

1.2 处理器是否支持 CPUID指令

EFLAGS寄存器的bit21位表明处理器是否支持CPUID指令,如果可以设置或者清除该标志位,那么代表处理器支持CPUID指令。 img img

1.3 指令返回基本信息

CPUID指令的第一组功能: 确定CPUID指令输出处理器的基本信息以及EAX寄存器所能接受的最大的输入值: 输入参数为 00 H ,汇编伪代码为: MOV EAX, 00H CPUID

该指令会返回处理器能接受的最大输入参数 Maximum(即EAX寄存器能接受的最大输入值),为了返回有效的CPU基本信息,因此输入参数必须得在0~ Maximum之间,同时该返回值也是保存在EAX寄存器中。以及供应商ID字符串(Vendor Identification String) 如下图所示: img

从图中可以看出,供应商ID字符串以EBX、EDX和ECX的形式返回。 对于Intel处理器,该字符串为" GenuineIntel ",并表示为: img 接下来我们来简单的演示一下,在 intel 主机上执行 lscpu命令: img 对于AMD处理器,该字符串为"AuthenticAMD " img

1.4 指令返回扩展信息

CPUID指令的第二组功能: 确定CPUID指令输出处理器的扩展信息以及EAX寄存器所能接受的最大的输入值: 输入参数为 80000000H ,汇编伪代码为: MOV EAX, 80000000H CPUID

该指令会返回处理器能接受的最大输入参数 Maximum(即EAX寄存器能接受的最大输入值),为了返回有效的CPU扩展信息,因此输入参数必须得在80000000H ~ Maximum之间,同时该返回值也是保存在EAX寄存器中。 img

2.1 简介

接下来我们主要来介绍输入参数为01是CPUID的功能。

这一章节的内容都与下篇文章intel LBR和BTS有关: Intel x86_64 LBR & BTS功能

我这里先简单的介绍下:简单来说就是LBR和BTS用来记录CPU产出的跳转指令:包括call、ret、jmp、jxx(间接跳转)、中断、异常。利用这个功能,就可以保存每个分支跳转指令的起始地址和目的地址。这样就可以了解到当前CPU执行代码的流程情况。 举一个简单的例子jmp指令:

4004d3: eb 03    jmp 4004d8
当执行下面这条jmp指令时,那么将产生一个 from 4004d3 to 4004d8的跳转,那么产生的LBR记录项就是
{  From:4004d3 ,To:4004d8 } /* jmp指令 */

再举一个简单的例子关于call ,ret 指令:

int add(int a, int b)
{ 
    return (a+b);
}
 
int main(void)
{ 
    return add(1,2);
}
上述两个函数将会产生两条LBR记录项:
{ From : main , to : add} /* call 指令*/
{ From : add , to : main} /* ret 指令*/

在二进制安全中,有很多攻击都是hook控制流,比如隐藏进程,隐藏文件,隐藏tcp,提权等等,使正在运行的系统按照攻击者的流程运行下去,这样就和系统正常的控制转移过程将会不一样,比如在Linux系统上,由于攻击者hook掉一些函数,当用户使用ls、top、ps、netstat等命令时,那么将会过滤掉一些进程,文件,tcp连接等等,这样用户调用shell命令时,便看不到过滤的进程,文件,tcp连接等,那么攻击者就会利用到这些进程、文件、tcp连接,对系统危害极大。

CPU当前正在产生的分支、中断和异常等跳转指令提供一种方法来确定系统当前运行中的控制转移过程。我们根据这些跳转指令便能够分析出系统是否被攻击了。 画个简图(只是简图描述的不太准确)来描述一下: img 上图中的不正常的跳转指令是操作系统text中不存在的跳转指令,那么我们就可以通过LBR,BTS获取CPU正在产生的跳转指令来对比操作系统中text中的跳转指令来判断系统是否被攻击了,描述的比较简单,下篇文章我将详细介绍。

从上文可以得知01参数属于第一组功能:返回处理器基本信息。

输入参数为 01 H ,汇编伪代码为: MOV EAX, 01H CPUID img

2.2 返回值EAX内容分析

img EAX返回的内容主要是 处理器的Model ID、 Family ID 、Processor Type、Stepping ID 。

2.3 返回值EBX内容分析

此处省略。。。。。。

2.4 返回值ECX内容分析

img 在这里主要解释一下3个参数,与LBR和BTS有关,有关LBR和BTS可看下文链接: Intel LBR & BTS 功能介绍 PDCM:Perfmon and Debug Capability 。该值为1表示处理器支持性能和调试能力。 DS-CPL:CPL Qualified Debug Store。该值为1表示处理器支持对Debug Store特性的扩展,允许根据当前系统处于的特权等级对 branch message 进行过滤。(0:表示内核态,3:表示用户态) DTES64:64-bit DS Area。该值为1表示处理器支持在DS Area存放64位地址。

2.5 返回值EDX内容分析

img 在这里主要解释一下2个参数,与LBR和BTS有关: DS:Debug Store。处理器支持将调试信息写入内存驻留缓冲区。BTS和PEBS使用该特性。可以理解为处理器是否支持BTS和 PEBS功能。 MSR :Model Specific Registers RDMSR and WRMSR Instructions。CPU是否支持指令rdmsr/wrmsr来读写MSR寄存器

3.1 linux应用层调用cpuid指令

我写了一个简单的应用程序来获取处理器厂商ID(vendor ID)和 family ,如下:

#include <stdio.h>

#define X86_VENDOR_INTEL 0
#define X86_VENDOR_AMD 1
#define X86_VENDOR_UNKNOWN 2

#define QCHAR(a, b, c, d) ((a) + ((b) << 8) + ((c) << 16) + ((d) << 24))
#define CPUID_INTEL1 QCHAR('G', 'e', 'n', 'u')
#define CPUID_INTEL2 QCHAR('i', 'n', 'e', 'I')
#define CPUID_INTEL3 QCHAR('n', 't', 'e', 'l')
#define CPUID_AMD1 QCHAR('A', 'u', 't', 'h')
#define CPUID_AMD2 QCHAR('e', 'n', 't', 'i')
#define CPUID_AMD3 QCHAR('c', 'A', 'M', 'D')

#define CPUID_IS(a, b, c, ebx, ecx, edx) \ (!((ebx ^ (a))|(edx ^ (b))|(ecx ^ (c))))

static inline void cpuid(int op, unsigned int *eax, unsigned int *ebx,
                             unsigned int *ecx, unsigned int *edx)
{ 
     asm volatile("cpuid" //asm 表示内核汇编,执行cpuid指令, volatile 表示告诉gcc编译器不要优化代码 
        : "=a" (*eax),   //第一个冒号后面: 是输出参数。
          "=b" (*ebx),   //输出操作数约束应该带有一个约束修饰符 "=",指定它是输出操作数
          "=c" (*ecx),
          "=d" (*edx)
        : "0" (*eax)    //第二个冒号后面: 是输入参数 Intel手册也说明ecx有时候也作为输入参数
        : "memory");     
}

static int x86_vendor(void)
{ 
    unsigned eax = 0x00000000;
    unsigned ebx, ecx = 0, edx;

    cpuid(0, &eax, &ebx, &ecx, &edx);

    if (CPUID_IS(CPUID_INTEL1, CPUID_INTEL2, CPUID_INTEL3, ebx, ecx, edx))
          printf("GenuineIntel\n");
        return X86_VENDOR_INTEL;

    if (CPUID_IS(CPUID_AMD1, CPUID_AMD2, CPUID_AMD3, ebx, ecx, edx))
          printf("AuthenticAMD\n");
        return X86_VENDOR_AMD;

    return X86_VENDOR_UNKNOWN;
}

static int x86_family(void)
{ 
    unsigned eax = 0x00000001;
    unsigned ebx, ecx = 0, edx;
    int x86;

    cpuid(1, &eax, &ebx, &ecx, &edx);

    x86 = (eax >> 8) & 0xf;
    if (x86 == 15)
        x86 += (eax >> 20) & 0xff;

    return x86;
}

int main()
{ 
    unsigned int eax = 0;
    unsigned int ebx = 0;
    unsigned int ecx = 0;
    unsigned int edx = 0;

     cpuid(0, &eax, &ebx, &ecx, &edx);
     printf("EBX ← %x (“Genu”)EDX ← %x (“ineI”) ECX ← %x (“ntel”)\n", ebx, edx ,ecx);

     int vendor = x86_vendor();
     int family = x86_family();

     printf("%d %d \n", vendor, family);

     return 0;

}

让我们来看看输出结果: img img img 结果显示与Intel手册,lscpu命令显示的一致,感兴趣的同学还可以打印跟多的cpu信息,在这里我就不继续介绍了。 补充一下:

内联汇编的基本格式
asm ( assembler template 
           : output operands                  /* optional */
           : input operands                   /* optional */
           : list of clobbered registers      /* optional */
           );

3.2 linux内核中调用cpuid指令

除了在应用层通过汇编指令调用cpuid指令外,还可以在内核模块直接调用cpuid函数接口 该接口定义在arch/x86/include/asm/processor.h (内核版本3.10.0)中:

/* * Generic CPUID function * clear %ecx since some cpus (Cyrix MII) do not set or clear %ecx * resulting in stale register contents being returned. */
static inline void cpuid(unsigned int op,
             unsigned int *eax, unsigned int *ebx,
             unsigned int *ecx, unsigned int *edx)
{ 
    *eax = op;
    *ecx = 0;
    __cpuid(eax, ebx, ecx, edx);
}

#define __cpuid native_cpuid

static inline void native_cpuid(unsigned int *eax, unsigned int *ebx,
                unsigned int *ecx, unsigned int *edx)
{ 
    /* ecx is often an input as well as an output. */
    asm volatile("cpuid"
        : "=a" (*eax),
          "=b" (*ebx),
          "=c" (*ecx),
          "=d" (*edx)
        : "0" (*eax), "2" (*ecx)
        : "memory");
}

现在我写个简单的内核模块来测试一下cpuid指令:

#include <linux/kernel.h>
#include <linux/module.h>


//内核模块初始化函数
static int __init lkm_init(void)
{ 
    unsigned int eax = 0;
    unsigned int ebx = 0;
    unsigned int ecx = 0;
    unsigned int edx = 0;

    cpuid(0, &eax, &ebx, &ecx, &edx);
    
    printk("EBX:%xh(“Genu”) EDX:%xh(“ineI”) ECX:%xh(“ntel”)\n", ebx, edx ,ecx);
        
    return 0;
}

//内核模块退出函数
static void __exit lkm_exit(void)
{ 
    printk(KERN_DEBUG "exit\n");
}

module_init(lkm_init);
module_exit(lkm_exit);

MODULE_LICENSE("GPL");

Makefile文件:

obj-m := kcpuid.o

all:
    make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules

clean:
    make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean

img 可以看出和intel手册显示的一致。感兴趣的同学可以写个内核模块在这里获取更多的cpu信息。 img

这条指令支持的内容十分丰富,在这里我只介绍了CPUID指令的一点点内容,并在应用层和内核模块各自写了一个简单的程序验证了一下,在下篇文章我会介绍这条指令的其它内容(与LBR和BTS有关),大家感兴趣的可以自行阅读 intel vol2 chapter3 3.2小节 关于CPUID指令的详细介绍。 img

https://blog.csdn.net/bin_linux96/article/details/104236808 https://blog.csdn.net/razor87/article/details/8711712 https://blog.csdn.net/subfate/article/details/50719396 linux内核源码 3.10.0 Intel官方手册 vol2 intel官方手册 vol3

本文为互联网自动采集或经作者授权后发布,本文观点不代表立场,若侵权下架请联系我们删帖处理!文章出自:https://blog.csdn.net/weixin_45030965/article/details/123779799
-- 展开阅读全文 --
BUUCTF Web [极客大挑战 2019]Knife
« 上一篇 06-24
安全面试之XSS(跨站脚本攻击)
下一篇 » 07-24

发表评论

成为第一个评论的人

热门文章

标签TAG

最近回复