5544992 发表于 2015-4-11 08:13:34

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

  在上文中,我们在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 = 0x0;
    dev->config = 0x0;
    for (r = 0; r < PCI_NUM_REGIONS; ++r) {    /*遍历所有的region,这个的region就是bar,清空region里面的IO地址*/
      PCIIORegion *region = &dev->io_regions;
      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;
  /* 如果没有注册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_addrUINT16_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交互。
  
  文章出处:http://blog.iyunv.com/yearn520/article/details/6577988
页: [1]
查看完整版本: [转载]KVM虚拟机代码揭秘——QEMU的PCI总线与设备(下)