0X01 设置BIOS
在将镜像加载到内存之后设置BIOS加载内核以及设置中断向量表。参考如下内存使用区域图
结合代码可以很清晰地看对应组建初始化
p = guest_flat_to_host(kvm, 0x00000400);
memset(p, 0, 0x000000ff);
p = guest_flat_to_host(kvm, 0x0009fc00);
memset(p, 0, 0x000003ff);
p = guest_flat_to_host(kvm, 0x000f0000);
memset(p, 0, 0x0000ffff);
p = guest_flat_to_host(kvm, 0x000c0000);
memset(p, 0, 0x00007fff);
p = guest_flat_to_host(kvm, 0x000f0000);
memcpy(p, bios_rom, bios_rom_size);
其中变量bios_rom需要提前编译,本次环境使用x86,因此需要编译x86/bios/entry.S
获取bios_rom以及bios_rom_size,通过Makefile之后同时获取编译完成后bios-rom.h中BIOS_OFFSET的各种值
0x02 e820设置
在BIOS探测万内存信息后,会将内存信息保存在BIOS的数据区。当bootloader或者OS通过软中断的方式向BIOS询问内存信息时,BIOS中的中断处理函数将从BIOS的数据去复制内存信息到调用者制定的位置。因此e820需要准备好内存段的信息。
e820 = guest_flat_to_host(kvm, E820_MAP_START);
mem_map = e820->map;
mem_map[i++] = (struct e820entry) {
.addr = REAL_MODE_IVT_BEGIN,
.size = EBDA_START - REAL_MODE_IVT_BEGIN,
.type = E820_RAM,
};
mem_map[i++] = (struct e820entry) {
.addr = EBDA_START,
.size = VGA_RAM_BEGIN - EBDA_START,
.type = E820_RESERVED,
};
mem_map[i++] = (struct e820entry) {
.addr = MB_BIOS_BEGIN,
.size = MB_BIOS_END - MB_BIOS_BEGIN,
.type = E820_RESERVED,
};
// 扩展内存区
if (kvm->ram_size < KVM_32BIT_GAP_START) {
mem_map[i++] = (struct e820entry) {
.addr = BZ_KERNEL_START,
.size = kvm->ram_size - BZ_KERNEL_START,
.type = E820_RAM,
};
} else {
mem_map[i++] = (struct e820entry) {
.addr = BZ_KERNEL_START,
.size = KVM_32BIT_GAP_START - BZ_KERNEL_START,
.type = E820_RAM,
};
mem_map[i++] = (struct e820entry) {
.addr = KVM_32BIT_MAX_MEM_SIZE,
.size = kvm->ram_size - KVM_32BIT_MAX_MEM_SIZE,
.type = E820_RAM,
};
}
if判断内存的大小是否比32位能够处理的内存更大,如果更大则需要创建5个内存段,否则只需要创建四个内存段。第1个内存段用于储存实模式的IVT(中断向量表);第2个内存段是传统的地段内存;第3个是BIOS,包括主板BIOS和显卡BIOS;最后面的即为扩展内存区,主要可用的内存都在这个段。扩展内存区的size减去的BZ_KERNEL_START
即为起始位置,值为0x100000UL
也就是1MB。
0x03 准备中断0x15的处理函数以及设置IVT
在对vga输出VESA初始化之后进行中断向量表的设置。当CPU执行指令int 0x15后,将进入中断处理流程,CPU将从IVT表中的第0x15项取出中断处理函数的地址,然后跳转到中断处理函数执行。这个中断处理函数由BIOS实现,存储在BIOS ROM中。在x86架构的32位系统中,系统启动后,主板的BIOS ROM将被映射到内存地址空间0x000F0000 ~ 0x000FFFFF处。也就是,中断发生后,最终是跳转到地址空间0x000F0000 ~ 0x000FFFFF中的中断0x15对应的处理函数处运行。因此也需要在Guest的0x000F0000 ~ 0x000FFFFF这段内存中准备好0x15号中断的中断处理函数,包括
- 在BIOS ROM中准备中断处理函数的实现
- 设置IVT中0x15号表项的地指向0x15号中断的处理函数
unsigned long address = MB_BIOS_BEGIN;
...
address = BIOS_IRQ_PA_ADDR(bios_intfake);
intr_desc = (struct real_intr_desc) {
.segment = REAL_SEGMENT(MB_BIOS_BEGIN),
.offset = address - MB_BIOS_BEGIN,
};
// 初始化存储在KVM的结构体
interrupt_table__setup(&kvm->arch.interrupt_table, &intr_desc);
for (i = 0; i < ARRAY_SIZE(bios_irq_handlers); i++)
setup_irq_handler(kvm, &bios_irq_handlers[i]);
// 将获取的中断地址存储在KVM结构体内
/* we almost done */
p = guest_flat_to_host(kvm, 0); // 代码[1]
interrupt_table__copy(&kvm->arch.interrupt_table, p, REAL_INTR_SIZE);
变量初始值MB_BIOS_BEGIN,是主板BIOS ROM映射在Guest内存地址空间中的起始位置。宏BIOS_IRQ_PA_ADDR即为BIOS ROm中确定0x15号中断的处理函数的起始存储位置,BIOS_OFFSET即为entry.S编译后给出的地址。具体代码如下
#define BIOS_IRQ_PA_ADDR(name) (MB_BIOS_BEGIN + BIOS_OFFSET__##name)
#define BIOS_IRQ_FUNC(name) ((char *)&bios_rom[BIOS_OFFSET__##name])
#define BIOS_IRQ_SIZE(name) (BIOS_ENTRY_SIZE(BIOS_OFFSET__##name))
static void setup_irq_handler(struct kvm *kvm, struct irq_handler *handler) {
// 创建临时的IVT表项
struct real_intr_desc intr_desc;
void *p;
p = guest_flat_to_host(kvm, handler->address);
memcpy(p, handler->handler, handler->size);
// 组织这个IVT表项
intr_desc = (struct real_intr_desc) {
.segment = REAL_SEGMENT(MB_BIOS_BEGIN),
.offset = handler->address - MB_BIOS_BEGIN,
};
DIE_IF((handler->address - MB_BIOS_BEGIN) > 0xffffUL);
// 记录到KVM数据结构中
interrupt_table__set(&kvm->arch.interrupt_table, &intr_desc, handler->irq);
}
setup_irq_handler
函数将0x15号中断的处理函数bios_int15的实现复制到这里,即BIOS ROM区域,address初始是Guest视角的GPA,因此需要转化为HVA。除了复制中断处理函数实现到BIOS ROM外,函数setup_irq_handler
还需要负责设置IVT表项。在组织好所有的表项之后调用函数interrupt_table__copy
将kvm->interrupt_table
中的IVT表项一次性地复制到IVT。因为IVT存储在物理内存0处,所以传给guest_flat_to_host的第二个参数为0 (代码[1])。
至此,KVM的BIOS所有设置完成
0XFF Reference
- Wiki real mode
- https://frankjkl.github.io/2019/03/12/Linux%E5%86%85%E6%A0%B8-%E5%BC%95%E5%AF%BC%E5%90%AF%E5%8A%A8/
- Inside the Linux Virtualization Principle and Implementation