前言

firmware可以理解为服务器上一类专用软件,它的主功能是对相关设备提供更加底层的控制。常见的firmware比如服务器上要使用的bios等,另外,网卡和vga等设备都需要使用自己固有的fw。在这篇文章当中,笔者打算结合i440fx machine 平台从以下三个方面给大家讲一下firmware的使能和数据访问机制: - bios 加载流程 - bios对相关数据访问机制的实现 - 相关设备firmware处理

BIOS加载

firwmare通常是要被加载到指定的ROM里面的,我们先来看一下在qemu当中这一块是如何实现的。具体的逻辑在pc_system_firmware_init 这个函数里面,这个函数会在i440fx machine的 pc_init1 中调用,相关调用流程如下:

pc_init1->pc_memory_init->pc_system_firmware_init

接下来我们看一下pc_system_firmware_init 具体实现,其核心逻辑会随着vm的启动方式不同而不同。

  • 如果走的是legacy的模式即通过使用seabios来启动,则会走到x86_bios_rom_init 这个函数。
void x86_bios_rom_init(MemoryRegion *rom_memory, bool isapc_ram_fw)
{
    char *filename;
    MemoryRegion *bios, *isa_bios;
    int bios_size, isa_bios_size;
    int ret;

    /* BIOS load */
    if (bios_name == NULL) {
        bios_name = BIOS_FILENAME;
    }

    filename = qemu_find_file(QEMU_FILE_TYPE_BIOS, bios_name);
    if (filename) {
        bios_size = get_image_size(filename);
    } else {
        bios_size = -1;
    }
    if (bios_size <= 0 ||
        (bios_size % 65536) != 0) {
        goto bios_error;
    }
    bios = g_malloc(sizeof(*bios));
    memory_region_init_ram(bios, NULL, "pc.bios", bios_size, &error_fatal);

    .......

    ret = rom_add_file_fixed(bios_name, (uint32_t)(-bios_size), -1);

    . ......

    /* map the last 128KB of the BIOS in ISA space */
    isa_bios_size = MIN(bios_size, 128 * KiB);
    isa_bios = g_malloc(sizeof(*isa_bios));
    memory_region_init_alias(isa_bios, NULL, "isa-bios", bios,
                             bios_size - isa_bios_size, isa_bios_size);
    memory_region_add_subregion_overlap(rom_memory,
                                        0x100000 - isa_bios_size,
                                        isa_bios,
                                        1);
   .......

    /* map all the bios at the top of memory */
    memory_region_add_subregion(rom_memory,
                                (uint32_t)(-bios_size),
                                bios);
}

核心实现如上所示,这个函数里面主要干如下几件事:

  • 找到名字为bios.bin 的文件,获取其大小然后通过rom_add_file_fixed 去加载bios.bin 的数据。
rom_add_file_fixed->rom_add_file

rom_add_file 将bios.bin的数据读到rom->data 所指向的内存里面,并将rom->addr 赋值为4G - bios.size,然后通过rom_insert 将这个rom 放到全局roms list里面

  • 另外一件核心的事情就是通过memory_region_init_ram 创建一个大小为bios_size 名为pc.biosram,同时通过alias的方式创建一个大小为128KB的isa-bios subregion,这样做的目的是为了将bios 最后的128KB的内容map到起始地址为0xe0000 的内存当中,最后将bios memregion添加为rom_memorysubregion,addr为4G - bios_size

从上面来看,pc.bios 的内存空间和真实的rom file 并没有关联起来,那这两者到底是在什么时候产生关联的呢?别着急后面会详细道来

  • 如果vm是通过uefi的方式来启动的话,则bios加载的核心逻辑在pc_system_flash_map 这个函数。再介绍这个函数之前,我们先介绍一下关于flash的背景知识。首先,我们看一下如果vm要通过uefi的方式启动则qemu侧的相关配置是怎么样的?具体如下:
-drive file=/usr/share/OVMF/OVMF_CODE.fd,if=pflash,format=raw,unit=0,readonly=on 
-drive file=/var/lib/volcstack/nvram/OVMF_VARS.fd,if=pflash,format=raw,unit=1

