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