您的位置:首页 > 编程语言

KVM虚拟机代码揭秘——QEMU的PCI总线与设备(下)

2011-06-30 19:44 369 查看
在上文中,我们在QEMU中已经成功的虚拟了一个PCI桥和一个PCI设备,接下来我们就来给他们分配固定的IO基地址。

要给PCI设备分配固定的IO基地址,那么就需要先了解PCI设备是如何刷新和分配IO基地址的。

1. PCI设备的重置与刷新

PCI在需要的时候,如第一次启动,IO重叠等就需要重置PCI设备,并且清空PCI bar上面的地址信息。主要调用函数pci_device_reset

void pci_device_reset(PCIDevice *dev)
{
int r;
... ...
... ...
dev->config[PCI_CACHE_LINE_SIZE] = 0x0;
dev->config[PCI_INTERRUPT_LINE] = 0x0;
for (r = 0; r < PCI_NUM_REGIONS; ++r) { /*遍历所有的region,这个的region就是bar,清空region里面的IO地址*/
PCIIORegion *region = &dev->io_regions[r];
if (!region->size) {
continue;
}
if (!(region->type & PCI_BASE_ADDRESS_SPACE_IO) &&
region->type & PCI_BASE_ADDRESS_MEM_TYPE_64) {
pci_set_quad(dev->config + pci_bar(dev, r), region->type);
} else {
/*用type将bar上所有的数据都覆盖,之前分配的IO基地址也没了*/
pci_set_long(dev->config + pci_bar(dev, r), region->type);
/*刷新设备*/
pci_update_mappings(dev);

}
}
/*刷新IO地址,更新IO读写映射*/
pci_update_mappings(dev);
}

刷新IO地址函数展开如下:
static void pci_update_mappings(PCIDevice *d)
{
PCIIORegion *r;
int i;
pcibus_t new_addr, filtered_size;
for(i = 0; i < PCI_NUM_REGIONS; i++) {
r = &d->io_regions[i];
/* 如果没有注册region,那么不进行任何操作*/
if (!r->size)
continue;
/* 得到设备bar上存储的基地址 */
new_addr = pci_bar_address(d, i, r->type, r->size);
/* bridge filtering */
filtered_size = r->size;
/* 如果分配了bar地址,那么比较设备地址与父桥的地址,看是否匹配*/
if (new_addr != PCI_BAR_UNMAPPED) {
pci_bridge_filter(d, &new_addr, &filtered_size, r->type);
}
/* 如果得到的新地址没有改变,大小也没变,那么不更新IO重映射,否则将IO读写进行重新映射。*/
if (new_addr == r->addr && filtered_size == r->filtered_size)
continue;
/* 调用IO读写映射函数 */
... ...
... ...

}
}
得到设备bar上存储的基地址的函数展开如下:
static pcibus_t pci_bar_address(PCIDevice *d, int reg, uint8_t type, pcibus_t size)
{
pcibus_t new_addr, last_addr;
/*获得region里基地址的偏移位置*/
int bar = pci_bar(d, reg);
/*检查PCI设备IO是否分配,分配以后command应该置1*/
uint16_t cmd = pci_get_word(d->config + PCI_COMMAND);
if (type & PCI_BASE_ADDRESS_SPACE_IO) {
/*如果没有设置type或者没有分配IO那么直接返回地址未映射,将基地址重新置成-1*/
if (!(cmd & PCI_COMMAND_IO)) {
return PCI_BAR_UNMAPPED;
}
/*将地址进行对齐,大小范围内清0,这个不是很好解释,因为前面我们这个size是制定为2的N此方的,所以减1就尾数全为1,取反为清0*/
new_addr = pci_get_long(d->config + bar) & ~(size - 1);
/*得到region结束地址*/
last_addr = new_addr + size - 1;
/* NOTE: we have only 64K ioports on PC */
/*检查地址是否合法*/
if (last_addr <= new_addr || new_addr == 0 || last_addr > UINT16_MAX) {
return PCI_BAR_UNMAPPED;
}
/*返回新地址*/
return new_addr;
}
... ...
... ...
}
从这里可以看出,要保证地址不被清空,只要保证之前有基地址,而且合法,所以,只要reset不清空地址,那么在这里只要地址合法,就不会清楚映射好的地址。
当刷新得到新地址以后就进行与父桥的地址匹配,函数展开如下:
static void pci_bridge_filter(PCIDevice *d, pcibus_t *addr, pcibus_t *size, uint8_t type)
{
... ...
... ...
/*取桥与设备基地址的最大值作为设备基地址,取桥与设备结束的最小值作为设备的结束地址,如果这个地址合法,那么保证设备在桥地址的范围内*/
base = MAX(base, pci_bridge_get_base(br, type));
limit = MIN(limit, pci_bridge_get_limit(br, type));
/*如果取得地址不匹配,说明设备不在桥的范围内,而且无法截断,将设备地址设置成无效,重新匹配*/
if (base > limit) {
goto no_map;
}
/*匹配成功*/
*addr = base;
*size = limit - base + 1;
return;
no_map:
*addr = PCI_BAR_UNMAPPED;
*size = 0;
}
从这个函数可以看出来,设备的地址分配是受桥的地址分配约束的,只要桥的地址分配了,设备的地址只能分配在桥的范围内,否则就会被置为无效,然后重新分配,一直到分配在桥的范围内为止。所以只要固定了桥的地址,自然就固定了设备的地址。