从上面的qemu cmd来看,uefi bios的加载是通过qemu当中类型为pflash的block drive来加载的,而且其包含了两部分:一部分为ovmf code即uefi bios 真实要运行的代码, 另一部分为ovmf vars即运行的时候需要的一些变量。至于这个drive如何被初始化,由于这个不是本篇文章的重点所以这里就不详细介绍了。下面我们来重点介绍一下存储uefi bios 的flash是如何被创建的。首先,我们来看一下flash 的创建流程相关调用关系

pc_machine_initfn->pc_system_flash_create->pc_pflash_create->qdev_new(TYPE_PFLASH_CFI01)

上面的核心逻辑就是在realiezed 之前,先把dev给创建出来。接着再回到pc_system_firmware_init 这个函数涉及到相关的逻辑如下

for (i = 0; i < ARRAY_SIZE(pcms->flash); i++) {
        //将flash跟系统当中type为IF_PFLASH的drive关联起来
        pflash_cfi01_legacy_drive(pcms->flash[i],
                                  drive_get(IF_PFLASH, 0, i));
        //获取相应的后端block backend这里就是raw格式的file
        pflash_blk[i] = pflash_cfi01_get_blk(pcms->flash[i]);
    }

具体逻辑见注释,下面我们还是接着看pc_system_flash_map ,其核心实现逻辑如下

    for (i = 0; i < ARRAY_SIZE(pcms->flash); i++) {
        system_flash = pcms->flash[i];
        blk = pflash_cfi01_get_blk(system_flash);
        if (!blk) {
            break;
        }
        size = blk_getlength(blk);

        .........

        total_size += size;
        qdev_prop_set_uint32(DEVICE(system_flash), "num-blocks",
                             size / FLASH_SECTOR_SIZE);
        //调用realized 函数对flash进行初始化 
        sysbus_realize_and_unref(SYS_BUS_DEVICE(system_flash), &error_fatal);
        sysbus_mmio_map(SYS_BUS_DEVICE(system_flash), 0,
                        0x100000000ULL - total_size);

        if (i == 0) {
            flash_mem = pflash_cfi01_get_memory(system_flash);
            pc_isa_bios_init(rom_memory, flash_mem, size);

            /* Encrypt the pflash boot ROM */
            if (kvm_memcrypt_enabled()) {
                flash_ptr = memory_region_get_ram_ptr(flash_mem);
                flash_size = memory_region_size(flash_mem);
                ret = kvm_memcrypt_encrypt_data(flash_ptr, flash_size);
                if (ret) {
                    error_report("failed to encrypt pflash rom");
                    exit(1);
                }
            }
        }
    }

结合上面的代码,我们来仔细理一下相关实现:

  • 首先,对flash进行realized ,具体调用逻辑如下
sysbus_realize_and_unref->pflash_cfi01_realize

最终实现在pflash_cfi01_realize 这个函数,节选核心逻辑如下

static void pflash_cfi01_realize(DeviceState *dev, Error **errp)
{
    ERRP_GUARD();
    PFlashCFI01 *pfl = PFLASH_CFI01(dev);
    uint64_t total_len;
    int ret;
    ......

    total_len = pfl->sector_len * pfl->nb_blocs;

    memory_region_init_rom_device(
        &pfl->mem, OBJECT(dev),
        &pflash_cfi01_ops,
        pfl,
        pfl->name, total_len, errp);
    if (*errp) {
        return;
    }

    pfl->storage = memory_region_get_ram_ptr(&pfl->mem);
    sysbus_init_mmio(SYS_BUS_DEVICE(dev), &pfl->mem);

    if (pfl->blk) {
        uint64_t perm;
        pfl->ro = !blk_supports_write_perm(pfl->blk);
        perm = BLK_PERM_CONSISTENT_READ | (pfl->ro ? 0 : BLK_PERM_WRITE);
        ret = blk_set_perm(pfl->blk, perm, BLK_PERM_ALL, errp);
        if (ret < 0) {
            return;
        }
    } else {
        pfl->ro = false;
    }

    if (pfl->blk) {
        if (!blk_check_size_and_read_all(pfl->blk, pfl->storage, total_len,
                                         errp)) {
            vmstate_unregister_ram(&pfl->mem, DEVICE(pfl));
            return;
        }
    }
    ......
}

结合上面的代码,我们分析一下具体的实现:

