Newer Linux kernel supports passing boot params without a devicetree. In this case, the bootloader should register a Linux-specific EFI configuration table, which contains memory mapping information or initrd address. This is the only option on some new platforms like LoongArch, and is necessary to allow booting Linux kernel without a DTB on aarch64/riscv64. Signed-off-by: Yao Zi <ziyao@disroot.org>
411 lines
13 KiB
C
411 lines
13 KiB
C
#if defined(__riscv) || defined(__aarch64__)
|
|
|
|
#include <stdint.h>
|
|
#include <stddef.h>
|
|
#include <stdnoreturn.h>
|
|
#include <protos/linux.h>
|
|
#include <fs/file.h>
|
|
#include <lib/libc.h>
|
|
#include <lib/misc.h>
|
|
#include <lib/term.h>
|
|
#include <lib/config.h>
|
|
#include <lib/print.h>
|
|
#include <lib/uri.h>
|
|
#include <mm/pmm.h>
|
|
#include <sys/idt.h>
|
|
#include <lib/fb.h>
|
|
#include <lib/acpi.h>
|
|
#include <lib/fdt.h>
|
|
#include <libfdt/libfdt.h>
|
|
|
|
// The following definitions and struct were copied and adapted from Linux
|
|
// kernel headers released under GPL-2.0 WITH Linux-syscall-note
|
|
// allowing their inclusion in non GPL compliant code.
|
|
|
|
struct linux_header {
|
|
uint32_t code0;
|
|
uint32_t code1;
|
|
uint64_t text_offset;
|
|
uint64_t image_size;
|
|
uint64_t flags;
|
|
uint32_t version;
|
|
uint32_t res1;
|
|
uint64_t res2;
|
|
uint64_t res3; // originally 'magic' field, deprecated
|
|
uint32_t magic2;
|
|
uint32_t res4;
|
|
} __attribute__((packed));
|
|
|
|
struct linux_efi_memreserve {
|
|
int size;
|
|
int count;
|
|
uint64_t next;
|
|
};
|
|
|
|
struct linux_efi_boot_memmap {
|
|
UINTN map_size;
|
|
UINTN desc_size;
|
|
uint32_t desc_ver;
|
|
UINTN map_key;
|
|
UINTN buff_size;
|
|
EFI_MEMORY_DESCRIPTOR descs[];
|
|
};
|
|
|
|
struct linux_efi_initrd {
|
|
UINTN base;
|
|
UINTN size;
|
|
};
|
|
|
|
// End of Linux code
|
|
|
|
struct boot_param {
|
|
void *kernel_base;
|
|
size_t kernel_size;
|
|
void *module_base;
|
|
size_t module_size;
|
|
char *cmdline;
|
|
void *dtb;
|
|
struct linux_efi_boot_memmap *memmap;
|
|
};
|
|
|
|
#if defined(__riscv)
|
|
#define LINUX_HEADER_MAGIC2 0x05435352
|
|
#define LINUX_HEADER_MAJOR_VER(ver) (((ver) >> 16) & 0xffff)
|
|
#define LINUX_HEADER_MINOR_VER(ver) (((ver) >> 0) & 0xffff)
|
|
#elif defined(__aarch64__)
|
|
#define LINUX_HEADER_MAGIC2 0x644d5241
|
|
#endif
|
|
|
|
const char *verify_kernel(struct boot_param *p) {
|
|
struct linux_header *header = p->kernel_base;
|
|
|
|
if (header->magic2 != LINUX_HEADER_MAGIC2) {
|
|
return "kernel header magic does not match";
|
|
}
|
|
|
|
// riscv-specific version requirements
|
|
#if defined(__riscv)
|
|
printv("linux: boot protocol version %d.%d\n",
|
|
LINUX_HEADER_MAJOR_VER(header->version),
|
|
LINUX_HEADER_MINOR_VER(header->version));
|
|
if (LINUX_HEADER_MAJOR_VER(header->version) == 0
|
|
&& LINUX_HEADER_MINOR_VER(header->version) < 2) {
|
|
return "linux: protocols < 0.2 are not supported";
|
|
}
|
|
#endif
|
|
|
|
return NULL;
|
|
}
|
|
|
|
void load_files(struct boot_param *p, char *config) {
|
|
char *dtb_path = config_get_value(config, 0, "DTB_PATH");
|
|
|
|
if (dtb_path) {
|
|
struct file_handle *dtb_file;
|
|
if ((dtb_file = uri_open(dtb_path)) == NULL)
|
|
panic(true, "linux: Failed to open device tree blob with path `%#`. Is the path correct?", dtb_path);
|
|
|
|
p->dtb = freadall(dtb_file, MEMMAP_BOOTLOADER_RECLAIMABLE);
|
|
fclose(dtb_file);
|
|
}
|
|
|
|
char *module_path = config_get_value(config, 0, "MODULE_PATH");
|
|
if (module_path) {
|
|
print("linux: Loading module `%#`...\n", module_path);
|
|
|
|
struct file_handle *module_file = uri_open(module_path);
|
|
if (!module_file) {
|
|
panic(true, "linux: failed to open module `%s`. Is the path correct?", module_path);
|
|
}
|
|
|
|
p->module_size = module_file->size;
|
|
p->module_base = ext_mem_alloc_type_aligned(
|
|
ALIGN_UP(p->module_size, 4096),
|
|
MEMMAP_KERNEL_AND_MODULES, 4096);
|
|
|
|
fread(module_file, p->module_base, 0, p->module_size);
|
|
fclose(module_file);
|
|
printv("linux: loaded module `%s` at %x, size %u\n", module_path, p->module_base, p->module_size);
|
|
}
|
|
}
|
|
|
|
void prepare_device_tree_blob(struct boot_param *p) {
|
|
void *dtb = p->dtb;
|
|
int ret;
|
|
|
|
// Delete all /memory@... nodes. Linux will use the given UEFI memory map
|
|
// instead.
|
|
while (true) {
|
|
int offset = fdt_subnode_offset_namelen(dtb, 0, "memory@", 7);
|
|
|
|
if (offset == -FDT_ERR_NOTFOUND) {
|
|
break;
|
|
}
|
|
|
|
if (offset < 0) {
|
|
panic(true, "linux: failed to find node: '%s'", fdt_strerror(offset));
|
|
}
|
|
|
|
ret = fdt_del_node(dtb, offset);
|
|
if (ret < 0) {
|
|
panic(true, "linux: failed to delete memory node: '%s'", fdt_strerror(ret));
|
|
}
|
|
}
|
|
|
|
if (p->module_base) {
|
|
ret = fdt_set_chosen_uint64(dtb, "linux,initrd-start", (uint64_t)p->module_base);
|
|
if (ret < 0) {
|
|
panic(true, "linux: cannot set initrd parameter: '%s'", fdt_strerror(ret));
|
|
}
|
|
|
|
ret = fdt_set_chosen_uint64(dtb, "linux,initrd-end", (uint64_t)(p->module_base + p->module_size));
|
|
if (ret < 0) {
|
|
panic(true, "linux: cannot set initrd parameter: '%s'", fdt_strerror(ret));
|
|
}
|
|
}
|
|
|
|
// Set the kernel command line arguments.
|
|
ret = fdt_set_chosen_string(dtb, "bootargs", p->cmdline);
|
|
if (ret < 0) {
|
|
panic(true, "linux: failed to set bootargs: '%s'", fdt_strerror(ret));
|
|
}
|
|
|
|
// Tell Linux about the UEFI memory map and system table.
|
|
ret = fdt_set_chosen_uint64(dtb, "linux,uefi-system-table", (uint64_t)gST);
|
|
if (ret < 0) {
|
|
panic(true, "linux: failed to set UEFI system table pointer: '%s'", fdt_strerror(ret));
|
|
}
|
|
|
|
// This property is not required by mainline Linux, but is required by
|
|
// Debian (and derivative) kernels, because Debian has a patch that adds
|
|
// this flag, and the existing logic that deals with it will just outright
|
|
// fail if any of the properties is missing. We don't care about Debian's
|
|
// hardening or whatever, so just always report that secure boot is off.
|
|
ret = fdt_set_chosen_uint32(dtb, "linux,uefi-secure-boot", 0);
|
|
if (ret < 0) {
|
|
panic(true, "linux: failed to set UEFI secure boot state: '%s'", fdt_strerror(ret));
|
|
}
|
|
}
|
|
|
|
void add_framebuffer(struct fb_info *fb) {
|
|
struct screen_info *screen_info = ext_mem_alloc(sizeof(struct screen_info));
|
|
|
|
screen_info->capabilities = VIDEO_CAPABILITY_64BIT_BASE | VIDEO_CAPABILITY_SKIP_QUIRKS;
|
|
screen_info->flags = VIDEO_FLAGS_NOCURSOR;
|
|
screen_info->lfb_base = (uint32_t)fb->framebuffer_addr;
|
|
screen_info->ext_lfb_base = (uint32_t)(fb->framebuffer_addr >> 32);
|
|
screen_info->lfb_size = fb->framebuffer_pitch * fb->framebuffer_height;
|
|
screen_info->lfb_width = fb->framebuffer_width;
|
|
screen_info->lfb_height = fb->framebuffer_height;
|
|
screen_info->lfb_depth = fb->framebuffer_bpp;
|
|
screen_info->lfb_linelength = fb->framebuffer_pitch;
|
|
screen_info->red_size = fb->red_mask_size;
|
|
screen_info->red_pos = fb->red_mask_shift;
|
|
screen_info->green_size = fb->green_mask_size;
|
|
screen_info->green_pos = fb->green_mask_shift;
|
|
screen_info->blue_size = fb->blue_mask_size;
|
|
screen_info->blue_pos = fb->blue_mask_shift;
|
|
|
|
screen_info->orig_video_isVGA = VIDEO_TYPE_EFI;
|
|
|
|
EFI_GUID screen_info_table_guid = {0xe03fc20a, 0x85dc, 0x406e, {0xb9, 0x0e, 0x4a, 0xb5, 0x02, 0x37, 0x1d, 0x95}};
|
|
EFI_STATUS ret = gBS->InstallConfigurationTable(&screen_info_table_guid, screen_info);
|
|
|
|
if (ret != EFI_SUCCESS) {
|
|
panic(true, "linux: failed to install screen info configuration table: '%x'", ret);
|
|
}
|
|
}
|
|
|
|
void prepare_efi_tables(struct boot_param *p, char *config) {
|
|
(void)p;
|
|
EFI_STATUS ret = 0;
|
|
|
|
{
|
|
size_t req_width = 0, req_height = 0, req_bpp = 0;
|
|
|
|
char *resolution = config_get_value(config, 0, "RESOLUTION");
|
|
if (resolution != NULL) {
|
|
parse_resolution(&req_width, &req_height, &req_bpp, resolution);
|
|
}
|
|
|
|
struct fb_info *fbs;
|
|
size_t fbs_count;
|
|
|
|
term_notready();
|
|
|
|
fb_init(&fbs, &fbs_count, req_width, req_height, req_bpp);
|
|
|
|
// TODO(qookie): Let the user pick a framebuffer if there's > 1
|
|
if (fbs_count > 0) {
|
|
add_framebuffer(&fbs[0]);
|
|
}
|
|
}
|
|
|
|
|
|
{
|
|
struct linux_efi_memreserve *rsv = ext_mem_alloc(sizeof(struct linux_efi_memreserve));
|
|
|
|
rsv->size = 0;
|
|
rsv->count = 0;
|
|
rsv->next = 0;
|
|
|
|
EFI_GUID memreserve_table_guid = {0x888eb0c6, 0x8ede, 0x4ff5, {0xa8, 0xf0, 0x9a, 0xee, 0x5c, 0xb9, 0x77, 0xc2}};
|
|
ret = gBS->InstallConfigurationTable(&memreserve_table_guid, rsv);
|
|
|
|
if (ret != EFI_SUCCESS) {
|
|
panic(true, "linux: failed to install memory reservation configuration table: '%x'", ret);
|
|
}
|
|
}
|
|
|
|
if (p->module_base) {
|
|
struct linux_efi_initrd *initrd_table;
|
|
|
|
ret = gBS->AllocatePool(EfiLoaderData, sizeof(*initrd_table), (void **)&initrd_table);
|
|
if (ret != EFI_SUCCESS) {
|
|
panic(true, "linux: failed to allocate Linux initrd table");
|
|
}
|
|
|
|
initrd_table->base = (UINTN)p->module_base;
|
|
initrd_table->size = p->module_size;
|
|
|
|
EFI_GUID initrd_table_guid = { 0x5568e427, 0x68fc, 0x4f3d, { 0xac, 0x74, 0xca, 0x55, 0x52, 0x31, 0xcc, 0x68}};
|
|
ret = gBS->InstallConfigurationTable(&initrd_table_guid, initrd_table);
|
|
if (ret != EFI_SUCCESS) {
|
|
panic(true, "linux: failed to install initrd\n");
|
|
}
|
|
}
|
|
|
|
{
|
|
EFI_MEMORY_DESCRIPTOR tmp_mmap[1];
|
|
size_t mmap_size = sizeof(tmp_mmap);
|
|
UINTN mmap_key = 0;
|
|
|
|
gBS->GetMemoryMap(&efi_mmap_size, tmp_mmap, &mmap_key, &efi_desc_size, &efi_desc_ver);
|
|
mmap_size += 4096 + sizeof(struct linux_efi_boot_memmap);
|
|
|
|
ret = gBS->AllocatePool(EfiLoaderData, efi_mmap_size, (void **)&p->memmap);
|
|
if (ret != EFI_SUCCESS) {
|
|
panic(true, "linux: failed to allocate UEFI memory map");
|
|
}
|
|
|
|
p->memmap->buff_size = mmap_size;
|
|
|
|
EFI_GUID memmap_table_guid = { 0x800f683f, 0xd08b, 0x423a, { 0xa2, 0x93, 0x96, 0x5c, 0x3c, 0x6f, 0xe2, 0xb4}};
|
|
ret = gBS->InstallConfigurationTable(&memmap_table_guid, p->memmap);
|
|
if (ret != EFI_SUCCESS) {
|
|
panic(true, "linux: failed to install UEFI memory map");
|
|
}
|
|
}
|
|
|
|
efi_exit_boot_services();
|
|
}
|
|
|
|
void prepare_mmap(struct boot_param *p) {
|
|
{
|
|
void *dtb = p->dtb;
|
|
int ret = fdt_set_chosen_uint64(dtb, "linux,uefi-mmap-start", (uint64_t)efi_mmap);
|
|
if (ret < 0) {
|
|
panic(true, "linux: failed to set UEFI memory map pointer: '%s'", fdt_strerror(ret));
|
|
}
|
|
|
|
ret = fdt_set_chosen_uint32(dtb, "linux,uefi-mmap-size", efi_mmap_size);
|
|
if (ret < 0) {
|
|
panic(true, "linux: failed to set UEFI memory map size: '%s'", fdt_strerror(ret));
|
|
}
|
|
|
|
ret = fdt_set_chosen_uint32(dtb, "linux,uefi-mmap-desc-size", efi_desc_size);
|
|
if (ret < 0) {
|
|
panic(true, "linux: failed to set UEFI memory map descriptor size: '%s'", fdt_strerror(ret));
|
|
}
|
|
|
|
ret = fdt_set_chosen_uint32(dtb, "linux,uefi-mmap-desc-ver", efi_desc_ver);
|
|
if (ret < 0) {
|
|
panic(true, "linux: failed to set UEFI memory map descriptor version: '%s'", fdt_strerror(ret));
|
|
}
|
|
}
|
|
|
|
p->memmap->map_size = efi_mmap_size;
|
|
p->memmap->desc_size = efi_desc_size;
|
|
p->memmap->desc_ver = efi_desc_ver;
|
|
p->memmap->map_key = efi_mmap_key;
|
|
|
|
size_t efi_mmap_entry_count = efi_mmap_size / efi_desc_size;
|
|
for (size_t i = 0; i < efi_mmap_entry_count; i++) {
|
|
EFI_MEMORY_DESCRIPTOR *entry = (void *)efi_mmap + i * efi_desc_size;
|
|
|
|
if (entry->Attribute & EFI_MEMORY_RUNTIME) {
|
|
entry->VirtualStart = entry->PhysicalStart;
|
|
}
|
|
}
|
|
|
|
memcpy(&p->memmap->descs, efi_mmap, efi_mmap_size);
|
|
|
|
EFI_STATUS status = gRT->SetVirtualAddressMap(efi_mmap_size, efi_desc_size, efi_desc_ver, efi_mmap);
|
|
if (status != EFI_SUCCESS) {
|
|
panic(false, "linux: failed to set UEFI virtual address map: '%x'", status);
|
|
}
|
|
}
|
|
|
|
noreturn void jump_to_kernel(struct boot_param *p) {
|
|
#if defined(__riscv)
|
|
printv("linux: bsp hart %d, device tree blob at %x\n", bsp_hartid, p->dtb);
|
|
|
|
void (*kernel_entry)(uint64_t hartid, uint64_t dtb) = p->kernel_base;
|
|
asm ("csrci sstatus, 0x2\n\t"
|
|
"csrw sie, zero\n\t");
|
|
kernel_entry(bsp_hartid, (uint64_t)p->dtb);
|
|
#elif defined(__aarch64__)
|
|
printv("linux: device tree blob at %x\n", p->dtb);
|
|
|
|
void (*kernel_entry)(uint64_t dtb, uint64_t res0, uint64_t res1, uint64_t res2) = p->kernel_base;
|
|
asm ("msr daifset, 0xF");
|
|
kernel_entry((uint64_t)p->dtb, 0, 0, 0);
|
|
#endif
|
|
__builtin_unreachable();
|
|
}
|
|
|
|
noreturn void linux_load(char *config, char *cmdline) {
|
|
struct boot_param p;
|
|
memset(&p, 0, sizeof(p));
|
|
p.cmdline = cmdline;
|
|
// Hopefully 4K should be enough (mainly depends on the length of cmdline)
|
|
p.dtb = get_device_tree_blob(0x1000);
|
|
|
|
struct file_handle *kernel_file;
|
|
|
|
char *kernel_path = config_get_value(config, 0, "KERNEL_PATH");
|
|
if (kernel_path == NULL) {
|
|
panic(true, "linux: KERNEL_PATH not specified");
|
|
}
|
|
|
|
print("linux: Loading kernel `%#`...\n", kernel_path);
|
|
|
|
if ((kernel_file = uri_open(kernel_path)) == NULL) {
|
|
panic(true, "linux: failed to open kernel `%s`. Is the path correct?", kernel_path);
|
|
}
|
|
|
|
p.kernel_size = kernel_file->size;
|
|
p.kernel_base = ext_mem_alloc_type_aligned(
|
|
ALIGN_UP(p.kernel_size, 4096),
|
|
MEMMAP_KERNEL_AND_MODULES, 2 * 1024 * 1024);
|
|
fread(kernel_file, p.kernel_base, 0, p.kernel_size);
|
|
fclose(kernel_file);
|
|
printv("linux: loaded kernel `%s` at %x, size %u\n", kernel_path, p.kernel_base, p.kernel_size);
|
|
|
|
const char *reason = verify_kernel(&p);
|
|
if (reason)
|
|
panic(true, "linux: invalid kernel image: %s", reason);
|
|
|
|
load_files(&p, config);
|
|
|
|
prepare_device_tree_blob(&p);
|
|
|
|
prepare_efi_tables(&p, config);
|
|
|
|
prepare_mmap(&p);
|
|
|
|
jump_to_kernel(&p);
|
|
}
|
|
|
|
#endif
|