所以只需要初始化桥的地址,并且在reset的时候跳过桥的基地址重置,就能实现设备和桥地址的固定。添加的函数和代码如下:
添加桥的初始地址,因为桥的地址固定写在bar3上,通过写20可以将基地址固定在0x2000上,同时还需要写命令位,置1.
static int dec_21154_initfn(PCIDevice *dev)
{
... ...
... ...
pci_set_word(dev->config + PCI_BASE_ADDRESS_3,0x2020);
pci_set_word(dev->config + PCI_COMMAND,0x1);
void pci_device_reset(PCIDevice *dev)

return 1;
}
在重置桥里面过滤我们的桥,通过dev的名字可以识别我们自己定义的设备,如果是我们的设备就不重置,直接进行更新IO映射。
void pci_device_reset(PCIDevice *dev)
{
if(strcmp(dev->name,"dec_name")==0){
pci_update_mappings(dev);
return
}
... ...
... ...
}
通过上面的步骤就能实现一般的IO基地址固定,我们可以在Linux中使用 cat /proc/ioports 命令来查看当前PCI设备的IO映射地址关系。

2. 直接重写config_write函数。

我用这种方法测试过几种操作系统,不同系统的PCI设备初始化可能会有区别,有些不能够自适应分配IO基地址设备的,那么我们就需要强行overide PCI配置读写函数。

在QEMU中,每一个PCI设备都要注册一个读写配置函数,用来提供给操作系统读写PCI设备的内存信息,通过读写这两个函数,就能实现对PCI设备IO基地址进行设置,而我们的IO基地址之所以会动态的变化,也就是因为这个函数将新的IO基地址写到了我们虚拟的PCI设备的bar里面,造成我们自己设置的基地址被覆盖。如果我们不重写它,就使用系统默认的配置函数,不改变重写的数值,如果我们有些特殊的需求,如强行给PCI内存赋值,就可以重写这个函数,虽然有些暴力,但是确实可行。
这样做我们需要修改之前定义的设备结构体。在结构体里面增添.config_write和.config_read。并且在write里面强行的把基地址写成我们想固定的地址。

static PCIDeviceInfo fpga_info={
.qdev.name = "fpga",
.qdev.size = sizeof(FPGAState),
.init = pci_fpga_init,
.config_write = fpga_config_write,
.config_read = fpga_config_read,
};
void fpga_write_config(PCIDevice *d, uint32_t addr, uint32_t val, int l)
{
/*如果是bar0 则是0x10,这个必须根据我们分配的bar不同而变化*/
if(addr = 0x10) pci_default_write_config(d,addr,0x20,l);
else pci_default_write_config(d,addr,val,l);
}

同样的方法我们也可以用在桥里面,将桥的IO基地址固定,然而桥的PCI桥地址的基地址是放在bar3上的,所以判断起来要判断1d,如:
if(addr==1d) pci_bridge_write_config(d,addr,0x20,l);
else pci_bridge_write_config(d,addr,val,l);

这样我们就强行的将两者的IO基地址固定了,这个我在操作系统上测试通过了,并且KVM IO拦截运行正常。

总结

通过上面两种改写就能够确保模拟出来的PCI总线设备和桥固定在我们想要的IO空间段,不用系统随机的分配。这样做可以满足我们一些特殊化得需求,如某些板子的某些设备是固定IO地址的,而相应的操作系统不是通过class和subclass,vendor,device ID这些来读取设备,而是通过固定IO来访问设备的就能起到作用。对一些固定的操作系统有更强的兼容性。另外也在一定的程度上帮助我们更深入的理解了PCI设备,理解了硬件与操作系统的IO交互。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: