efi/x86: add support for generic EFI mixed mode boot
Ard Biesheuvel
maintainercover efi/x86: add support for generic EFI mixed mode boot
This series is another part of my effort to reduce the level of knowledge on the part of the bootloader or firmware of internal per-architecture details regarding where/how the kernel is loaded and where its initrd and other context data are passed.
The x86 architecture has a so-called 'EFI handover protocol', which defines how the bootparams struct should be populated, and how it should be interpreted to figure out where to load the kernel, and at which offset in the binary the entrypoint is located. This scheme allows the initrd to be loaded beforehand, and allows 32-bit firmware to invoke a 64-bit kernel via a special entrypoint that manages the state transitions between the two execution modes.
Due to this, x86 loaders currently do not rely on LoadImage and StartImage, and therefore, are forced to re-implement things like image authentication for secure boot and taking the measurements for measured boot in their open coded clones of these routines.
My previous series on this topic [0] implements a generic way to load the initrd from any source supported by the loader without relying on something like device trees or bootparams structures, and so native boot should not need the EFI handover protocol anymore after those change are merged.
What remains is mixed mode boot, which also needs the EFI handover protocol regardless of whether an initrd is loaded or not. So let's get rid of that requirement, and take advantage of the fact that EDK2 based firmware does support LoadImage() for X64 binaries on IA32 firmware, which means we can rely on the secure boot and measured boot checks being performed by the firmware. The only thing we need to put on top is a way to discover the non-native entrypoint into the binary in a way that does not rely on x86 specific headers and data structures.
So let's introduce a new .compat header in the PE/COFF metadata of the bzImage, and populate it with a <machine type, entrypoint> tuple, allowing a generic EFI loader to decide whether the entrypoint supports its native machine type, and invoke it as an ordinary EFI application entrypoint. Since we will not be passing a bootparams structure, we need to discover the base of the image (which contains the setup header) via the loaded image protocol before we can enter the kernel in 32-bit mode at startup_32()
A loader implementation for OVMF can be found at [1]. Note that this loader code is fully generic, and could be used without modifications if other architectures ever emerge that support kernels that can be invoked from a non-native (but cross-type supported) loader.
[0] https://lore.kernel.org/linux-arm-kernel/[email protected]/
[1] https://github.com/ardbiesheuvel/edk2/commits/linux-efi-generic
Ard Biesheuvel (3):
efi/x86: drop redundant .bss section
efi/x86: add true mixed mode entry point into .compat section efi/x86: implement mixed mode boot without the handover protocol
arch/x86/boot/Makefile | 2 +- arch/x86/boot/compressed/head_64.S | 61 +++++++++++++++++- arch/x86/boot/header.S | 23 ++++--- arch/x86/boot/tools/build.c | 67 +++++++++++++------- 4 files changed, 115 insertions(+), 38 deletions(-)
Signature · 1 line
2.17.1
Ard Biesheuvel
maintainerRFC PATCH 1/3 efi/x86: drop redundant .bss section
In commit c7fb93ec51d462ec ("x86/efi: Include a .bss section within the PE/COFF headers"), we added a separate .bss section to the PE/COFF header of the compressed kernel describing the static memory footprint of the decompressor, to ensure that it has enough headroom to decompress itself.
We can achieve the exact same result by increasing the virtual size of the .text section, without changing the raw size, which, as per the PE/COFF specification, requires the loader to zero initialize the delta.
Doing so frees up a slot in the section table, which we will use later to describe the mixed mode entrypoint.
--- arch/x86/boot/header.S | 21 +----------- arch/x86/boot/tools/build.c | 35 ++++++++------------ 2 files changed, 14 insertions(+), 42 deletions(-)
arch/x86/boot/header.S +1 -20
@@ -106,7 +106,7 @@ coff_header: #else .word 0x8664 # x86-64 #endif - .word 4 # nr_sections + .word 3 # nr_sections .long 0 # TimeDateStamp .long 0 # PointerToSymbolTable .long 1 # NumberOfSymbols
@@ -248,25 +248,6 @@ section_table: .word 0 # NumberOfLineNumbers .long 0x60500020 # Characteristics (section flags) - # - # The offset & size fields are filled in by build.c. - # - .ascii ".bss" - .byte 0 - .byte 0 - .byte 0 - .byte 0 - .long 0 - .long 0x0 - .long 0 # Size of initialized data - # on disk - .long 0x0 - .long 0 # PointerToRelocations - .long 0 # PointerToLineNumbers - .word 0 # NumberOfRelocations - .word 0 # NumberOfLineNumbers - .long 0xc8000080 # Characteristics (section flags) - #endif /* CONFIG_EFI_STUB */ # Kernel attributes; used by setup. This is part 1 of the
arch/x86/boot/tools/build.c +13 -22
@@ -203,10 +203,12 @@ static void update_pecoff_setup_and_reloc(unsigned int size)put_unaligned_le32 (10 , &buf[reloc_offset +4 ]); } -static void update_pecoff_text (unsigned int text_start,unsigned int file_sz) +static void update_pecoff_text (unsigned int text_start,unsigned int file_sz, +unsigned int init_sz) {unsigned int pe_header;unsigned int text_sz = file_sz - text_start; +unsigned int bss_sz = init_sz - file_sz; pe_header =get_unaligned_le32 (&buf[0x3c ]);
@@ -216,28 +218,19 @@ static void update_pecoff_text(unsigned int text_start, unsigned int file_sz)*/ put_unaligned_le32 (file_sz -512 , &buf[pe_header +0x1c ]); -/* -* Address of entry point for PE/COFF executable -*/ -put_unaligned_le32 (text_start + efi_pe_entry, &buf[pe_header +0x28 ]); - -update_pecoff_section_header (".text" , text_start, text_sz); -} - -static void update_pecoff_bss (unsigned int file_sz,unsigned int init_sz) -{ -unsigned int pe_header; -unsigned int bss_sz = init_sz - file_sz; - - pe_header =get_unaligned_le32 (&buf[0x3c ]); -/* Size of uninitialized data */ put_unaligned_le32 (bss_sz, &buf[pe_header +0x24 ]);/* Size of image */ put_unaligned_le32 (init_sz, &buf[pe_header +0x50 ]); -update_pecoff_section_header_fields (".bss" , file_sz, bss_sz,0 ,0 ); +/* +* Address of entry point for PE/COFF executable +*/ +put_unaligned_le32 (text_start + efi_pe_entry, &buf[pe_header +0x28 ]); + +update_pecoff_section_header_fields (".text" , text_start, text_sz + bss_sz, + text_sz, text_start); }static int reserve_pecoff_reloc_section (int c)
@@ -278,9 +271,8 @@ static void efi_stub_entry_update(void)static inline void update_pecoff_setup_and_reloc (unsigned int size) {}static inline void update_pecoff_text (unsigned int text_start, -unsigned int file_sz) {} -static inline void update_pecoff_bss (unsigned int file_sz, -unsigned int init_sz) {} +unsigned int file_sz, +unsigned int init_sz) {}static inline void efi_stub_defaults (void ) {}static inline void efi_stub_entry_update (void ) {}
@@ -406,9 +398,8 @@ int main(int argc, char ** argv) buf[0x1f1 ] = setup_sectors-1 ;put_unaligned_le32 (sys_size, &buf[0x1f4 ]); -update_pecoff_text (setup_sectors *512 , i + (sys_size *16 )); init_sz =get_unaligned_le32 (&buf[0x260 ]); -update_pecoff_bss (i + (sys_size *16 ), init_sz); +update_pecoff_text (setup_sectors *512 , i + (sys_size *16 ), init_sz);efi_stub_entry_update ();
Signature · 1 line
2.17.1
Ard Biesheuvel
maintainerRFC PATCH 2/3 efi/x86: add true mixed mode entry point into .compat section
In reply to Ard BiesheuvelCurrently, mixed mode is closely tied to the EFI handover protocol and relies on intimate knowledge of the bootparams structure, setup header etc, all of which are rather byzantine and entirely specific to x86.
Even though no other EFI supported architectures are currently known that could support something like mixed mode, it makes sense to abstract a bit from this, and make it part of a generic Linux on EFI boot protocol.
To that end, add a .compat section to the mixed mode binary, and populate it with the PE machine type and entry point address, allowing firmware implementations to match it to their native machine type, and invoke non-native binaries using a secondary entry point.
--- arch/x86/boot/Makefile | 2 +- arch/x86/boot/header.S | 20 +++++++++++- arch/x86/boot/tools/build.c | 32 +++++++++++++++++++- 3 files changed, 51 insertions(+), 3 deletions(-)
arch/x86/boot/Makefile +1 -1
@@ -88,7 +88,7 @@ $(obj)/vmlinux.bin: $(obj)/compressed/vmlinux FORCE SETUP_OBJS = $(addprefix $(obj)/,$(setup-y)) -sed-zoffset := -e 's/^\([0-9a-fA-F]*\) [a-zA-Z] \(startup_32\|startup_64\|efi32_stub_entry\|efi64_stub_entry\|efi_pe_entry\|input_data\|kernel_info\|_end\|_ehead\|_text\|z_.*\)$$/\#define ZO_\2 0x\1/p' +sed-zoffset := -e 's/^\([0-9a-fA-F]*\) [a-zA-Z] \(startup_32\|startup_64\|efi32_stub_entry\|efi64_stub_entry\|efi_pe_entry\|efi32_pe_entry\|input_data\|kernel_info\|_end\|_ehead\|_text\|z_.*\)$$/\#define ZO_\2 0x\1/p' quiet_cmd_zoffset = ZOFFSET $@ cmd_zoffset = $(NM) $< | sed -n $(sed-zoffset) > $@
arch/x86/boot/header.S +19 -1
@@ -106,7 +106,7 @@ coff_header: #else .word 0x8664 # x86-64 #endif - .word 3 # nr_sections + .word section_count # nr_sections .long 0 # TimeDateStamp .long 0 # PointerToSymbolTable .long 1 # NumberOfSymbols
@@ -230,6 +230,23 @@ section_table: .word 0 # NumberOfLineNumbers .long 0x42100040 # Characteristics (section flags) +#ifdef CONFIG_EFI_MIXED + # + # The offset & size fields are filled in by build.c. + # + .asciz ".compat" + .long 0 + .long 0x0 + .long 0 # Size of initialized data + # on disk + .long 0x0 + .long 0 # PointerToRelocations + .long 0 # PointerToLineNumbers + .word 0 # NumberOfRelocations + .word 0 # NumberOfLineNumbers + .long 0x60500020 # Characteristics (section flags) +#endif + # # The offset & size fields are filled in by build.c. #
@@ -248,6 +265,7 @@ section_table: .word 0 # NumberOfLineNumbers .long 0x60500020 # Characteristics (section flags) + .set section_count, (. - section_table) / 40 #endif /* CONFIG_EFI_STUB */ # Kernel attributes; used by setup. This is part 1 of the
arch/x86/boot/tools/build.c +31 -1
@@ -53,9 +53,16 @@ u8 buf[SETUP_SECT_MAX*512];#define PECOFF_RELOC_RESERVE 0x20 +#ifdef CONFIG_EFI_MIXED +#define PECOFF_COMPAT_RESERVE 0x20 +#else +#define PECOFF_COMPAT_RESERVE 0x0 +#endif +unsigned long efi32_stub_entry;unsigned long efi64_stub_entry;unsigned long efi_pe_entry; +unsigned long efi32_pe_entry;unsigned long kernel_info;unsigned long startup_64;
@@ -189,7 +196,8 @@ static void update_pecoff_section_header(char *section_name, u32 offset, u32 sizstatic void update_pecoff_setup_and_reloc (unsigned int size) {u32 setup_offset =0x200 ; -u32 reloc_offset = size -PECOFF_RELOC_RESERVE ; +u32 reloc_offset = size -PECOFF_RELOC_RESERVE -PECOFF_COMPAT_RESERVE ; +u32 compat_offset = reloc_offset +PECOFF_RELOC_RESERVE ;u32 setup_size = reloc_offset - setup_offset;update_pecoff_section_header (".setup" , setup_offset, setup_size);
@@ -201,6 +209,20 @@ static void update_pecoff_setup_and_reloc(unsigned int size)*/ put_unaligned_le32 (reloc_offset +10 , &buf[reloc_offset]);put_unaligned_le32 (10 , &buf[reloc_offset +4 ]); + +#ifdef CONFIG_EFI_MIXED +update_pecoff_section_header (".compat" , compat_offset,PECOFF_COMPAT_RESERVE ); + +/* +* Put the IA-32 machine type (0x14c) and the associated entry point +* address in the .compat section, so loaders can figure out which other +* execution modes this image supports. +*/ + buf[compat_offset] =0x1 ; + buf[compat_offset +1 ] =0x8 ; +put_unaligned_le16 (0x14c , &buf[compat_offset +2 ]); +put_unaligned_le32 (efi32_pe_entry + size, &buf[compat_offset +4 ]); +#endif } static void update_pecoff_text(unsigned int text_start, unsigned int file_sz,
@@ -282,6 +304,12 @@ static inline int reserve_pecoff_reloc_section(int c) }#endif /* CONFIG_EFI_STUB */ +static int reserve_pecoff_compat_section (int c) +{ +/* Reserve 0x20 bytes for .compat section */ +memset (buf+c,0 ,PECOFF_COMPAT_RESERVE ); +return PECOFF_COMPAT_RESERVE ; +}/* * Parse zoffset.h and find the entry points. We could just #include zoffset.h
@@ -314,6 +342,7 @@ static void parse_zoffset(char *fname)PARSE_ZOFS (p, efi32_stub_entry);PARSE_ZOFS (p, efi64_stub_entry);PARSE_ZOFS (p, efi_pe_entry); +PARSE_ZOFS (p, efi32_pe_entry);PARSE_ZOFS (p, kernel_info);PARSE_ZOFS (p, startup_64);
@@ -357,6 +386,7 @@ int main(int argc, char ** argv)die ("Boot block hasn't got boot flag (0xAA55)" );fclose (file); + c +=reserve_pecoff_compat_section (c); c +=reserve_pecoff_reloc_section (c);/* Pad unused space with zeros */
Signature · 1 line
2.17.1
Re: [RFC PATCH 2/3] efi/x86: add true mixed mode entry point into .compat section
Quoted 10 lines · Ard Biesheuvel
Currently, mixed mode is closely tied to the EFI handover protocol and relies on intimate knowledge of the bootparams structure, setup header etc, all of which are rather byzantine and entirely specific to x86.
Even though no other EFI supported architectures are currently known that could support something like mixed mode, it makes sense to abstract a bit from this, and make it part of a generic Linux on EFI boot protocol.
Quoted 4 lines
To that end, add a .compat section to the mixed mode binary, and populate it with the PE machine type and entry point address, allowing firmware implementations to match it to their native machine type, and invoke non-native binaries using a secondary entry point.
This patch refers to efi32_pe_entry which is only defined in the next one.
Ard Biesheuvel
maintainerRe: [RFC PATCH 2/3] efi/x86: add true mixed mode entry point into .compat section
Quoted 12 lines · Arvind Sankar
Quoted 10 lines · Ard Biesheuvel
Currently, mixed mode is closely tied to the EFI handover protocol and relies on intimate knowledge of the bootparams structure, setup header etc, all of which are rather byzantine and entirely specific to x86.
Even though no other EFI supported architectures are currently known that could support something like mixed mode, it makes sense to abstract a bit from this, and make it part of a generic Linux on EFI boot protocol.
Quoted 7 lines
Quoted 4 lines
To that end, add a .compat section to the mixed mode binary, and populate it with the PE machine type and entry point address, allowing firmware implementations to match it to their native machine type, and invoke non-native binaries using a secondary entry point.
This patch refers to
efi32_pe_entrywhich is only defined in the next one.
Indeed. So the .compat section will be emitted with a bogus entry point when applying this patch but not the next one, so perhaps I should simply reorder them .
Ard Biesheuvel
maintainerRFC PATCH 3/3 efi/x86: implement mixed mode boot without the handover protocol
In reply to Ard BiesheuvelAdd support for booting 64-bit x86 kernels from 32-bit firmware running on 64-bit capable CPUs without requiring the bootloader to implement the EFI handover protocol, allocate the setup block etc etc, all of which can be done by the stub using code we have already implemented.
Instead, create an ordinary EFI application entrypoint but implemented in 32-bit code, so that it can be invoked by 32-bit firmware, and stash the address of this 32-bit entrypoint in the .compat section where the bootloader can find it.
Note that we use the setup block embedded in the binary to go through startup_32(), but it gets reallocated and copied in efi_pe_entry(), using the same code that runs when the x86 kernel is booted in EFI mode from native firmware. This requires the loaded image protocol to be installed on the kernel image's EFI handle, and point to the kernel image itself and not to its loader. This, in turn, requires the bootloader to use the LoadImage() boot services to load the 64-bit image from 32-bit firmware, which is in fact supported by firmware based on EDK2. (Only StartImage() will fail, and instead, the newly added entrypoint needs to be invoked)
--- arch/x86/boot/compressed/head_64.S | 61 +++++++++++++++++++- 1 file changed, 59 insertions(+), 2 deletions(-)
arch/x86/boot/compressed/head_64.S +59 -2
@@ -207,8 +207,8 @@ SYM_FUNC_START(startup_32) cmp $0, %edi jz 1f leal efi64_stub_entry(%ebp), %eax - movl %esi, %edx movl efi32_boot_args+4(%ebp), %esi + movl efi32_boot_args+8(%ebp), %edx // saved bootparams pointer 1: #endif pushl %eax
@@ -233,6 +233,8 @@ SYM_FUNC_START(efi32_stub_entry) 1: pop %ebp subl $1b, %ebp + movl %esi, efi32_boot_args+8(%ebp) +2: movl %ecx, efi32_boot_args(%ebp) movl %edx, efi32_boot_args+4(%ebp) movb $0, efi_is64(%ebp)
@@ -249,6 +251,49 @@ SYM_FUNC_START(efi32_stub_entry) jmp startup_32 SYM_FUNC_END(efi32_stub_entry) + +#define ST32_boottime 60 // offsetof(efi_system_table_32_t, boottime) +#define BS32_handle_protocol 88 // offsetof(efi_boot_services_32_t, handle_protocol) +#define LI32_image_base 32 // offsetof(efi_loaded_image_32_t, image_base) + + .subsection 1 + .code32 +SYM_FUNC_START(efi32_pe_entry) + pushl %ebp + + call 3f +3: pop %ebp + subl $3b, %ebp + + /* Get the loaded image protocol pointer from the image handle */ + subl $12, %esp // space for the loaded image pointer + pushl %esp // pass its address + leal 5f(%ebp), %eax + pushl %eax // pass the GUID address + pushl 28(%esp) // pass the image handle + + movl 36(%esp), %eax // sys_table + movl ST32_boottime(%eax), %eax // sys_table->boottime + call *BS32_handle_protocol(%eax) // sys_table->boottime->handle_protocol + cmp $0, %eax + jnz 4f + + movl 32(%esp), %ecx // image_handle + movl 36(%esp), %edx // sys_table + movl 12(%esp), %esi // loaded_image + movl LI32_image_base(%esi), %esi // loaded_image->image_base + jmp 2b + +4: addl $24, %esp + popl %ebp + ret +SYM_FUNC_END(efi32_pe_entry) + + /* EFI loaded image protocol GUID */ +5: .long 0x5B1B31A1 + .word 0x9562, 0x11d2 + .byte 0x8E, 0x3F, 0x00, 0xA0, 0xC9, 0x69, 0x72, 0x3B + .previous #endif .code64
@@ -465,11 +510,23 @@ SYM_CODE_END(startup_64) SYM_FUNC_START(efi64_stub_entry) SYM_FUNC_START_ALIAS(efi_stub_entry) and $~0xf, %rsp /* realign the stack */ +#ifdef CONFIG_EFI_MIXED + cmpl $0, %edx + jz 0f +#endif call efi_main movq %rax,%rsi movl BP_code32_start(%esi), %eax leaq startup_64(%rax), %rax jmp *%rax + +#ifdef CONFIG_EFI_MIXED +0: movl %edi, %ecx // MS calling convention + movl %esi, %edx + call efi_pe_entry +1: hlt + jmp 1b +#endif SYM_FUNC_END(efi64_stub_entry) SYM_FUNC_END_ALIAS(efi_stub_entry) #endif
@@ -641,7 +698,7 @@ SYM_DATA_START_LOCAL(gdt) SYM_DATA_END_LABEL(gdt, SYM_L_LOCAL, gdt_end) #ifdef CONFIG_EFI_MIXED -SYM_DATA_LOCAL(efi32_boot_args, .long 0, 0) +SYM_DATA_LOCAL(efi32_boot_args, .long 0, 0, 0) SYM_DATA(efi_is64, .byte 1) #endif
Signature · 1 line
2.17.1
Re: [RFC PATCH 3/3] efi/x86: implement mixed mode boot without the handover protocol
Quoted 41 lines · Ard Biesheuvel
Add support for booting 64-bit x86 kernels from 32-bit firmware running on 64-bit capable CPUs without requiring the bootloader to implement the EFI handover protocol, allocate the setup block etc etc, all of which can be done by the stub using code we have already implemented.
Instead, create an ordinary EFI application entrypoint but implemented in 32-bit code, so that it can be invoked by 32-bit firmware, and stash the address of this 32-bit entrypoint in the .compat section where the bootloader can find it.
Note that we use the setup block embedded in the binary to go through
startup_32(), but it gets reallocated and copied inefi_pe_entry(), using the same code that runs when the x86 kernel is booted in EFI mode from native firmware. This requires the loaded image protocol to be installed on the kernel image's EFI handle, and point to the kernel image itself and not to its loader. This, in turn, requires the bootloader to use theLoadImage()boot services to load the 64-bit image from 32-bit firmware, which is in fact supported by firmware based on EDK2. (OnlyStartImage()will fail, and instead, the newly added entrypoint needs to be invoked)Signed-off-by Ard Biesheuvel <[email protected]>--- arch/x86/boot/compressed/head_64.S | 61 +++++++++++++++++++- 1 file changed, 59 insertions(+), 2 deletions(-)arch/x86/boot/compressed/head_64.S +1
-1@@ -207,8 +207,8 @@ SYM_FUNC_START(startup_32) cmp $0, %edi jz 1f leal efi64_stub_entry(%ebp), %eax - movl %esi, %edx movl efi32_boot_args+4(%ebp), %esi + movl efi32_boot_args+8(%ebp), %edx // saved bootparams pointer 1: #endif pushl %eax
arch/x86/boot/compressed/head_64.S SYM_FUNC_START(efi32_stub_entry) · line 237
Quoted 5 lines
arch/x86/boot/compressed/head_64.S +2
-0@@ -233,6 +233,8 @@ SYM_FUNC_START(efi32_stub_entry) 1: pop %ebp subl $1b, %ebp + movl %esi, efi32_boot_args+8(%ebp) +2:
I think it would be easier to read if this were turned into a proper SYM_INNER_LABEL. You could then also just put efi32_pe_entry into the .text section instead of moving it to the end of .head.text with .subsection. Another option is to use a separate flag in .data to indicate that we entered via efi32_pe_entry, then you could just jump to efi32_stub_entry.
Quoted 40 lines
arch/x86/boot/compressed/head_64.S +33
-0movl %ecx, efi32_boot_args(%ebp) movl %edx, efi32_boot_args+4(%ebp) movb $0, efi_is64(%ebp)@@ -249,6 +251,49 @@ SYM_FUNC_START(efi32_stub_entry) jmp startup_32 SYM_FUNC_END(efi32_stub_entry) + +#define ST32_boottime 60 // offsetof(efi_system_table_32_t, boottime) +#define BS32_handle_protocol 88 // offsetof(efi_boot_services_32_t, handle_protocol) +#define LI32_image_base 32 // offsetof(efi_loaded_image_32_t, image_base) + + .subsection 1 + .code32 +SYM_FUNC_START(efi32_pe_entry) + pushl %ebp + + call 3f +3: pop %ebp + subl $3b, %ebp + + /* Get the loaded image protocol pointer from the image handle */ + subl $12, %esp // space for the loaded image pointer + pushl %esp // pass its address + leal 5f(%ebp), %eax + pushl %eax // pass the GUID address + pushl 28(%esp) // pass the image handle + + movl 36(%esp), %eax // sys_table + movl ST32_boottime(%eax), %eax // sys_table->boottime + call *BS32_handle_protocol(%eax) // sys_table->boottime->handle_protocol + cmp $0, %eax + jnz 4f + + movl 32(%esp), %ecx // image_handle + movl 36(%esp), %edx // sys_table + movl 12(%esp), %esi // loaded_image + movl LI32_image_base(%esi), %esi // loaded_image->image_base + jmp 2b +
arch/x86/boot/compressed/head_64.S SYM_FUNC_START(efi32_stub_entry) · line 296
Quoted 10 lines
arch/x86/boot/compressed/head_64.S +10
-0+4: addl $24, %esp + popl %ebp + ret +SYM_FUNC_END(efi32_pe_entry) + + /* EFI loaded image protocol GUID */ +5: .long 0x5B1B31A1 + .word 0x9562, 0x11d2 + .byte 0x8E, 0x3F, 0x00, 0xA0, 0xC9, 0x69, 0x72, 0x3B + .previous
Any reason for this not to live in .data (or .rodata)?
Quoted 39 lines
arch/x86/boot/compressed/head_64.S +13
-1#endif .code64@@ -465,11 +510,23 @@ SYM_CODE_END(startup_64) SYM_FUNC_START(efi64_stub_entry) SYM_FUNC_START_ALIAS(efi_stub_entry) and $~0xf, %rsp /* realign the stack */ +#ifdef CONFIG_EFI_MIXED + cmpl $0, %edx + jz 0f +#endif call efi_main movq %rax,%rsi movl BP_code32_start(%esi), %eax leaq startup_64(%rax), %rax jmp *%rax + +#ifdef CONFIG_EFI_MIXED +0: movl %edi, %ecx // MS calling convention + movl %esi, %edx + call efi_pe_entry +1: hlt + jmp 1b +#endif SYM_FUNC_END(efi64_stub_entry) SYM_FUNC_END_ALIAS(efi_stub_entry) #endif@@ -641,7 +698,7 @@ SYM_DATA_START_LOCAL(gdt) SYM_DATA_END_LABEL(gdt, SYM_L_LOCAL, gdt_end) #ifdef CONFIG_EFI_MIXED -SYM_DATA_LOCAL(efi32_boot_args, .long 0, 0) +SYM_DATA_LOCAL(efi32_boot_args, .long 0, 0, 0) SYM_DATA(efi_is64, .byte 1) #endifSignature · 1 line
2.17.1
Ard Biesheuvel
maintainerRe: [RFC PATCH 3/3] efi/x86: implement mixed mode boot without the handover protocol
Quoted 46 lines · Arvind Sankar
Quoted 44 lines · Ard Biesheuvel
Add support for booting 64-bit x86 kernels from 32-bit firmware running on 64-bit capable CPUs without requiring the bootloader to implement the EFI handover protocol, allocate the setup block etc etc, all of which can be done by the stub using code we have already implemented.
Instead, create an ordinary EFI application entrypoint but implemented in 32-bit code, so that it can be invoked by 32-bit firmware, and stash the address of this 32-bit entrypoint in the .compat section where the bootloader can find it.
Note that we use the setup block embedded in the binary to go through
startup_32(), but it gets reallocated and copied inefi_pe_entry(), using the same code that runs when the x86 kernel is booted in EFI mode from native firmware. This requires the loaded image protocol to be installed on the kernel image's EFI handle, and point to the kernel image itself and not to its loader. This, in turn, requires the bootloader to use theLoadImage()boot services to load the 64-bit image from 32-bit firmware, which is in fact supported by firmware based on EDK2. (OnlyStartImage()will fail, and instead, the newly added entrypoint needs to be invoked)Signed-off-by Ard Biesheuvel <[email protected]>--- arch/x86/boot/compressed/head_64.S | 61 +++++++++++++++++++- 1 file changed, 59 insertions(+), 2 deletions(-)arch/x86/boot/compressed/head_64.S +1
-1@@ -207,8 +207,8 @@ SYM_FUNC_START(startup_32) cmp $0, %edi jz 1f leal efi64_stub_entry(%ebp), %eax - movl %esi, %edx movl efi32_boot_args+4(%ebp), %esi + movl efi32_boot_args+8(%ebp), %edx // saved bootparams pointer 1: #endif pushl %eax@@ -233,6 +233,8 @@ SYM_FUNC_START(efi32_stub_entry) 1: pop %ebp subl $1b, %ebp
Quoted 10 lines
arch/x86/boot/compressed/head_64.S SYM_FUNC_START(efi32_stub_entry) · line 237
Quoted 2 lines
arch/x86/boot/compressed/head_64.S +2
-0+ movl %esi, efi32_boot_args+8(%ebp) +2:I think it would be easier to read if this were turned into a proper
SYM_INNER_LABEL. You could then also just putefi32_pe_entryinto the .text section instead of moving it to the end of .head.text with .subsection. Another option is to use a separate flag in .data to indicate that we entered viaefi32_pe_entry, then you could just jump toefi32_stub_entry.
Indeed.
Quoted 44 lines
Quoted 44 lines
arch/x86/boot/compressed/head_64.S +37
-0movl %ecx, efi32_boot_args(%ebp) movl %edx, efi32_boot_args+4(%ebp) movb $0, efi_is64(%ebp)@@ -249,6 +251,49 @@ SYM_FUNC_START(efi32_stub_entry) jmp startup_32 SYM_FUNC_END(efi32_stub_entry) + +#define ST32_boottime 60 // offsetof(efi_system_table_32_t, boottime) +#define BS32_handle_protocol 88 // offsetof(efi_boot_services_32_t, handle_protocol) +#define LI32_image_base 32 // offsetof(efi_loaded_image_32_t, image_base) + + .subsection 1 + .code32 +SYM_FUNC_START(efi32_pe_entry) + pushl %ebp + + call 3f +3: pop %ebp + subl $3b, %ebp + + /* Get the loaded image protocol pointer from the image handle */ + subl $12, %esp // space for the loaded image pointer + pushl %esp // pass its address + leal 5f(%ebp), %eax + pushl %eax // pass the GUID address + pushl 28(%esp) // pass the image handle + + movl 36(%esp), %eax // sys_table + movl ST32_boottime(%eax), %eax // sys_table->boottime + call *BS32_handle_protocol(%eax) // sys_table->boottime->handle_protocol + cmp $0, %eax + jnz 4f + + movl 32(%esp), %ecx // image_handle + movl 36(%esp), %edx // sys_table + movl 12(%esp), %esi // loaded_image + movl LI32_image_base(%esi), %esi // loaded_image->image_base + jmp 2b + +4: addl $24, %esp + popl %ebp + ret +SYM_FUNC_END(efi32_pe_entry)
Quoted 8 lines
arch/x86/boot/compressed/head_64.S SYM_FUNC_START(efi32_stub_entry) · line 296
Quoted 6 lines
arch/x86/boot/compressed/head_64.S +6
-0+ + /* EFI loaded image protocol GUID */ +5: .long 0x5B1B31A1 + .word 0x9562, 0x11d2 + .byte 0x8E, 0x3F, 0x00, 0xA0, 0xC9, 0x69, 0x72, 0x3B + .previousAny reason for this not to live in .data (or .rodata)?
None other than laziness ...
Quoted 39 lines
Quoted 39 lines
arch/x86/boot/compressed/head_64.S +13
-2#endif .code64@@ -465,11 +510,23 @@ SYM_CODE_END(startup_64) SYM_FUNC_START(efi64_stub_entry) SYM_FUNC_START_ALIAS(efi_stub_entry) and $~0xf, %rsp /* realign the stack */ +#ifdef CONFIG_EFI_MIXED + cmpl $0, %edx + jz 0f +#endif call efi_main movq %rax,%rsi movl BP_code32_start(%esi), %eax leaq startup_64(%rax), %rax jmp *%rax + +#ifdef CONFIG_EFI_MIXED +0: movl %edi, %ecx // MS calling convention + movl %esi, %edx + call efi_pe_entry +1: hlt + jmp 1b +#endif SYM_FUNC_END(efi64_stub_entry) SYM_FUNC_END_ALIAS(efi_stub_entry) #endif@@ -641,7 +698,7 @@ SYM_DATA_START_LOCAL(gdt) SYM_DATA_END_LABEL(gdt, SYM_L_LOCAL, gdt_end) #ifdef CONFIG_EFI_MIXED -SYM_DATA_LOCAL(efi32_boot_args, .long 0, 0) +SYM_DATA_LOCAL(efi32_boot_args, .long 0, 0, 0) SYM_DATA(efi_is64, .byte 1) #endif --2.17.1
Quoted 40 lines · Ard Biesheuvel
This series is another part of my effort to reduce the level of knowledge on the part of the bootloader or firmware of internal per-architecture details regarding where/how the kernel is loaded and where its initrd and other context data are passed.
The x86 architecture has a so-called 'EFI handover protocol', which defines how the bootparams struct should be populated, and how it should be interpreted to figure out where to load the kernel, and at which offset in the binary the entrypoint is located. This scheme allows the initrd to be loaded beforehand, and allows 32-bit firmware to invoke a 64-bit kernel via a special entrypoint that manages the state transitions between the two execution modes.
Due to this, x86 loaders currently do not rely on LoadImage and StartImage, and therefore, are forced to re-implement things like image authentication for secure boot and taking the measurements for measured boot in their open coded clones of these routines.
My previous series on this topic [0] implements a generic way to load the initrd from any source supported by the loader without relying on something like device trees or bootparams structures, and so native boot should not need the EFI handover protocol anymore after those change are merged.
What remains is mixed mode boot, which also needs the EFI handover protocol regardless of whether an initrd is loaded or not. So let's get rid of that requirement, and take advantage of the fact that EDK2 based firmware does support
LoadImage()for X64 binaries on IA32 firmware, which means we can rely on the secure boot and measured boot checks being performed by the firmware. The only thing we need to put on top is a way to discover the non-native entrypoint into the binary in a way that does not rely on x86 specific headers and data structures.So let's introduce a new .compat header in the PE/COFF metadata of the bzImage, and populate it with a <machine type, entrypoint> tuple, allowing a generic EFI loader to decide whether the entrypoint supports its native machine type, and invoke it as an ordinary EFI application entrypoint. Since we will not be passing a bootparams structure, we need to discover the base of the image (which contains the setup header) via the loaded image protocol before we can enter the kernel in 32-bit mode at
startup_32()
Quoted 8 lines
A loader implementation for OVMF can be found at [1]. Note that this loader code is fully generic, and could be used without modifications if other architectures ever emerge that support kernels that can be invoked from a non-native (but cross-type supported) loader.
[0] https://lore.kernel.org/linux-arm-kernel/[email protected]/
[1] https://github.com/ardbiesheuvel/edk2/commits/linux-efi-generic
As an alternative to the new section, how about having a CONFIG option to emit the 64-bit kernel with a 32-bit PE header instead, which would point to efi32_pe_entry? In that case it could be directly loaded by existing firmware already. You could even have a tool that can mangle an existing bzImage's header from 64-bit to 32-bit, say using the newly added kernel_info structure to record the existence and location of efi32_pe_entry.
Also, the PE header can live anywhere inside the image, right? Is there any reason to struggle to shoehorn it into the "boot sector"?
Ard Biesheuvel
maintainerQuoted 51 lines · Arvind Sankar
Quoted 40 lines · Ard Biesheuvel
This series is another part of my effort to reduce the level of knowledge on the part of the bootloader or firmware of internal per-architecture details regarding where/how the kernel is loaded and where its initrd and other context data are passed.
The x86 architecture has a so-called 'EFI handover protocol', which defines how the bootparams struct should be populated, and how it should be interpreted to figure out where to load the kernel, and at which offset in the binary the entrypoint is located. This scheme allows the initrd to be loaded beforehand, and allows 32-bit firmware to invoke a 64-bit kernel via a special entrypoint that manages the state transitions between the two execution modes.
Due to this, x86 loaders currently do not rely on LoadImage and StartImage, and therefore, are forced to re-implement things like image authentication for secure boot and taking the measurements for measured boot in their open coded clones of these routines.
My previous series on this topic [0] implements a generic way to load the initrd from any source supported by the loader without relying on something like device trees or bootparams structures, and so native boot should not need the EFI handover protocol anymore after those change are merged.
What remains is mixed mode boot, which also needs the EFI handover protocol regardless of whether an initrd is loaded or not. So let's get rid of that requirement, and take advantage of the fact that EDK2 based firmware does support
LoadImage()for X64 binaries on IA32 firmware, which means we can rely on the secure boot and measured boot checks being performed by the firmware. The only thing we need to put on top is a way to discover the non-native entrypoint into the binary in a way that does not rely on x86 specific headers and data structures.So let's introduce a new .compat header in the PE/COFF metadata of the bzImage, and populate it with a <machine type, entrypoint> tuple, allowing a generic EFI loader to decide whether the entrypoint supports its native machine type, and invoke it as an ordinary EFI application entrypoint. Since we will not be passing a bootparams structure, we need to discover the base of the image (which contains the setup header) via the loaded image protocol before we can enter the kernel in 32-bit mode at
startup_32()Quoted 8 lines
A loader implementation for OVMF can be found at [1]. Note that this loader code is fully generic, and could be used without modifications if other architectures ever emerge that support kernels that can be invoked from a non-native (but cross-type supported) loader.
[0] https://lore.kernel.org/linux-arm-kernel/[email protected]/
[1] https://github.com/ardbiesheuvel/edk2/commits/linux-efi-generic
Quoted 8 lines
As an alternative to the new section, how about having a CONFIG option to emit the 64-bit kernel with a 32-bit PE header instead, which would point to
efi32_pe_entry? In that case it could be directly loaded by existing firmware already. You could even have a tool that can mangle an existing bzImage's header from 64-bit to 32-bit, say using the newly addedkernel_infostructure to record the existence and location ofefi32_pe_entry.
That wouldn't work with, say, signed distro kernels.
Quoted 2 lines
Also, the PE header can live anywhere inside the image, right? Is there any reason to struggle to shoehorn it into the "boot sector"?
It cannot. It must live outside a region described by the section headers.
Quoted 11 lines · Ard Biesheuvel
Quoted 8 lines · Arvind Sankar
As an alternative to the new section, how about having a CONFIG option to emit the 64-bit kernel with a 32-bit PE header instead, which would point to
efi32_pe_entry? In that case it could be directly loaded by existing firmware already. You could even have a tool that can mangle an existing bzImage's header from 64-bit to 32-bit, say using the newly addedkernel_infostructure to record the existence and location ofefi32_pe_entry.That wouldn't work with, say, signed distro kernels.
No, the idea would be that the distro would distribute two signed images, one 32-bit and one 64-bit, which are identical except for the header. At install time, the installer chooses based on the system's firmware bit-ness.
Quoted 5 lines
Quoted 3 lines
Also, the PE header can live anywhere inside the image, right? Is there any reason to struggle to shoehorn it into the "boot sector"?
It cannot. It must live outside a region described by the section headers.
It could still be inserted after .setup, or at the very end of the file, no?
Ard Biesheuvel
maintainerQuoted 12 lines · Arvind Sankar
Quoted 10 lines · Ard Biesheuvel
Quoted 8 lines · Arvind Sankar
As an alternative to the new section, how about having a CONFIG option to emit the 64-bit kernel with a 32-bit PE header instead, which would point to
efi32_pe_entry? In that case it could be directly loaded by existing firmware already. You could even have a tool that can mangle an existing bzImage's header from 64-bit to 32-bit, say using the newly addedkernel_infostructure to record the existence and location ofefi32_pe_entry.
Quoted 7 lines
Quoted 1 line
That wouldn't work with, say, signed distro kernels.
No, the idea would be that the distro would distribute two signed images, one 32-bit and one 64-bit, which are identical except for the header. At install time, the installer chooses based on the system's firmware bit-ness.
I guess it would be possible, but then we'd need two different images while today, we can run the same image on both kinds of firmwares. The only thing I am trying to do is remove all the quirky bootparams stuff from the loader so that we can switch to LoadImage
Quoted 7 lines
Quoted 5 lines
Quoted 3 lines
Also, the PE header can live anywhere inside the image, right? Is there any reason to struggle to shoehorn it into the "boot sector"?
It cannot. It must live outside a region described by the section headers.
It could still be inserted after .setup, or at the very end of the file, no?
The PE/COFF spec mentions that the COFF header needs to follow the signature. Also, the SizeOfHeaders field would become somewhat ambiguous if the header is split up like that.
Quoted 21 lines · Ard Biesheuvel
Quoted 12 lines · Arvind Sankar
Quoted 10 lines · Ard Biesheuvel
Quoted 8 lines · Arvind Sankar
As an alternative to the new section, how about having a CONFIG option to emit the 64-bit kernel with a 32-bit PE header instead, which would point to
efi32_pe_entry? In that case it could be directly loaded by existing firmware already. You could even have a tool that can mangle an existing bzImage's header from 64-bit to 32-bit, say using the newly addedkernel_infostructure to record the existence and location ofefi32_pe_entry.Quoted 7 lines
Quoted 1 line
That wouldn't work with, say, signed distro kernels.
No, the idea would be that the distro would distribute two signed images, one 32-bit and one 64-bit, which are identical except for the header. At install time, the installer chooses based on the system's firmware bit-ness.
Quoted 4 lines
I guess it would be possible, but then we'd need two different images while today, we can run the same image on both kinds of firmwares. The only thing I am trying to do is remove all the quirky bootparams stuff from the loader so that we can switch to LoadImage
Yeah, but doing that will allow you to boot directly from firmware on existing machines, and only one image needs to be chosen at install time, so it just adds a few MiB to the package. I guess most people will still use a boot manager or loader that can be easily enhanced to use LoadImage and the new section, but it would be nice to have the option to avoid that.
Quoted 5 lines
Quoted 5 lines
Quoted 5 lines
Quoted 4 lines
Also, the PE header can live anywhere inside the image, right? Is there any reason to struggle to shoehorn it into the "boot sector"?
Quoted 7 lines
Quoted 3 lines
Quoted 1 line
It cannot. It must live outside a region described by the section headers.
It could still be inserted after .setup, or at the very end of the file, no?
The PE/COFF spec mentions that the COFF header needs to follow the signature. Also, the SizeOfHeaders field would become somewhat ambiguous if the header is split up like that.
Ah, the definition of SizeOfHeaders doesn't make much sense if the headers weren't contiguous with the MS-DOS stub. I guess they just wanted the MS-DOS stub to potentially vary in size, but still want the header to immediately follow it, drat.
Quoted 27 lines · Arvind Sankar
Quoted 21 lines · Ard Biesheuvel
Quoted 12 lines · Arvind Sankar
Quoted 10 lines · Ard Biesheuvel
Quoted 8 lines · Arvind Sankar
As an alternative to the new section, how about having a CONFIG option to emit the 64-bit kernel with a 32-bit PE header instead, which would point to
efi32_pe_entry? In that case it could be directly loaded by existing firmware already. You could even have a tool that can mangle an existing bzImage's header from 64-bit to 32-bit, say using the newly addedkernel_infostructure to record the existence and location ofefi32_pe_entry.Quoted 7 lines
Quoted 1 line
That wouldn't work with, say, signed distro kernels.
No, the idea would be that the distro would distribute two signed images, one 32-bit and one 64-bit, which are identical except for the header. At install time, the installer chooses based on the system's firmware bit-ness.
Quoted 4 lines
I guess it would be possible, but then we'd need two different images while today, we can run the same image on both kinds of firmwares. The only thing I am trying to do is remove all the quirky bootparams stuff from the loader so that we can switch to LoadImage
Quoted 6 lines
Yeah, but doing that will allow you to boot directly from firmware on existing machines, and only one image needs to be chosen at install time, so it just adds a few MiB to the package. I guess most people will still use a boot manager or loader that can be easily enhanced to use LoadImage and the new section, but it would be nice to have the option to avoid that.
Also not quite today, right? You still need this patchset and the modifications to bootloaders to get away with one image.
Ard Biesheuvel
maintainerQuoted 29 lines · Arvind Sankar
Quoted 27 lines · Arvind Sankar
Quoted 25 lines · Ard Biesheuvel
Quoted 12 lines · Arvind Sankar
Quoted 10 lines · Ard Biesheuvel
Quoted 8 lines · Arvind Sankar
As an alternative to the new section, how about having a CONFIG option to emit the 64-bit kernel with a 32-bit PE header instead, which would point to
efi32_pe_entry? In that case it could be directly loaded by existing firmware already. You could even have a tool that can mangle an existing bzImage's header from 64-bit to 32-bit, say using the newly addedkernel_infostructure to record the existence and location ofefi32_pe_entry.Quoted 7 lines
Quoted 1 line
That wouldn't work with, say, signed distro kernels.
No, the idea would be that the distro would distribute two signed images, one 32-bit and one 64-bit, which are identical except for the header. At install time, the installer chooses based on the system's firmware bit-ness.
I guess it would be possible, but then we'd need two different images while today, we can run the same image on both kinds of firmwares. The only thing I am trying to do is remove all the quirky bootparams stuff from the loader so that we can switch to LoadImage
Quoted 6 lines
Quoted 6 lines
Yeah, but doing that will allow you to boot directly from firmware on existing machines, and only one image needs to be chosen at install time, so it just adds a few MiB to the package. I guess most people will still use a boot manager or loader that can be easily enhanced to use LoadImage and the new section, but it would be nice to have the option to avoid that.
I see the value of having a 64-bit image that can boot natively on 32-bit firmware, but I am not expecting any buy in from the distros for this scheme.
Quoted 3 lines
Also not quite today, right? You still need this patchset and the modifications to bootloaders to get away with one image.
Sure. But we already have mixed mode support today that doesn't require this, so it's going to be a difficult sell to switch to a new scheme that requires infrastructure to distribute different kernels, and logic to choose between the two.
A generic EFI bootloader/firmware will need to implement the initrd loadfile2 protocol as well, so some Linux specific features will need to be implemented anyway. This series is intended to ensure that mixed mode doesn't get left behind, even though very few people use it today.
Quoted 6 lines · Ard Biesheuvel
I see the value of having a 64-bit image that can boot natively on 32-bit firmware, but I am not expecting any buy in from the distros for this scheme.
Ok.