这部分代码的主要作用是维护一个保存了Unikraft boot信息的结构体ukplat_bootinfo
,其中最重要的是维护ukplat_bootinfo
结构体中的内存区域描述符链表(一个ukplat_memregion_list结构体)mrds
。mrds
的主体是一个由内存区域描述符结构体ukplat_memregion_desc
组成的链表,ukplat_memregion_desc
存储了unikraft内存区域的重要信息。
/** Unikraft boot info */
struct ukplat_bootinfo {
/** Magic value to recognize Unikraft boot info */
#define UKPLAT_BOOTINFO_MAGIC 0xb007b0b0 /* Boot Bobo */
__u32 magic;
/** Version of the boot info */
#define UKPLAT_BOOTINFO_VERSION 0x01
__u8 version;
__u8 _pad0[3];
/** Null-terminated boot loader identifier */
char bootloader[16];
/** Null-terminated boot protocol identifier */
char bootprotocol[16];
/** Address of the kernel command line. The string is not necessarily
* null-terminated.
*/
__u64 cmdline;
/** Size of the kernel command-line without the null terminator */
__u64 cmdline_len;
/** Address of the devicetree blob */
__u64 dtb;
/** Address of UEFI System Table */
__u64 efi_st;
/**
* List of memory regions. Must be the last member as the
* memory regions directly follow this boot information structure
*/
struct ukplat_memregion_list mrds;
} __packed __align(__SIZEOF_LONG__);
struct ukplat_memregion_list {
/** Maximum regions in the list */
__u32 capacity;
/** Current number of regions in the list */
__u32 count;
/** Array of memory regions */
struct ukplat_memregion_desc mrds[];
} __packed __align(__SIZEOF_LONG__);
/**
* Descriptor of a memory region
*/
struct ukplat_memregion_desc {
/** Physical base address */
__paddr_t pbase;
/** Virtual base address */
__vaddr_t vbase;
/** Length in bytes */
__sz len;
/** Memory region type (see UKPLAT_MEMRT_*) */
__u16 type;
/** Memory region flags (see UKPLAT_MEMRF_*) */
__u16 flags;
#ifdef CONFIG_UKPLAT_MEMRNAME
/** Region name */
char name[36];
#endif /* CONFIG_UKPLAT_MEMRNAME */
} __packed __align(__SIZEOF_LONG__);
接下来介绍修改ukplat_memregion_list
结构体的几个重要函数,它们位于"workdir/unikraft/plat/common/memory.c"和"workdir/unikraft/plat/common/include/uk/plat/common/memory.h"文件中:
- ukplat_bootinfo_get:获取存放在.uk_bootinfo setion的boot信息,返回一个ukplat_bootinfo指针。
- ukplat_memregion_list_insert:向ukplat_memregion_list中插入一个内存区域描述符(按照预设的规则自动寻找插入位置)。
- ukplat_memregion_list_insert_at_idx:向ukplat_memregion_list中指定的索引位置插入一个内存区域描述符。
- ukplat_memregion_list_delete:删除指定索引位置的内存区域描述符。
- ukplat_memregion_list_coalesce:由于上述对ukplat_memregion_list的插入操作不保证内存描述符指代的内存区域互不重叠(在源码中我们能看到),multiboot_entry这部分的代码在最后使用ukplat_memregion_list_coalesce函数合并重叠的内存区域。
multiboot_entry函数首先使用ukplat_bootinfo_get函数获取一个指向存放在.uk_bootinfo setion
的ukplat_bootinfo
结构体的指针bi
。bi
指向的结构体中一部分的字段的初始化工作在unikraft镜像构建的过程中完成了。通过全局搜索.uk_bootinfo
符号,我们可以找到"workdir/build/helloworld_qemu-x86_64.bootinfo.cmd"文件这样一条命令:
/home/ubuntu_wsl/unikraft_apps/helloworld/workdir/unikraft/support/scripts/mkbootinfo.py /home/ubuntu_wsl/unikraft_apps/helloworld/workdir/build/helloworld_qemu-x86_64 /home/ubuntu_wsl/unikraft_apps/helloworld/workdir/build/helloworld_qemu-x86_64.bootinfo -a "x86_64" && ""objcopy /home/ubuntu_wsl/unikraft_apps/helloworld/workdir/build/helloworld_qemu-x86_64 --update-section .uk_bootinfo=/home/ubuntu_wsl/unikraft_apps/helloworld/workdir/build/helloworld_qemu-x86_64.bootinfo
它在make构建的过程中被使用,通过查找make debug信息可以找到它:
printf ' %-7s %s\n' 'UKBI' 'helloworld_qemu-x86_64.bootinfo' && /bin/bash /home/ubuntu_wsl/unikraft_apps/helloworld/workdir/build/helloworld_qemu-x86_64.bootinfo.cmd
可以看到.uk_bootinfo
处的ukplat_bootinfo
结构体被mkbootinfo.py生成的helloworld_qemu-x86_64.bootinfo
中的内容替换(objcopy --update-section替换指定section的内容),打开mkbootinfo.py文件,可以看到生成相关字段的代码:
# The boot info is a struct ukplat_bootinfo
# (see plat/common/include/uk/plat/common/bootinfo.h) followed by a list of
# struct ukplat_memregion_desc (see include/uk/plat/memory.h).
with open(opt.output, 'wb') as secobj:
nsecs = 0
cap = bootsec_size
cap = cap - UKPLAT_BOOTINFO_SIZE
cap = cap // mrd_size
secobj.write(UKPLAT_BOOTINFO_MAGIC.to_bytes(4, endianness)) # magic
secobj.write(UKPLAT_BOOTINFO_VERSION.to_bytes(1, endianness)) # version
secobj.write(b'\0' * 3) # _pad0
secobj.write(b'\0' * 16) # bootloader
secobj.write(b'\0' * 16) # bootprotocol
secobj.write(b'\0' * 8) # cmdline
secobj.write(b'\0' * 8) # cmdline_len
secobj.write(b'\0' * 8) # dtb
secobj.write(b'\0' * 8) # efi_st
secobj.write(cap.to_bytes(4, endianness)) # mrds.capacity
secobj.write(b'\0' * 4) # mrds.count
然后向bi->mrds
中插入一些列内存区域描述符(再在此之前做一些重定向工作),包括以下几种:
- high memory(0xA0000UL到0xCFFFFUL)和BIOS只读的内存区域(0xE0000UL到0xFFFFFUL),通过
ukplat_memregion_list_insert_legacy_hi_mem
函数插入 - 参数
mi
中的cmdline和boot_loader_name字符串以及bi->bootprotocol
字符串的内存区域 - multiboot提供的的multiboot module和bios memory map
上述的插入操作通过mrd_insert
调用ukplat_memregion_list_insert
完成。
static inline int
ukplat_memregion_list_insert(struct ukplat_memregion_list *list,
const struct ukplat_memregion_desc *mrd)
{
struct ukplat_memregion_desc *p;
__u32 i;
if (unlikely(!mrd->len))
return -EINVAL;
if (unlikely(list->count == list->capacity))
return -ENOMEM;
/* We start from the tail as it is more likely that we add memory
* regions in semi-sorted order.
*/
p = &list->mrds[list->count];
for (i = list->count; i > 0; i--) {
--p;
if (p->pbase + p->len <= mrd->pbase) {
++p;
break;
}
}
// 将起始地址为p,长度为内存描述符表从p开始到结束的那部分大小的内存移动到起始地址为p + 1的位置
memmove(p + 1, p, sizeof(*p) * (list->count - i));
*p = *mrd;
list->count++;
return (int)i;
}
ukplat_memregion_list_insert
的插入规则是从后向前找到第一个内存区域终止边界(p->pbase + p->len)不大于要插入的内存区域起始边界的那个内存区域描述描述符,然后将后面的所有内存区域描述描述符向后挪一个位置(使用memmove实现),要插入的内存区域描述描述符放在它的后面。
ukplat_memregion_list_insert
插入操作只能保证第i+1个的内存区域描述符的起始地址不小于第i个内存区域描述符的终止地址,被插入的内存区域之间可能存在包含或者重叠关系,并且内存区域的访问权限可能不同。函数ukplat_memregion_list_coalesce
用于合并重叠或包含的内存区域描述符,它的合并规则总结如下:
- 内存区域按照类型划分为3个优先级:需要被保留的(reserved)内存区域优先级最高,类型为free的内存区域优先级最低,其他分配给unikraft的内存区域具有中等优先级。
- 内存区域先按照页面大小对齐再合并(优先级为free的内存区域可能会恢复到原来的大小)
- 相同优先级别的内存区域如果有重叠或相互包含则合并成区域
- 不同优先级别的内存区域如果有重叠则重叠的部分并入高优先级的内存区域,低优先级的内存区域将去掉重叠的部分
- 高优先级别的内存区域包含了低优先级的内存区域,删除低优先级的内存区域描述符
- 低优先级别的内存区域包含了高优先级的内存区域,怎加低优先级的内存区域描述符(原来的低优先级内存区域一分为三,去掉中间重复的部分)
最后进入将准备好的指向ukplat_bootinfo
结构体指针bi作为参数传入并执行_ukplat_entry
函数。
_ukplat_entry中需要做一些列必要组件以及可配置组件的初始化工作,这些包括:
- 初始化console控制台
- 初始化中断向量表
- 初始化中断请求控制器(IRQ)
- 初始化引导CPU的状态(这里在之前设置过滤,为了其他的配置选项?)
- 初始化存储命令行参数的全局变量cmdline和cmdline_len
- 为boot栈bstack分配内存
- 初始化内存,在memregion_list插入没有映射的地址区域的描述符
- smp初始化(可选配置项)
- 初始化syscall(可选配置项)
这个函数主要用于解析命令行参数,然后进入ukplat_entry
。
这里需要根据Unikraft的配置情况(配置CONFIG_LIBUKXXXX等变量),可选地启动Unikraft的一些组件,包括:
- 内存分配器
- 多线程以及任务调度
- IRQ子系统等
然后进入do_main函数。
这部分代码主要用于提供可选的POSIX环境,然后再执行main函数前构造一些其他的应用实例(application constructor),最后跳转进main函数。