Qemu-KVM的CPUID初始化和自定义CPU Model显示

在上一篇Blog:/proc/cpuinfo里的CPU型号怎么来的?里,可以知道Linux系统是根据CPUID指令来显示具体的CPU型号的。所以很自然的一个想法:是不是可以自定义显示的内容呢?

答案显而易见,必然是可以的。但是如果要改物理CPU的寄存器,那确实会有些困难,不过没关系,我们还有虚拟机嘛,理论上虚拟机可以虚拟这些东西,那改动起来应该也是比较方便的。

想要修改这些寄存器,首先得先看看CPUID指令在Qemu里是怎么处理的:

经过一些搜索,发现KVM提供了一个接口KVM_SET_CPUID2,通过这个接口,可以在用户空间设置需要模拟的CPUID的信息,而我们使用Qemu,肯定是会打开KVM加速的,因此,只需要看看Qemu在这方面是怎么处理的就可以了。

知道了相关代码的关键字,找相关的逻辑就不难了,在target/i386/kvm.c文件里,定义了一个int kvm_arch_init_vcpu(CPUState *cs)函数:

int kvm_arch_init_vcpu(CPUState *cs)
{
    struct {
        struct kvm_cpuid2 cpuid;
        struct kvm_cpuid_entry2 entries[KVM_MAX_CPUID_ENTRIES];
    } cpuid_data;
    /*
     * The kernel defines these structs with padding fields so there
     * should be no extra padding in our cpuid_data struct.
     */
    QEMU_BUILD_BUG_ON(sizeof(cpuid_data) !=
                      sizeof(struct kvm_cpuid2) +
                      sizeof(struct kvm_cpuid_entry2) * KVM_MAX_CPUID_ENTRIES);

    X86CPU *cpu = X86_CPU(cs);
    CPUX86State *env = &cpu->env;
    uint32_t limit, i, j, cpuid_i;
    uint32_t unused;
    struct kvm_cpuid_entry2 *c;
    uint32_t signature[3];
    int kvm_base = KVM_CPUID_SIGNATURE;
    int max_nested_state_len;
    int r;
    Error *local_err = NULL;

    // ...
    cpu_x86_cpuid(env, 0, 0, &limit, &unused, &unused, &unused);

    for (i = 0; i <= limit; i++) {
        // ...
        switch (i) {
        // ...
        default:
            c->function = i;
            c->flags = 0;
            cpu_x86_cpuid(env, i, 0, &c->eax, &c->ebx, &c->ecx, &c->edx);
            if (!c->eax && !c->ebx && !c->ecx && !c->edx) {
                /*
                 * KVM already returns all zeroes if a CPUID entry is missing,
                 * so we can omit it and avoid hitting KVM's 80-entry limit.
                 */
                cpuid_i--;
            }
            break;
        }
    }


    cpu_x86_cpuid(env, 0x80000000, 0, &limit, &unused, &unused, &unused);

    for (i = 0x80000000; i <= limit; i++) {
        if (cpuid_i == KVM_MAX_CPUID_ENTRIES) {
            fprintf(stderr, "unsupported xlevel value: 0x%x\n", limit);
            abort();
        }
        c = &cpuid_data.entries[cpuid_i++];

        switch (i) {
        // ...
        default:
            c->function = i;
            c->flags = 0;
            cpu_x86_cpuid(env, i, 0, &c->eax, &c->ebx, &c->ecx, &c->edx);
            if (!c->eax && !c->ebx && !c->ecx && !c->edx) {
                /*
                 * KVM already returns all zeroes if a CPUID entry is missing,
                 * so we can omit it and avoid hitting KVM's 80-entry limit.
                 */
                cpuid_i--;
            }
            break;
        }
    }

    cpuid_data.cpuid.nent = cpuid_i;
    cpuid_data.cpuid.padding = 0;

    // 上面的代码都是在构造一个完整的cpuid_data
    r = kvm_vcpu_ioctl(cs, KVM_SET_CPUID2, &cpuid_data);    // 通过KVM接口设置CPUID
    if (r) {
        goto fail;
    }

    // ...
    return 0;

 fail:
    migrate_del_blocker(invtsc_mig_blocker);

    return r;
}

函数实现挺长,不过大部分都是些判断逻辑,最主要的两个逻辑:一个是构造KVM需要的cpuid_data数据,主要就是循环获取所有的CPUID信息,填充结构体;然后就是通过KVM_SET_CPUID2接口把数据设置给KVM。

其中,在获取CPUID信息的时候,调用了cpu_x86_cpuid()这个函数,这个函数的定义在target/i386/cpu.c:

void cpu_x86_cpuid(CPUX86State *env, uint32_t index, uint32_t count,
                   uint32_t *eax, uint32_t *ebx,
                   uint32_t *ecx, uint32_t *edx)
{
    X86CPU *cpu = env_archcpu(env);
    CPUState *cs = env_cpu(env);
    uint32_t die_offset;
    uint32_t limit;
    uint32_t signature[3];
    X86CPUTopoInfo topo_info;
    // ... 

    switch(index) {
    // ... 
    case 0x80000002:
    case 0x80000003:
    case 0x80000004:
        *eax = env->cpuid_model[(index - 0x80000002) * 4 + 0];
        *ebx = env->cpuid_model[(index - 0x80000002) * 4 + 1];
        *ecx = env->cpuid_model[(index - 0x80000002) * 4 + 2];
        *edx = env->cpuid_model[(index - 0x80000002) * 4 + 3];
        break;
    // ...
    default:
        /* reserved values: zero */
        *eax = 0;
        *ebx = 0;
        *ecx = 0;
        *edx = 0;
        break;
    }
}

这个函数实现也是非常长,也是很多的case分支,但是大部分我们不用关心,只需要看0x800000020x80000004这几个case就行,代码也很简单,就是把env->cpuid_model的值赋值到对应的寄存器里。

看到这里,修改寄存器的方式就很明确了,直接修改env->cpuid_model里的值就可以了。其实还会有些小问题,比如参数里的CPUX86State *env具体是从哪来的,这个问题比较复杂,但是也很值得去研究,下次会专门开文章分析这部分逻辑。

要修改env->cpuid_model,先看看定义,在target/i386/cpu.h被定义成uint32_t cpuid_model[12],很合理,三个ID,每个ID 4个寄存器,一共12个uint32。

然后呢,还需要寻找一个string到uint32_t的转换逻辑,简单看了一下代码里有个x86_cpuid_set_model_id函数

static void x86_cpuid_set_model_id(Object *obj, const char *model_id,
                                   Error **errp)
{
    X86CPU *cpu = X86_CPU(obj);
    CPUX86State *env = &cpu->env;
    int c, len, i;

    if (model_id == NULL) {
        model_id = "";
    }
    len = strlen(model_id);
    memset(env->cpuid_model, 0, 48);
    for (i = 0; i < 48; i++) {
        if (i >= len) {
            c = '\0';
        } else {
            c = (uint8_t)model_id[i];
        }
        env->cpuid_model[i >> 2] |= c << (8 * (i & 3));
    }
}

稍微有些区别,因为x86_cpuid_set_model_id函数参是一个X86CPU类型,但是问题不大,我们稍微修改一下逻辑,新建个函数set_fake_cpuid_model,把cpuid_model修改成Intel(R) Xeon(R) A Really Fast CPU @ 10.0 GHz:

static void set_fake_cpuid_model(uint32_t fake_cpuid_model[12])
{
    // 这里修改成任何想填的信息
    const char *fake_model_id = "Intel(R) Xeon(R) A Really Fast CPU @ 10.0 GHz";
    memset(fake_cpuid_model, 0, 48);
    int c, len, i;
    len = strlen(fake_model_id);
    for (i = 0; i < 48; i++) {
        if (i >= len) {
            c = '\0';
        } else {
            c = (uint8_t)fake_model_id[i];
        }
        fake_cpuid_model[i >> 2] |= c << (8 * (i & 3));
    }
}

然后在cpu_x86_cpuid函数里多加一行:

void cpu_x86_cpuid(CPUX86State *env, uint32_t index, uint32_t count,
                   uint32_t *eax, uint32_t *ebx,
                   uint32_t *ecx, uint32_t *edx)
{
    X86CPU *cpu = env_archcpu(env);
    CPUState *cs = env_cpu(env);
    uint32_t die_offset;
    uint32_t limit;
    uint32_t signature[3];
    X86CPUTopoInfo topo_info;
    // ... 

    switch(index) {
    // ... 
    case 0x80000002:
    case 0x80000003:
    case 0x80000004:
        set_fake_cpuid_model(env->cpuid_model); // 将CPUID设置成我们需要的
        *eax = env->cpuid_model[(index - 0x80000002) * 4 + 0];
        *ebx = env->cpuid_model[(index - 0x80000002) * 4 + 1];
        *ecx = env->cpuid_model[(index - 0x80000002) * 4 + 2];
        *edx = env->cpuid_model[(index - 0x80000002) * 4 + 3];
        break;
    // ...
    }
}

虽然暴力了点,但是作为测试的话,先实现测试的功能就好。如果确实需要有类似的逻辑,理论上放到X86CPU结构体初始化的地方,或者干脆自定义一个CPU类型,会比较友好。

最后,编译,运行!
lscpu