1) memory_region_init_rom_device 初始化一段rom的内存,并绑定相应的mem ops。qemu当中rom 内存都是只读的且作为一段ram map到guest内存空间。对这段内存的读就直接走普通的内存读写的方式,写的话因为这段内存是read only 所以会trap到qemu然后执行该段内存绑定的ops所对应的write

2) sysbus_init_mmio 初始化flash 设备对应的mmio空间。

3) 将数据从磁盘读到flash所对应的内存当中。

  • 接着调用 sysbus_mmio_map 对上面mmio空间进行映射,起始地址为4G - file.size

  • 最后调用pc_isa_bios_init 将 uefi bios最后128KB映射到isa space,具体做法跟leacy模式一样这里就不细讲了。

BIOS对相关数据访问

上面聊完了bios的加载,下面我们来聊聊qemu当中BIOS对相关数据访问实现。为了BIOS更加方便的访问相关的数据比如cpu个数,ACPI表,mem等信息qemu里面引入了fw_cfg 机制。简单的来说fw_cfg当中定义了一系列的key和value,然后bios通过port io来进行相关的访问。下面我们来看一下相关实现,具体函数为fw_cfg_arch_create ,在其实现当中主要做了下面几个事情:

  • 调用fw_cfg_init_io_dma 创建TYPE_FW_CFG_IO 设备和绑定相关port io。其中iobase 为0x511, dma_iobase 为0x515
FWCfgState *fw_cfg_init_io_dma(uint32_t iobase, uint32_t dma_iobase,
                                AddressSpace *dma_as)
{
    DeviceState *dev;
    SysBusDevice *sbd;
    FWCfgIoState *ios;
    FWCfgState *s;
    bool dma_requested = dma_iobase && dma_as;
    //创建TYPE_FW_CFG_IO设备
    dev = qdev_new(TYPE_FW_CFG_IO);
    if (!dma_requested) {
        qdev_prop_set_bit(dev, "dma_enabled", false);
    }

    object_property_add_child(OBJECT(qdev_get_machine()), TYPE_FW_CFG,
                              OBJECT(dev));

    sbd = SYS_BUS_DEVICE(dev);
    //调用dev的realized函数
    sysbus_realize_and_unref(sbd, &error_fatal);
    ios = FW_CFG_IO(dev);
    //将com_iomem与iobase绑定
    sysbus_add_io(sbd, iobase, &ios->comb_iomem);

    s = FW_CFG(dev);

    if (s->dma_enabled) {
        /* 64 bits for the address field */
        s->dma_as = dma_as;
        s->dma_addr = 0;
        //将dam_iomem与dma_iobase绑定
        sysbus_add_io(sbd, dma_iobase, &s->dma_iomem);
    }

    return s;
}

相关逻辑如上,这里重点讲一下sysbus_realize_and_unref ,它最终调用的函数为fw_cfg_io_realize,其核心逻辑如下

static void fw_cfg_io_realize(DeviceState *dev, Error **errp)
{
    ERRP_GUARD();
    FWCfgIoState *s = FW_CFG_IO(dev);

    fw_cfg_file_slots_allocate(FW_CFG(s), errp);
    if (*errp) {
        return;
    }

    /* when using port i/o, the 8-bit data register ALWAYS overlaps
     * with half of the 16-bit control register. Hence, the total size
     * of the i/o region used is FW_CFG_CTL_SIZE */
    memory_region_init_io(&s->comb_iomem, OBJECT(s), &fw_cfg_comb_mem_ops,
                          FW_CFG(s), "fwcfg", FW_CFG_CTL_SIZE);

    if (FW_CFG(s)->dma_enabled) {
        memory_region_init_io(&FW_CFG(s)->dma_iomem, OBJECT(s),
                              &fw_cfg_dma_mem_ops, FW_CFG(s), "fwcfg.dma",
                              sizeof(dma_addr_t));
    }

    fw_cfg_common_realize(dev, errp);
}

1) 调用memory_region_init_iocomb_iomemdma_iomem 相关联的mem_ops 进行初始化

2) 调用fw_cfg_common_realizefw_cfg 通用部分进行初始化,比如FW_CFG_UUIDFW_CFG_IDFW_CFG_BOOT_MENU 等。另外就是设置fw_cfg_machine_ready call back。

回到fw_cfg_arch_create 继续往下看,上面初始化完io之后开始调用fw_cfg_add_xxx的接口添加一些key和value。比较重要的有

