From: Wei Zhang <zhangwei...@gmail.com> Currently GRUB boots linux with 32-bit protocol for 64 bit kernel. Thus if both GRUB and linux kernel are in 64-bit, we'll have to go through 64-bit grub -> 32-bit boot protocol -> 64-bit kernel transitions, and extra instructions have to be executed in the kernel.
Since linux has long ago supported 64-bit boot protocol, we can take advantage of that to directly boot to 64-bit kernel. To do this, first we determine whether the kernel is 64-bit by xloadflags (since linux boot protocol 2.12), then we build the identity-mapped page table required by the 64-bit kernel, and that's it. The memory needed by the page table is allocated after the protected kernel image proper. So if we're in 32-bit GRUB to boot a 64-bit kernel, the transition will happen before handing over to the kernel, and if we're in 64-bit GRUB, we don't have to go down to 32-bit and back to 64-bit. The 32-bit kernel boot process will not be affected. Tested on my 64-bit machine and QEMU. Signed-off-by: Wei Zhang <zhangwei...@126.com> --- grub-core/loader/i386/linux.c | 93 ++++++++++++++++++++++++++++++++--- include/grub/i386/linux.h | 2 +- 2 files changed, 86 insertions(+), 9 deletions(-) diff --git a/grub-core/loader/i386/linux.c b/grub-core/loader/i386/linux.c index c5984d4b2..376b72aba 100644 --- a/grub-core/loader/i386/linux.c +++ b/grub-core/loader/i386/linux.c @@ -63,12 +63,24 @@ GRUB_MOD_LICENSE ("GPLv3+"); #define ACCEPTS_PURE_TEXT 1 #endif +#define PG_P 0x001 +#define PG_RW 0x002 +#define PG_US 0x004 +#define PG_PS 0x080 +#define PG_G 0x100 +#define LINUX_KERNEL_64 0x0001 +#define LINUX_STARTUP_64 0x200 +#define LINUX_PGT_SIZE_64 (6 << 12) + static grub_dl_t my_mod; static grub_size_t linux_mem_size; static int loaded; static void *prot_mode_mem; static grub_addr_t prot_mode_target; +static int is_64bit = 0; +static grub_uint8_t *pgtable; +static grub_addr_t pgtable_target; static void *initrd_mem; static grub_addr_t initrd_mem_target; static grub_size_t prot_init_space; @@ -199,6 +211,11 @@ allocate_pages (grub_size_t prot_size, grub_size_t *align, goto fail; prot_mode_mem = get_virtual_current_address (ch); prot_mode_target = get_physical_target_address (ch); + if (is_64bit) + { + pgtable = (grub_uint8_t *)((grub_addr_t) prot_mode_mem + prot_size - LINUX_PGT_SIZE_64); + pgtable_target = prot_mode_target + prot_size - LINUX_PGT_SIZE_64; + } } grub_dprintf ("linux", "prot_mode_mem = %p, prot_mode_target = %lx, prot_size = %x\n", @@ -398,13 +415,52 @@ grub_linux_boot_mmap_fill (grub_uint64_t addr, grub_uint64_t size, return 0; } +/* Fill linux identity map page table. */ +static void +grub_fill_linux64_pgtable (void) +{ + grub_uint64_t *p4d, *pud, *pmd; + grub_addr_t pudt, pmdt; + int i; + + p4d = (grub_uint64_t *) pgtable; + pud = (grub_uint64_t *) (pgtable + 0x1000); + pmd = (grub_uint64_t *) (pgtable + 0x2000); + + pudt = pgtable_target + 0x1000; + pmdt = pgtable_target + 0x2000; + + grub_memset (pgtable, 0, LINUX_PGT_SIZE_64); + + /* First entry for the 3rd level page */ + p4d[0] = (grub_addr_t) (pudt); + p4d[0] |= (PG_P | PG_RW | PG_US); + + /* Map the whole 4G physical address */ + for (i = 0; i < 4; i++) + { + pud[i] = (grub_addr_t) (pmdt + i * 0x1000); + pud[i] |= (PG_P | PG_RW | PG_US); + } + + /* Identity map with 2MB pages */ + for (i = 0; i < 2048; i++) + { + pmd[i] = (grub_addr_t) (i) * 0x200000; + pmd[i] |= (PG_P | PG_RW | PG_PS | PG_G); + } +} + static grub_err_t grub_linux_boot (void) { grub_err_t err = 0; const char *modevar; char *tmp; - struct grub_relocator32_state state; + union { + struct grub_relocator32_state _32; + struct grub_relocator64_state _64; + } state; void *real_mode_mem; struct grub_linux_boot_ctx ctx = { .real_mode_target = 0 @@ -624,13 +680,24 @@ grub_linux_boot (void) } #endif - /* FIXME. */ - /* asm volatile ("lidt %0" : : "m" (idt_desc)); */ - state.ebp = state.edi = state.ebx = 0; - state.esi = ctx.real_mode_target; - state.esp = ctx.real_mode_target; - state.eip = ctx.params->code32_start; - return grub_relocator32_boot (relocator, state, 0); + if (is_64bit) + { + state._64.rsi = ctx.real_mode_target; + state._64.rip = ctx.params->code32_start + LINUX_STARTUP_64; + grub_fill_linux64_pgtable(); + state._64.cr3 = (grub_addr_t) pgtable_target; + return grub_relocator64_boot (relocator, state._64, 0x1000, 0x9a000); + } + else + { + /* FIXME. */ + /* asm volatile ("lidt %0" : : "m" (idt_desc)); */ + state._32.ebp = state._32.edi = state._32.ebx = 0; + state._32.esi = ctx.real_mode_target; + state._32.esp = ctx.real_mode_target; + state._32.eip = ctx.params->code32_start; + return grub_relocator32_boot (relocator, state._32, 0); + } } static grub_err_t @@ -748,6 +815,16 @@ grub_cmd_linux (grub_command_t cmd __attribute__ ((unused)), { min_align = lh.min_alignment; prot_size = grub_le_to_cpu32 (lh.init_size); + if (grub_le_to_cpu16 (lh.version) >= 0x020c + && (lh.xloadflags & LINUX_KERNEL_64)) + { + is_64bit = 1; + prot_size += LINUX_PGT_SIZE_64; + grub_dprintf ("linux", "using 64-bit boot protocol, " + "extra %d bytes will be allocated\n", LINUX_PGT_SIZE_64); + } + else + is_64bit = 0; prot_init_space = page_align (prot_size); if (relocatable) preferred_address = grub_le_to_cpu64 (lh.pref_address); diff --git a/include/grub/i386/linux.h b/include/grub/i386/linux.h index 0fd6e1212..9b511eb21 100644 --- a/include/grub/i386/linux.h +++ b/include/grub/i386/linux.h @@ -138,7 +138,7 @@ struct linux_i386_kernel_header grub_uint32_t kernel_alignment; grub_uint8_t relocatable; grub_uint8_t min_alignment; - grub_uint8_t pad[2]; + grub_uint16_t xloadflags; grub_uint32_t cmdline_size; grub_uint32_t hardware_subarch; grub_uint64_t hardware_subarch_data; -- 2.34.1 _______________________________________________ Grub-devel mailing list Grub-devel@gnu.org https://lists.gnu.org/mailman/listinfo/grub-devel