Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Linux下获取ARMv8-A CPU详情的3种方法 #7

Open
ZhengZhenyu opened this issue Apr 3, 2020 · 0 comments
Open

Linux下获取ARMv8-A CPU详情的3种方法 #7

ZhengZhenyu opened this issue Apr 3, 2020 · 0 comments

Comments

@ZhengZhenyu
Copy link
Contributor

ZhengZhenyu commented Apr 3, 2020

作者:郑振宇

在ARM平台上进行软件适配时,经常遇到需要根据不同CPU的具体型号、额外属性等信息进行分支处理的需求,因而需要获取CPU的详情信息;ARM架构CPU与X86架构芯片在CPU详情信息的呈现上有很大不同。本文将简述ARM CPU与CPU详情相关的知识及在Linux下获取ARMv8-A CPU详情的三种方法。

ARM CPU中有关CPU详情的寄存器

根据ARM CPU官方技术手册,ARM CPU的CPU型号、Vendor、版本等信息存于MIDR_EL1寄存器中:
MIDR
其中从低至高第0-3 bit表示revision,代表固件版本的小版本号,如r1p3中的p3;
第4-15 bit表示part number(id),代表这款CPU在所在vendor产品中定义的产品代码,如在HiSilicon产品中,part_id=0xd01代表Kunpeng-920芯片;
第16-19 bit表示architecture,即架构版本,0x8即ARMv8;
第20-23 bit表示variant,即固件版本的大版本号,如r1p3中的r1;
第24-31 bit表示implementer,即vendor id,如vendor_id=0x48表示HiSilicon

想要知道一款ARM CPU的具体型号,则需要首先解析vendor_id(implementer) 然后再在该Vendor的所有型号中匹配part_id,才能获取到具体的信息;这里列出目前系统中已有的Vendor列表和其ID对应关系

Vendor Name Vendor ID
ARM 0x41
Broadcom 0x42
Cavium 0x43
DigitalEquipment 0x44
HiSilicon 0x48
Infineon 0x49
Freescale 0x4D
NVIDIA 0x4E
APM 0x50
Qualcomm 0x51
Marvell 0x56
Intel 0x69

而对于具体型号来说,对应关系则更为复杂,这里就不一一列举,可以参考本站文章util-linux/lscpu工具中的相关具体实现来获取完整的映射关系,lscpu工具我们则将在后面的部分中进行介绍。

上面介绍过,除了CPU型号之外,我们通常还会关注CPU是否支持我们需要的特性(扩展指令集,CPU Flags, CPU features);与X86相差较大(CPU features定义集中在EBX,ECX和EDX寄存器中),ARM架构的这些特性分散于ID_PFR0_EL1, ID_PFR1_EL1, ID_DFR0_EL1, ID_ISAR0_EL1 等等若干个专用寄存器中,解析起来难度较高,后面我们会详细讨论如何获取这些内容。

在Linux下如何获取CPU详情信息

在介绍具体的方法前,首先需要介绍一下ARMv8架构下的安全分层机制(Exception Level):
image
如上图所示,ARMv8架构是专为数据中心场景而设计的架构,相比较早的ARM架构,新增了EL2层用于实现硬件虚拟化;较高层的用户是无权直接限访问下一层的数据内容的,对于我们的场景来说,由上面介绍的内容中可以看到,前面所有介绍的寄存器都存在于EL1层,而我们通常使用的应用程序都处于EL0层,因此是无法直接访问到这些寄存器的。那么该如何读取这些内容呢?

1. 从文件节点获取

OS在启动时,会将底层硬件信息载入到相应的文件节点中,这样,位于EL0层的用户就可以通过读取这些文件节点来获取这些信息,比较常用的有两个:

  1. /sys/devices/system/cpu:
    该文件夹下保存了较全的CPU信息文件,并按单个CPU进行区分,读取其中某一个的regs文件目录就可以获得相应的CPU详情信息,如我们尝试获取CPU0的相关信息:
    image
    可以看到,我们读取的仍然是MIDR_EL1寄存器相对应的信息,并且是未解析的数据,需要对应上文介绍的方法进行解析。并且目前没有在这个文件夹下找到CPU Flags的相关的信息,如果后续找到其所在位置,会刷新。
  2. /proc/cpuinfo:
    image
    通过读取cpuinfo可以看到,通过这种方法获取的CPU详情,是进行过解析的,对原始数据进行了拆分,并且是包含了CPU Flag信息的,但仍与X86下的结果有较大不同,各个key所对应的信息仍然需要根据表单进行解析才能转变为人为可读的信息。

