diff options
Diffstat (limited to 'tools')
-rw-r--r-- | tools/elf2efi.lds | 49 | ||||
-rwxr-xr-x | tools/elf2efi.py | 578 |
2 files changed, 627 insertions, 0 deletions
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() |