前言
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.bios
的ram
,同时通过alias的方式创建一个大小为128KB的isa-bios subregion,这样做的目的是为了将bios
最后的128KB的内容map到起始地址为0xe0000
的内存当中,最后将bios memregion
添加为rom_memory
的subregion
,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_io
对comb_iomem
和dma_iomem
相关联的mem_ops
进行初始化
2) 调用fw_cfg_common_realize
对fw_cfg
通用部分进行初始化,比如FW_CFG_UUID
,FW_CFG_ID
,FW_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_vga
和rom_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
。注意这个时候mr
跟data
还是没有关联起来,那到底是什么时候发生关联的呢?上面也有所提及,这里我们花点时间看一下这个问题。答案就是在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
来读取。