summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--README1
-rw-r--r--meson.build27
-rw-r--r--src/boot/efi/boot.c1
-rw-r--r--src/boot/efi/efi-string.h2
-rw-r--r--src/boot/efi/meson.build176
-rw-r--r--src/boot/efi/stub.c1
-rw-r--r--src/boot/efi/util.h24
-rw-r--r--src/fundamental/sbat.h2
-rw-r--r--tools/elf2efi.lds49
-rwxr-xr-xtools/elf2efi.py578
10 files changed, 843 insertions, 18 deletions
diff --git a/README b/README
index 8421db75e6..a94f71cc66 100644
--- a/README
+++ b/README
@@ -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()