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分支,但是大部分我们不用关心,只需要看0x80000002
到0x80000004
这几个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类型,会比较友好。
最后,编译,运行!