diff options
-rw-r--r-- | README | 1 | ||||
-rw-r--r-- | meson.build | 27 | ||||
-rw-r--r-- | src/boot/efi/boot.c | 1 | ||||
-rw-r--r-- | src/boot/efi/efi-string.h | 2 | ||||
-rw-r--r-- | src/boot/efi/meson.build | 176 | ||||
-rw-r--r-- | src/boot/efi/stub.c | 1 | ||||
-rw-r--r-- | src/boot/efi/util.h | 24 | ||||
-rw-r--r-- | src/fundamental/sbat.h | 2 | ||||
-rw-r--r-- | tools/elf2efi.lds | 49 | ||||
-rwxr-xr-x | tools/elf2efi.py | 578 |
10 files changed, 843 insertions, 18 deletions
@@ -216,6 +216,7 @@ REQUIREMENTS: awk, sed, grep, and similar tools clang >= 10.0, llvm >= 10.0 (optional, required to build BPF programs from source code in C) + pyelftools (optional, required for systemd-boot) During runtime, you need the following additional dependencies: diff --git a/meson.build b/meson.build index 8c4e50de73..cde0c88e48 100644 --- a/meson.build +++ b/meson.build @@ -1972,11 +1972,28 @@ if get_option('bootloader') != 'false' and efi_arch != '' elif get_option('bootloader') == 'true' and efi_arch == '' error('EFI not supported for this arch.') endif + +efi_arch_alt = '' +if efi_arch == 'x64' and cc.links(''' + #include <limits.h> + int main(int argc, char *argv[]) { + return __builtin_popcount(argc - CHAR_MAX); + }''', args : ['-m32', '-march=i686'], name : '32bit build possible') + efi_arch_alt = 'ia32' +endif + +have_pyelftools = pymod.find_installation('python3', required : false, modules : ['elftools']).found() +if get_option('bootloader') == 'true' and (not python_39 or not have_pyelftools) + error('EFI bootloader support requires Python >= 3.9 and pyelftools.') +endif + conf.set10( 'ENABLE_BOOTLOADER', get_option('efi') and get_option('bootloader') in ['auto', 'true'] and - efi_arch != '', + efi_arch != '' and + python_39 and + have_pyelftools, ) if get_option('ukify') == 'auto' @@ -1990,18 +2007,20 @@ conf.set10('ENABLE_UKIFY', want_ukify) ############################################################ # +elf2efi_lds = project_source_root / 'tools/elf2efi.lds' +elf2efi_py = find_program('tools/elf2efi.py') +export_dbus_interfaces_py = find_program('tools/dbus_exporter.py') generate_gperfs = find_program('tools/generate-gperfs.py') make_autosuspend_rules_py = find_program('tools/make-autosuspend-rules.py') make_directive_index_py = find_program('tools/make-directive-index.py') make_man_index_py = find_program('tools/make-man-index.py') meson_render_jinja2 = find_program('tools/meson-render-jinja2.py') update_dbus_docs_py = find_program('tools/update-dbus-docs.py') -update_man_rules_py = find_program('tools/update-man-rules.py') -update_hwdb_sh = find_program('tools/update-hwdb.sh') update_hwdb_autosuspend_sh = find_program('tools/update-hwdb-autosuspend.sh') +update_hwdb_sh = find_program('tools/update-hwdb.sh') +update_man_rules_py = find_program('tools/update-man-rules.py') update_syscall_tables_sh = find_program('tools/update-syscall-tables.sh') xml_helper_py = find_program('tools/xml_helper.py') -export_dbus_interfaces_py = find_program('tools/dbus_exporter.py') ############################################################ diff --git a/src/boot/efi/boot.c b/src/boot/efi/boot.c index ff249c8a2e..02c3568062 100644 --- a/src/boot/efi/boot.c +++ b/src/boot/efi/boot.c @@ -21,6 +21,7 @@ #include "shim.h" #include "ticks.h" #include "util.h" +#include "version.h" #include "vmm.h" /* Magic string for recognizing our own binaries */ diff --git a/src/boot/efi/efi-string.h b/src/boot/efi/efi-string.h index 4df37a8505..b97e16990a 100644 --- a/src/boot/efi/efi-string.h +++ b/src/boot/efi/efi-string.h @@ -126,7 +126,7 @@ _gnu_printf_(2, 0) _warn_unused_result_ char16_t *xvasprintf_status(EFI_STATUS s /* inttypes.h is provided by libc instead of the compiler and is not supposed to be used in freestanding * environments. We could use clang __*_FMT*__ constants for this, bug gcc does not have them. :( */ -# if defined(__ILP32__) || defined(__arm__) +# if defined(__ILP32__) || defined(__arm__) || defined(__i386__) # define PRI64_PREFIX "ll" # elif defined(__LP64__) # define PRI64_PREFIX "l" diff --git a/src/boot/efi/meson.build b/src/boot/efi/meson.build index 6677dc65d8..297eb0f5ee 100644 --- a/src/boot/efi/meson.build +++ b/src/boot/efi/meson.build @@ -57,7 +57,7 @@ elif get_option('sbat-distro') != '' endif endif -summary({'UEFI architecture' : efi_arch}, +summary({'UEFI architectures' : efi_arch + (efi_arch_alt == '' ? '' : ', ' + efi_arch_alt)}, section : 'UEFI') if efi_conf.get('SBAT_DISTRO', '') != '' @@ -76,6 +76,97 @@ configure_file( ############################################################ +efi_includes = [fundamental_include, include_directories('.')] + +efi_c_args = [ + '-DSD_BOOT=1', + '-ffreestanding', + '-fno-strict-aliasing', + '-fshort-wchar', + '-include', 'efi_config.h', +] + +efi_c_args += cc.get_supported_arguments( + '-fwide-exec-charset=UCS2', + # gcc docs says this is required for ms_abi to work correctly. + '-maccumulate-outgoing-args', +) + +efi_c_ld_args = [ + # We only support bfd. gold is going away, lld has issues with LTO on x86 + # and mold does not support linker scripts. + '-fuse-ld=bfd', + + '-lgcc', + '-nostdlib', + '-static-pie', + '-Wl,--entry=efi_main', + '-Wl,--fatal-warnings', + + # These flags should be passed by -static-pie, but seem to be missing sometimes. + '-Wl,--no-dynamic-linker', + '-z', 'text', + + # EFI has 4KiB pages. + '-z', 'common-page-size=4096', + '-z', 'max-page-size=4096', + + '-z', 'noexecstack', + '-z', 'norelro', + '-T' + elf2efi_lds, +] + +# efi_c_args is explicitly passed to targets so that they can override distro-provided flags +# that should not be used for EFI binaries. +efi_disabled_c_args = cc.get_supported_arguments( + '-fcf-protection=none', + '-fno-asynchronous-unwind-tables', + '-fno-exceptions', + '-fno-trapv', + '-fno-sanitize=all', + '-fno-stack-clash-protection', + '-fno-stack-protector', + '-fno-unwind-tables', +) +efi_c_args += efi_disabled_c_args +efi_c_ld_args += efi_disabled_c_args +efi_override_options = [ + 'b_coverage=false', + 'b_pgo=off', + 'b_sanitize=none', +] + +if cc.get_id() == 'clang' + # clang is too picky sometimes. + efi_c_args += '-Wno-unused-command-line-argument' + efi_c_ld_args += '-Wno-unused-command-line-argument' +endif + +if host_machine.cpu_family() == 'arm' + # libgcc is not compiled with -fshort-wchar, but it does not use it anyways, + # so it's fine to link against it. + efi_c_ld_args += '-Wl,--no-wchar-size-warning' +endif + +efi_c_args_primary = [efi_c_args, '-DEFI_MACHINE_TYPE_NAME="' + efi_arch + '"'] +efi_c_args_alt = [efi_c_args, '-DEFI_MACHINE_TYPE_NAME="' + efi_arch_alt + '"'] +efi_c_ld_args_primary = efi_c_ld_args +efi_c_ld_args_alt = efi_c_ld_args + +efi_c_args_primary += { + 'aarch64' : ['-mgeneral-regs-only'], + 'arm' : ['-mgeneral-regs-only'], + 'x86_64' : ['-march=x86-64', '-mno-red-zone', '-mgeneral-regs-only'], + 'x86' : ['-march=i686', '-mgeneral-regs-only'], +}.get(host_machine.cpu_family(), []) + +if efi_arch_alt == 'ia32' + efi_c_args_alt += ['-m32', '-march=i686', '-mgeneral-regs-only'] + efi_c_ld_args_alt += '-m32' +endif + +############################################################ + libefi_sources = files( 'console.c', 'device-path-util.c', @@ -155,3 +246,86 @@ if host_machine.cpu_family() in ['aarch64', 'arm', 'x86_64', 'x86'] }, ] endif + +boot_targets = [] +efi_elf_binaries = [] +efi_archspecs = [ + { + 'arch' : efi_arch, + 'c_args' : efi_c_args_primary, + 'link_args' : efi_c_ld_args_primary, + }, +] +if efi_arch_alt != '' + efi_archspecs += { + 'arch' : efi_arch_alt, + 'c_args' : efi_c_args_alt, + 'link_args' : efi_c_ld_args_alt, + } +endif + +foreach archspec : efi_archspecs + libefi = static_library( + 'efi' + archspec['arch'], + fundamental_sources, + libefi_sources, + include_directories : efi_includes, + c_args : archspec['c_args'], + dependencies : versiondep, + gnu_symbol_visibility : 'hidden', + override_options : efi_override_options, + pic : true) + + efi_elf_binaries += executable( + 'systemd-boot' + archspec['arch'], + systemd_boot_sources, + include_directories : efi_includes, + c_args : archspec['c_args'], + link_args : archspec['link_args'], + link_with : libefi, + link_depends : elf2efi_lds, + dependencies : versiondep, + gnu_symbol_visibility : 'hidden', + override_options : efi_override_options, + name_suffix : 'elf', + pie : true) + + efi_elf_binaries += executable( + 'linux' + archspec['arch'], + stub_sources, + include_directories : efi_includes, + c_args : archspec['c_args'], + link_args : archspec['link_args'], + link_with : libefi, + link_depends : elf2efi_lds, + dependencies : versiondep, + gnu_symbol_visibility : 'hidden', + override_options : efi_override_options, + name_suffix : 'elf.stub', + pie : true) +endforeach + +foreach efi_elf_binary : efi_elf_binaries + # FIXME: Use build_tgt.name() with meson >= 0.54.0 + name = fs.name(efi_elf_binary.full_path()).split('.')[0] + name += name.startswith('linux') ? '.efi.stub' : '.efi' + boot_targets += custom_target( + name, + output : name, + input : efi_elf_binary, + install : true, + install_dir : bootlibdir, + install_tag : 'systemd-boot', + command : [ + elf2efi_py, + '--version-major=' + meson.project_version(), + '--version-minor=0', + '--efi-major=1', + '--efi-minor=1', + '--subsystem=10', + '@INPUT@', + '@OUTPUT@', + ]) +endforeach + +alias_target('systemd-boot', boot_targets) diff --git a/src/boot/efi/stub.c b/src/boot/efi/stub.c index 5e813a6eb6..6339d82ddc 100644 --- a/src/boot/efi/stub.c +++ b/src/boot/efi/stub.c @@ -14,6 +14,7 @@ #include "splash.h" #include "tpm-pcr.h" #include "util.h" +#include "version.h" #include "vmm.h" /* magic string to find in the binary image */ diff --git a/src/boot/efi/util.h b/src/boot/efi/util.h index a28228c4cc..5e1085c788 100644 --- a/src/boot/efi/util.h +++ b/src/boot/efi/util.h @@ -168,18 +168,18 @@ void hexdump(const char16_t *prefix, const void *data, size_t size); # define notify_debugger(i, w) #endif -#define DEFINE_EFI_MAIN_FUNCTION(func, identity, wait_for_debugger) \ - EFI_SYSTEM_TABLE *ST; \ - EFI_BOOT_SERVICES *BS; \ - EFI_RUNTIME_SERVICES *RT; \ - EFI_STATUS efi_main(EFI_HANDLE image, EFI_SYSTEM_TABLE *system_table) { \ - ST = system_table; \ - BS = system_table->BootServices; \ - RT = system_table->RuntimeServices; \ - notify_debugger((identity), (wait_for_debugger)); \ - EFI_STATUS err = func(image); \ - log_wait(); \ - return err; \ +#define DEFINE_EFI_MAIN_FUNCTION(func, identity, wait_for_debugger) \ + EFI_SYSTEM_TABLE *ST; \ + EFI_BOOT_SERVICES *BS; \ + EFI_RUNTIME_SERVICES *RT; \ + EFIAPI EFI_STATUS efi_main(EFI_HANDLE image, EFI_SYSTEM_TABLE *system_table) { \ + ST = system_table; \ + BS = system_table->BootServices; \ + RT = system_table->RuntimeServices; \ + notify_debugger((identity), (wait_for_debugger)); \ + EFI_STATUS err = func(image); \ + log_wait(); \ + return err; \ } #if defined(__i386__) || defined(__x86_64__) diff --git a/src/fundamental/sbat.h b/src/fundamental/sbat.h index b3c09dcb4c..a18da48038 100644 --- a/src/fundamental/sbat.h +++ b/src/fundamental/sbat.h @@ -1,5 +1,7 @@ /* SPDX-License-Identifier: LGPL-2.1-or-later */ +#include "version.h" + #ifdef SBAT_DISTRO # define SBAT_SECTION_TEXT \ "sbat,1,SBAT Version,sbat,1,https://github.com/rhboot/shim/blob/main/SBAT.md\n" \ diff --git a/tools/elf2efi.lds b/tools/elf2efi.lds new file mode 100644 index 0000000000..eed9e279e9 --- /dev/null +++ b/tools/elf2efi.lds @@ -0,0 +1,49 @@ +SECTIONS { + /* We skip the first page because the space will be occupied by the PE headers after conversion. */ + . = CONSTANT(MAXPAGESIZE); + .text ALIGN(CONSTANT(MAXPAGESIZE)) : { + *(.text .text.*) + } + .rodata ALIGN(CONSTANT(MAXPAGESIZE)) : { + *(.rodata .rodata.*) + *(.srodata .srodata.*) + } + .data ALIGN(CONSTANT(MAXPAGESIZE)) : { + *(.data .data.*) + *(.sdata .sdata.*) + *(.got .got.*) + *(.got.plt .got.plt.*) + + /* EDK2 says some firmware cannot handle BSS sections properly. */ + *(.bss .bss.*) + *(.sbss .sbss.*) + *(COMMON) + } + + .sdmagic ALIGN(CONSTANT(MAXPAGESIZE)) : { *(.sdmagic) } + .osrel ALIGN(CONSTANT(MAXPAGESIZE)) : { *(.osrel) } + .sbat ALIGN(CONSTANT(MAXPAGESIZE)) : { *(.sbat) } + + /* These are used for PE conversion and then discarded. */ + .dynsym : { *(.dynsym) } + .dynstr : { *(.dynstr) } + .dynamic : { *(.dynamic) } + .rel.dyn : { *(.rel.dyn) } + .rela.dyn : { *(.rela.dyn) } + + /* These aren't needed and could be discarded. Just in case that they're useful to the debugger + * we keep them, but move them out of the way to keep the PE binary more compact. */ + .ARM.exidx : { *(.ARM.exidx) } + .eh_frame : { *(.eh_frame) } + .eh_frame_hdr : { *(.eh_frame_hdr) } + .gnu.hash : { *(.gnu.hash) } + .hash : { *(.hash) } + .note.gnu.build-id : { *(.note.gnu.build-id ) } + + /DISCARD/ : { + *(.ARM.attributes) + *(.comment) + *(.note.*) + *(.riscv.attributes) + } +} diff --git a/tools/elf2efi.py b/tools/elf2efi.py new file mode 100755 index 0000000000..b26af1f38d --- /dev/null +++ b/tools/elf2efi.py @@ -0,0 +1,578 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: LGPL-2.1-or-later + +# Convert ELF static PIE to PE/EFI image. + +# To do so we simply copy desired ELF sections while preserving their memory layout to ensure that +# code still runs as expected. We then translate ELF relocations to PE relocations so that the EFI +# loader/firmware can properly load the binary to any address at runtime. +# +# To make this as painless as possible we only operate on static PIEs as they should only contain +# base relocations that are easy to handle as they have a one-to-one mapping to PE relocations. +# +# EDK2 does a similar process using their GenFw tool. The main difference is that they use the +# --emit-relocs linker flag, which emits a lot of different (static) ELF relocation types that have +# to be handled differently for each architecture and is overall more work than its worth. +# +# Note that on arches where binutils has PE support (x86/x86_64 mostly, aarch64 only recently) +# objcopy can be used to convert ELF to PE. But this will still not convert ELF relocations, making +# the resulting binary useless. gnu-efi relies on this method and contains a stub that performs the +# ELF dynamic relocations at runtime. + +# pylint: disable=missing-docstring,invalid-name,attribute-defined-outside-init + +import argparse +import hashlib +import io +import os +import pathlib +import time +from ctypes import ( + c_char, + c_uint8, + c_uint16, + c_uint32, + c_uint64, + LittleEndianStructure, + sizeof, +) + +from elftools.elf.constants import SH_FLAGS +from elftools.elf.elffile import ELFFile, Section as ELFSection +from elftools.elf.enums import ( + ENUM_DT_FLAGS_1, + ENUM_RELOC_TYPE_AARCH64, + ENUM_RELOC_TYPE_ARM, + ENUM_RELOC_TYPE_i386, + ENUM_RELOC_TYPE_x64, +) +from elftools.elf.relocation import ( + Relocation as ElfRelocation, + RelocationTable as ElfRelocationTable, +) + + +class PeCoffHeader(LittleEndianStructure): + _fields_ = ( + ("Machine", c_uint16), + ("NumberOfSections", c_uint16), + ("TimeDateStamp", c_uint32), + ("PointerToSymbolTable", c_uint32), + ("NumberOfSymbols", c_uint32), + ("SizeOfOptionalHeader", c_uint16), + ("Characteristics", c_uint16), + ) + + +class PeDataDirectory(LittleEndianStructure): + _fields_ = ( + ("VirtualAddress", c_uint32), + ("Size", c_uint32), + ) + + +class PeRelocationBlock(LittleEndianStructure): + _fields_ = ( + ("PageRVA", c_uint32), + ("BlockSize", c_uint32), + ) + + def __init__(self, PageRVA: int): + super().__init__(PageRVA) + self.entries: list[PeRelocationEntry] = [] + + +class PeRelocationEntry(LittleEndianStructure): + _fields_ = ( + ("Offset", c_uint16, 12), + ("Type", c_uint16, 4), + ) + + +class PeOptionalHeaderStart(LittleEndianStructure): + _fields_ = ( + ("Magic", c_uint16), + ("MajorLinkerVersion", c_uint8), + ("MinorLinkerVersion", c_uint8), + ("SizeOfCode", c_uint32), + ("SizeOfInitializedData", c_uint32), + ("SizeOfUninitializedData", c_uint32), + ("AddressOfEntryPoint", c_uint32), + ("BaseOfCode", c_uint32), + ) + + +class PeOptionalHeaderMiddle(LittleEndianStructure): + _fields_ = ( + ("SectionAlignment", c_uint32), + ("FileAlignment", c_uint32), + ("MajorOperatingSystemVersion", c_uint16), + ("MinorOperatingSystemVersion", c_uint16), + ("MajorImageVersion", c_uint16), + ("MinorImageVersion", c_uint16), + ("MajorSubsystemVersion", c_uint16), + ("MinorSubsystemVersion", c_uint16), + ("Win32VersionValue", c_uint32), + ("SizeOfImage", c_uint32), + ("SizeOfHeaders", c_uint32), + ("CheckSum", c_uint32), + ("Subsystem", c_uint16), + ("DllCharacteristics", c_uint16), + ) + + +class PeOptionalHeaderEnd(LittleEndianStructure): + _fields_ = ( + ("LoaderFlags", c_uint32), + ("NumberOfRvaAndSizes", c_uint32), + ("ExportTable", PeDataDirectory), + ("ImportTable", PeDataDirectory), + ("ResourceTable", PeDataDirectory), + ("ExceptionTable", PeDataDirectory), + ("CertificateTable", PeDataDirectory), + ("BaseRelocationTable", PeDataDirectory), + ("Debug", PeDataDirectory), + ("Architecture", PeDataDirectory), + ("GlobalPtr", PeDataDirectory), + ("TLSTable", PeDataDirectory), + ("LoadConfigTable", PeDataDirectory), + ("BoundImport", PeDataDirectory), + ("IAT", PeDataDirectory), + ("DelayImportDescriptor", PeDataDirectory), + ("CLRRuntimeHeader", PeDataDirectory), + ("Reserved", PeDataDirectory), + ) + + +class PeOptionalHeader(LittleEndianStructure): + pass + + +class PeOptionalHeader32(PeOptionalHeader): + _anonymous_ = ("Start", "Middle", "End") + _fields_ = ( + ("Start", PeOptionalHeaderStart), + ("BaseOfData", c_uint32), + ("ImageBase", c_uint32), + ("Middle", PeOptionalHeaderMiddle), + ("SizeOfStackReserve", c_uint32), + ("SizeOfStackCommit", c_uint32), + ("SizeOfHeapReserve", c_uint32), + ("SizeOfHeapCommit", c_uint32), + ("End", PeOptionalHeaderEnd), + ) + + +class PeOptionalHeader32Plus(PeOptionalHeader): + _anonymous_ = ("Start", "Middle", "End") + _fields_ = ( + ("Start", PeOptionalHeaderStart), + ("ImageBase", c_uint64), + ("Middle", PeOptionalHeaderMiddle), + ("SizeOfStackReserve", c_uint64), + ("SizeOfStackCommit", c_uint64), + ("SizeOfHeapReserve", c_uint64), + ("SizeOfHeapCommit", c_uint64), + ("End", PeOptionalHeaderEnd), + ) + + +class PeSection(LittleEndianStructure): + _fields_ = ( + ("Name", c_char * 8), + ("VirtualSize", c_uint32), + ("VirtualAddress", c_uint32), + ("SizeOfRawData", c_uint32), + ("PointerToRawData", c_uint32), + ("PointerToRelocations", c_uint32), + ("PointerToLinenumbers", c_uint32), + ("NumberOfRelocations", c_uint16), + ("NumberOfLinenumbers", c_uint16), + ("Characteristics", c_uint32), + ) + + def __init__(self): + super().__init__() + self.data = bytearray() + + +N_DATA_DIRECTORY_ENTRIES = 16 + +assert sizeof(PeSection) == 40 +assert sizeof(PeCoffHeader) == 20 +assert sizeof(PeOptionalHeader32) == 224 +assert sizeof(PeOptionalHeader32Plus) == 240 + +# EFI mandates 4KiB memory pages. +SECTION_ALIGNMENT = 4096 +FILE_ALIGNMENT = 512 + +# Nobody cares about DOS headers, so put the PE header right after. +PE_OFFSET = 64 + + +def align_to(x: int, align: int) -> int: + return (x + align - 1) & ~(align - 1) + + +def use_section(elf_s: ELFSection) -> bool: + # These sections are either needed during conversion to PE or are otherwise not needed + # in the final PE image. + IGNORE_SECTIONS = [ + ".ARM.exidx", + ".dynamic", + ".dynstr", + ".dynsym", + ".eh_frame_hdr", + ".eh_frame", + ".gnu.hash", + ".hash", + ".note.gnu.build-id", + ".rel.dyn", + ".rela.dyn", + ] + + # Known sections we care about and want to be in the final PE. + COPY_SECTIONS = [ + ".data", + ".osrel", + ".rodata", + ".sbat", + ".sdmagic", + ".text", + ] + + # By only dealing with allocating sections we effectively filter out debug sections. + if not elf_s["sh_flags"] & SH_FLAGS.SHF_ALLOC: + return False + + if elf_s.name in IGNORE_SECTIONS: + return False + + # For paranoia we only handle sections we know of. Any new sections that come up should + # be added to IGNORE_SECTIONS/COPY_SECTIONS and/or the linker script. + if elf_s.name not in COPY_SECTIONS: + raise RuntimeError(f"Unknown section {elf_s.name}, refusing.") + + if elf_s["sh_addr"] % SECTION_ALIGNMENT != 0: + raise RuntimeError(f"Section {elf_s.name} is not aligned.") + if len(elf_s.name) > 8: + raise RuntimeError(f"ELF section name {elf_s.name} too long.") + + return True + + +def convert_elf_section(elf_s: ELFSection) -> PeSection: + pe_s = PeSection() + pe_s.Name = elf_s.name.encode() + pe_s.VirtualSize = elf_s.data_size + pe_s.VirtualAddress = elf_s["sh_addr"] + pe_s.SizeOfRawData = align_to(elf_s.data_size, FILE_ALIGNMENT) + pe_s.data = bytearray(elf_s.data()) + + if elf_s["sh_flags"] & SH_FLAGS.SHF_EXECINSTR: + pe_s.Characteristics = 0x60000020 # CNT_CODE|MEM_READ|MEM_EXECUTE + elif elf_s["sh_flags"] & SH_FLAGS.SHF_WRITE: + pe_s.Characteristics = 0xC0000040 # CNT_INITIALIZED_DATA|MEM_READ|MEM_WRITE + else: + pe_s.Characteristics = 0x40000040 # CNT_INITIALIZED_DATA|MEM_READ + + return pe_s + + +def copy_sections(elf: ELFFile, opt: PeOptionalHeader) -> list[PeSection]: + sections = [] + + for elf_s in elf.iter_sections(): + if not use_section(elf_s): + continue + + pe_s = convert_elf_section(elf_s) + if pe_s.Name == b".text": + opt.BaseOfCode = pe_s.VirtualAddress + opt.SizeOfCode += pe_s.VirtualSize + else: + opt.SizeOfInitializedData += pe_s.VirtualSize + + if pe_s.Name == b".data" and isinstance(opt, PeOptionalHeader32): + opt.BaseOfData = pe_s.VirtualAddress + + sections.append(pe_s) + + return sections + + +def apply_elf_relative_relocation( + reloc: ElfRelocation, image_base: int, sections: list[PeSection], addend_size: int +): + # fmt: off + [target] = [ + pe_s for pe_s in sections + if pe_s.VirtualAddress <= reloc["r_offset"] < pe_s.VirtualAddress + len(pe_s.data) + ] + # fmt: on + + addend_offset = reloc["r_offset"] - target.VirtualAddress + + if reloc.is_RELA(): + addend = reloc["r_addend"] + else: + addend = target.data[addend_offset : addend_offset + addend_size] + addend = int.from_bytes(addend, byteorder="little") + + # This currently assumes that the ELF file has an image base of 0. + value = (image_base + addend).to_bytes(addend_size, byteorder="little") + target.data[addend_offset : addend_offset + addend_size] = value + + +def convert_elf_reloc_table( + elf: ELFFile, + elf_reloc_table: ElfRelocationTable, + image_base: int, + sections: list[PeSection], + pe_reloc_blocks: dict[int, PeRelocationBlock], +): + NONE_RELOC = { + "EM_386": ENUM_RELOC_TYPE_i386["R_386_NONE"], + "EM_AARCH64": ENUM_RELOC_TYPE_AARCH64["R_AARCH64_NONE"], + "EM_ARM": ENUM_RELOC_TYPE_ARM["R_ARM_NONE"], + "EM_RISCV": 0, + "EM_X86_64": ENUM_RELOC_TYPE_x64["R_X86_64_NONE"], + }[elf["e_machine"]] + + RELATIVE_RELOC = { + "EM_386": ENUM_RELOC_TYPE_i386["R_386_RELATIVE"], + "EM_AARCH64": ENUM_RELOC_TYPE_AARCH64["R_AARCH64_RELATIVE"], + "EM_ARM": ENUM_RELOC_TYPE_ARM["R_ARM_RELATIVE"], + "EM_RISCV": 3, + "EM_X86_64": ENUM_RELOC_TYPE_x64["R_X86_64_RELATIVE"], + }[elf["e_machine"]] + + for reloc in elf_reloc_table.iter_relocations(): + if reloc["r_info_type"] == NONE_RELOC: + continue + + if reloc["r_info_type"] == RELATIVE_RELOC: + apply_elf_relative_relocation( + reloc, image_base, sections, elf.elfclass // 8 + ) + + # Now that the ELF relocation has been applied, we can create a PE relocation. + block_rva = reloc["r_offset"] & ~0xFFF + if block_rva not in pe_reloc_blocks: + pe_reloc_blocks[block_rva] = PeRelocationBlock(block_rva) + + entry = PeRelocationEntry() + entry.Offset = reloc["r_offset"] & 0xFFF + # REL_BASED_HIGHLOW or REL_BASED_DIR64 + entry.Type = 3 if elf.elfclass == 32 else 10 + pe_reloc_blocks[block_rva].entries.append(entry) + + continue + + raise RuntimeError(f"Unsupported relocation {reloc}") + + +def convert_elf_relocations( + elf: ELFFile, opt: PeOptionalHeader, sections: list[PeSection] +) -> PeSection: + dynamic = elf.get_section_by_name(".dynamic") + if dynamic is None: + raise RuntimeError("ELF .dynamic section is missing.") + + [flags_tag] = dynamic.iter_tags("DT_FLAGS_1") + if not flags_tag["d_val"] & ENUM_DT_FLAGS_1["DF_1_PIE"]: + raise RuntimeError("ELF file is not a PIE.") + + pe_reloc_blocks: dict[int, PeRelocationBlock] = {} + for reloc_type, reloc_table in dynamic.get_relocation_tables().items(): + if reloc_type not in ["REL", "RELA"]: + raise RuntimeError("Unsupported relocation type {elf_reloc_type}.") + convert_elf_reloc_table( + elf, reloc_table, opt.ImageBase, sections, pe_reloc_blocks + ) + + data = bytearray() + for rva in sorted(pe_reloc_blocks): + block = pe_reloc_blocks[rva] + n_relocs = len(block.entries) + + # Each block must start on a 32-bit boundary. Because each entry is 16 bits + # the len has to be even. We pad by adding a none relocation. + if n_relocs % 2 != 0: + n_relocs += 1 + block.entries.append(PeRelocationEntry()) + + block.BlockSize = ( + sizeof(PeRelocationBlock) + sizeof(PeRelocationEntry) * n_relocs + ) + data += block + for entry in sorted(block.entries, key=lambda e: e.Offset): + data += entry + + pe_reloc_s = PeSection() + pe_reloc_s.Name = b".reloc" + pe_reloc_s.data = data + pe_reloc_s.VirtualSize = len(data) + pe_reloc_s.SizeOfRawData = align_to(len(data), FILE_ALIGNMENT) + pe_reloc_s.VirtualAddress = align_to( + sections[-1].VirtualAddress + sections[-1].VirtualSize, SECTION_ALIGNMENT + ) + # CNT_INITIALIZED_DATA|MEM_READ|MEM_DISCARDABLE + pe_reloc_s.Characteristics = 0x42000040 + + sections.append(pe_reloc_s) + opt.SizeOfInitializedData += pe_reloc_s.VirtualSize + return pe_reloc_s + + +def write_pe( + file, coff: PeCoffHeader, opt: PeOptionalHeader, sections: list[PeSection] +): + file.write(b"MZ") + file.seek(0x3C, io.SEEK_SET) + file.write(PE_OFFSET.to_bytes(2, byteorder="little")) + file.seek(PE_OFFSET, io.SEEK_SET) + file.write(b"PE\0\0") + file.write(coff) + file.write(opt) + + offset = opt.SizeOfHeaders + for pe_s in sorted(sections, key=lambda s: s.VirtualAddress): + if pe_s.VirtualAddress < opt.SizeOfHeaders: + # Linker script should make sure this does not happen. + raise RuntimeError(f"Section {pe_s.Name} overlapping PE headers.") + + pe_s.PointerToRawData = offset + file.write(pe_s) + offset = align_to(offset + len(pe_s.data), FILE_ALIGNMENT) + + for pe_s in sections: + file.seek(pe_s.PointerToRawData, io.SEEK_SET) + file.write(pe_s.data) + + file.truncate(offset) + + +def elf2efi(args: argparse.Namespace): + elf = ELFFile(args.ELF) + if not elf.little_endian: + raise RuntimeError("ELF file is not little-endian.") + if elf["e_type"] not in ["ET_DYN", "ET_EXEC"]: + raise RuntimeError("Unsupported ELF type.") + + pe_arch = { + "EM_386": 0x014C, + "EM_AARCH64": 0xAA64, + "EM_ARM": 0x01C2, + "EM_RISCV": 0x5064, + "EM_X86_64": 0x8664, + }.get(elf["e_machine"]) + if pe_arch is None: + raise RuntimeError(f"Unuspported ELF arch {elf['e_machine']}") + + coff = PeCoffHeader() + opt = PeOptionalHeader32() if elf.elfclass == 32 else PeOptionalHeader32Plus() + + # We relocate to a unique image base to reduce the chances for runtime relocation to occur. + base_name = pathlib.Path(args.PE.name).name.encode() + opt.ImageBase = int(hashlib.sha1(base_name).hexdigest()[0:8], 16) + if elf.elfclass == 32: + opt.ImageBase = (0x400000 + opt.ImageBase) & 0xFFFF0000 + else: + opt.ImageBase = (0x100000000 + opt.ImageBase) & 0x1FFFF0000 + + sections = copy_sections(elf, opt) + pe_reloc_s = convert_elf_relocations(elf, opt, sections) + + coff.Machine = pe_arch + coff.NumberOfSections = len(sections) + coff.TimeDateStamp = int(os.environ.get("SOURCE_DATE_EPOCH", time.time())) + coff.SizeOfOptionalHeader = sizeof(opt) + # EXECUTABLE_IMAGE|LINE_NUMS_STRIPPED|LOCAL_SYMS_STRIPPED|DEBUG_STRIPPED + # and (32BIT_MACHINE or LARGE_ADDRESS_AWARE) + coff.Characteristics = 0x30E if elf.elfclass == 32 else 0x22E + + opt.AddressOfEntryPoint = elf["e_entry"] + opt.SectionAlignment = SECTION_ALIGNMENT + opt.FileAlignment = FILE_ALIGNMENT + opt.MajorImageVersion = args.version_major + opt.MinorImageVersion = args.version_minor + opt.MajorSubsystemVersion = args.efi_major + opt.MinorSubsystemVersion = args.efi_minor + opt.Subsystem = args.subsystem + opt.Magic = 0x10B if elf.elfclass == 32 else 0x20B + opt.SizeOfImage = align_to( + sections[-1].VirtualAddress + sections[-1].VirtualSize, SECTION_ALIGNMENT + ) + opt.SizeOfHeaders = align_to( + PE_OFFSET + + coff.SizeOfOptionalHeader + + sizeof(PeSection) * coff.NumberOfSections, + FILE_ALIGNMENT, + ) + # DYNAMIC_BASE|NX_COMPAT|HIGH_ENTROPY_VA or DYNAMIC_BASE|NX_COMPAT + opt.DllCharacteristics = 0x160 if elf.elfclass == 64 else 0x140 + + # These values are taken from a natively built PE binary (although, unused by EDK2/EFI). + opt.SizeOfStackReserve = 0x100000 + opt.SizeOfStackCommit = 0x001000 + opt.SizeOfHeapReserve = 0x100000 + opt.SizeOfHeapCommit = 0x001000 + + opt.NumberOfRvaAndSizes = N_DATA_DIRECTORY_ENTRIES + opt.BaseRelocationTable = PeDataDirectory( + pe_reloc_s.VirtualAddress, pe_reloc_s.VirtualSize + ) + + write_pe(args.PE, coff, opt, sections) + + +def main(): + parser = argparse.ArgumentParser(description="Convert ELF binaries to PE/EFI") + parser.add_argument( + "--version-major", + type=int, + default=0, + help="Major image version of EFI image", + ) + parser.add_argument( + "--version-minor", + type=int, + default=0, + help="Minor image version of EFI image", + ) + parser.add_argument( + "--efi-major", + type=int, + default=0, + help="Minimum major EFI subsystem version", + ) + parser.add_argument( + "--efi-minor", + type=int, + default=0, + help="Minimum minor EFI subsystem version", + ) + parser.add_argument( + "--subsystem", + type=int, + default=10, + help="PE subsystem", + ) + parser.add_argument( + "ELF", + type=argparse.FileType("rb"), + help="Input ELF file", + ) + parser.add_argument( + "PE", + type=argparse.FileType("wb"), + help="Output PE/EFI file", + ) + + elf2efi(parser.parse_args()) + + +if __name__ == "__main__": + main() |