2.使用LSCPU命令读取

Linux内核的开发者显然也发现了上文介绍的两种方法获取信息不够全面且需要二次解析的问题,因此在Linux外围工具组util-linux/lscpu(wiki)中进行了改进,从2.32版本开始增加了对ARM平台CPU信息的解析,从而提供人为可读的内容(由于2019年11月才合入相关Patch,HiSilicon芯片的解析需要手动编译最新主干代码才能实现)。
image
从上图可以看到,lscpu提供了非常丰富且直观的内容。

3.使用内联汇编和辅助向量直接解析

上文介绍的两种方法相对来说比较简单,但需要进行读取文件、运行外部命令等操作;当想在自己的程序中引用上述信息时,速度会相对较慢且会引入新的依赖(lscpu)。因此最快速的方法是通过内联汇编直接读取并解析相应的寄存器;上文中已经提到,用户在EL0无法直接读取到位于EL1的寄存器内的内容,那么该如何去做呢?

ARM已经为我们准备好了一切,用户可以通过MRS指令将程序状态寄存器的内容传送到通用寄存器中,再进行进一步的解析,因此我们可以这样做:

    /* read the cpuid data from MIDR_EL1 register */
    asm("mrs %0, MIDR_EL1" : "=r" (cpuid));
    VIR_DEBUG("CPUID read from register:  0x%016lx", cpuid);

    /* parse the coresponding part_id bits */
    data->pvr = cpuid>>4&0xFFF;
    /* parse the coresponding vendor_id bits */
    data->vendor_id = cpuid>>24&0xFF;

这样就可以快速的获取CPUID相关的具体内容,在根据表格进行映射即可获得Vendor和Model信息;

对于CPU Flags,由于牵扯到的寄存器众多,读者可以根据ARM64 CPU Feature Registers中的示例程序进行依次进行寄存器读取,再根据相应的映射关系进行解析,也可以使用下面将要介绍的可读性更高的另一种方法。

Linux内核提供了getauxval()方法,用于读取辅助向量(auxiliary vector, 一个从内核到用户空间的信息交流机制),通过读取相应的辅助向量,我们就能获取相应的硬件信息;辅助向量有很多,感兴趣的读者可以查看上面的链接,对于我们读取CPU Flags来说,关心的是AT_HWCAP这个辅助向量,通过getauxval()读取这个向量的值,可以获得整合过的CPU Flags信息,其bit定位规则则在hwcap.h,当然,每种架构下的对应关系不相同,需要根据需要进行查找。

那么,我们就可以采用下面的方法进行解析:

#include <stdio.h>
#include <sys/auxv.h>

/* 通过移位Bit Mask来读取相应的标志位 */
#define BIT_SHIFTS(n)			(UL(1) << (n))

int main() {
    unsigned long hwcaps = getauxval(AT_HWCAP);
    int i;
    /* 目前ARMv8架构只有32种CPU Flags */
    char *list[32] = {"fp\n", "asimd\n", "evtstrm\n", "aes\n", "pmull\n", "sha1",
                              "sha2\n", "crc32\n", "atomics\n", "fphp\n", "asimdhp\n",
                              "cpuid\n", "asimdrdm\n","jscvt\n", "fcma\n", "lrcpc\n",
                              "dcpop\n", "sha3\n", "sm3\n", "sm4\n", "asimddp\n" ,
                              "sha512\n", "sve\n", "asimdfhm\n", "dit\n", "uscat\n",
                              "ilrcpc\n", "flagm\n", "ssbs\n", "sb\n", "paca\n","pacg\n",};
    for (i = 0; i< 32; i++){
	if (hwcaps & BIT_SHIFTS(i)) {
	    printf("%s\n",list[i]);
	}
    }
}

或者,可以直接通过hwcap.h中预先定义好的宏来做bit mask:

#include <stdio.h>
#include <sys/auxv.h>
#include <asm/hwcap.h>

int main()
{
    long hwcaps= getauxval(AT_HWCAP);

    if(hwcaps & HWCAP_AES){
        printf("AES instructions are available\n");
    }
    if(hwcaps & HWCAP_CRC32){
        printf("CRC32 instructions are available\n");
    }
    if(hwcaps & HWCAP_PMULL){
        printf("PMULL/PMULL2 instructions that operate on 64-bit data are available\n");
    }
    if(hwcaps & HWCAP_SHA1){
        printf("SHA1 instructions are available\n");
    }
    if(hwcaps & HWCAP_SHA2){
        printf("SHA2 instructions are available\n");
    }
    return 0;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

1 participant