1#!/usr/bin/env python3
2# Copyright (c) 2020 Intel Corporation
3# SPDX-License-Identifier: Apache-2.0
4
5import os.path
6import subprocess
7import elftools.elf.elffile
8import argparse
9
10ENTRY_SYM = "__start64"
11BOOTARGS_SYM = "efi_bootargs"
12
13args = None
14
15def verbose(msg):
16    if args.verbose:
17        print(msg)
18
19def build_elf(elf_file, include_dirs):
20    base_dir = os.path.dirname(os.path.abspath(__file__))
21
22    cfile = os.path.join(base_dir, "zefi.c")
23    ldscript = os.path.join(base_dir, "efi.ld")
24
25    assert os.path.isfile(cfile)
26    assert os.path.isfile(ldscript)
27
28    #
29    # Open the ELF file up and find our entry point
30    #
31    fp = open(elf_file, "rb")
32    ef = elftools.elf.elffile.ELFFile(fp)
33
34    symtab = ef.get_section_by_name(".symtab")
35    entry_addr = symtab.get_symbol_by_name(ENTRY_SYM)[0].entry.st_value
36
37    verbose("Entry point address (symbol: %s) 0x%x" % (ENTRY_SYM, entry_addr))
38
39    #
40    # Parse the ELF file and extract segment data
41    #
42
43    data_blob = b''
44    data_segs = []
45    zero_segs = []
46
47    for seg in ef.iter_segments():
48        h = seg.header
49        if h.p_type != "PT_LOAD":
50            continue
51
52        assert h.p_memsz >= h.p_filesz
53        assert len(seg.data()) == h.p_filesz
54
55        if h.p_filesz > 0:
56            sd = seg.data()
57            verbose("%d bytes of data at 0x%x, data offset %d"
58                % (len(sd), h.p_vaddr, len(data_blob)))
59            data_segs.append((h.p_vaddr, len(sd), len(data_blob)))
60            data_blob = data_blob + sd
61
62        if h.p_memsz > h.p_filesz:
63            bytesz = h.p_memsz - h.p_filesz
64            addr = h.p_vaddr + h.p_filesz
65            verbose("%d bytes of zero-fill at 0x%x" % (bytesz, addr))
66            zero_segs.append((addr, bytesz))
67
68    verbose(f"{len(data_blob)} bytes of data to include in image")
69
70    #
71    # Emit a C header containing the metadata
72    #
73    cf = open("zefi-segments.h", "w")
74
75    cf.write("/* GENERATED CODE.  DO NOT EDIT. */\n\n")
76
77    cf.write("/* Sizes and offsets specified in 4-byte units.\n")
78    cf.write(" * All addresses 4-byte aligned.\n")
79    cf.write(" */\n")
80
81    cf.write("struct data_seg { uint64_t addr; uint32_t sz; uint32_t off; };\n\n")
82
83    cf.write("static struct data_seg zefi_dsegs[] = {\n")
84    for s in data_segs:
85        cf.write("    { 0x%x, %d, %d },\n"
86                % (s[0], s[1], s[2]))
87    cf.write("};\n\n")
88
89    cf.write("struct zero_seg { uint64_t addr; uint32_t sz; };\n\n")
90
91    cf.write("static struct zero_seg zefi_zsegs[] = {\n")
92    for s in zero_segs:
93        cf.write("    { 0x%x, %d },\n"
94                % (s[0], s[1]))
95    cf.write("};\n\n")
96
97    cf.write("static uintptr_t zefi_entry = 0x%xUL;\n" % (entry_addr))
98
99    if symtab.get_symbol_by_name(BOOTARGS_SYM):
100        cf.write("static uintptr_t zefi_bootargs = 0x%xUL;\n" % (symtab.get_symbol_by_name(BOOTARGS_SYM)[0].entry.st_value))
101
102    cf.close()
103
104    verbose("Metadata header generated.")
105
106    #
107    # Build
108    #
109
110    # First stage ELF binary.  Flag notes:
111    #  + Stack protector is default on some distros and needs library support
112    #  + We need pic to enforce that the linker adds no relocations
113    #  + UEFI can take interrupts on our stack, so no red zone
114    #  + UEFI API assumes 16-bit wchar_t
115    includes = []
116    for include_dir in include_dirs:
117        includes.extend(["-I", include_dir])
118    cmd = ([args.compiler, "-shared", "-Wall", "-Werror", "-I."] + includes +
119           ["-fno-stack-protector", "-fpic", "-mno-red-zone", "-fshort-wchar",
120            "-Wl,-nostdlib", "-T", ldscript, "-o", "zefi.elf", cfile])
121    verbose(" ".join(cmd))
122    subprocess.run(cmd, check = True)
123
124    # Extract the .data segment and append our extra blob
125    cmd = [args.objcopy, "-O", "binary", "-j", ".data", "zefi.elf", "data.dat"]
126    verbose(" ".join(cmd))
127    subprocess.run(cmd, check = True)
128
129    assert (os.stat("data.dat").st_size % 8) == 0
130    df = open("data.dat", "ab")
131    df.write(data_blob)
132    df.close()
133
134    # FIXME: this generates warnings about our unused trash section having to be moved to make room.  Set its address far away...
135    subprocess.run([args.objcopy, "--update-section", ".data=data.dat",
136                    "zefi.elf"], check = True)
137
138    # Convert it to a PE-COFF DLL.
139    cmd = [args.objcopy, "--target=efi-app-x86_64",
140        "-j", ".text", "-j", ".reloc", "-j", ".data",
141        "zefi.elf", "zephyr.efi"]
142    verbose(" ".join(cmd))
143    subprocess.run(cmd, check = True)
144
145    verbose("Build complete; zephyr.efi wrapper binary is ready")
146
147
148def parse_args():
149    parser = argparse.ArgumentParser(
150        description=__doc__,
151        formatter_class=argparse.RawDescriptionHelpFormatter, allow_abbrev=False)
152
153    parser.add_argument("-c", "--compiler", required=True, help="Compiler to be used")
154    parser.add_argument("-o", "--objcopy", required=True, help="objcopy to be used")
155    parser.add_argument("-f", "--elf-file", required=True, help="Input file")
156    parser.add_argument("-v", "--verbose", action="store_true", help="Verbose output")
157    parser.add_argument("-i", "--includes", required=True, nargs="+",
158                        help="Zephyr base include directories")
159
160    return parser.parse_args()
161
162if __name__ == "__main__":
163
164    args = parse_args()
165    verbose(f"Working on {args.elf_file} with {args.includes}...")
166    build_elf(args.elf_file, args.includes)
167