]> git.ipfire.org Git - thirdparty/systemd.git/blame - tools/elf2efi.py
boot: Bring back bootloader builds
[thirdparty/systemd.git] / tools / elf2efi.py
CommitLineData
2afeaf16
JJ
1#!/usr/bin/env python3
2# SPDX-License-Identifier: LGPL-2.1-or-later
3
4# Convert ELF static PIE to PE/EFI image.
5
6# To do so we simply copy desired ELF sections while preserving their memory layout to ensure that
7# code still runs as expected. We then translate ELF relocations to PE relocations so that the EFI
8# loader/firmware can properly load the binary to any address at runtime.
9#
10# To make this as painless as possible we only operate on static PIEs as they should only contain
11# base relocations that are easy to handle as they have a one-to-one mapping to PE relocations.
12#
13# EDK2 does a similar process using their GenFw tool. The main difference is that they use the
14# --emit-relocs linker flag, which emits a lot of different (static) ELF relocation types that have
15# to be handled differently for each architecture and is overall more work than its worth.
16#
17# Note that on arches where binutils has PE support (x86/x86_64 mostly, aarch64 only recently)
18# objcopy can be used to convert ELF to PE. But this will still not convert ELF relocations, making
19# the resulting binary useless. gnu-efi relies on this method and contains a stub that performs the
20# ELF dynamic relocations at runtime.
21
22# pylint: disable=missing-docstring,invalid-name,attribute-defined-outside-init
23
24import argparse
25import hashlib
26import io
27import os
28import pathlib
29import time
30from ctypes import (
31 c_char,
32 c_uint8,
33 c_uint16,
34 c_uint32,
35 c_uint64,
36 LittleEndianStructure,
37 sizeof,
38)
39
40from elftools.elf.constants import SH_FLAGS
41from elftools.elf.elffile import ELFFile, Section as ELFSection
42from elftools.elf.enums import (
43 ENUM_DT_FLAGS_1,
44 ENUM_RELOC_TYPE_AARCH64,
45 ENUM_RELOC_TYPE_ARM,
46 ENUM_RELOC_TYPE_i386,
47 ENUM_RELOC_TYPE_x64,
48)
49from elftools.elf.relocation import (
50 Relocation as ElfRelocation,
51 RelocationTable as ElfRelocationTable,
52)
53
54
55class PeCoffHeader(LittleEndianStructure):
56 _fields_ = (
57 ("Machine", c_uint16),
58 ("NumberOfSections", c_uint16),
59 ("TimeDateStamp", c_uint32),
60 ("PointerToSymbolTable", c_uint32),
61 ("NumberOfSymbols", c_uint32),
62 ("SizeOfOptionalHeader", c_uint16),
63 ("Characteristics", c_uint16),
64 )
65
66
67class PeDataDirectory(LittleEndianStructure):
68 _fields_ = (
69 ("VirtualAddress", c_uint32),
70 ("Size", c_uint32),
71 )
72
73
74class PeRelocationBlock(LittleEndianStructure):
75 _fields_ = (
76 ("PageRVA", c_uint32),
77 ("BlockSize", c_uint32),
78 )
79
80 def __init__(self, PageRVA: int):
81 super().__init__(PageRVA)
82 self.entries: list[PeRelocationEntry] = []
83
84
85class PeRelocationEntry(LittleEndianStructure):
86 _fields_ = (
87 ("Offset", c_uint16, 12),
88 ("Type", c_uint16, 4),
89 )
90
91
92class PeOptionalHeaderStart(LittleEndianStructure):
93 _fields_ = (
94 ("Magic", c_uint16),
95 ("MajorLinkerVersion", c_uint8),
96 ("MinorLinkerVersion", c_uint8),
97 ("SizeOfCode", c_uint32),
98 ("SizeOfInitializedData", c_uint32),
99 ("SizeOfUninitializedData", c_uint32),
100 ("AddressOfEntryPoint", c_uint32),
101 ("BaseOfCode", c_uint32),
102 )
103
104
105class PeOptionalHeaderMiddle(LittleEndianStructure):
106 _fields_ = (
107 ("SectionAlignment", c_uint32),
108 ("FileAlignment", c_uint32),
109 ("MajorOperatingSystemVersion", c_uint16),
110 ("MinorOperatingSystemVersion", c_uint16),
111 ("MajorImageVersion", c_uint16),
112 ("MinorImageVersion", c_uint16),
113 ("MajorSubsystemVersion", c_uint16),
114 ("MinorSubsystemVersion", c_uint16),
115 ("Win32VersionValue", c_uint32),
116 ("SizeOfImage", c_uint32),
117 ("SizeOfHeaders", c_uint32),
118 ("CheckSum", c_uint32),
119 ("Subsystem", c_uint16),
120 ("DllCharacteristics", c_uint16),
121 )
122
123
124class PeOptionalHeaderEnd(LittleEndianStructure):
125 _fields_ = (
126 ("LoaderFlags", c_uint32),
127 ("NumberOfRvaAndSizes", c_uint32),
128 ("ExportTable", PeDataDirectory),
129 ("ImportTable", PeDataDirectory),
130 ("ResourceTable", PeDataDirectory),
131 ("ExceptionTable", PeDataDirectory),
132 ("CertificateTable", PeDataDirectory),
133 ("BaseRelocationTable", PeDataDirectory),
134 ("Debug", PeDataDirectory),
135 ("Architecture", PeDataDirectory),
136 ("GlobalPtr", PeDataDirectory),
137 ("TLSTable", PeDataDirectory),
138 ("LoadConfigTable", PeDataDirectory),
139 ("BoundImport", PeDataDirectory),
140 ("IAT", PeDataDirectory),
141 ("DelayImportDescriptor", PeDataDirectory),
142 ("CLRRuntimeHeader", PeDataDirectory),
143 ("Reserved", PeDataDirectory),
144 )
145
146
147class PeOptionalHeader(LittleEndianStructure):
148 pass
149
150
151class PeOptionalHeader32(PeOptionalHeader):
152 _anonymous_ = ("Start", "Middle", "End")
153 _fields_ = (
154 ("Start", PeOptionalHeaderStart),
155 ("BaseOfData", c_uint32),
156 ("ImageBase", c_uint32),
157 ("Middle", PeOptionalHeaderMiddle),
158 ("SizeOfStackReserve", c_uint32),
159 ("SizeOfStackCommit", c_uint32),
160 ("SizeOfHeapReserve", c_uint32),
161 ("SizeOfHeapCommit", c_uint32),
162 ("End", PeOptionalHeaderEnd),
163 )
164
165
166class PeOptionalHeader32Plus(PeOptionalHeader):
167 _anonymous_ = ("Start", "Middle", "End")
168 _fields_ = (
169 ("Start", PeOptionalHeaderStart),
170 ("ImageBase", c_uint64),
171 ("Middle", PeOptionalHeaderMiddle),
172 ("SizeOfStackReserve", c_uint64),
173 ("SizeOfStackCommit", c_uint64),
174 ("SizeOfHeapReserve", c_uint64),
175 ("SizeOfHeapCommit", c_uint64),
176 ("End", PeOptionalHeaderEnd),
177 )
178
179
180class PeSection(LittleEndianStructure):
181 _fields_ = (
182 ("Name", c_char * 8),
183 ("VirtualSize", c_uint32),
184 ("VirtualAddress", c_uint32),
185 ("SizeOfRawData", c_uint32),
186 ("PointerToRawData", c_uint32),
187 ("PointerToRelocations", c_uint32),
188 ("PointerToLinenumbers", c_uint32),
189 ("NumberOfRelocations", c_uint16),
190 ("NumberOfLinenumbers", c_uint16),
191 ("Characteristics", c_uint32),
192 )
193
194 def __init__(self):
195 super().__init__()
196 self.data = bytearray()
197
198
199N_DATA_DIRECTORY_ENTRIES = 16
200
201assert sizeof(PeSection) == 40
202assert sizeof(PeCoffHeader) == 20
203assert sizeof(PeOptionalHeader32) == 224
204assert sizeof(PeOptionalHeader32Plus) == 240
205
206# EFI mandates 4KiB memory pages.
207SECTION_ALIGNMENT = 4096
208FILE_ALIGNMENT = 512
209
210# Nobody cares about DOS headers, so put the PE header right after.
211PE_OFFSET = 64
212
213
214def align_to(x: int, align: int) -> int:
215 return (x + align - 1) & ~(align - 1)
216
217
218def use_section(elf_s: ELFSection) -> bool:
219 # These sections are either needed during conversion to PE or are otherwise not needed
220 # in the final PE image.
221 IGNORE_SECTIONS = [
222 ".ARM.exidx",
223 ".dynamic",
224 ".dynstr",
225 ".dynsym",
226 ".eh_frame_hdr",
227 ".eh_frame",
228 ".gnu.hash",
229 ".hash",
230 ".note.gnu.build-id",
231 ".rel.dyn",
232 ".rela.dyn",
233 ]
234
235 # Known sections we care about and want to be in the final PE.
236 COPY_SECTIONS = [
237 ".data",
238 ".osrel",
239 ".rodata",
240 ".sbat",
241 ".sdmagic",
242 ".text",
243 ]
244
245 # By only dealing with allocating sections we effectively filter out debug sections.
246 if not elf_s["sh_flags"] & SH_FLAGS.SHF_ALLOC:
247 return False
248
249 if elf_s.name in IGNORE_SECTIONS:
250 return False
251
252 # For paranoia we only handle sections we know of. Any new sections that come up should
253 # be added to IGNORE_SECTIONS/COPY_SECTIONS and/or the linker script.
254 if elf_s.name not in COPY_SECTIONS:
255 raise RuntimeError(f"Unknown section {elf_s.name}, refusing.")
256
257 if elf_s["sh_addr"] % SECTION_ALIGNMENT != 0:
258 raise RuntimeError(f"Section {elf_s.name} is not aligned.")
259 if len(elf_s.name) > 8:
260 raise RuntimeError(f"ELF section name {elf_s.name} too long.")
261
262 return True
263
264
265def convert_elf_section(elf_s: ELFSection) -> PeSection:
266 pe_s = PeSection()
267 pe_s.Name = elf_s.name.encode()
268 pe_s.VirtualSize = elf_s.data_size
269 pe_s.VirtualAddress = elf_s["sh_addr"]
270 pe_s.SizeOfRawData = align_to(elf_s.data_size, FILE_ALIGNMENT)
271 pe_s.data = bytearray(elf_s.data())
272
273 if elf_s["sh_flags"] & SH_FLAGS.SHF_EXECINSTR:
274 pe_s.Characteristics = 0x60000020 # CNT_CODE|MEM_READ|MEM_EXECUTE
275 elif elf_s["sh_flags"] & SH_FLAGS.SHF_WRITE:
276 pe_s.Characteristics = 0xC0000040 # CNT_INITIALIZED_DATA|MEM_READ|MEM_WRITE
277 else:
278 pe_s.Characteristics = 0x40000040 # CNT_INITIALIZED_DATA|MEM_READ
279
280 return pe_s
281
282
283def copy_sections(elf: ELFFile, opt: PeOptionalHeader) -> list[PeSection]:
284 sections = []
285
286 for elf_s in elf.iter_sections():
287 if not use_section(elf_s):
288 continue
289
290 pe_s = convert_elf_section(elf_s)
291 if pe_s.Name == b".text":
292 opt.BaseOfCode = pe_s.VirtualAddress
293 opt.SizeOfCode += pe_s.VirtualSize
294 else:
295 opt.SizeOfInitializedData += pe_s.VirtualSize
296
297 if pe_s.Name == b".data" and isinstance(opt, PeOptionalHeader32):
298 opt.BaseOfData = pe_s.VirtualAddress
299
300 sections.append(pe_s)
301
302 return sections
303
304
305def apply_elf_relative_relocation(
306 reloc: ElfRelocation, image_base: int, sections: list[PeSection], addend_size: int
307):
308 # fmt: off
309 [target] = [
310 pe_s for pe_s in sections
311 if pe_s.VirtualAddress <= reloc["r_offset"] < pe_s.VirtualAddress + len(pe_s.data)
312 ]
313 # fmt: on
314
315 addend_offset = reloc["r_offset"] - target.VirtualAddress
316
317 if reloc.is_RELA():
318 addend = reloc["r_addend"]
319 else:
320 addend = target.data[addend_offset : addend_offset + addend_size]
321 addend = int.from_bytes(addend, byteorder="little")
322
323 # This currently assumes that the ELF file has an image base of 0.
324 value = (image_base + addend).to_bytes(addend_size, byteorder="little")
325 target.data[addend_offset : addend_offset + addend_size] = value
326
327
328def convert_elf_reloc_table(
329 elf: ELFFile,
330 elf_reloc_table: ElfRelocationTable,
331 image_base: int,
332 sections: list[PeSection],
333 pe_reloc_blocks: dict[int, PeRelocationBlock],
334):
335 NONE_RELOC = {
336 "EM_386": ENUM_RELOC_TYPE_i386["R_386_NONE"],
337 "EM_AARCH64": ENUM_RELOC_TYPE_AARCH64["R_AARCH64_NONE"],
338 "EM_ARM": ENUM_RELOC_TYPE_ARM["R_ARM_NONE"],
339 "EM_RISCV": 0,
340 "EM_X86_64": ENUM_RELOC_TYPE_x64["R_X86_64_NONE"],
341 }[elf["e_machine"]]
342
343 RELATIVE_RELOC = {
344 "EM_386": ENUM_RELOC_TYPE_i386["R_386_RELATIVE"],
345 "EM_AARCH64": ENUM_RELOC_TYPE_AARCH64["R_AARCH64_RELATIVE"],
346 "EM_ARM": ENUM_RELOC_TYPE_ARM["R_ARM_RELATIVE"],
347 "EM_RISCV": 3,
348 "EM_X86_64": ENUM_RELOC_TYPE_x64["R_X86_64_RELATIVE"],
349 }[elf["e_machine"]]
350
351 for reloc in elf_reloc_table.iter_relocations():
352 if reloc["r_info_type"] == NONE_RELOC:
353 continue
354
355 if reloc["r_info_type"] == RELATIVE_RELOC:
356 apply_elf_relative_relocation(
357 reloc, image_base, sections, elf.elfclass // 8
358 )
359
360 # Now that the ELF relocation has been applied, we can create a PE relocation.
361 block_rva = reloc["r_offset"] & ~0xFFF
362 if block_rva not in pe_reloc_blocks:
363 pe_reloc_blocks[block_rva] = PeRelocationBlock(block_rva)
364
365 entry = PeRelocationEntry()
366 entry.Offset = reloc["r_offset"] & 0xFFF
367 # REL_BASED_HIGHLOW or REL_BASED_DIR64
368 entry.Type = 3 if elf.elfclass == 32 else 10
369 pe_reloc_blocks[block_rva].entries.append(entry)
370
371 continue
372
373 raise RuntimeError(f"Unsupported relocation {reloc}")
374
375
376def convert_elf_relocations(
377 elf: ELFFile, opt: PeOptionalHeader, sections: list[PeSection]
378) -> PeSection:
379 dynamic = elf.get_section_by_name(".dynamic")
380 if dynamic is None:
381 raise RuntimeError("ELF .dynamic section is missing.")
382
383 [flags_tag] = dynamic.iter_tags("DT_FLAGS_1")
384 if not flags_tag["d_val"] & ENUM_DT_FLAGS_1["DF_1_PIE"]:
385 raise RuntimeError("ELF file is not a PIE.")
386
387 pe_reloc_blocks: dict[int, PeRelocationBlock] = {}
388 for reloc_type, reloc_table in dynamic.get_relocation_tables().items():
389 if reloc_type not in ["REL", "RELA"]:
390 raise RuntimeError("Unsupported relocation type {elf_reloc_type}.")
391 convert_elf_reloc_table(
392 elf, reloc_table, opt.ImageBase, sections, pe_reloc_blocks
393 )
394
395 data = bytearray()
396 for rva in sorted(pe_reloc_blocks):
397 block = pe_reloc_blocks[rva]
398 n_relocs = len(block.entries)
399
400 # Each block must start on a 32-bit boundary. Because each entry is 16 bits
401 # the len has to be even. We pad by adding a none relocation.
402 if n_relocs % 2 != 0:
403 n_relocs += 1
404 block.entries.append(PeRelocationEntry())
405
406 block.BlockSize = (
407 sizeof(PeRelocationBlock) + sizeof(PeRelocationEntry) * n_relocs
408 )
409 data += block
410 for entry in sorted(block.entries, key=lambda e: e.Offset):
411 data += entry
412
413 pe_reloc_s = PeSection()
414 pe_reloc_s.Name = b".reloc"
415 pe_reloc_s.data = data
416 pe_reloc_s.VirtualSize = len(data)
417 pe_reloc_s.SizeOfRawData = align_to(len(data), FILE_ALIGNMENT)
418 pe_reloc_s.VirtualAddress = align_to(
419 sections[-1].VirtualAddress + sections[-1].VirtualSize, SECTION_ALIGNMENT
420 )
421 # CNT_INITIALIZED_DATA|MEM_READ|MEM_DISCARDABLE
422 pe_reloc_s.Characteristics = 0x42000040
423
424 sections.append(pe_reloc_s)
425 opt.SizeOfInitializedData += pe_reloc_s.VirtualSize
426 return pe_reloc_s
427
428
429def write_pe(
430 file, coff: PeCoffHeader, opt: PeOptionalHeader, sections: list[PeSection]
431):
432 file.write(b"MZ")
433 file.seek(0x3C, io.SEEK_SET)
434 file.write(PE_OFFSET.to_bytes(2, byteorder="little"))
435 file.seek(PE_OFFSET, io.SEEK_SET)
436 file.write(b"PE\0\0")
437 file.write(coff)
438 file.write(opt)
439
440 offset = opt.SizeOfHeaders
441 for pe_s in sorted(sections, key=lambda s: s.VirtualAddress):
442 if pe_s.VirtualAddress < opt.SizeOfHeaders:
443 # Linker script should make sure this does not happen.
444 raise RuntimeError(f"Section {pe_s.Name} overlapping PE headers.")
445
446 pe_s.PointerToRawData = offset
447 file.write(pe_s)
448 offset = align_to(offset + len(pe_s.data), FILE_ALIGNMENT)
449
450 for pe_s in sections:
451 file.seek(pe_s.PointerToRawData, io.SEEK_SET)
452 file.write(pe_s.data)
453
454 file.truncate(offset)
455
456
457def elf2efi(args: argparse.Namespace):
458 elf = ELFFile(args.ELF)
459 if not elf.little_endian:
460 raise RuntimeError("ELF file is not little-endian.")
461 if elf["e_type"] not in ["ET_DYN", "ET_EXEC"]:
462 raise RuntimeError("Unsupported ELF type.")
463
464 pe_arch = {
465 "EM_386": 0x014C,
466 "EM_AARCH64": 0xAA64,
467 "EM_ARM": 0x01C2,
468 "EM_RISCV": 0x5064,
469 "EM_X86_64": 0x8664,
470 }.get(elf["e_machine"])
471 if pe_arch is None:
472 raise RuntimeError(f"Unuspported ELF arch {elf['e_machine']}")
473
474 coff = PeCoffHeader()
475 opt = PeOptionalHeader32() if elf.elfclass == 32 else PeOptionalHeader32Plus()
476
477 # We relocate to a unique image base to reduce the chances for runtime relocation to occur.
478 base_name = pathlib.Path(args.PE.name).name.encode()
479 opt.ImageBase = int(hashlib.sha1(base_name).hexdigest()[0:8], 16)
480 if elf.elfclass == 32:
481 opt.ImageBase = (0x400000 + opt.ImageBase) & 0xFFFF0000
482 else:
483 opt.ImageBase = (0x100000000 + opt.ImageBase) & 0x1FFFF0000
484
485 sections = copy_sections(elf, opt)
486 pe_reloc_s = convert_elf_relocations(elf, opt, sections)
487
488 coff.Machine = pe_arch
489 coff.NumberOfSections = len(sections)
490 coff.TimeDateStamp = int(os.environ.get("SOURCE_DATE_EPOCH", time.time()))
491 coff.SizeOfOptionalHeader = sizeof(opt)
492 # EXECUTABLE_IMAGE|LINE_NUMS_STRIPPED|LOCAL_SYMS_STRIPPED|DEBUG_STRIPPED
493 # and (32BIT_MACHINE or LARGE_ADDRESS_AWARE)
494 coff.Characteristics = 0x30E if elf.elfclass == 32 else 0x22E
495
496 opt.AddressOfEntryPoint = elf["e_entry"]
497 opt.SectionAlignment = SECTION_ALIGNMENT
498 opt.FileAlignment = FILE_ALIGNMENT
499 opt.MajorImageVersion = args.version_major
500 opt.MinorImageVersion = args.version_minor
501 opt.MajorSubsystemVersion = args.efi_major
502 opt.MinorSubsystemVersion = args.efi_minor
503 opt.Subsystem = args.subsystem
504 opt.Magic = 0x10B if elf.elfclass == 32 else 0x20B
505 opt.SizeOfImage = align_to(
506 sections[-1].VirtualAddress + sections[-1].VirtualSize, SECTION_ALIGNMENT
507 )
508 opt.SizeOfHeaders = align_to(
509 PE_OFFSET
510 + coff.SizeOfOptionalHeader
511 + sizeof(PeSection) * coff.NumberOfSections,
512 FILE_ALIGNMENT,
513 )
514 # DYNAMIC_BASE|NX_COMPAT|HIGH_ENTROPY_VA or DYNAMIC_BASE|NX_COMPAT
515 opt.DllCharacteristics = 0x160 if elf.elfclass == 64 else 0x140
516
517 # These values are taken from a natively built PE binary (although, unused by EDK2/EFI).
518 opt.SizeOfStackReserve = 0x100000
519 opt.SizeOfStackCommit = 0x001000
520 opt.SizeOfHeapReserve = 0x100000
521 opt.SizeOfHeapCommit = 0x001000
522
523 opt.NumberOfRvaAndSizes = N_DATA_DIRECTORY_ENTRIES
524 opt.BaseRelocationTable = PeDataDirectory(
525 pe_reloc_s.VirtualAddress, pe_reloc_s.VirtualSize
526 )
527
528 write_pe(args.PE, coff, opt, sections)
529
530
531def main():
532 parser = argparse.ArgumentParser(description="Convert ELF binaries to PE/EFI")
533 parser.add_argument(
534 "--version-major",
535 type=int,
536 default=0,
537 help="Major image version of EFI image",
538 )
539 parser.add_argument(
540 "--version-minor",
541 type=int,
542 default=0,
543 help="Minor image version of EFI image",
544 )
545 parser.add_argument(
546 "--efi-major",
547 type=int,
548 default=0,
549 help="Minimum major EFI subsystem version",
550 )
551 parser.add_argument(
552 "--efi-minor",
553 type=int,
554 default=0,
555 help="Minimum minor EFI subsystem version",
556 )
557 parser.add_argument(
558 "--subsystem",
559 type=int,
560 default=10,
561 help="PE subsystem",
562 )
563 parser.add_argument(
564 "ELF",
565 type=argparse.FileType("rb"),
566 help="Input ELF file",
567 )
568 parser.add_argument(
569 "PE",
570 type=argparse.FileType("wb"),
571 help="Output PE/EFI file",
572 )
573
574 elf2efi(parser.parse_args())
575
576
577if __name__ == "__main__":
578 main()