• Intel x86_64 LBR功能


    前言

    本篇文章介绍 Intel 处理器的LBR指令追踪功能,通过该功能可以从硬件层面获取CPU执行的指令信息,原理和流程大致如下:

    1. 通过CPUID指令读取处理器的各种标识和特性信息,判断是否支持硬件调试功能,rdmsr / wrmsr 指令是否可用;
    2. 通过wrmsr指令设置 IA32_DEBUGCTL MSR 寄存器,开启LBR功能;
    3. 通过rdmsr指令读取 IA32_DEBUGCTL MSR 寄存器,获取跳转指令信息;

    下面我们将对这几个方能进行更加详细的介绍。

    一、CPUID指令

    1.1 CPUID功能简介

    该指令可以从读取处理器的各种标识和特性信息(比如:CPU型号和支持的功能),并将指令执行完后返回的信息保存在 EAX, EBX, ECX,和 EDX 寄存器中。
    CPUID指令有两组功能:一组返回的是基本信息,另一组返回的是扩展信息。
    该指令有一个输入参数(可能会有两个),该参数会传递给EAX(ECX)寄存器,一般情况下只输入一个参数,根据输入参数的不同,返回给EAX, EBX, ECX, and EDX寄存器的信息也不一样。简介如下:
    在这里插入图片描述
    针对不同的输入,返回结果如下:
    在这里插入图片描述

    1.2 输入参数01H返回结果

    这里我们重点关注EAX = 01H时,ECX,EDX返回的结果。

    1.2.1 ECX返回结果

    在这里插入图片描述

    • 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位地址。

    1.2.2 EDX返回结果

    在这里插入图片描述
    在这里主要解释一下2个参数,与LBR和BTS有关:

    • DS:Debug Store。处理器支持将调试信息写入内存驻留缓冲区。BTS和PEBS使用该特性。可以理解为处理器是否支持BTS和 PEBS功能。
    • MSR :Model Specific Registers RDMSR and WRMSR Instructions。CPU是否支持指令rdmsr/wrmsr来读写MSR寄存器

    1.3 Linux中CPUID指令

    1.3.1 应用层调用cpid指令

    这里给出一个简单的应用程序来获取处理器厂商ID(vendor ID)和 family ,如下:

    #include 
    
    #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;
    
    }
    
    • 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
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80

    执行结果如下:
    在这里插入图片描述
    该结果与Intel软件发开者手册一致。
    在这里插入图片描述
    使用lscpu命令也可以看到Vendor IDCPU family信息。
    在这里插入图片描述

    1.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");
    }
    
    
    • 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

    通过一个简单的模块测试cpuid指令:

    #include 
    #include 
    
    
    //内核模块初始化函数
    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");
    
    
    • 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

    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
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    在这里插入图片描述

    二、MSR寄存器

    2.1 MSR 寄存器简介

    MSR(Model Specific Register)是x86架构中的概念,指的是在x86架构处理器中,一系列用于控制CPU运行、功能开关、调试、跟踪程序执行、监测CPU性能等方面的寄存器。
    不同的CPU型号或不同的CPU厂商(Intel&AMD),它的MSR寄存器可能是不一样的,它会根据具体的CPU型号的变化而变化,每款新的CPU都有可能引入新的MSR寄存器。

    2.2 RDMSR,WRMSR指令介绍

    在前面我们提到,通过CPUID可以查询当前CPU是否支持RDMSR,WRMSR指令,而这两条指令正是Intel处理器提供用来读取/写入 MSR寄存器中数据的。
    这两条指令必须在 privilege level 0(linux中的内核态)或实模式下执行:
    在这里插入图片描述
    在这里插入图片描述
    而在linux内核提供了两个接口,位于arch/x86/include/asm/msr.h文件中:在这里插入图片描述

    2.3 IA32_DEBUGCTL MSR 寄存器

    A32_DEBUGCTL 寄存器其地址为 01D9H(不同的CPU family名字可能会不一样,比如MSR_DEBUGCTLA, MSR_DEBUGCTLB),可以用来调试,跟踪中断、LBR、BTS等等。
    下面介绍几个较重要的位:

    1. bit0: LBR (last branch/interrupt/exception) flag 。该位被设置后,处理器开始记录跟踪处理器产生的最近的分支、中断和/或异常,并储存在 LBR stack MSRs中。
    2. bit1: BTF (single-step on branches) flag 。该位被设置后,处理器将EFLAGS寄存器中的TF标志视为:TF as single-step on branches instead of single-step on instructions。(x86_64的gdb单步调试功能就是用EFLAGS寄存器中的TF位—single-step on instructions 来实现的,后面会写一篇x86_64的gdb单步调试跟踪原理的文章)
    3. bit6: TR (trace message enable) flag。该位被设置后,当处理器检测到一个已经产生的分支,中断,或异常;它将这个分支记录作为 branch trace message(BTM)发送到系统总线上。
    4. bit7: BTS (branch trace store) flag。该位被设置后,BTS能够将BTMS保存到DS save area的内存驻留BTS buffer中。
    5. bit8: BTINT (branch trace interrupt) flag。该位被设置后,当BTS缓冲区满时,BTS会产生一个中断。
    6. bit9: BTS_OFF_OS (branch trace off in privileged code) flag。该位被设置后,如果CPL为0,则跳过BTS或BTM,即:在内核态不启用bts功能。
    7. bit10:BTS_OFF_USR (branch trace off in user code) flag。该位被设置后,如果CPL大于0,则跳过BTS或BTM。即:在用户态不启用bts功能。

    因此可以通过这两bit BTS_OFF_OS/BTS_OFF_USR 来设置是获取用户态的分支跳转指令还是内核态的分支跳转指令。
    CPL:Current Privilege Level,该值为0代表最高优先级,该值为3代表最低优先级,linux 中,0为内核态,3为用户态。
    在这里插入图片描述
    到这里我们已经介绍了如何查看CPU特性,什么是MSR寄存器,如何操作MSR寄存器,以及IA32_DEBUGCTL 这个专门负责调试的MSR寄存器,下面我们讲进一步介绍IA32_DEBUGCTL 寄存器中LRB功能,并给出一个利用LBR功能获取CPU中跳转指令数据的脚本程序。

    三、LBR

    3.1 LBR简介

    IA32_DEBUGCTL MSR 寄存器的bit0被设置后,处理器开始自动记录 产生的分支,中断,异常等branch records,并存储在 LBR stack MSRs中。下面来介绍下 LBR stack与 TOS Pointer

    1. Last Branch Record (LBR) Stack :LBR由N对msr寄存器组成(N是LBR堆栈大小,如下表所示),msr存储了最近分支的源地址和目的地址。
    2. Last Branch Record Top-of-Stack (TOS) Pointer:TOS Pointer MSR中最低有效的M位包含了一个指向LBR堆栈中MSR的M位指针,该指针包含了记录的最近的分支、中断或异常。

    在使用LBR stack记录分支信息时,TOS寄存器指示stack当前位置. 从而可以从LBR stack中正确读取分支记录.

    从下表可以看出,LBR堆栈中msr的个数和TOS指针值的有效范围对于不同的处理器家族中会有所不同。
    在这里插入图片描述
    LBR msr是64位的寄存器。在64位模式下, last branch records存储完整地址。32位模式下,高32位置0,低32为存储最近分支记录。
    MSR_LASTBRANCH_0_FROM_IP — (N-1) MSR address 存储分支记录源地址
    MSR_LASTBRANCH_0_TO_IP — (N-1) MSR address 存储分支记录目的地址
    在这里插入图片描述

    3.2 LBR的用法

    (1)查询LBR stack存储格式 :IA32_PERF_CAPABILITIES MSR (调用rdmsrl)
    在这里插入图片描述
    (2)开启LBR功能,设置 IA32_DEBUGCTL MSR寄存的 bit0 = 1 (调用wrmsrl)
    在这里插入图片描述
    (3) 读取TOS指针位置 (调用rdmsrl)
    读取 MSR_LASTBRANCH_TOS 寄存器 ,请参考 Intel vol4
    (4) 读取LBR stack寄存器 (调用rdmsrl)
    读取 MSR_LASTBRANCH_x_FROM_IP / MSR_LASTBRANCH_x_TO_IP 寄存器 ,请参考 Intel vol4

    LBR的优点:分支记录存储在寄存器中,几乎没有性能开销。
    LBR的缺点:寄存器组数量有限,这样我们保存的分支记录也有限。

    3.3 代码演示

    实验平台:
    Intel x86_64、centos 7.8
    注意是在物理机上实验,虚拟机不支持LBR。
    这里我为了简单起见,采用shell脚本来进行代码演示,用来捕获用户态的代码执行流的记录。

    3.3.1 用户态demo

    这里是一个最简单的while循环demo,这个dmeo将会产生许多的jmp指令。

    #include 
    
    int main()
    {
        int i = 0;
        while(1) {
            i++;
        }
    
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    让我们来看看其二进制反汇编代码,objdump -d a.out:
    在这里插入图片描述
    从反汇编我们可以看出while循环一直在执行jmp指令,产生的跳转记录如下:

    {From : 4004fc , to : 4004f8}   //jmp指令
    
    • 1

    3.3.2 msr-tools

    在这里我通过msr-tools工具包在linux shell命令上来读取或写MSR寄存器值。
    下载地址有两个,我选择的是下面的一个:
    https://pkgs.org/download/msr-tools
    https://mirrors.edge.kernel.org/pub/linux/utils/cpu/msr-tools/
    在这里插入图片描述

    3.3.3 taskset

    taskset 用于在给定 PID 的情况下设置或读取正在运行的进程的 CPU 亲和性或者启动一个新的进程时设置其 CPU亲和性。CPU 亲和性是一种调度程序属性,它将进程“绑定”到系统上给定的一组 CPU上。Linux 调度程序将遵循给定的 CPU 亲和性,并且该进程不会在任何其他 CPU 上运行

    我这里主要是用来把给定的进程指定在某个cpu上工作。

    (1)运行a.out程序时,将该进程绑定在CPU 1上运行

    taskset -c 1 ./a.out 
    
    • 1

    (2)获取a.out进程运行在哪个CPU上:

    taskset -p 进程号
    
    • 1

    3.3.4 MSR寄存器地址

    查询当前CPU与LBR有关的MSR寄存器地址
    可以通过cpuid指令获取CPU的DF_DM,我这里直接通过lscpu命令查看:
    在这里插入图片描述

    根据DF_DM查询Intel手册:
    MSR_IA32_DEBUGCTL 寄存器的地址 0x1D9
    MSR_LBR_TOS寄存器的地址:0x1C9
    MSR_LBR_SELECT寄存器的地址:0x1C8

    支持32对 FROM TO 记录:
    MSR_LASTBRANCH_0_FROM_IP - MSR_LASTBRANCH_31_FROM_IP:0x680 - 0x69F
    MSR_LASTBRANCH_0_TO_IP - MSR_LASTBRANCH_31_TO_IP:0x6C0 - 0x6DF

    3.3.5 完整代码

    # Model Specific Registers address
    MSR_LASTBRANCH_0_FROM_IP=680
    MSR_LASTBRANCH_0_TO_IP=6C0
    MSR_IA32_DEBUGCTL=1D9
    MSR_LBR_TOS=1C9
    MSR_LBR_SELECT=1C8
    
    # Define ADDR_FROM and ADDR_FROM Var
    ADDR_FROM=$MSR_LASTBRANCH_0_FROM_IP
    ADDR_TO=$MSR_LASTBRANCH_0_TO_IP
    
    
    # Configuration
    CORE=1   # Run the target workload on core 1 (taskset -c 1 process)
    N_LBR=32 # Number of LBR records
    
    # enable MSR kernel module
    sudo modprobe msr
    
    # enable LBR
    sudo ./wrmsr -p ${CORE} 0x${MSR_IA32_DEBUGCTL} 0x1
    
    # do not capture branches in ring 0
    sudo ./wrmsr -p ${CORE} 0x${MSR_LBR_SELECT} 0x1
    
    # wait a bit for the workload to issue enough branches
    sleep 0.1
    
    # read all LBR records
    for i in `seq 1 ${N_LBR}`;
    #for(( i = 0; i < ${N_LBR}; i++))
    do
        echo "LBR record : $i"
        echo -n 0x$ADDR_FROM
        echo -n ", from address: "
        sudo ./rdmsr -p ${CORE} 0x${ADDR_FROM}
        echo -n 0x$ADDR_TO
        echo -n ", to   address: "
        sudo ./rdmsr -p ${CORE} 0x${ADDR_TO}
    
        # increament ADDR_FROM (in hex) by 1
        ADDR_FROM=`echo "obase=16; ibase=16; ${ADDR_FROM} + 1;" | bc`
    
        # increament ADDR_TO (in hex) by 1
        ADDR_TO=`echo "obase=16; ibase=16; ${ADDR_TO} + 1;" | bc`
    done
    
    • 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

    (1)设置进程CPU亲和性:

    taskset -c 1 ./demo &
    
    • 1

    1:表示CPU1(运行在第二个CPU上)。
    22896 :表示进程的PID号。

    (2)运行shell脚本,查看结果:
    在这里插入图片描述
    可见获取到了用户态程序的控制执行流程,并与预期一致:

    {From : 4004fc , to : 4004f8}   //jmp指令
    
    • 1

    参考资料

    1. Intel x86_64 CPUID指令介绍
    2. Intel x86_64 LBR & BTS功能
    3. 再谈Intel x86_64 LBR功能
    4. Intel® 64 and IA-32 Architectures Software Developer Manuals
  • 相关阅读:
    线上展厅怎么做要多长时间
    【信息科学技术与创新】机器学习 深度学习 人工神经网络相关分析
    并发原理—如何保证多条指令的原子性(二)
    (2020行人再识别综述)Person Re-Identification using Deep Learning Networks: A Systematic Review
    kprobe 内核实现原理
    2min速览:从设计、实现和优化角度浅谈Alluxio元数据同步
    CentOS 7 安装RabbitMQ
    亚马逊云与生成式 AI 的融合——生成式AI的应用领域
    Ubuntu 22.04 Docker安装笔记
    Unity UGUI的Dropdown(下拉菜单)组件的介绍及使用
  • 原文地址:https://blog.csdn.net/SGchi/article/details/134272367