FW_CFG_NB_CPUS FW_CFG_MAX_XPUS FW_CFG_ACPI_TABLES FW_CFG_E820_TABLE FW_CFG_NUMA

建议大家可以翻代码再仔细看一,这些信息在bios阶段需要读取的一些信息。

设备fw的处理

在qemu当中分为pci设备和非pci设备比如挂在isa bus上的isa device。我们先来看一下pci设备fw的加载,具体实现在pci_add_option_rom通常pci设备有一个专门的rom bar,设备的fw会被加载到这个rom bar里面。核心逻辑节选如下

    ......

    //初始化一段rom内存
    memory_region_init_rom(&pdev->rom, OBJECT(pdev), name, pdev->romsize, &error_fatal);
    ptr = memory_region_get_ram_ptr(&pdev->rom);
    //romfile加载到上面分配的内存当中
    if (load_image_size(path, ptr, size) < 0) {
        error_setg(errp, "failed to load romfile \"%s\"", pdev->romfile);
        g_free(path);
        return;
    }
    g_free(path);

    if (is_default_rom) {
        /* Only the default rom images will be patched (if needed). */
        //对相关romfile信息进行确认
        pci_patch_ids(pdev, ptr, size);
    }
    //注册rom bar 
    pci_register_bar(pdev, PCI_ROM_SLOT, 0, &pdev->rom);

    ......

非要pci设备主要是通过rom_add_vgarom_add_option 这两个rom操作函数。他们最终调用的函数都是rom_add_file 上面也有介绍,这里面再重点描述一下他的逻辑。

ssize_t rom_add_file(const char *file, const char *fw_dir,
                     hwaddr addr, int32_t bootindex,
                     bool option_rom, MemoryRegion *mr,
                     AddressSpace *as)

首先会把rom_file 数据读到rom->data 里面,然后执行rom_insert。接下来如果fw_dir不为空则通过fw_cfg_add_file 将rom数据添加到fw_cfg 当中。反之,如果fw_dir为空但mr不空则将mr赋给rom->mr。注意这个时候mrdata 还是没有关联起来,那到底是什么时候发生关联的呢?上面也有所提及,这里我们花点时间看一下这个问题。答案就是在rom_reset 的时候,我们先来看一下rom_reset核心逻辑

static void rom_reset(void *unused)
{
    Rom *rom;

    QTAILQ_FOREACH(rom, &roms, next) {
        if (rom->fw_file) {
            continue;
        }
        /*
         * We don't need to fill in the RAM with ROM data because we'll fill
         * the data in during the next incoming migration in all cases.  Note
         * that some of those RAMs can actually be modified by the guest.
         */
        if (runstate_check(RUN_STATE_INMIGRATE)) {
            if (rom->data && rom->isrom) {
                /*
                 * Free it so that a rom_reset after migration doesn't
                 * overwrite a potentially modified 'rom'.
                 */
                rom_free_data(rom);
            }
            continue;
        }

        if (rom->data == NULL) {
            continue;
        }
        if (rom->mr) {
            //如果已经指定了rom mr,则将数据copy到mr的内存区域
            void *host = memory_region_get_ram_ptr(rom->mr);
            memcpy(host, rom->data, rom->datasize);
            memset(host + rom->datasize, 0, rom->romsize - rom->datasize);
        } else {
            //通过地址找到相应的内存空间,比如上面的pc.bios
            address_space_write_rom(rom->as, rom->addr, MEMTXATTRS_UNSPECIFIED,rom->data, rom->datasize);

            address_space_set(rom->as, rom->addr + rom->datasize, 0,
                              rom->romsize - rom->datasize,
                              MEMTXATTRS_UNSPECIFIED);
        }
    ........
    }
}

其中rom_reset 调用流程为

qemu_system_wakeup->qemu_device_reset->rom_reset

注:rom_reset为注册在reset handler list上的一个callback函数

总结

基于上面所讲我们来总结一下,首先bios会被加载到一段rom的内存当中,这段read only的内存会被map到guest内存空间。另外 ,为了让bios在启动的过程当中更好的去访问某些数据,qemu当中实现了fw_cfg机制。最后,设备相关的rom file的处理方式也分为两种:一种是pci设备会有专门的rom bar,另一种是非pci设备则可以加载到内存当中也可以通过fw_cfg来读取。


Published

Category

articles

Tags